@mulmoclaude/core 4.9.0 → 4.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/helps/bug-report-faq.md +12 -0
- package/assets/helps/error-recovery.md +144 -29
- package/assets/helps/sandbox.md +2 -2
- package/dist/collection/index.cjs +3 -2
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +3 -2
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +3 -3
- package/dist/collection/registry/server/index.js +3 -3
- package/dist/collection/server/index.cjs +2 -2
- package/dist/collection/server/index.js +2 -2
- package/dist/collection-watchers/index.cjs +4 -4
- package/dist/collection-watchers/index.js +4 -4
- package/dist/{discovery-BY-nMCuh.js → discovery-BkIKvdh0.js} +3 -3
- package/dist/discovery-BkIKvdh0.js.map +1 -0
- package/dist/{discovery-2TvulIz8.cjs → discovery-tvFo-bUJ.cjs} +3 -3
- package/dist/discovery-tvFo-bUJ.cjs.map +1 -0
- package/dist/{dist-D8zokgGo.js → dist-BzoA9pDR.js} +6 -1
- package/dist/dist-BzoA9pDR.js.map +1 -0
- package/dist/{dist-CsgSfWwR.cjs → dist-Gj7ygaW2.cjs} +6 -1
- package/dist/dist-Gj7ygaW2.cjs.map +1 -0
- package/dist/errorMessage-BQNpSYdT.js +1 -0
- package/dist/errorMessage-CT-va_Pv.cjs +1 -0
- package/dist/errors-C7TI_t_4.js +10 -0
- package/dist/errors-C7TI_t_4.js.map +1 -0
- package/dist/errors-CdXsfFCF.cjs +15 -0
- package/dist/errors-CdXsfFCF.cjs.map +1 -0
- package/dist/feeds/server/index.cjs +4 -4
- package/dist/feeds/server/index.js +4 -4
- package/dist/google/index.cjs +3 -2
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +3 -2
- package/dist/google/index.js.map +1 -1
- package/dist/{listen-UZA68Upt.cjs → listen-BXthb-9C.cjs} +2 -2
- package/dist/{listen-UZA68Upt.cjs.map → listen-BXthb-9C.cjs.map} +1 -1
- package/dist/{listen-D5av7GzB.js → listen-Dca-NoBi.js} +2 -2
- package/dist/{listen-D5av7GzB.js.map → listen-Dca-NoBi.js.map} +1 -1
- package/dist/notifier/index.cjs +1 -1
- package/dist/notifier/index.js +1 -1
- package/dist/notifier/types.d.ts +20 -11
- package/dist/{notifier-DZJj0sOh.cjs → notifier-B65Mvngb.cjs} +12 -7
- package/dist/notifier-B65Mvngb.cjs.map +1 -0
- package/dist/{notifier-DhnwR82U.js → notifier-vAp5t6wL.js} +12 -7
- package/dist/notifier-vAp5t6wL.js.map +1 -0
- package/dist/{promptSafety-Bte62OvU.js → promptSafety-BpBTh5xO.js} +2 -2
- package/dist/{promptSafety-Bte62OvU.js.map → promptSafety-BpBTh5xO.js.map} +1 -1
- package/dist/{promptSafety-Cv-SoTd4.cjs → promptSafety-caJWd1ZQ.cjs} +2 -2
- package/dist/{promptSafety-Cv-SoTd4.cjs.map → promptSafety-caJWd1ZQ.cjs.map} +1 -1
- package/dist/remote-host/index.cjs +1 -1
- package/dist/remote-host/index.js +1 -1
- package/dist/remote-host/server/index.cjs +4 -3
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.js +4 -3
- package/dist/remote-host/server/index.js.map +1 -1
- package/dist/{remote-host-CFnl1I-L.js → remote-host-Cjox2F2J.js} +2 -2
- package/dist/{remote-host-CFnl1I-L.js.map → remote-host-Cjox2F2J.js.map} +1 -1
- package/dist/{remote-host-EQPlccB9.cjs → remote-host-DhAHBYzN.cjs} +2 -2
- package/dist/{remote-host-EQPlccB9.cjs.map → remote-host-DhAHBYzN.cjs.map} +1 -1
- package/dist/remote-view/index.cjs +1 -1
- package/dist/remote-view/index.js +1 -1
- package/dist/scheduler/index.cjs +2 -1
- package/dist/scheduler/index.cjs.map +1 -1
- package/dist/scheduler/index.js +2 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/{server-DXUt34Rt.cjs → server-Div1i_k5.cjs} +5 -4
- package/dist/{server-DXUt34Rt.cjs.map → server-Div1i_k5.cjs.map} +1 -1
- package/dist/{server-DyYwfNB6.js → server-Dk9x1VLG.js} +5 -4
- package/dist/{server-DyYwfNB6.js.map → server-Dk9x1VLG.js.map} +1 -1
- package/dist/utils/index.cjs +3 -10
- package/dist/utils/index.js +2 -9
- package/dist/whisper/index.cjs +2 -1
- package/dist/whisper/index.cjs.map +1 -1
- package/dist/whisper/index.js +2 -1
- package/dist/whisper/index.js.map +1 -1
- package/dist/wiki/index.cjs +1 -1
- package/dist/wiki/index.js +1 -1
- package/dist/workspace-setup/index.js +5 -0
- package/dist/workspace-setup/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/discovery-2TvulIz8.cjs.map +0 -1
- package/dist/discovery-BY-nMCuh.js.map +0 -1
- package/dist/dist-CsgSfWwR.cjs.map +0 -1
- package/dist/dist-D8zokgGo.js.map +0 -1
- package/dist/notifier-DZJj0sOh.cjs.map +0 -1
- package/dist/notifier-DhnwR82U.js.map +0 -1
- package/dist/utils/index.cjs.map +0 -1
- package/dist/utils/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-host-EQPlccB9.cjs","names":[],"sources":["../src/remote-host/health.ts","../src/remote-host/index.ts"],"sourcesContent":["// Health of the remote-host command channel, as reported by the resilient runner\n// and rendered by a host's toolbar control. Browser-safe on purpose: the client\n// narrows the parsed HTTP payload with the same guard the server writes it from,\n// so the two sides cannot drift on the state names.\n//\n// online — the Firestore subscription is up; the phone can reach this host\n// reconnecting — it died and is being re-subscribed with backoff (self-healing)\n// offline — re-subscribing stopped helping, or nothing is connected at all;\n// recovering needs a re-auth from the browser's parked session\n//\n// Deliberately no UI wording here — how a state reads to a user is each host's\n// i18n, and core owning it would make the shared package a translation authority.\nexport const RUNNER_HEALTH_STATES = [\"online\", \"reconnecting\", \"offline\"] as const;\nexport type RunnerHealthState = (typeof RUNNER_HEALTH_STATES)[number];\n\nexport interface RunnerHealth {\n state: RunnerHealthState;\n /** Last channel error seen, for the popover and the log. Null before the first one. */\n lastError: string | null;\n /** ms epoch of the last state change, so the UI can say how long it has been down. */\n changedAt: number;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null;\n\nexport const isRunnerHealthState = (value: unknown): value is RunnerHealthState => RUNNER_HEALTH_STATES.some((state) => state === value);\n\n/** Narrows a parsed HTTP payload. The client renders whatever this accepts, so a\n * half-shaped health has to read as \"no health reported\" rather than as a state. */\nexport const isRunnerHealth = (value: unknown): value is RunnerHealth =>\n isRecord(value) &&\n isRunnerHealthState(value.state) &&\n (value.lastError === null || typeof value.lastError === \"string\") &&\n typeof value.changedAt === \"number\";\n","// Remote-host command-channel protocol — the browser-safe contract shared by a\n// host (MulmoClaude, MulmoTerminal) and the remote/mobile client (mulmoserver).\n//\n// A host signs in to Firebase as the user, listens to that user's per-host\n// command queue in Firestore, runs a handler, and writes the result back; the\n// remote writes commands and reads results via a real-time listener. This module\n// owns the wire types + the Firestore path helpers. It is the single source of\n// truth so the host runner and the client never drift on the protocol.\n//\n// Ported from ../mulmoserver/src/firestore/commandChannel.ts and the per-host\n// copy that lived in MulmoClaude's server/remoteHost/. The one change vs. those\n// copies: the path helpers take the `firestore` instance as a parameter (rather\n// than importing a module-level singleton) so a single extracted module serves\n// every host's own Firebase init. The hostId is host-specific (\"mulmoclaude\",\n// \"mulmoterminal\") and is supplied by each host — there is no discovery.\nimport { CollectionReference, DocumentData, DocumentReference, Firestore, collection, doc } from \"firebase/firestore\";\nimport { isRecord } from \"@mulmoclaude/common\";\n\n// JSON payloads carried by the command channel. Explicit JSON types keep the\n// channel typed without resorting to any/unknown.\nexport type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };\nexport type JsonObject = Record<string, JsonValue>;\n\n/** Structural JSON view of `T`, recursively.\n *\n * TypeScript gives an implicit index signature to type aliases and mapped\n * types but NOT to interfaces, so a payload assembled from domain interfaces\n * (`Shortcut`, `FeedSummary`, …) cannot satisfy `Record<string, JsonValue>`\n * structurally — even though it is plain JSON at runtime. Mapping over `T`\n * reconstructs it as an anonymous type, which does get that index signature.\n *\n * Recursive on purpose: a top-level-only map would still leave nested\n * interfaces (`{ shortcuts: Shortcut[] }`) unassignable, which is the case\n * every handler here actually has. */\n// The function branch must come BEFORE the object branch: a function IS an\n// object to TypeScript, so without it a function maps to `{}` and sails\n// through — the helper would accept a payload that serialises to nothing.\n// Verified: `toJsonObject({ callback: () => undefined })` compiled clean until\n// this branch existed (CodeRabbit, #2596).\nexport type Jsonify<T> = T extends JsonValue\n ? T\n : T extends (...args: never[]) => unknown\n ? never\n : T extends (infer U)[]\n ? Jsonify<U>[]\n : T extends object\n ? { [K in keyof T]: Jsonify<T[K]> }\n : never;\n\n/** Widen a JSON-shaped handler payload to the channel's `JsonObject`.\n *\n * Exists so the `Jsonify` reasoning above lives in ONE place. Before this,\n * eight remote-host handlers each carried their own `as unknown as JsonObject`\n * with the justification re-argued in eight slightly different comments —\n * which is how a rule stops being reviewable. */\nexport const toJsonObject = <T extends object>(payload: Jsonify<T>): JsonObject => payload as JsonObject;\n\nconst describeNonJson = (value: unknown): string => {\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"object\") return \"a non-plain object\";\n return `a ${typeof value}`;\n};\n\n/** Anything carrying its own JSON form — `Date` above all — must be asked for\n * it rather than walked, because walking a `Date`'s own enumerable keys finds\n * none and flattens the timestamp to `{}`. This is the step `JSON.stringify`\n * performs before it recurses, and the channel used to get it for free. */\nconst hasToJson = (value: object): value is { toJSON: () => unknown } => \"toJSON\" in value && typeof value.toJSON === \"function\";\n\nconst jsonRepresentationOf = (value: object): unknown => (hasToJson(value) ? value.toJSON() : value);\n\n/** Rebuild `value` as JSON, or throw naming the property that cannot be. */\nfunction toJsonValue(value: unknown, path: string): JsonValue {\n if (Array.isArray(value)) return toJsonItems(value, path);\n if (isRecord(value)) {\n const represented = jsonRepresentationOf(value);\n if (represented !== value) return toJsonValue(represented, path);\n return toJsonEntries(value, path);\n }\n return toJsonScalar(value, path);\n}\n\n/** JSON's four scalar forms. Anything else — a function, a class instance, a\n * non-finite number — is what the channel cannot carry. */\nfunction toJsonScalar(value: unknown, path: string): JsonValue {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n throw new Error(`${path} is ${describeNonJson(value)}, which JSON cannot represent`);\n}\n\nfunction toJsonItems(items: unknown[], path: string): JsonValue[] {\n // An absent element becomes `null`, matching `JSON.stringify` — an array has\n // to keep its length, so a hole cannot simply be dropped the way a key is.\n // `Array.from` rather than `map`, which SKIPS holes and would leave them in\n // the result: `JSON.stringify` renders a hole as null and hides that, but\n // `1 in arr` / `Object.keys` / `forEach` all still see the gap.\n return Array.from(items, (entry, index) => (entry === undefined ? null : toJsonValue(entry, `${path}[${index}]`)));\n}\n\nfunction toJsonEntries(record: Record<string, unknown>, path: string): JsonObject {\n const usable = Object.entries(record).filter(([, value]) => value !== undefined);\n return Object.fromEntries(usable.map(([key, value]) => [key, toJsonValue(value, `${path}.${key}`)]));\n}\n\n/** Runtime counterpart to `toJsonObject`, for payloads whose values are typed\n * `unknown` — a collection record, a projected view row — so no amount of\n * mapped-type work can PROVE them JSON.\n *\n * Walks the payload and rebuilds it from the values it actually inspected, so\n * the returned `JsonObject` is earned rather than asserted. Absent (`undefined`)\n * properties are dropped exactly as `JSON.stringify` drops them; anything the\n * channel could not carry — a function, a class instance, `NaN` — throws\n * naming its path, instead of reaching Firestore as a silently mangled write. */\nexport const coerceJsonObject = (payload: Record<string, unknown>): JsonObject => toJsonEntries(payload, \"payload\");\n\n// A channel routes commands to one specific host. Both sides agree on a\n// hardcoded hostId per use case (e.g. \"mulmoclaude\", \"mulmoterminal\"); there is\n// no discovery — the remote and host just share the id.\nexport interface Channel {\n uid: string;\n hostId: string;\n}\n\nexport type CommandStatus = \"queued\" | \"processing\" | \"done\" | \"error\";\n\nexport interface CommandError {\n code: string;\n message: string;\n}\n\n// One document in a channel's commands subcollection is one API-call-like\n// request. The remote (mobile) writes method/params; the host writes\n// result/error/status.\nexport interface Command {\n method: string;\n params: JsonObject;\n status: CommandStatus;\n result: JsonValue;\n error: CommandError | null;\n createdBy: \"remote\" | \"host\";\n // Offline-queue fields (all optional; absent ⇒ pre-offline-queue behaviour, so\n // this is backward-compatible with every deployed client). Epoch-millisecond\n // NUMBERS set by the remote at enqueue time — deliberately plain numbers, not\n // Firestore Timestamps, so `isExpired` / `byCreatedAt` stay pure + browser-safe\n // and unit-testable without a Firestore fake. Clock skew over a multi-day expiry\n // window is immaterial. See plans/done/feat-remote-offline-queue.md.\n createdAt?: number; // enqueue time — age/display + best-effort dispatch bias (NOT a strict order guarantee; chat is async)\n expiresAt?: number; // deadline; past it the host deletes the command + its staged attachments\n queuedOffline?: boolean; // emitted while the host was offline (gates the remote's attachment rollback)\n}\n\n// A command is expired once `now` reaches its remote-set deadline. Absent\n// `expiresAt` ⇒ it never expires (pre-offline-queue commands). Pure with an\n// injected `now` for deterministic tests; the runner passes `Date.now()`.\nexport const isExpired = (command: Pick<Command, \"expiresAt\">, now: number): boolean => typeof command.expiresAt === \"number\" && now >= command.expiresAt;\n\n// Best-effort dispatch bias for a drained batch: oldest enqueue first. This is\n// NOT an ordering guarantee — commands run concurrently and may complete out of\n// order (chat is asynchronous, by design); it only nudges which one starts first.\n// A command with no `createdAt` sorts as oldest (0) so it is never starved.\nexport const byCreatedAt = (left: Pick<Command, \"createdAt\">, right: Pick<Command, \"createdAt\">): number => (left.createdAt ?? 0) - (right.createdAt ?? 0);\n\nexport type CommandHandler = (params: JsonObject) => JsonValue | Promise<JsonValue>;\nexport type CommandHandlers = Record<string, CommandHandler>;\n\n/** The param name a collection-serving command uses to say WHICH project's\n * collections it means. Reserved and documented now, before a phone client\n * ships, because the parts that are hard to change later are the ones being\n * written today. Four rules go with it:\n *\n * 1. **It is an OPAQUE scope, never a path.** The phone is a genuinely remote\n * client; an absolute root in a command, an artifact or a token publishes\n * the user's home directory over the wire. Mint a project id host-side and\n * resolve it host-side.\n * 2. **The phone must be able to LEARN the list.** A picker needs\n * `{ id, label }` pairs from the host — a command of its own, or a field on\n * an existing listing. Designing the scope value now is what keeps that the\n * ONLY new thing when the feature lands.\n * 3. **Handlers RESOLVE a scope; they do not hard-code one.** Write each\n * collection handler as \"read the scope from params, defaulting to the\n * host's root\" rather than calling the workspace accessor inline. Today\n * every call resolves the default and behaves exactly as it does now; the\n * day the param arrives, no handler changes.\n * 4. **The artifact stays host-built.** A remote view's srcdoc, its inlined\n * image thumbnails and its token are assembled on the host, so the phone\n * never resolves a path itself. That is what makes (1) hold without\n * trusting the client. */\nexport const COMMAND_SCOPE_PARAM = \"project\";\n\n/** Read the opaque project scope off a command's params, or `undefined` for\n * \"the host's own root\" — which is what every command means today. A\n * non-string (or empty) value is treated as absent rather than as an error: a\n * scope the host cannot resolve must fall back to the default, never to a\n * guess. Hosts pass the result to their own id → root resolver; this module\n * deliberately never sees a path. */\nexport const readCommandScope = (params: JsonObject): string | undefined => {\n const raw = params[COMMAND_SCOPE_PARAM];\n return typeof raw === \"string\" && raw.trim().length > 0 ? raw.trim() : undefined;\n};\n\n// Bumped when the command-channel wire protocol changes in a way the remote must\n// gate on. Advertised in the presence doc so the remote can check compatibility\n// before issuing commands.\n//\n// v2: offline queueing. The host honours `expiresAt` (deletes an expired command\n// + its staged attachments instead of spawning a stale chat). A remote MUST see\n// protocolVersion >= 2 before queueing a startChat while the host is offline —\n// a v1 host silently ignores `expiresAt`, so a queued chat would spawn stale on\n// reconnect with its uploads never cleaned up.\nexport const REMOTE_HOST_PROTOCOL_VERSION = 2;\n\n// The presence doc's payload: online flag + a capability advertisement. Written\n// by the host on every heartbeat; the remote reads it from the presence listener\n// it already runs (no extra round trip, known the instant the host is online).\n// Browser-safe so the mobile client compiles against the same shape.\n// `updatedAt` (a Firestore serverTimestamp) is added by the runner at write time\n// and is intentionally not part of this capability contract.\nexport interface HostPresence {\n online: boolean;\n hostId: string;\n protocolVersion: number;\n // Method names the host serves — the keys of the live handler table.\n capabilities: string[];\n}\n\n// Build the presence payload from the live handler table. Capabilities are\n// `Object.keys(handlers)` so registering a handler is the ONLY step needed to\n// advertise it — there is no second list to keep in sync.\nexport const buildHostPresence = (channel: Channel, handlers: CommandHandlers, online: boolean): HostPresence => ({\n online,\n hostId: channel.hostId,\n protocolVersion: REMOTE_HOST_PROTOCOL_VERSION,\n capabilities: Object.keys(handlers),\n});\n\n// Per-host command queue: users/{uid}/hosts/{hostId}/commands.\nexport const commandsCollection = (firestore: Firestore, channel: Channel): CollectionReference<DocumentData> =>\n collection(firestore, \"users\", channel.uid, \"hosts\", channel.hostId, \"commands\");\n\n// Presence doc for a host: users/{uid}/hosts/{hostId}. The host heartbeats\n// { online, updatedAt } here; the remote reads it to know if the host is up.\nexport const hostDoc = (firestore: Firestore, channel: Channel): DocumentReference<DocumentData> =>\n doc(firestore, \"users\", channel.uid, \"hosts\", channel.hostId);\n\n// Channel health as the resilient runner reports it. Browser-safe alongside the\n// wire types because the control that renders it runs in the client.\nexport { RUNNER_HEALTH_STATES, isRunnerHealth, isRunnerHealthState } from \"./health.js\";\nexport type { RunnerHealth, RunnerHealthState } from \"./health.js\";\n"],"mappings":";;;AAYA,IAAa,uBAAuB;CAAC;CAAU;CAAgB;AAAS;AAWxE,IAAM,YAAY,UAAqD,OAAO,UAAU,YAAY,UAAU;AAE9G,IAAa,uBAAuB,UAA+C,qBAAqB,MAAM,UAAU,UAAU,KAAK;;;AAIvI,IAAa,kBAAkB,UAC7B,SAAS,KAAK,KACd,oBAAoB,MAAM,KAAK,MAC9B,MAAM,cAAc,QAAQ,OAAO,MAAM,cAAc,aACxD,OAAO,MAAM,cAAc;;;;;;;;;ACsB7B,IAAa,gBAAkC,YAAoC;AAEnF,IAAM,mBAAmB,UAA2B;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,OAAO,KAAK,OAAO;AACrB;;;;;AAMA,IAAM,aAAa,UAAsD,YAAY,SAAS,OAAO,MAAM,WAAW;AAEtH,IAAM,wBAAwB,UAA4B,UAAU,KAAK,IAAI,MAAM,OAAO,IAAI;;AAG9F,SAAS,YAAY,OAAgB,MAAyB;CAC5D,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,YAAY,OAAO,IAAI;CACxD,IAAI,aAAA,SAAS,KAAK,GAAG;EACnB,MAAM,cAAc,qBAAqB,KAAK;EAC9C,IAAI,gBAAgB,OAAO,OAAO,YAAY,aAAa,IAAI;EAC/D,OAAO,cAAc,OAAO,IAAI;CAClC;CACA,OAAO,aAAa,OAAO,IAAI;AACjC;;;AAIA,SAAS,aAAa,OAAgB,MAAyB;CAC7D,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW,OAAO;CACtF,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG,OAAO;CAChE,MAAM,IAAI,MAAM,GAAG,KAAK,MAAM,gBAAgB,KAAK,EAAE,8BAA8B;AACrF;AAEA,SAAS,YAAY,OAAkB,MAA2B;CAMhE,OAAO,MAAM,KAAK,QAAQ,OAAO,UAAW,UAAU,KAAA,IAAY,OAAO,YAAY,OAAO,GAAG,KAAK,GAAG,MAAM,EAAE,CAAE;AACnH;AAEA,SAAS,cAAc,QAAiC,MAA0B;CAChF,MAAM,SAAS,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS;CAC/E,OAAO,OAAO,YAAY,OAAO,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,YAAY,OAAO,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;AACrG;;;;;;;;;;AAWA,IAAa,oBAAoB,YAAiD,cAAc,SAAS,SAAS;AAyClH,IAAa,aAAa,SAAqC,QAAyB,OAAO,QAAQ,cAAc,YAAY,OAAO,QAAQ;AAMhJ,IAAa,eAAe,MAAkC,WAA+C,KAAK,aAAa,MAAM,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;AA2BxJ,IAAa,sBAAsB;;;;;;;AAQnC,IAAa,oBAAoB,WAA2C;CAC1E,MAAM,MAAM,OAAO;CACnB,OAAO,OAAO,QAAQ,YAAY,IAAI,KAAK,CAAC,CAAC,SAAS,IAAI,IAAI,KAAK,IAAI,KAAA;AACzE;AAWA,IAAa,+BAA+B;AAmB5C,IAAa,qBAAqB,SAAkB,UAA2B,YAAmC;CAChH;CACA,QAAQ,QAAQ;CAChB,iBAAA;CACA,cAAc,OAAO,KAAK,QAAQ;AACpC;AAGA,IAAa,sBAAsB,WAAsB,aAAA,GACvD,mBAAA,WAAA,CAAW,WAAW,SAAS,QAAQ,KAAK,SAAS,QAAQ,QAAQ,UAAU;AAIjF,IAAa,WAAW,WAAsB,aAAA,GAC5C,mBAAA,IAAA,CAAI,WAAW,SAAS,QAAQ,KAAK,SAAS,QAAQ,MAAM"}
|
|
1
|
+
{"version":3,"file":"remote-host-DhAHBYzN.cjs","names":[],"sources":["../src/remote-host/health.ts","../src/remote-host/index.ts"],"sourcesContent":["// Health of the remote-host command channel, as reported by the resilient runner\n// and rendered by a host's toolbar control. Browser-safe on purpose: the client\n// narrows the parsed HTTP payload with the same guard the server writes it from,\n// so the two sides cannot drift on the state names.\n//\n// online — the Firestore subscription is up; the phone can reach this host\n// reconnecting — it died and is being re-subscribed with backoff (self-healing)\n// offline — re-subscribing stopped helping, or nothing is connected at all;\n// recovering needs a re-auth from the browser's parked session\n//\n// Deliberately no UI wording here — how a state reads to a user is each host's\n// i18n, and core owning it would make the shared package a translation authority.\nexport const RUNNER_HEALTH_STATES = [\"online\", \"reconnecting\", \"offline\"] as const;\nexport type RunnerHealthState = (typeof RUNNER_HEALTH_STATES)[number];\n\nexport interface RunnerHealth {\n state: RunnerHealthState;\n /** Last channel error seen, for the popover and the log. Null before the first one. */\n lastError: string | null;\n /** ms epoch of the last state change, so the UI can say how long it has been down. */\n changedAt: number;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null;\n\nexport const isRunnerHealthState = (value: unknown): value is RunnerHealthState => RUNNER_HEALTH_STATES.some((state) => state === value);\n\n/** Narrows a parsed HTTP payload. The client renders whatever this accepts, so a\n * half-shaped health has to read as \"no health reported\" rather than as a state. */\nexport const isRunnerHealth = (value: unknown): value is RunnerHealth =>\n isRecord(value) &&\n isRunnerHealthState(value.state) &&\n (value.lastError === null || typeof value.lastError === \"string\") &&\n typeof value.changedAt === \"number\";\n","// Remote-host command-channel protocol — the browser-safe contract shared by a\n// host (MulmoClaude, MulmoTerminal) and the remote/mobile client (mulmoserver).\n//\n// A host signs in to Firebase as the user, listens to that user's per-host\n// command queue in Firestore, runs a handler, and writes the result back; the\n// remote writes commands and reads results via a real-time listener. This module\n// owns the wire types + the Firestore path helpers. It is the single source of\n// truth so the host runner and the client never drift on the protocol.\n//\n// Ported from ../mulmoserver/src/firestore/commandChannel.ts and the per-host\n// copy that lived in MulmoClaude's server/remoteHost/. The one change vs. those\n// copies: the path helpers take the `firestore` instance as a parameter (rather\n// than importing a module-level singleton) so a single extracted module serves\n// every host's own Firebase init. The hostId is host-specific (\"mulmoclaude\",\n// \"mulmoterminal\") and is supplied by each host — there is no discovery.\nimport { CollectionReference, DocumentData, DocumentReference, Firestore, collection, doc } from \"firebase/firestore\";\nimport { isRecord } from \"@mulmoclaude/common\";\n\n// JSON payloads carried by the command channel. Explicit JSON types keep the\n// channel typed without resorting to any/unknown.\nexport type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };\nexport type JsonObject = Record<string, JsonValue>;\n\n/** Structural JSON view of `T`, recursively.\n *\n * TypeScript gives an implicit index signature to type aliases and mapped\n * types but NOT to interfaces, so a payload assembled from domain interfaces\n * (`Shortcut`, `FeedSummary`, …) cannot satisfy `Record<string, JsonValue>`\n * structurally — even though it is plain JSON at runtime. Mapping over `T`\n * reconstructs it as an anonymous type, which does get that index signature.\n *\n * Recursive on purpose: a top-level-only map would still leave nested\n * interfaces (`{ shortcuts: Shortcut[] }`) unassignable, which is the case\n * every handler here actually has. */\n// The function branch must come BEFORE the object branch: a function IS an\n// object to TypeScript, so without it a function maps to `{}` and sails\n// through — the helper would accept a payload that serialises to nothing.\n// Verified: `toJsonObject({ callback: () => undefined })` compiled clean until\n// this branch existed (CodeRabbit, #2596).\nexport type Jsonify<T> = T extends JsonValue\n ? T\n : T extends (...args: never[]) => unknown\n ? never\n : T extends (infer U)[]\n ? Jsonify<U>[]\n : T extends object\n ? { [K in keyof T]: Jsonify<T[K]> }\n : never;\n\n/** Widen a JSON-shaped handler payload to the channel's `JsonObject`.\n *\n * Exists so the `Jsonify` reasoning above lives in ONE place. Before this,\n * eight remote-host handlers each carried their own `as unknown as JsonObject`\n * with the justification re-argued in eight slightly different comments —\n * which is how a rule stops being reviewable. */\nexport const toJsonObject = <T extends object>(payload: Jsonify<T>): JsonObject => payload as JsonObject;\n\nconst describeNonJson = (value: unknown): string => {\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"object\") return \"a non-plain object\";\n return `a ${typeof value}`;\n};\n\n/** Anything carrying its own JSON form — `Date` above all — must be asked for\n * it rather than walked, because walking a `Date`'s own enumerable keys finds\n * none and flattens the timestamp to `{}`. This is the step `JSON.stringify`\n * performs before it recurses, and the channel used to get it for free. */\nconst hasToJson = (value: object): value is { toJSON: () => unknown } => \"toJSON\" in value && typeof value.toJSON === \"function\";\n\nconst jsonRepresentationOf = (value: object): unknown => (hasToJson(value) ? value.toJSON() : value);\n\n/** Rebuild `value` as JSON, or throw naming the property that cannot be. */\nfunction toJsonValue(value: unknown, path: string): JsonValue {\n if (Array.isArray(value)) return toJsonItems(value, path);\n if (isRecord(value)) {\n const represented = jsonRepresentationOf(value);\n if (represented !== value) return toJsonValue(represented, path);\n return toJsonEntries(value, path);\n }\n return toJsonScalar(value, path);\n}\n\n/** JSON's four scalar forms. Anything else — a function, a class instance, a\n * non-finite number — is what the channel cannot carry. */\nfunction toJsonScalar(value: unknown, path: string): JsonValue {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n throw new Error(`${path} is ${describeNonJson(value)}, which JSON cannot represent`);\n}\n\nfunction toJsonItems(items: unknown[], path: string): JsonValue[] {\n // An absent element becomes `null`, matching `JSON.stringify` — an array has\n // to keep its length, so a hole cannot simply be dropped the way a key is.\n // `Array.from` rather than `map`, which SKIPS holes and would leave them in\n // the result: `JSON.stringify` renders a hole as null and hides that, but\n // `1 in arr` / `Object.keys` / `forEach` all still see the gap.\n return Array.from(items, (entry, index) => (entry === undefined ? null : toJsonValue(entry, `${path}[${index}]`)));\n}\n\nfunction toJsonEntries(record: Record<string, unknown>, path: string): JsonObject {\n const usable = Object.entries(record).filter(([, value]) => value !== undefined);\n return Object.fromEntries(usable.map(([key, value]) => [key, toJsonValue(value, `${path}.${key}`)]));\n}\n\n/** Runtime counterpart to `toJsonObject`, for payloads whose values are typed\n * `unknown` — a collection record, a projected view row — so no amount of\n * mapped-type work can PROVE them JSON.\n *\n * Walks the payload and rebuilds it from the values it actually inspected, so\n * the returned `JsonObject` is earned rather than asserted. Absent (`undefined`)\n * properties are dropped exactly as `JSON.stringify` drops them; anything the\n * channel could not carry — a function, a class instance, `NaN` — throws\n * naming its path, instead of reaching Firestore as a silently mangled write. */\nexport const coerceJsonObject = (payload: Record<string, unknown>): JsonObject => toJsonEntries(payload, \"payload\");\n\n// A channel routes commands to one specific host. Both sides agree on a\n// hardcoded hostId per use case (e.g. \"mulmoclaude\", \"mulmoterminal\"); there is\n// no discovery — the remote and host just share the id.\nexport interface Channel {\n uid: string;\n hostId: string;\n}\n\nexport type CommandStatus = \"queued\" | \"processing\" | \"done\" | \"error\";\n\nexport interface CommandError {\n code: string;\n message: string;\n}\n\n// One document in a channel's commands subcollection is one API-call-like\n// request. The remote (mobile) writes method/params; the host writes\n// result/error/status.\nexport interface Command {\n method: string;\n params: JsonObject;\n status: CommandStatus;\n result: JsonValue;\n error: CommandError | null;\n createdBy: \"remote\" | \"host\";\n // Offline-queue fields (all optional; absent ⇒ pre-offline-queue behaviour, so\n // this is backward-compatible with every deployed client). Epoch-millisecond\n // NUMBERS set by the remote at enqueue time — deliberately plain numbers, not\n // Firestore Timestamps, so `isExpired` / `byCreatedAt` stay pure + browser-safe\n // and unit-testable without a Firestore fake. Clock skew over a multi-day expiry\n // window is immaterial. See plans/done/feat-remote-offline-queue.md.\n createdAt?: number; // enqueue time — age/display + best-effort dispatch bias (NOT a strict order guarantee; chat is async)\n expiresAt?: number; // deadline; past it the host deletes the command + its staged attachments\n queuedOffline?: boolean; // emitted while the host was offline (gates the remote's attachment rollback)\n}\n\n// A command is expired once `now` reaches its remote-set deadline. Absent\n// `expiresAt` ⇒ it never expires (pre-offline-queue commands). Pure with an\n// injected `now` for deterministic tests; the runner passes `Date.now()`.\nexport const isExpired = (command: Pick<Command, \"expiresAt\">, now: number): boolean => typeof command.expiresAt === \"number\" && now >= command.expiresAt;\n\n// Best-effort dispatch bias for a drained batch: oldest enqueue first. This is\n// NOT an ordering guarantee — commands run concurrently and may complete out of\n// order (chat is asynchronous, by design); it only nudges which one starts first.\n// A command with no `createdAt` sorts as oldest (0) so it is never starved.\nexport const byCreatedAt = (left: Pick<Command, \"createdAt\">, right: Pick<Command, \"createdAt\">): number => (left.createdAt ?? 0) - (right.createdAt ?? 0);\n\nexport type CommandHandler = (params: JsonObject) => JsonValue | Promise<JsonValue>;\nexport type CommandHandlers = Record<string, CommandHandler>;\n\n/** The param name a collection-serving command uses to say WHICH project's\n * collections it means. Reserved and documented now, before a phone client\n * ships, because the parts that are hard to change later are the ones being\n * written today. Four rules go with it:\n *\n * 1. **It is an OPAQUE scope, never a path.** The phone is a genuinely remote\n * client; an absolute root in a command, an artifact or a token publishes\n * the user's home directory over the wire. Mint a project id host-side and\n * resolve it host-side.\n * 2. **The phone must be able to LEARN the list.** A picker needs\n * `{ id, label }` pairs from the host — a command of its own, or a field on\n * an existing listing. Designing the scope value now is what keeps that the\n * ONLY new thing when the feature lands.\n * 3. **Handlers RESOLVE a scope; they do not hard-code one.** Write each\n * collection handler as \"read the scope from params, defaulting to the\n * host's root\" rather than calling the workspace accessor inline. Today\n * every call resolves the default and behaves exactly as it does now; the\n * day the param arrives, no handler changes.\n * 4. **The artifact stays host-built.** A remote view's srcdoc, its inlined\n * image thumbnails and its token are assembled on the host, so the phone\n * never resolves a path itself. That is what makes (1) hold without\n * trusting the client. */\nexport const COMMAND_SCOPE_PARAM = \"project\";\n\n/** Read the opaque project scope off a command's params, or `undefined` for\n * \"the host's own root\" — which is what every command means today. A\n * non-string (or empty) value is treated as absent rather than as an error: a\n * scope the host cannot resolve must fall back to the default, never to a\n * guess. Hosts pass the result to their own id → root resolver; this module\n * deliberately never sees a path. */\nexport const readCommandScope = (params: JsonObject): string | undefined => {\n const raw = params[COMMAND_SCOPE_PARAM];\n return typeof raw === \"string\" && raw.trim().length > 0 ? raw.trim() : undefined;\n};\n\n// Bumped when the command-channel wire protocol changes in a way the remote must\n// gate on. Advertised in the presence doc so the remote can check compatibility\n// before issuing commands.\n//\n// v2: offline queueing. The host honours `expiresAt` (deletes an expired command\n// + its staged attachments instead of spawning a stale chat). A remote MUST see\n// protocolVersion >= 2 before queueing a startChat while the host is offline —\n// a v1 host silently ignores `expiresAt`, so a queued chat would spawn stale on\n// reconnect with its uploads never cleaned up.\nexport const REMOTE_HOST_PROTOCOL_VERSION = 2;\n\n// The presence doc's payload: online flag + a capability advertisement. Written\n// by the host on every heartbeat; the remote reads it from the presence listener\n// it already runs (no extra round trip, known the instant the host is online).\n// Browser-safe so the mobile client compiles against the same shape.\n// `updatedAt` (a Firestore serverTimestamp) is added by the runner at write time\n// and is intentionally not part of this capability contract.\nexport interface HostPresence {\n online: boolean;\n hostId: string;\n protocolVersion: number;\n // Method names the host serves — the keys of the live handler table.\n capabilities: string[];\n}\n\n// Build the presence payload from the live handler table. Capabilities are\n// `Object.keys(handlers)` so registering a handler is the ONLY step needed to\n// advertise it — there is no second list to keep in sync.\nexport const buildHostPresence = (channel: Channel, handlers: CommandHandlers, online: boolean): HostPresence => ({\n online,\n hostId: channel.hostId,\n protocolVersion: REMOTE_HOST_PROTOCOL_VERSION,\n capabilities: Object.keys(handlers),\n});\n\n// Per-host command queue: users/{uid}/hosts/{hostId}/commands.\nexport const commandsCollection = (firestore: Firestore, channel: Channel): CollectionReference<DocumentData> =>\n collection(firestore, \"users\", channel.uid, \"hosts\", channel.hostId, \"commands\");\n\n// Presence doc for a host: users/{uid}/hosts/{hostId}. The host heartbeats\n// { online, updatedAt } here; the remote reads it to know if the host is up.\nexport const hostDoc = (firestore: Firestore, channel: Channel): DocumentReference<DocumentData> =>\n doc(firestore, \"users\", channel.uid, \"hosts\", channel.hostId);\n\n// Channel health as the resilient runner reports it. Browser-safe alongside the\n// wire types because the control that renders it runs in the client.\nexport { RUNNER_HEALTH_STATES, isRunnerHealth, isRunnerHealthState } from \"./health.js\";\nexport type { RunnerHealth, RunnerHealthState } from \"./health.js\";\n"],"mappings":";;;AAYA,IAAa,uBAAuB;CAAC;CAAU;CAAgB;AAAS;AAWxE,IAAM,YAAY,UAAqD,OAAO,UAAU,YAAY,UAAU;AAE9G,IAAa,uBAAuB,UAA+C,qBAAqB,MAAM,UAAU,UAAU,KAAK;;;AAIvI,IAAa,kBAAkB,UAC7B,SAAS,KAAK,KACd,oBAAoB,MAAM,KAAK,MAC9B,MAAM,cAAc,QAAQ,OAAO,MAAM,cAAc,aACxD,OAAO,MAAM,cAAc;;;;;;;;;ACsB7B,IAAa,gBAAkC,YAAoC;AAEnF,IAAM,mBAAmB,UAA2B;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,OAAO,KAAK,OAAO;AACrB;;;;;AAMA,IAAM,aAAa,UAAsD,YAAY,SAAS,OAAO,MAAM,WAAW;AAEtH,IAAM,wBAAwB,UAA4B,UAAU,KAAK,IAAI,MAAM,OAAO,IAAI;;AAG9F,SAAS,YAAY,OAAgB,MAAyB;CAC5D,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,YAAY,OAAO,IAAI;CACxD,IAAI,aAAA,SAAS,KAAK,GAAG;EACnB,MAAM,cAAc,qBAAqB,KAAK;EAC9C,IAAI,gBAAgB,OAAO,OAAO,YAAY,aAAa,IAAI;EAC/D,OAAO,cAAc,OAAO,IAAI;CAClC;CACA,OAAO,aAAa,OAAO,IAAI;AACjC;;;AAIA,SAAS,aAAa,OAAgB,MAAyB;CAC7D,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW,OAAO;CACtF,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG,OAAO;CAChE,MAAM,IAAI,MAAM,GAAG,KAAK,MAAM,gBAAgB,KAAK,EAAE,8BAA8B;AACrF;AAEA,SAAS,YAAY,OAAkB,MAA2B;CAMhE,OAAO,MAAM,KAAK,QAAQ,OAAO,UAAW,UAAU,KAAA,IAAY,OAAO,YAAY,OAAO,GAAG,KAAK,GAAG,MAAM,EAAE,CAAE;AACnH;AAEA,SAAS,cAAc,QAAiC,MAA0B;CAChF,MAAM,SAAS,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS;CAC/E,OAAO,OAAO,YAAY,OAAO,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,YAAY,OAAO,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;AACrG;;;;;;;;;;AAWA,IAAa,oBAAoB,YAAiD,cAAc,SAAS,SAAS;AAyClH,IAAa,aAAa,SAAqC,QAAyB,OAAO,QAAQ,cAAc,YAAY,OAAO,QAAQ;AAMhJ,IAAa,eAAe,MAAkC,WAA+C,KAAK,aAAa,MAAM,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;AA2BxJ,IAAa,sBAAsB;;;;;;;AAQnC,IAAa,oBAAoB,WAA2C;CAC1E,MAAM,MAAM,OAAO;CACnB,OAAO,OAAO,QAAQ,YAAY,IAAI,KAAK,CAAC,CAAC,SAAS,IAAI,IAAI,KAAK,IAAI,KAAA;AACzE;AAWA,IAAa,+BAA+B;AAmB5C,IAAa,qBAAqB,SAAkB,UAA2B,YAAmC;CAChH;CACA,QAAQ,QAAQ;CAChB,iBAAA;CACA,cAAc,OAAO,KAAK,QAAQ;AACpC;AAGA,IAAa,sBAAsB,WAAsB,aAAA,GACvD,mBAAA,WAAA,CAAW,WAAW,SAAS,QAAQ,KAAK,SAAS,QAAQ,QAAQ,UAAU;AAIjF,IAAa,WAAW,WAAsB,aAAA,GAC5C,mBAAA,IAAA,CAAI,WAAW,SAAS,QAAQ,KAAK,SAAS,QAAQ,MAAM"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-Gj7ygaW2.cjs");
|
|
3
3
|
const require_project = require("../project-C1ep9pvo.cjs");
|
|
4
4
|
//#region src/remote-view/index.ts
|
|
5
5
|
/** Bump when the bootstrap/message contract changes shape; the bootstrap
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as isRecord } from "../dist-
|
|
1
|
+
import { s as isRecord } from "../dist-BzoA9pDR.js";
|
|
2
2
|
import { n as projectRecordFields } from "../project-B0j_Vf_Q.js";
|
|
3
3
|
//#region src/remote-view/index.ts
|
|
4
4
|
/** Bump when the bootstrap/message contract changes shape; the bootstrap
|
package/dist/scheduler/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("../rolldown-runtime-VH7oDXx4.cjs");
|
|
3
|
-
const require_dist = require("../dist-
|
|
3
|
+
const require_dist = require("../dist-Gj7ygaW2.cjs");
|
|
4
|
+
require("../errors-CdXsfFCF.cjs");
|
|
4
5
|
let node_fs = require("node:fs");
|
|
5
6
|
let node_path = require("node:path");
|
|
6
7
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/scheduler/schedule-window.ts","../../src/scheduler/task-manager.ts","../../src/scheduler/adapter.ts"],"sourcesContent":["// One definition of \"when does this schedule fire\". The host expresses\n// intervals in milliseconds and the library (@receptron/task-scheduler) in\n// seconds, so every place that answers \"is it due now?\", \"when is the next\n// run?\" or \"which windows were missed?\" has to cross that boundary. Doing it\n// twice is what let the tick engine drift away from `nextWindowAfter`: its own\n// interval check counted from UTC midnight, so anything longer than a day\n// collapsed to \"every day at 00:00\" while the UI kept showing the real,\n// epoch-anchored next run (#2937).\n\nimport { SCHEDULE_TYPES, isDueAt, nextWindowAfter, parseTimeToMs, type TaskSchedule as LibrarySchedule } from \"@receptron/task-scheduler\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_DAY_MS = 24 * 60 * 60 * ONE_SECOND_MS;\n\n/** A schedule as the host states it: intervals in milliseconds, or a daily\n * `HH:MM` UTC time. */\nexport type TaskSchedule = { type: typeof SCHEDULE_TYPES.interval; intervalMs: number } | { type: typeof SCHEDULE_TYPES.daily; time: string };\n\n/** An interval this engine can never turn into a window. Zero divides by zero\n * inside the library's `Math.ceil(afterMs / intervalMs)`; a negative or\n * non-finite one poisons the arithmetic with NaN, which compares false\n * everywhere and so hides itself. */\nfunction isUsableInterval(intervalMs: number): boolean {\n return Number.isFinite(intervalMs) && intervalMs > 0;\n}\n\n/** Milliseconds past UTC midnight for a daily `HH:MM`, or null when the string\n * does not name a time of day. Out-of-range values are rejected, not clamped\n * and not wrapped: `\"24:00\"` would otherwise land on the NEXT day's midnight\n * and `\"-1:00\"` on an hour before it, so a typo that used to be inert would\n * start firing — at 00:00 daily, or 61 times a day. */\nfunction dailyOffsetMs(time: string): number | null {\n const offsetMs = parseTimeToMs(time);\n return Number.isFinite(offsetMs) && offsetMs >= 0 && offsetMs < ONE_DAY_MS ? offsetMs : null;\n}\n\n/** Convert to the library's schedule shape. Milliseconds are divided, never\n * rounded: rounding a sub-second interval to `intervalSec: 0` is what made\n * `nextWindowAfter` answer NaN and `new Date(NaN).toISOString()` throw. */\nexport function toLibrarySchedule(schedule: TaskSchedule): LibrarySchedule {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return { type: SCHEDULE_TYPES.interval, intervalSec: schedule.intervalMs / ONE_SECOND_MS };\n }\n return { type: SCHEDULE_TYPES.daily, time: schedule.time };\n}\n\n/** True when a scheduled window falls inside the tick ending at `nowMs`.\n * Windows are anchored to the epoch, not to the current UTC day, so an\n * interval longer than 24h keeps its real period. */\nexport function isScheduleDueAt(schedule: TaskSchedule, nowMs: number, tickMs: number): boolean {\n if (unfireableScheduleReason(schedule) !== null) return false;\n return isDueAt(toLibrarySchedule(schedule), nowMs, tickMs);\n}\n\n/** The furthest instant `Date` can represent. A window past it is finite\n * arithmetic but not a date, and `toISOString()` answers that with a throw —\n * which, inside the state writer, silently loses the whole run record. */\nconst MAX_DATE_MS = 8.64e15;\n\n/** A window as an ISO timestamp, or null when the number is not a date. */\nexport function windowToIso(windowMs: number | null): string | null {\n if (windowMs === null || !Number.isFinite(windowMs) || Math.abs(windowMs) > MAX_DATE_MS) return null;\n return new Date(windowMs).toISOString();\n}\n\n/** The most recent window at or before `nowMs`, or null when the schedule has\n * none. `nextWindowAfter` only ever looks FORWARD, so asking it from\n * `nowMs - one period` answers the previous window whenever `nowMs` sits\n * exactly on a boundary, and a daily schedule read after its minute answers\n * TOMORROW. Both schedule kinds have a fixed period, so stepping one period\n * back off the next window is exact. */\nexport function latestWindowAtOrBefore(schedule: TaskSchedule, nowMs: number): number | null {\n if (unfireableScheduleReason(schedule) !== null) return null;\n const next = nextWindowAfter(toLibrarySchedule(schedule), nowMs);\n if (next === null || !Number.isFinite(next)) return null;\n if (next <= nowMs) return next;\n const periodMs = schedule.type === SCHEDULE_TYPES.interval ? schedule.intervalMs : ONE_DAY_MS;\n return next - periodMs;\n}\n\n/** Name the field that makes a schedule unfireable, or null when it is fine.\n * Returns the offending value (not a boolean) so the caller can put it in the\n * log — a task that silently never runs leaves no evidence to start from\n * (#2765). */\nexport function unfireableScheduleReason(schedule: TaskSchedule): { field: string; value: string } | null {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return isUsableInterval(schedule.intervalMs) ? null : { field: \"intervalMs\", value: String(schedule.intervalMs) };\n }\n return dailyOffsetMs(schedule.time) !== null ? null : { field: \"time\", value: schedule.time };\n}\n","// Generic dependency-ordered cron tick engine. Host-agnostic: the only\n// host coupling (a logger) is injected via options. Schedules are either\n// fixed intervals or a daily UTC time; tasks may declare a `dependsOn`\n// edge so an ordering like \"news fetch → journal → memory extraction\"\n// runs in sequence within one tick.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { isScheduleDueAt, unfireableScheduleReason, type TaskSchedule } from \"./schedule-window.js\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\n\n// Gap between the START of each independently-due task within one tick. When\n// many tasks come due at the same minute (system journal + feed-refresh + a few\n// user tasks all at 20:00 UTC), firing every chat in the same event-loop turn\n// floods the machine; the mulmoclaude MCP broker each chat spawns then boots\n// under contention and can lose the startup race to the CLI's first tool call,\n// so that turn fails with `handlePermission not found` (#2057). Spacing the\n// launches by a second gives each broker room to connect. Far under one tick\n// (a handful of tasks spread over a few seconds), so nothing is delayed past\n// its window.\nconst DEFAULT_FIRING_STAGGER_MS = ONE_SECOND_MS;\n\n// The total stagger must stay well inside one tick, or the tail tasks start in a\n// LATER tick window and two ticks overlap. Cap all starts to this fraction of\n// `tickMs` regardless of task count or the configured gap — this keeps a debug\n// `tickMs === firingStaggerMs` (see server boot) from degenerating.\nconst MAX_STAGGER_FRACTION_OF_TICK = 0.5;\n\nconst realSleep = (delayMs: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, delayMs));\n\n// Per-task stagger step: the configured gap, shrunk so the last of `count` tasks\n// still starts within MAX_STAGGER_FRACTION_OF_TICK of the tick. 0 disables it.\nfunction staggerStepMs(staggerMs: number, tickMs: number, count: number): number {\n if (staggerMs <= 0 || count <= 1) return 0;\n const maxStepMs = (tickMs * MAX_STAGGER_FRACTION_OF_TICK) / (count - 1);\n return Math.min(staggerMs, maxStepMs);\n}\n\n/** Minimal logger the engine logs through. Absent one, runs silent. */\nexport type SchedulerLogger = MinimalLogger;\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\n// Re-exported, not redefined: the schedule shape belongs with the window\n// arithmetic that interprets it, and hosts have always imported it from here.\nexport type { TaskSchedule } from \"./schedule-window.js\";\n\nexport interface TaskRunContext {\n taskId: string;\n now: Date;\n}\n\nexport interface TaskDefinition {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n enabled?: boolean; // default: true\n /** If set, this task only fires after the named task has completed\n * successfully in the current tick cycle. Enforces ordering like\n * \"news fetch → journal → memory extraction\". */\n dependsOn?: string;\n run: (ctx: TaskRunContext) => Promise<void>;\n}\n\nexport interface TaskSummary {\n id: string;\n description?: string | undefined;\n schedule: TaskSchedule;\n dependsOn?: string | undefined;\n}\n\nexport interface ITaskManager {\n registerTask: (def: TaskDefinition) => void;\n removeTask: (taskId: string) => void;\n /** Update the schedule of an existing task. Returns false if not found. */\n updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;\n start: () => void;\n stop: () => void;\n /** Run one tick manually (for testing). */\n tick: () => Promise<void>;\n listTasks: () => TaskSummary[];\n}\n\nexport interface TaskManagerOptions {\n tickMs?: number; // default: ONE_MINUTE_MS\n now?: () => Date; // default: () => new Date()\n log?: SchedulerLogger; // default: noop\n /** Gap between the start of each independently-due task in a tick (#2057).\n * Default DEFAULT_FIRING_STAGGER_MS; 0 fires them all at once. */\n firingStaggerMs?: number;\n /** Injected so tests advance time without real timers. Default setTimeout. */\n sleep?: (delayMs: number) => Promise<void>;\n}\n\nfunction isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {\n return isScheduleDueAt(schedule, now.getTime(), tickMs);\n}\n\n/** Say out loud that this schedule can never fire. The task is still accepted —\n * `isDue` answers false forever, which is the safe outcome — but silence here\n * is what made \"the task I scheduled has never run once\" a bug with no\n * evidence anywhere to start from (#2765). Deliberately not a throw: a\n * consumer whose task has been quietly dead would get a boot crash instead. */\nfunction reportUnfireable(log: SchedulerLogger, taskId: string, schedule: TaskSchedule): void {\n const reason = unfireableScheduleReason(schedule);\n if (reason === null) return;\n log.error(`schedule has an unusable ${reason.field} — this task will never run`, { id: taskId, [reason.field]: reason.value });\n}\n\n/** Split the due tasks into those that may run immediately and those gated\n * behind a `dependsOn` edge (resolved later in the same tick cycle). */\nexport function collectDueTasks(\n currentTime: Date,\n registry: Map<string, TaskDefinition>,\n tickMs: number,\n): { independent: TaskDefinition[]; dependent: TaskDefinition[] } {\n const independent: TaskDefinition[] = [];\n const dependent: TaskDefinition[] = [];\n for (const def of registry.values()) {\n if (def.enabled === false) continue;\n if (!isDue(currentTime, def.schedule, tickMs)) continue;\n if (def.dependsOn) {\n dependent.push(def);\n } else {\n independent.push(def);\n }\n }\n return { independent, dependent };\n}\n\nasync function runAndTrack(def: TaskDefinition, currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n try {\n await def.run({ taskId: def.id, now: currentTime });\n succeeded.add(def.id);\n } catch (err) {\n log.error(\"task failed\", {\n id: def.id,\n error: String(err),\n });\n }\n}\n\nasync function runDependentChain(dependent: TaskDefinition[], currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n let remaining = [...dependent];\n let progress = true;\n while (remaining.length > 0 && progress) {\n progress = false;\n const next: TaskDefinition[] = [];\n for (const def of remaining) {\n const dep = def.dependsOn;\n if (!dep || !succeeded.has(dep)) {\n next.push(def);\n continue;\n }\n await runAndTrack(def, currentTime, succeeded, log);\n progress = true;\n }\n remaining = next;\n }\n}\n\ninterface TickConfig {\n tickMs: number;\n staggerMs: number;\n sleep: (delayMs: number) => Promise<void>;\n log: SchedulerLogger;\n}\n\nasync function runTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime, registry, cfg.tickMs);\n\n // Per-invocation set — success does not leak across tick() calls.\n const succeeded = new Set<string>();\n\n // Staggered start (#2057), still concurrent: each fires after its own capped\n // delay but the tick awaits them all, preserving the previous \"all due tasks\n // ran this tick\" contract.\n const stepMs = staggerStepMs(cfg.staggerMs, cfg.tickMs, independent.length);\n await Promise.all(\n independent.map(async (def, index) => {\n if (stepMs > 0 && index > 0) await cfg.sleep(index * stepMs);\n await runAndTrack(def, currentTime, succeeded, cfg.log);\n }),\n );\n\n await runDependentChain(dependent, currentTime, succeeded, cfg.log);\n}\n\nexport function listTaskSummaries(registry: Map<string, TaskDefinition>): TaskSummary[] {\n return [...registry.values()].map((taskDef) => ({\n id: taskDef.id,\n description: taskDef.description,\n schedule: taskDef.schedule,\n dependsOn: taskDef.dependsOn,\n }));\n}\n\n// A tick runner that skips re-entry: if the previous tick is still draining its\n// staggered starts, drop this one so a slow tick + short tickMs can never run\n// two ticks concurrently.\nfunction makeGuardedTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): () => Promise<void> {\n let ticking = false;\n return async () => {\n if (ticking) return;\n ticking = true;\n try {\n await runTick(now, registry, cfg);\n } finally {\n ticking = false;\n }\n };\n}\n\nfunction resolveTickConfig(options?: TaskManagerOptions): { tickMs: number; now: () => Date; cfg: TickConfig } {\n const tickMs = options?.tickMs ?? ONE_MINUTE_MS;\n return {\n tickMs,\n now: options?.now ?? (() => new Date()),\n cfg: {\n tickMs,\n staggerMs: options?.firingStaggerMs ?? DEFAULT_FIRING_STAGGER_MS,\n sleep: options?.sleep ?? realSleep,\n log: options?.log ?? NOOP_LOG,\n },\n };\n}\n\nexport function createTaskManager(options?: TaskManagerOptions): ITaskManager {\n const { tickMs, now, cfg } = resolveTickConfig(options);\n const { log } = cfg;\n const registry = new Map<string, TaskDefinition>();\n let timer: ReturnType<typeof setInterval> | null = null;\n const onTick = makeGuardedTick(now, registry, cfg);\n\n return {\n async tick() {\n await onTick();\n },\n\n registerTask(def: TaskDefinition) {\n if (registry.has(def.id)) {\n throw new Error(`[task-manager] Task \"${def.id}\" is already registered`);\n }\n reportUnfireable(log, def.id, def.schedule);\n registry.set(def.id, def);\n log.info(\"registered\", { id: def.id });\n },\n\n updateSchedule(taskId: string, schedule: TaskSchedule): boolean {\n const def = registry.get(taskId);\n if (!def) return false;\n // Checked here as well as at registration: an override applied at run\n // time (`applyScheduleOverride`) never passes through `registerTask`.\n reportUnfireable(log, taskId, schedule);\n def.schedule = schedule;\n log.info(\"schedule updated\", { id: taskId });\n return true;\n },\n\n removeTask(taskId: string) {\n if (registry.delete(taskId)) {\n log.info(\"removed\", { id: taskId });\n }\n },\n\n start() {\n if (timer) return;\n // `onTick` guards re-entry but not failure — it has a `finally`, no\n // `catch` — so handing it straight to setInterval let a rejected tick\n // escape as an unhandled rejection, which the host turns into a process\n // exit. The scheduler must outlive one bad tick.\n timer = setInterval(() => {\n onTick().catch((err: unknown) => log.error(\"tick failed\", { error: String(err) }));\n }, tickMs);\n log.info(\"started\", { tickMs });\n },\n\n stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n log.info(\"stopped\");\n }\n },\n\n listTasks() {\n return listTaskSummaries(registry);\n },\n };\n}\n","// Adapter that wires the pure scheduler library (@receptron/task-scheduler)\n// to a host's task-manager + workspace. Registers system tasks, runs\n// catch-up on startup, and persists execution state + logs.\n//\n// Host-agnostic: the workspace root, the atomic file writer, and the\n// logger are injected via `configureScheduler`. The host supplies its OWN\n// system tasks (journal / feeds / user-cron in MulmoClaude) to\n// `initScheduler` — the package owns no task definitions. Deliberately\n// thin: all complex scheduling logic lives in @receptron/task-scheduler.\n\nimport { existsSync } from \"node:fs\";\nimport { readFile, appendFile, mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n type TaskExecutionState,\n type TaskLogEntry,\n type CatchUpTask,\n type TaskTrigger,\n emptyState,\n computeCatchUpPlan,\n nextWindowAfter,\n loadState,\n updateAndSave,\n appendLogEntry,\n queryLog,\n TASK_RESULTS,\n TASK_TRIGGERS,\n type MISSED_RUN_POLICIES,\n type StateMap,\n type StateDeps,\n type LogDeps,\n} from \"@receptron/task-scheduler\";\nimport type { ITaskManager, TaskDefinition, SchedulerLogger } from \"./task-manager.js\";\nimport { latestWindowAtOrBefore, toLibrarySchedule, unfireableScheduleReason, windowToIso } from \"./schedule-window.js\";\nimport { errorMessage } from \"../utils/errors.js\";\n\nconst SCHEDULER_CONFIG_DIR = \"config/scheduler\";\nconst SCHEDULER_DATA_DIR = \"data/scheduler/logs\";\n\n// ── Host injection ────────────────────────────────────────────────\n\nexport interface SchedulerConfig {\n /** Absolute workspace root — state.json + logs hang off it. */\n workspaceRoot: string;\n /** Host atomic file writer (used with `uniqueTmp` for the state file). */\n writeFileAtomic: (filePath: string, content: string, opts: { uniqueTmp: boolean }) => Promise<void>;\n /** Optional logger. */\n log?: SchedulerLogger;\n}\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nlet config: SchedulerConfig | null = null;\n\n/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */\nexport function configureScheduler(injected: SchedulerConfig): void {\n config = injected;\n}\n\nfunction requireConfig(): SchedulerConfig {\n if (!config) throw new Error(\"scheduler: configureScheduler() not called\");\n return config;\n}\n\nfunction logger(): SchedulerLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n// ── Paths ─────────────────────────────────────────────────────────\n\nfunction stateFilePath(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, \"state.json\");\n}\n\nfunction logsDir(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);\n}\n\n// ── I/O deps (real filesystem) ────────────────────────────────────\n\nfunction stateDeps(): StateDeps {\n return {\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n writeFileAtomic: (filePath: string, content: string) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),\n exists: existsSync,\n };\n}\n\nconst logDeps: LogDeps = {\n appendFile: (filePath: string, content: string) => appendFile(filePath, content),\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n exists: existsSync,\n ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),\n};\n\n// ── System task registry ──────────────────────────────────────────\n\nexport interface SystemTaskDef {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: typeof MISSED_RUN_POLICIES.skip | typeof MISSED_RUN_POLICIES.runOnce | typeof MISSED_RUN_POLICIES.runAll;\n run: () => Promise<void>;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nlet stateMap: StateMap = new Map();\nconst systemTasks: SystemTaskDef[] = [];\nlet taskManagerRef: ITaskManager | null = null;\n\n/**\n * Initialize the scheduler adapter. Call once at server startup AFTER the\n * task-manager is created but BEFORE `taskManager.start()`.\n */\nexport async function initScheduler(taskManager: ITaskManager, tasks: SystemTaskDef[]): Promise<void> {\n await mkdir(path.dirname(stateFilePath()), { recursive: true });\n await mkdir(logsDir(), { recursive: true });\n\n stateMap = await loadState(stateFilePath(), stateDeps());\n systemTasks.length = 0;\n systemTasks.push(...tasks);\n taskManagerRef = taskManager;\n\n const plan = computeCatchUpPlan(catchUpTasksFor(tasks), stateMap, Date.now());\n\n for (const skip of plan.skipped) {\n logger().info(\"catch-up skipped\", { taskId: skip.taskId, windows: skip.windowCount });\n await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });\n }\n\n if (plan.runs.length > 0) {\n logger().info(\"catch-up enqueued\", { runs: plan.runs.length });\n for (const run of plan.runs) {\n const task = tasks.find((taskDef) => taskDef.id === run.taskId);\n if (!task) continue;\n await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);\n }\n }\n\n // Register with task-manager for ongoing ticks\n for (const task of tasks) {\n taskManager.registerTask({\n id: task.id,\n description: task.description,\n schedule: task.schedule,\n run: async (ctx) => {\n const windowIso = computeCurrentWindow(task, ctx.now.getTime());\n await executeAndLog(task, windowIso, TASK_TRIGGERS.scheduled);\n },\n });\n }\n\n logger().info(\"initialized\", { tasks: tasks.map((taskDef) => taskDef.id), stateEntries: stateMap.size });\n}\n\n/** The catch-up input, with the tasks that can never fire left out. Their\n * windows come back NaN rather than null, which `listMissedWindows` cannot\n * see the end of — it fills to its cap and the first `new Date(NaN)` throws\n * `RangeError`, taking every other task's catch-up down with it at startup.\n * Registration still happens, so the task-manager reports the bad schedule. */\nfunction catchUpTasksFor(tasks: SystemTaskDef[]): CatchUpTask[] {\n return tasks\n .filter((taskDef) => {\n const reason = unfireableScheduleReason(taskDef.schedule);\n if (reason === null) return true;\n logger().error(`schedule has an unusable ${reason.field} — skipping catch-up, this task will never run`, {\n taskId: taskDef.id,\n [reason.field]: reason.value,\n });\n return false;\n })\n .map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n schedule: toLibrarySchedule(taskDef.schedule),\n missedRunPolicy: taskDef.missedRunPolicy,\n enabled: true,\n }));\n}\n\n/** Apply a schedule override to a running system task. Updates the\n * in-memory task definition, the task-manager, and recalculates\n * nextScheduledAt in persisted state. */\nexport async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef[\"schedule\"]): Promise<boolean> {\n const task = systemTasks.find((taskDef) => taskDef.id === taskId);\n if (!task || !taskManagerRef) return false;\n if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;\n task.schedule = schedule;\n\n // Recalculate next window so the UI reflects the new schedule\n const nextScheduledAt = computeNextScheduledFor(task.schedule);\n await safeUpdateState(taskId, { nextScheduledAt });\n\n return true;\n}\n\n/** Query execution logs — used by API routes. */\nexport async function getSchedulerLogs(opts: { since?: string | undefined; taskId?: string | undefined; limit?: number | undefined }): Promise<TaskLogEntry[]> {\n return queryLog(logsDir(), opts, logDeps);\n}\n\n/** Get all task states — used by API routes. */\nexport function getSchedulerTasks(): {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: string;\n state: TaskExecutionState;\n}[] {\n return systemTasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n description: taskDef.description,\n schedule: taskDef.schedule,\n missedRunPolicy: taskDef.missedRunPolicy,\n state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),\n }));\n}\n\n/** Read the persisted execution state for any task id (system, user, or\n * skill). Returns an empty state when the id has never run — used by the API\n * route to attach last-run / next-run / history state to user + skill tasks\n * it lists from other sources. */\nexport function getSchedulerTaskState(taskId: string): TaskExecutionState {\n return stateMap.get(taskId) ?? emptyState(taskId);\n}\n\n/** Record a run of an EXTERNAL (skill / user) task — one registered directly\n * on the task-manager rather than through `initScheduler`. Persists a log\n * entry + updates state so these tasks get the same history / last-run /\n * next-run as system tasks. Because they are fire-and-forget (`startChat`\n * spawns an async chat), `errorMessage` reflects whether the *dispatch*\n * succeeded, not the chat's eventual outcome; `chatSessionId` links the run\n * to the spawned session. */\nexport async function recordExternalRun(params: {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errorMessage: string | null;\n chatSessionId?: string | undefined;\n}): Promise<void> {\n await persistRun({\n meta: { id: params.id, name: params.name, schedule: params.schedule },\n scheduledFor: params.scheduledFor,\n startedAt: params.startedAt,\n durationMs: params.durationMs,\n trigger: params.trigger,\n errMsg: params.errorMessage,\n chatSessionId: params.chatSessionId,\n });\n}\n\n/** Test-only: clear config + in-memory state. */\nexport function resetSchedulerForTesting(): void {\n config = null;\n stateMap = new Map();\n systemTasks.length = 0;\n taskManagerRef = null;\n}\n\n// ── Internal ──────────────────────────────────────────────────────\n\nasync function executeAndLog(task: SystemTaskDef, scheduledFor: string, trigger: TaskTrigger): Promise<void> {\n const startedAt = new Date().toISOString();\n const startMs = Date.now();\n let errMsg: string | null = null;\n try {\n await task.run();\n } catch (err) {\n errMsg = errorMessage(err);\n logger().error(\"task failed\", { taskId: task.id, error: errMsg });\n }\n const durationMs = Date.now() - startMs;\n // Persistence is best-effort — never let disk failures propagate to the\n // tick loop or abort startup catch-up.\n await persistRun({ meta: { id: task.id, name: task.name, schedule: task.schedule }, scheduledFor, startedAt, durationMs, trigger, errMsg });\n}\n\n/** The minimum a run needs to persist state + a log entry — shared by system\n * tasks (`executeAndLog`) and external skill/user runs (`recordExternalRun`). */\ninterface TaskRunMeta {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n}\n\ninterface RunRecord {\n meta: TaskRunMeta;\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errMsg: string | null;\n chatSessionId?: string | undefined;\n}\n\n/** Best-effort persistence — state and log are independent, so one failing\n * never blocks the other and neither propagates upward. */\nasync function persistRun(run: RunRecord): Promise<void> {\n await writeRunState(run);\n await writeRunLog(run);\n}\n\nasync function writeRunState(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, durationMs, errMsg } = run;\n const isSuccess = errMsg === null;\n const currentState = stateMap.get(meta.id);\n try {\n await updateAndSave(\n stateFilePath(),\n stateMap,\n meta.id,\n {\n lastRunAt: scheduledFor,\n lastRunResult: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n lastRunDurationMs: durationMs,\n lastErrorMessage: errMsg,\n consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,\n totalRuns: (currentState?.totalRuns ?? 0) + 1,\n nextScheduledAt: computeNextScheduledFor(meta.schedule),\n },\n stateDeps(),\n );\n } catch (err) {\n logger().warn(\"state persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\nasync function writeRunLog(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, startedAt, durationMs, trigger, errMsg, chatSessionId } = run;\n const isSuccess = errMsg === null;\n try {\n await appendLogEntry(\n logsDir(),\n {\n taskId: meta.id,\n taskName: meta.name,\n scheduledFor,\n startedAt,\n completedAt: new Date().toISOString(),\n result: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n durationMs,\n trigger,\n ...(errMsg !== null && { errorMessage: errMsg }),\n ...(chatSessionId !== undefined && { chatSessionId }),\n },\n logDeps,\n );\n } catch (err) {\n logger().warn(\"log persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\n/** Safe state update — swallows errors. */\nasync function safeUpdateState(taskId: string, patch: Partial<TaskExecutionState>): Promise<void> {\n try {\n await updateAndSave(stateFilePath(), stateMap, taskId, patch, stateDeps());\n } catch (err) {\n logger().warn(\"state update failed\", { taskId, error: String(err) });\n }\n}\n\n/** Compute the window boundary that the current tick belongs to. For\n * scheduled runs, this is the epoch-aligned window — not the wall-clock\n * time of execution. This keeps lastRunAt consistent with catch-up's\n * window-based accounting. `tickMs` is the tick's own clock, not\n * `Date.now()`: the two drift apart by however long the tick took to reach\n * this task, and under an injected clock they are unrelated. */\nfunction computeCurrentWindow(task: SystemTaskDef, tickMs: number): string {\n return windowToIso(latestWindowAtOrBefore(task.schedule, tickMs)) ?? new Date(tickMs).toISOString();\n}\n\nfunction computeNextScheduledFor(schedule: TaskDefinition[\"schedule\"]): string | null {\n // The engine will never fire this one, so promising a next run in the UI\n // would be a lie — and the library still hands back a window for some of\n // them (`\"24:00\"` resolves to tomorrow's midnight).\n if (unfireableScheduleReason(schedule) !== null) return null;\n // `windowToIso` is what keeps a window that is not a date (NaN from a\n // degenerate schedule, or an interval so large the next window is past the\n // end of `Date`) from throwing here and taking the whole state write down.\n return windowToIso(nextWindowAfter(toLibrarySchedule(schedule), Date.now() + 1));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/scheduler/schedule-window.ts","../../src/scheduler/task-manager.ts","../../src/scheduler/adapter.ts"],"sourcesContent":["// One definition of \"when does this schedule fire\". The host expresses\n// intervals in milliseconds and the library (@receptron/task-scheduler) in\n// seconds, so every place that answers \"is it due now?\", \"when is the next\n// run?\" or \"which windows were missed?\" has to cross that boundary. Doing it\n// twice is what let the tick engine drift away from `nextWindowAfter`: its own\n// interval check counted from UTC midnight, so anything longer than a day\n// collapsed to \"every day at 00:00\" while the UI kept showing the real,\n// epoch-anchored next run (#2937).\n\nimport { SCHEDULE_TYPES, isDueAt, nextWindowAfter, parseTimeToMs, type TaskSchedule as LibrarySchedule } from \"@receptron/task-scheduler\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_DAY_MS = 24 * 60 * 60 * ONE_SECOND_MS;\n\n/** A schedule as the host states it: intervals in milliseconds, or a daily\n * `HH:MM` UTC time. */\nexport type TaskSchedule = { type: typeof SCHEDULE_TYPES.interval; intervalMs: number } | { type: typeof SCHEDULE_TYPES.daily; time: string };\n\n/** An interval this engine can never turn into a window. Zero divides by zero\n * inside the library's `Math.ceil(afterMs / intervalMs)`; a negative or\n * non-finite one poisons the arithmetic with NaN, which compares false\n * everywhere and so hides itself. */\nfunction isUsableInterval(intervalMs: number): boolean {\n return Number.isFinite(intervalMs) && intervalMs > 0;\n}\n\n/** Milliseconds past UTC midnight for a daily `HH:MM`, or null when the string\n * does not name a time of day. Out-of-range values are rejected, not clamped\n * and not wrapped: `\"24:00\"` would otherwise land on the NEXT day's midnight\n * and `\"-1:00\"` on an hour before it, so a typo that used to be inert would\n * start firing — at 00:00 daily, or 61 times a day. */\nfunction dailyOffsetMs(time: string): number | null {\n const offsetMs = parseTimeToMs(time);\n return Number.isFinite(offsetMs) && offsetMs >= 0 && offsetMs < ONE_DAY_MS ? offsetMs : null;\n}\n\n/** Convert to the library's schedule shape. Milliseconds are divided, never\n * rounded: rounding a sub-second interval to `intervalSec: 0` is what made\n * `nextWindowAfter` answer NaN and `new Date(NaN).toISOString()` throw. */\nexport function toLibrarySchedule(schedule: TaskSchedule): LibrarySchedule {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return { type: SCHEDULE_TYPES.interval, intervalSec: schedule.intervalMs / ONE_SECOND_MS };\n }\n return { type: SCHEDULE_TYPES.daily, time: schedule.time };\n}\n\n/** True when a scheduled window falls inside the tick ending at `nowMs`.\n * Windows are anchored to the epoch, not to the current UTC day, so an\n * interval longer than 24h keeps its real period. */\nexport function isScheduleDueAt(schedule: TaskSchedule, nowMs: number, tickMs: number): boolean {\n if (unfireableScheduleReason(schedule) !== null) return false;\n return isDueAt(toLibrarySchedule(schedule), nowMs, tickMs);\n}\n\n/** The furthest instant `Date` can represent. A window past it is finite\n * arithmetic but not a date, and `toISOString()` answers that with a throw —\n * which, inside the state writer, silently loses the whole run record. */\nconst MAX_DATE_MS = 8.64e15;\n\n/** A window as an ISO timestamp, or null when the number is not a date. */\nexport function windowToIso(windowMs: number | null): string | null {\n if (windowMs === null || !Number.isFinite(windowMs) || Math.abs(windowMs) > MAX_DATE_MS) return null;\n return new Date(windowMs).toISOString();\n}\n\n/** The most recent window at or before `nowMs`, or null when the schedule has\n * none. `nextWindowAfter` only ever looks FORWARD, so asking it from\n * `nowMs - one period` answers the previous window whenever `nowMs` sits\n * exactly on a boundary, and a daily schedule read after its minute answers\n * TOMORROW. Both schedule kinds have a fixed period, so stepping one period\n * back off the next window is exact. */\nexport function latestWindowAtOrBefore(schedule: TaskSchedule, nowMs: number): number | null {\n if (unfireableScheduleReason(schedule) !== null) return null;\n const next = nextWindowAfter(toLibrarySchedule(schedule), nowMs);\n if (next === null || !Number.isFinite(next)) return null;\n if (next <= nowMs) return next;\n const periodMs = schedule.type === SCHEDULE_TYPES.interval ? schedule.intervalMs : ONE_DAY_MS;\n return next - periodMs;\n}\n\n/** Name the field that makes a schedule unfireable, or null when it is fine.\n * Returns the offending value (not a boolean) so the caller can put it in the\n * log — a task that silently never runs leaves no evidence to start from\n * (#2765). */\nexport function unfireableScheduleReason(schedule: TaskSchedule): { field: string; value: string } | null {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return isUsableInterval(schedule.intervalMs) ? null : { field: \"intervalMs\", value: String(schedule.intervalMs) };\n }\n return dailyOffsetMs(schedule.time) !== null ? null : { field: \"time\", value: schedule.time };\n}\n","// Generic dependency-ordered cron tick engine. Host-agnostic: the only\n// host coupling (a logger) is injected via options. Schedules are either\n// fixed intervals or a daily UTC time; tasks may declare a `dependsOn`\n// edge so an ordering like \"news fetch → journal → memory extraction\"\n// runs in sequence within one tick.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { isScheduleDueAt, unfireableScheduleReason, type TaskSchedule } from \"./schedule-window.js\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\n\n// Gap between the START of each independently-due task within one tick. When\n// many tasks come due at the same minute (system journal + feed-refresh + a few\n// user tasks all at 20:00 UTC), firing every chat in the same event-loop turn\n// floods the machine; the mulmoclaude MCP broker each chat spawns then boots\n// under contention and can lose the startup race to the CLI's first tool call,\n// so that turn fails with `handlePermission not found` (#2057). Spacing the\n// launches by a second gives each broker room to connect. Far under one tick\n// (a handful of tasks spread over a few seconds), so nothing is delayed past\n// its window.\nconst DEFAULT_FIRING_STAGGER_MS = ONE_SECOND_MS;\n\n// The total stagger must stay well inside one tick, or the tail tasks start in a\n// LATER tick window and two ticks overlap. Cap all starts to this fraction of\n// `tickMs` regardless of task count or the configured gap — this keeps a debug\n// `tickMs === firingStaggerMs` (see server boot) from degenerating.\nconst MAX_STAGGER_FRACTION_OF_TICK = 0.5;\n\nconst realSleep = (delayMs: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, delayMs));\n\n// Per-task stagger step: the configured gap, shrunk so the last of `count` tasks\n// still starts within MAX_STAGGER_FRACTION_OF_TICK of the tick. 0 disables it.\nfunction staggerStepMs(staggerMs: number, tickMs: number, count: number): number {\n if (staggerMs <= 0 || count <= 1) return 0;\n const maxStepMs = (tickMs * MAX_STAGGER_FRACTION_OF_TICK) / (count - 1);\n return Math.min(staggerMs, maxStepMs);\n}\n\n/** Minimal logger the engine logs through. Absent one, runs silent. */\nexport type SchedulerLogger = MinimalLogger;\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\n// Re-exported, not redefined: the schedule shape belongs with the window\n// arithmetic that interprets it, and hosts have always imported it from here.\nexport type { TaskSchedule } from \"./schedule-window.js\";\n\nexport interface TaskRunContext {\n taskId: string;\n now: Date;\n}\n\nexport interface TaskDefinition {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n enabled?: boolean; // default: true\n /** If set, this task only fires after the named task has completed\n * successfully in the current tick cycle. Enforces ordering like\n * \"news fetch → journal → memory extraction\". */\n dependsOn?: string;\n run: (ctx: TaskRunContext) => Promise<void>;\n}\n\nexport interface TaskSummary {\n id: string;\n description?: string | undefined;\n schedule: TaskSchedule;\n dependsOn?: string | undefined;\n}\n\nexport interface ITaskManager {\n registerTask: (def: TaskDefinition) => void;\n removeTask: (taskId: string) => void;\n /** Update the schedule of an existing task. Returns false if not found. */\n updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;\n start: () => void;\n stop: () => void;\n /** Run one tick manually (for testing). */\n tick: () => Promise<void>;\n listTasks: () => TaskSummary[];\n}\n\nexport interface TaskManagerOptions {\n tickMs?: number; // default: ONE_MINUTE_MS\n now?: () => Date; // default: () => new Date()\n log?: SchedulerLogger; // default: noop\n /** Gap between the start of each independently-due task in a tick (#2057).\n * Default DEFAULT_FIRING_STAGGER_MS; 0 fires them all at once. */\n firingStaggerMs?: number;\n /** Injected so tests advance time without real timers. Default setTimeout. */\n sleep?: (delayMs: number) => Promise<void>;\n}\n\nfunction isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {\n return isScheduleDueAt(schedule, now.getTime(), tickMs);\n}\n\n/** Say out loud that this schedule can never fire. The task is still accepted —\n * `isDue` answers false forever, which is the safe outcome — but silence here\n * is what made \"the task I scheduled has never run once\" a bug with no\n * evidence anywhere to start from (#2765). Deliberately not a throw: a\n * consumer whose task has been quietly dead would get a boot crash instead. */\nfunction reportUnfireable(log: SchedulerLogger, taskId: string, schedule: TaskSchedule): void {\n const reason = unfireableScheduleReason(schedule);\n if (reason === null) return;\n log.error(`schedule has an unusable ${reason.field} — this task will never run`, { id: taskId, [reason.field]: reason.value });\n}\n\n/** Split the due tasks into those that may run immediately and those gated\n * behind a `dependsOn` edge (resolved later in the same tick cycle). */\nexport function collectDueTasks(\n currentTime: Date,\n registry: Map<string, TaskDefinition>,\n tickMs: number,\n): { independent: TaskDefinition[]; dependent: TaskDefinition[] } {\n const independent: TaskDefinition[] = [];\n const dependent: TaskDefinition[] = [];\n for (const def of registry.values()) {\n if (def.enabled === false) continue;\n if (!isDue(currentTime, def.schedule, tickMs)) continue;\n if (def.dependsOn) {\n dependent.push(def);\n } else {\n independent.push(def);\n }\n }\n return { independent, dependent };\n}\n\nasync function runAndTrack(def: TaskDefinition, currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n try {\n await def.run({ taskId: def.id, now: currentTime });\n succeeded.add(def.id);\n } catch (err) {\n log.error(\"task failed\", {\n id: def.id,\n error: String(err),\n });\n }\n}\n\nasync function runDependentChain(dependent: TaskDefinition[], currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n let remaining = [...dependent];\n let progress = true;\n while (remaining.length > 0 && progress) {\n progress = false;\n const next: TaskDefinition[] = [];\n for (const def of remaining) {\n const dep = def.dependsOn;\n if (!dep || !succeeded.has(dep)) {\n next.push(def);\n continue;\n }\n await runAndTrack(def, currentTime, succeeded, log);\n progress = true;\n }\n remaining = next;\n }\n}\n\ninterface TickConfig {\n tickMs: number;\n staggerMs: number;\n sleep: (delayMs: number) => Promise<void>;\n log: SchedulerLogger;\n}\n\nasync function runTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime, registry, cfg.tickMs);\n\n // Per-invocation set — success does not leak across tick() calls.\n const succeeded = new Set<string>();\n\n // Staggered start (#2057), still concurrent: each fires after its own capped\n // delay but the tick awaits them all, preserving the previous \"all due tasks\n // ran this tick\" contract.\n const stepMs = staggerStepMs(cfg.staggerMs, cfg.tickMs, independent.length);\n await Promise.all(\n independent.map(async (def, index) => {\n if (stepMs > 0 && index > 0) await cfg.sleep(index * stepMs);\n await runAndTrack(def, currentTime, succeeded, cfg.log);\n }),\n );\n\n await runDependentChain(dependent, currentTime, succeeded, cfg.log);\n}\n\nexport function listTaskSummaries(registry: Map<string, TaskDefinition>): TaskSummary[] {\n return [...registry.values()].map((taskDef) => ({\n id: taskDef.id,\n description: taskDef.description,\n schedule: taskDef.schedule,\n dependsOn: taskDef.dependsOn,\n }));\n}\n\n// A tick runner that skips re-entry: if the previous tick is still draining its\n// staggered starts, drop this one so a slow tick + short tickMs can never run\n// two ticks concurrently.\nfunction makeGuardedTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): () => Promise<void> {\n let ticking = false;\n return async () => {\n if (ticking) return;\n ticking = true;\n try {\n await runTick(now, registry, cfg);\n } finally {\n ticking = false;\n }\n };\n}\n\nfunction resolveTickConfig(options?: TaskManagerOptions): { tickMs: number; now: () => Date; cfg: TickConfig } {\n const tickMs = options?.tickMs ?? ONE_MINUTE_MS;\n return {\n tickMs,\n now: options?.now ?? (() => new Date()),\n cfg: {\n tickMs,\n staggerMs: options?.firingStaggerMs ?? DEFAULT_FIRING_STAGGER_MS,\n sleep: options?.sleep ?? realSleep,\n log: options?.log ?? NOOP_LOG,\n },\n };\n}\n\nexport function createTaskManager(options?: TaskManagerOptions): ITaskManager {\n const { tickMs, now, cfg } = resolveTickConfig(options);\n const { log } = cfg;\n const registry = new Map<string, TaskDefinition>();\n let timer: ReturnType<typeof setInterval> | null = null;\n const onTick = makeGuardedTick(now, registry, cfg);\n\n return {\n async tick() {\n await onTick();\n },\n\n registerTask(def: TaskDefinition) {\n if (registry.has(def.id)) {\n throw new Error(`[task-manager] Task \"${def.id}\" is already registered`);\n }\n reportUnfireable(log, def.id, def.schedule);\n registry.set(def.id, def);\n log.info(\"registered\", { id: def.id });\n },\n\n updateSchedule(taskId: string, schedule: TaskSchedule): boolean {\n const def = registry.get(taskId);\n if (!def) return false;\n // Checked here as well as at registration: an override applied at run\n // time (`applyScheduleOverride`) never passes through `registerTask`.\n reportUnfireable(log, taskId, schedule);\n def.schedule = schedule;\n log.info(\"schedule updated\", { id: taskId });\n return true;\n },\n\n removeTask(taskId: string) {\n if (registry.delete(taskId)) {\n log.info(\"removed\", { id: taskId });\n }\n },\n\n start() {\n if (timer) return;\n // `onTick` guards re-entry but not failure — it has a `finally`, no\n // `catch` — so handing it straight to setInterval let a rejected tick\n // escape as an unhandled rejection, which the host turns into a process\n // exit. The scheduler must outlive one bad tick.\n timer = setInterval(() => {\n onTick().catch((err: unknown) => log.error(\"tick failed\", { error: String(err) }));\n }, tickMs);\n log.info(\"started\", { tickMs });\n },\n\n stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n log.info(\"stopped\");\n }\n },\n\n listTasks() {\n return listTaskSummaries(registry);\n },\n };\n}\n","// Adapter that wires the pure scheduler library (@receptron/task-scheduler)\n// to a host's task-manager + workspace. Registers system tasks, runs\n// catch-up on startup, and persists execution state + logs.\n//\n// Host-agnostic: the workspace root, the atomic file writer, and the\n// logger are injected via `configureScheduler`. The host supplies its OWN\n// system tasks (journal / feeds / user-cron in MulmoClaude) to\n// `initScheduler` — the package owns no task definitions. Deliberately\n// thin: all complex scheduling logic lives in @receptron/task-scheduler.\n\nimport { existsSync } from \"node:fs\";\nimport { readFile, appendFile, mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n type TaskExecutionState,\n type TaskLogEntry,\n type CatchUpTask,\n type TaskTrigger,\n emptyState,\n computeCatchUpPlan,\n nextWindowAfter,\n loadState,\n updateAndSave,\n appendLogEntry,\n queryLog,\n TASK_RESULTS,\n TASK_TRIGGERS,\n type MISSED_RUN_POLICIES,\n type StateMap,\n type StateDeps,\n type LogDeps,\n} from \"@receptron/task-scheduler\";\nimport type { ITaskManager, TaskDefinition, SchedulerLogger } from \"./task-manager.js\";\nimport { latestWindowAtOrBefore, toLibrarySchedule, unfireableScheduleReason, windowToIso } from \"./schedule-window.js\";\nimport { errorMessage } from \"../utils/errors.js\";\n\nconst SCHEDULER_CONFIG_DIR = \"config/scheduler\";\nconst SCHEDULER_DATA_DIR = \"data/scheduler/logs\";\n\n// ── Host injection ────────────────────────────────────────────────\n\nexport interface SchedulerConfig {\n /** Absolute workspace root — state.json + logs hang off it. */\n workspaceRoot: string;\n /** Host atomic file writer (used with `uniqueTmp` for the state file). */\n writeFileAtomic: (filePath: string, content: string, opts: { uniqueTmp: boolean }) => Promise<void>;\n /** Optional logger. */\n log?: SchedulerLogger;\n}\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nlet config: SchedulerConfig | null = null;\n\n/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */\nexport function configureScheduler(injected: SchedulerConfig): void {\n config = injected;\n}\n\nfunction requireConfig(): SchedulerConfig {\n if (!config) throw new Error(\"scheduler: configureScheduler() not called\");\n return config;\n}\n\nfunction logger(): SchedulerLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n// ── Paths ─────────────────────────────────────────────────────────\n\nfunction stateFilePath(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, \"state.json\");\n}\n\nfunction logsDir(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);\n}\n\n// ── I/O deps (real filesystem) ────────────────────────────────────\n\nfunction stateDeps(): StateDeps {\n return {\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n writeFileAtomic: (filePath: string, content: string) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),\n exists: existsSync,\n };\n}\n\nconst logDeps: LogDeps = {\n appendFile: (filePath: string, content: string) => appendFile(filePath, content),\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n exists: existsSync,\n ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),\n};\n\n// ── System task registry ──────────────────────────────────────────\n\nexport interface SystemTaskDef {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: typeof MISSED_RUN_POLICIES.skip | typeof MISSED_RUN_POLICIES.runOnce | typeof MISSED_RUN_POLICIES.runAll;\n run: () => Promise<void>;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nlet stateMap: StateMap = new Map();\nconst systemTasks: SystemTaskDef[] = [];\nlet taskManagerRef: ITaskManager | null = null;\n\n/**\n * Initialize the scheduler adapter. Call once at server startup AFTER the\n * task-manager is created but BEFORE `taskManager.start()`.\n */\nexport async function initScheduler(taskManager: ITaskManager, tasks: SystemTaskDef[]): Promise<void> {\n await mkdir(path.dirname(stateFilePath()), { recursive: true });\n await mkdir(logsDir(), { recursive: true });\n\n stateMap = await loadState(stateFilePath(), stateDeps());\n systemTasks.length = 0;\n systemTasks.push(...tasks);\n taskManagerRef = taskManager;\n\n const plan = computeCatchUpPlan(catchUpTasksFor(tasks), stateMap, Date.now());\n\n for (const skip of plan.skipped) {\n logger().info(\"catch-up skipped\", { taskId: skip.taskId, windows: skip.windowCount });\n await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });\n }\n\n if (plan.runs.length > 0) {\n logger().info(\"catch-up enqueued\", { runs: plan.runs.length });\n for (const run of plan.runs) {\n const task = tasks.find((taskDef) => taskDef.id === run.taskId);\n if (!task) continue;\n await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);\n }\n }\n\n // Register with task-manager for ongoing ticks\n for (const task of tasks) {\n taskManager.registerTask({\n id: task.id,\n description: task.description,\n schedule: task.schedule,\n run: async (ctx) => {\n const windowIso = computeCurrentWindow(task, ctx.now.getTime());\n await executeAndLog(task, windowIso, TASK_TRIGGERS.scheduled);\n },\n });\n }\n\n logger().info(\"initialized\", { tasks: tasks.map((taskDef) => taskDef.id), stateEntries: stateMap.size });\n}\n\n/** The catch-up input, with the tasks that can never fire left out. Their\n * windows come back NaN rather than null, which `listMissedWindows` cannot\n * see the end of — it fills to its cap and the first `new Date(NaN)` throws\n * `RangeError`, taking every other task's catch-up down with it at startup.\n * Registration still happens, so the task-manager reports the bad schedule. */\nfunction catchUpTasksFor(tasks: SystemTaskDef[]): CatchUpTask[] {\n return tasks\n .filter((taskDef) => {\n const reason = unfireableScheduleReason(taskDef.schedule);\n if (reason === null) return true;\n logger().error(`schedule has an unusable ${reason.field} — skipping catch-up, this task will never run`, {\n taskId: taskDef.id,\n [reason.field]: reason.value,\n });\n return false;\n })\n .map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n schedule: toLibrarySchedule(taskDef.schedule),\n missedRunPolicy: taskDef.missedRunPolicy,\n enabled: true,\n }));\n}\n\n/** Apply a schedule override to a running system task. Updates the\n * in-memory task definition, the task-manager, and recalculates\n * nextScheduledAt in persisted state. */\nexport async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef[\"schedule\"]): Promise<boolean> {\n const task = systemTasks.find((taskDef) => taskDef.id === taskId);\n if (!task || !taskManagerRef) return false;\n if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;\n task.schedule = schedule;\n\n // Recalculate next window so the UI reflects the new schedule\n const nextScheduledAt = computeNextScheduledFor(task.schedule);\n await safeUpdateState(taskId, { nextScheduledAt });\n\n return true;\n}\n\n/** Query execution logs — used by API routes. */\nexport async function getSchedulerLogs(opts: { since?: string | undefined; taskId?: string | undefined; limit?: number | undefined }): Promise<TaskLogEntry[]> {\n return queryLog(logsDir(), opts, logDeps);\n}\n\n/** Get all task states — used by API routes. */\nexport function getSchedulerTasks(): {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: string;\n state: TaskExecutionState;\n}[] {\n return systemTasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n description: taskDef.description,\n schedule: taskDef.schedule,\n missedRunPolicy: taskDef.missedRunPolicy,\n state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),\n }));\n}\n\n/** Read the persisted execution state for any task id (system, user, or\n * skill). Returns an empty state when the id has never run — used by the API\n * route to attach last-run / next-run / history state to user + skill tasks\n * it lists from other sources. */\nexport function getSchedulerTaskState(taskId: string): TaskExecutionState {\n return stateMap.get(taskId) ?? emptyState(taskId);\n}\n\n/** Record a run of an EXTERNAL (skill / user) task — one registered directly\n * on the task-manager rather than through `initScheduler`. Persists a log\n * entry + updates state so these tasks get the same history / last-run /\n * next-run as system tasks. Because they are fire-and-forget (`startChat`\n * spawns an async chat), `errorMessage` reflects whether the *dispatch*\n * succeeded, not the chat's eventual outcome; `chatSessionId` links the run\n * to the spawned session. */\nexport async function recordExternalRun(params: {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errorMessage: string | null;\n chatSessionId?: string | undefined;\n}): Promise<void> {\n await persistRun({\n meta: { id: params.id, name: params.name, schedule: params.schedule },\n scheduledFor: params.scheduledFor,\n startedAt: params.startedAt,\n durationMs: params.durationMs,\n trigger: params.trigger,\n errMsg: params.errorMessage,\n chatSessionId: params.chatSessionId,\n });\n}\n\n/** Test-only: clear config + in-memory state. */\nexport function resetSchedulerForTesting(): void {\n config = null;\n stateMap = new Map();\n systemTasks.length = 0;\n taskManagerRef = null;\n}\n\n// ── Internal ──────────────────────────────────────────────────────\n\nasync function executeAndLog(task: SystemTaskDef, scheduledFor: string, trigger: TaskTrigger): Promise<void> {\n const startedAt = new Date().toISOString();\n const startMs = Date.now();\n let errMsg: string | null = null;\n try {\n await task.run();\n } catch (err) {\n errMsg = errorMessage(err);\n logger().error(\"task failed\", { taskId: task.id, error: errMsg });\n }\n const durationMs = Date.now() - startMs;\n // Persistence is best-effort — never let disk failures propagate to the\n // tick loop or abort startup catch-up.\n await persistRun({ meta: { id: task.id, name: task.name, schedule: task.schedule }, scheduledFor, startedAt, durationMs, trigger, errMsg });\n}\n\n/** The minimum a run needs to persist state + a log entry — shared by system\n * tasks (`executeAndLog`) and external skill/user runs (`recordExternalRun`). */\ninterface TaskRunMeta {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n}\n\ninterface RunRecord {\n meta: TaskRunMeta;\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errMsg: string | null;\n chatSessionId?: string | undefined;\n}\n\n/** Best-effort persistence — state and log are independent, so one failing\n * never blocks the other and neither propagates upward. */\nasync function persistRun(run: RunRecord): Promise<void> {\n await writeRunState(run);\n await writeRunLog(run);\n}\n\nasync function writeRunState(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, durationMs, errMsg } = run;\n const isSuccess = errMsg === null;\n const currentState = stateMap.get(meta.id);\n try {\n await updateAndSave(\n stateFilePath(),\n stateMap,\n meta.id,\n {\n lastRunAt: scheduledFor,\n lastRunResult: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n lastRunDurationMs: durationMs,\n lastErrorMessage: errMsg,\n consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,\n totalRuns: (currentState?.totalRuns ?? 0) + 1,\n nextScheduledAt: computeNextScheduledFor(meta.schedule),\n },\n stateDeps(),\n );\n } catch (err) {\n logger().warn(\"state persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\nasync function writeRunLog(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, startedAt, durationMs, trigger, errMsg, chatSessionId } = run;\n const isSuccess = errMsg === null;\n try {\n await appendLogEntry(\n logsDir(),\n {\n taskId: meta.id,\n taskName: meta.name,\n scheduledFor,\n startedAt,\n completedAt: new Date().toISOString(),\n result: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n durationMs,\n trigger,\n ...(errMsg !== null && { errorMessage: errMsg }),\n ...(chatSessionId !== undefined && { chatSessionId }),\n },\n logDeps,\n );\n } catch (err) {\n logger().warn(\"log persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\n/** Safe state update — swallows errors. */\nasync function safeUpdateState(taskId: string, patch: Partial<TaskExecutionState>): Promise<void> {\n try {\n await updateAndSave(stateFilePath(), stateMap, taskId, patch, stateDeps());\n } catch (err) {\n logger().warn(\"state update failed\", { taskId, error: String(err) });\n }\n}\n\n/** Compute the window boundary that the current tick belongs to. For\n * scheduled runs, this is the epoch-aligned window — not the wall-clock\n * time of execution. This keeps lastRunAt consistent with catch-up's\n * window-based accounting. `tickMs` is the tick's own clock, not\n * `Date.now()`: the two drift apart by however long the tick took to reach\n * this task, and under an injected clock they are unrelated. */\nfunction computeCurrentWindow(task: SystemTaskDef, tickMs: number): string {\n return windowToIso(latestWindowAtOrBefore(task.schedule, tickMs)) ?? new Date(tickMs).toISOString();\n}\n\nfunction computeNextScheduledFor(schedule: TaskDefinition[\"schedule\"]): string | null {\n // The engine will never fire this one, so promising a next run in the UI\n // would be a lie — and the library still hands back a window for some of\n // them (`\"24:00\"` resolves to tomorrow's midnight).\n if (unfireableScheduleReason(schedule) !== null) return null;\n // `windowToIso` is what keeps a window that is not a date (NaN from a\n // degenerate schedule, or an interval so large the next window is past the\n // end of `Date`) from throwing here and taking the whole state write down.\n return windowToIso(nextWindowAfter(toLibrarySchedule(schedule), Date.now() + 1));\n}\n"],"mappings":"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"}
|
package/dist/scheduler/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { t as errorMessage } from "../dist-
|
|
1
|
+
import { t as errorMessage } from "../dist-BzoA9pDR.js";
|
|
2
|
+
import "../errors-C7TI_t_4.js";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/scheduler/schedule-window.ts","../../src/scheduler/task-manager.ts","../../src/scheduler/adapter.ts"],"sourcesContent":["// One definition of \"when does this schedule fire\". The host expresses\n// intervals in milliseconds and the library (@receptron/task-scheduler) in\n// seconds, so every place that answers \"is it due now?\", \"when is the next\n// run?\" or \"which windows were missed?\" has to cross that boundary. Doing it\n// twice is what let the tick engine drift away from `nextWindowAfter`: its own\n// interval check counted from UTC midnight, so anything longer than a day\n// collapsed to \"every day at 00:00\" while the UI kept showing the real,\n// epoch-anchored next run (#2937).\n\nimport { SCHEDULE_TYPES, isDueAt, nextWindowAfter, parseTimeToMs, type TaskSchedule as LibrarySchedule } from \"@receptron/task-scheduler\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_DAY_MS = 24 * 60 * 60 * ONE_SECOND_MS;\n\n/** A schedule as the host states it: intervals in milliseconds, or a daily\n * `HH:MM` UTC time. */\nexport type TaskSchedule = { type: typeof SCHEDULE_TYPES.interval; intervalMs: number } | { type: typeof SCHEDULE_TYPES.daily; time: string };\n\n/** An interval this engine can never turn into a window. Zero divides by zero\n * inside the library's `Math.ceil(afterMs / intervalMs)`; a negative or\n * non-finite one poisons the arithmetic with NaN, which compares false\n * everywhere and so hides itself. */\nfunction isUsableInterval(intervalMs: number): boolean {\n return Number.isFinite(intervalMs) && intervalMs > 0;\n}\n\n/** Milliseconds past UTC midnight for a daily `HH:MM`, or null when the string\n * does not name a time of day. Out-of-range values are rejected, not clamped\n * and not wrapped: `\"24:00\"` would otherwise land on the NEXT day's midnight\n * and `\"-1:00\"` on an hour before it, so a typo that used to be inert would\n * start firing — at 00:00 daily, or 61 times a day. */\nfunction dailyOffsetMs(time: string): number | null {\n const offsetMs = parseTimeToMs(time);\n return Number.isFinite(offsetMs) && offsetMs >= 0 && offsetMs < ONE_DAY_MS ? offsetMs : null;\n}\n\n/** Convert to the library's schedule shape. Milliseconds are divided, never\n * rounded: rounding a sub-second interval to `intervalSec: 0` is what made\n * `nextWindowAfter` answer NaN and `new Date(NaN).toISOString()` throw. */\nexport function toLibrarySchedule(schedule: TaskSchedule): LibrarySchedule {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return { type: SCHEDULE_TYPES.interval, intervalSec: schedule.intervalMs / ONE_SECOND_MS };\n }\n return { type: SCHEDULE_TYPES.daily, time: schedule.time };\n}\n\n/** True when a scheduled window falls inside the tick ending at `nowMs`.\n * Windows are anchored to the epoch, not to the current UTC day, so an\n * interval longer than 24h keeps its real period. */\nexport function isScheduleDueAt(schedule: TaskSchedule, nowMs: number, tickMs: number): boolean {\n if (unfireableScheduleReason(schedule) !== null) return false;\n return isDueAt(toLibrarySchedule(schedule), nowMs, tickMs);\n}\n\n/** The furthest instant `Date` can represent. A window past it is finite\n * arithmetic but not a date, and `toISOString()` answers that with a throw —\n * which, inside the state writer, silently loses the whole run record. */\nconst MAX_DATE_MS = 8.64e15;\n\n/** A window as an ISO timestamp, or null when the number is not a date. */\nexport function windowToIso(windowMs: number | null): string | null {\n if (windowMs === null || !Number.isFinite(windowMs) || Math.abs(windowMs) > MAX_DATE_MS) return null;\n return new Date(windowMs).toISOString();\n}\n\n/** The most recent window at or before `nowMs`, or null when the schedule has\n * none. `nextWindowAfter` only ever looks FORWARD, so asking it from\n * `nowMs - one period` answers the previous window whenever `nowMs` sits\n * exactly on a boundary, and a daily schedule read after its minute answers\n * TOMORROW. Both schedule kinds have a fixed period, so stepping one period\n * back off the next window is exact. */\nexport function latestWindowAtOrBefore(schedule: TaskSchedule, nowMs: number): number | null {\n if (unfireableScheduleReason(schedule) !== null) return null;\n const next = nextWindowAfter(toLibrarySchedule(schedule), nowMs);\n if (next === null || !Number.isFinite(next)) return null;\n if (next <= nowMs) return next;\n const periodMs = schedule.type === SCHEDULE_TYPES.interval ? schedule.intervalMs : ONE_DAY_MS;\n return next - periodMs;\n}\n\n/** Name the field that makes a schedule unfireable, or null when it is fine.\n * Returns the offending value (not a boolean) so the caller can put it in the\n * log — a task that silently never runs leaves no evidence to start from\n * (#2765). */\nexport function unfireableScheduleReason(schedule: TaskSchedule): { field: string; value: string } | null {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return isUsableInterval(schedule.intervalMs) ? null : { field: \"intervalMs\", value: String(schedule.intervalMs) };\n }\n return dailyOffsetMs(schedule.time) !== null ? null : { field: \"time\", value: schedule.time };\n}\n","// Generic dependency-ordered cron tick engine. Host-agnostic: the only\n// host coupling (a logger) is injected via options. Schedules are either\n// fixed intervals or a daily UTC time; tasks may declare a `dependsOn`\n// edge so an ordering like \"news fetch → journal → memory extraction\"\n// runs in sequence within one tick.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { isScheduleDueAt, unfireableScheduleReason, type TaskSchedule } from \"./schedule-window.js\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\n\n// Gap between the START of each independently-due task within one tick. When\n// many tasks come due at the same minute (system journal + feed-refresh + a few\n// user tasks all at 20:00 UTC), firing every chat in the same event-loop turn\n// floods the machine; the mulmoclaude MCP broker each chat spawns then boots\n// under contention and can lose the startup race to the CLI's first tool call,\n// so that turn fails with `handlePermission not found` (#2057). Spacing the\n// launches by a second gives each broker room to connect. Far under one tick\n// (a handful of tasks spread over a few seconds), so nothing is delayed past\n// its window.\nconst DEFAULT_FIRING_STAGGER_MS = ONE_SECOND_MS;\n\n// The total stagger must stay well inside one tick, or the tail tasks start in a\n// LATER tick window and two ticks overlap. Cap all starts to this fraction of\n// `tickMs` regardless of task count or the configured gap — this keeps a debug\n// `tickMs === firingStaggerMs` (see server boot) from degenerating.\nconst MAX_STAGGER_FRACTION_OF_TICK = 0.5;\n\nconst realSleep = (delayMs: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, delayMs));\n\n// Per-task stagger step: the configured gap, shrunk so the last of `count` tasks\n// still starts within MAX_STAGGER_FRACTION_OF_TICK of the tick. 0 disables it.\nfunction staggerStepMs(staggerMs: number, tickMs: number, count: number): number {\n if (staggerMs <= 0 || count <= 1) return 0;\n const maxStepMs = (tickMs * MAX_STAGGER_FRACTION_OF_TICK) / (count - 1);\n return Math.min(staggerMs, maxStepMs);\n}\n\n/** Minimal logger the engine logs through. Absent one, runs silent. */\nexport type SchedulerLogger = MinimalLogger;\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\n// Re-exported, not redefined: the schedule shape belongs with the window\n// arithmetic that interprets it, and hosts have always imported it from here.\nexport type { TaskSchedule } from \"./schedule-window.js\";\n\nexport interface TaskRunContext {\n taskId: string;\n now: Date;\n}\n\nexport interface TaskDefinition {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n enabled?: boolean; // default: true\n /** If set, this task only fires after the named task has completed\n * successfully in the current tick cycle. Enforces ordering like\n * \"news fetch → journal → memory extraction\". */\n dependsOn?: string;\n run: (ctx: TaskRunContext) => Promise<void>;\n}\n\nexport interface TaskSummary {\n id: string;\n description?: string | undefined;\n schedule: TaskSchedule;\n dependsOn?: string | undefined;\n}\n\nexport interface ITaskManager {\n registerTask: (def: TaskDefinition) => void;\n removeTask: (taskId: string) => void;\n /** Update the schedule of an existing task. Returns false if not found. */\n updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;\n start: () => void;\n stop: () => void;\n /** Run one tick manually (for testing). */\n tick: () => Promise<void>;\n listTasks: () => TaskSummary[];\n}\n\nexport interface TaskManagerOptions {\n tickMs?: number; // default: ONE_MINUTE_MS\n now?: () => Date; // default: () => new Date()\n log?: SchedulerLogger; // default: noop\n /** Gap between the start of each independently-due task in a tick (#2057).\n * Default DEFAULT_FIRING_STAGGER_MS; 0 fires them all at once. */\n firingStaggerMs?: number;\n /** Injected so tests advance time without real timers. Default setTimeout. */\n sleep?: (delayMs: number) => Promise<void>;\n}\n\nfunction isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {\n return isScheduleDueAt(schedule, now.getTime(), tickMs);\n}\n\n/** Say out loud that this schedule can never fire. The task is still accepted —\n * `isDue` answers false forever, which is the safe outcome — but silence here\n * is what made \"the task I scheduled has never run once\" a bug with no\n * evidence anywhere to start from (#2765). Deliberately not a throw: a\n * consumer whose task has been quietly dead would get a boot crash instead. */\nfunction reportUnfireable(log: SchedulerLogger, taskId: string, schedule: TaskSchedule): void {\n const reason = unfireableScheduleReason(schedule);\n if (reason === null) return;\n log.error(`schedule has an unusable ${reason.field} — this task will never run`, { id: taskId, [reason.field]: reason.value });\n}\n\n/** Split the due tasks into those that may run immediately and those gated\n * behind a `dependsOn` edge (resolved later in the same tick cycle). */\nexport function collectDueTasks(\n currentTime: Date,\n registry: Map<string, TaskDefinition>,\n tickMs: number,\n): { independent: TaskDefinition[]; dependent: TaskDefinition[] } {\n const independent: TaskDefinition[] = [];\n const dependent: TaskDefinition[] = [];\n for (const def of registry.values()) {\n if (def.enabled === false) continue;\n if (!isDue(currentTime, def.schedule, tickMs)) continue;\n if (def.dependsOn) {\n dependent.push(def);\n } else {\n independent.push(def);\n }\n }\n return { independent, dependent };\n}\n\nasync function runAndTrack(def: TaskDefinition, currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n try {\n await def.run({ taskId: def.id, now: currentTime });\n succeeded.add(def.id);\n } catch (err) {\n log.error(\"task failed\", {\n id: def.id,\n error: String(err),\n });\n }\n}\n\nasync function runDependentChain(dependent: TaskDefinition[], currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n let remaining = [...dependent];\n let progress = true;\n while (remaining.length > 0 && progress) {\n progress = false;\n const next: TaskDefinition[] = [];\n for (const def of remaining) {\n const dep = def.dependsOn;\n if (!dep || !succeeded.has(dep)) {\n next.push(def);\n continue;\n }\n await runAndTrack(def, currentTime, succeeded, log);\n progress = true;\n }\n remaining = next;\n }\n}\n\ninterface TickConfig {\n tickMs: number;\n staggerMs: number;\n sleep: (delayMs: number) => Promise<void>;\n log: SchedulerLogger;\n}\n\nasync function runTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime, registry, cfg.tickMs);\n\n // Per-invocation set — success does not leak across tick() calls.\n const succeeded = new Set<string>();\n\n // Staggered start (#2057), still concurrent: each fires after its own capped\n // delay but the tick awaits them all, preserving the previous \"all due tasks\n // ran this tick\" contract.\n const stepMs = staggerStepMs(cfg.staggerMs, cfg.tickMs, independent.length);\n await Promise.all(\n independent.map(async (def, index) => {\n if (stepMs > 0 && index > 0) await cfg.sleep(index * stepMs);\n await runAndTrack(def, currentTime, succeeded, cfg.log);\n }),\n );\n\n await runDependentChain(dependent, currentTime, succeeded, cfg.log);\n}\n\nexport function listTaskSummaries(registry: Map<string, TaskDefinition>): TaskSummary[] {\n return [...registry.values()].map((taskDef) => ({\n id: taskDef.id,\n description: taskDef.description,\n schedule: taskDef.schedule,\n dependsOn: taskDef.dependsOn,\n }));\n}\n\n// A tick runner that skips re-entry: if the previous tick is still draining its\n// staggered starts, drop this one so a slow tick + short tickMs can never run\n// two ticks concurrently.\nfunction makeGuardedTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): () => Promise<void> {\n let ticking = false;\n return async () => {\n if (ticking) return;\n ticking = true;\n try {\n await runTick(now, registry, cfg);\n } finally {\n ticking = false;\n }\n };\n}\n\nfunction resolveTickConfig(options?: TaskManagerOptions): { tickMs: number; now: () => Date; cfg: TickConfig } {\n const tickMs = options?.tickMs ?? ONE_MINUTE_MS;\n return {\n tickMs,\n now: options?.now ?? (() => new Date()),\n cfg: {\n tickMs,\n staggerMs: options?.firingStaggerMs ?? DEFAULT_FIRING_STAGGER_MS,\n sleep: options?.sleep ?? realSleep,\n log: options?.log ?? NOOP_LOG,\n },\n };\n}\n\nexport function createTaskManager(options?: TaskManagerOptions): ITaskManager {\n const { tickMs, now, cfg } = resolveTickConfig(options);\n const { log } = cfg;\n const registry = new Map<string, TaskDefinition>();\n let timer: ReturnType<typeof setInterval> | null = null;\n const onTick = makeGuardedTick(now, registry, cfg);\n\n return {\n async tick() {\n await onTick();\n },\n\n registerTask(def: TaskDefinition) {\n if (registry.has(def.id)) {\n throw new Error(`[task-manager] Task \"${def.id}\" is already registered`);\n }\n reportUnfireable(log, def.id, def.schedule);\n registry.set(def.id, def);\n log.info(\"registered\", { id: def.id });\n },\n\n updateSchedule(taskId: string, schedule: TaskSchedule): boolean {\n const def = registry.get(taskId);\n if (!def) return false;\n // Checked here as well as at registration: an override applied at run\n // time (`applyScheduleOverride`) never passes through `registerTask`.\n reportUnfireable(log, taskId, schedule);\n def.schedule = schedule;\n log.info(\"schedule updated\", { id: taskId });\n return true;\n },\n\n removeTask(taskId: string) {\n if (registry.delete(taskId)) {\n log.info(\"removed\", { id: taskId });\n }\n },\n\n start() {\n if (timer) return;\n // `onTick` guards re-entry but not failure — it has a `finally`, no\n // `catch` — so handing it straight to setInterval let a rejected tick\n // escape as an unhandled rejection, which the host turns into a process\n // exit. The scheduler must outlive one bad tick.\n timer = setInterval(() => {\n onTick().catch((err: unknown) => log.error(\"tick failed\", { error: String(err) }));\n }, tickMs);\n log.info(\"started\", { tickMs });\n },\n\n stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n log.info(\"stopped\");\n }\n },\n\n listTasks() {\n return listTaskSummaries(registry);\n },\n };\n}\n","// Adapter that wires the pure scheduler library (@receptron/task-scheduler)\n// to a host's task-manager + workspace. Registers system tasks, runs\n// catch-up on startup, and persists execution state + logs.\n//\n// Host-agnostic: the workspace root, the atomic file writer, and the\n// logger are injected via `configureScheduler`. The host supplies its OWN\n// system tasks (journal / feeds / user-cron in MulmoClaude) to\n// `initScheduler` — the package owns no task definitions. Deliberately\n// thin: all complex scheduling logic lives in @receptron/task-scheduler.\n\nimport { existsSync } from \"node:fs\";\nimport { readFile, appendFile, mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n type TaskExecutionState,\n type TaskLogEntry,\n type CatchUpTask,\n type TaskTrigger,\n emptyState,\n computeCatchUpPlan,\n nextWindowAfter,\n loadState,\n updateAndSave,\n appendLogEntry,\n queryLog,\n TASK_RESULTS,\n TASK_TRIGGERS,\n type MISSED_RUN_POLICIES,\n type StateMap,\n type StateDeps,\n type LogDeps,\n} from \"@receptron/task-scheduler\";\nimport type { ITaskManager, TaskDefinition, SchedulerLogger } from \"./task-manager.js\";\nimport { latestWindowAtOrBefore, toLibrarySchedule, unfireableScheduleReason, windowToIso } from \"./schedule-window.js\";\nimport { errorMessage } from \"../utils/errors.js\";\n\nconst SCHEDULER_CONFIG_DIR = \"config/scheduler\";\nconst SCHEDULER_DATA_DIR = \"data/scheduler/logs\";\n\n// ── Host injection ────────────────────────────────────────────────\n\nexport interface SchedulerConfig {\n /** Absolute workspace root — state.json + logs hang off it. */\n workspaceRoot: string;\n /** Host atomic file writer (used with `uniqueTmp` for the state file). */\n writeFileAtomic: (filePath: string, content: string, opts: { uniqueTmp: boolean }) => Promise<void>;\n /** Optional logger. */\n log?: SchedulerLogger;\n}\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nlet config: SchedulerConfig | null = null;\n\n/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */\nexport function configureScheduler(injected: SchedulerConfig): void {\n config = injected;\n}\n\nfunction requireConfig(): SchedulerConfig {\n if (!config) throw new Error(\"scheduler: configureScheduler() not called\");\n return config;\n}\n\nfunction logger(): SchedulerLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n// ── Paths ─────────────────────────────────────────────────────────\n\nfunction stateFilePath(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, \"state.json\");\n}\n\nfunction logsDir(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);\n}\n\n// ── I/O deps (real filesystem) ────────────────────────────────────\n\nfunction stateDeps(): StateDeps {\n return {\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n writeFileAtomic: (filePath: string, content: string) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),\n exists: existsSync,\n };\n}\n\nconst logDeps: LogDeps = {\n appendFile: (filePath: string, content: string) => appendFile(filePath, content),\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n exists: existsSync,\n ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),\n};\n\n// ── System task registry ──────────────────────────────────────────\n\nexport interface SystemTaskDef {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: typeof MISSED_RUN_POLICIES.skip | typeof MISSED_RUN_POLICIES.runOnce | typeof MISSED_RUN_POLICIES.runAll;\n run: () => Promise<void>;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nlet stateMap: StateMap = new Map();\nconst systemTasks: SystemTaskDef[] = [];\nlet taskManagerRef: ITaskManager | null = null;\n\n/**\n * Initialize the scheduler adapter. Call once at server startup AFTER the\n * task-manager is created but BEFORE `taskManager.start()`.\n */\nexport async function initScheduler(taskManager: ITaskManager, tasks: SystemTaskDef[]): Promise<void> {\n await mkdir(path.dirname(stateFilePath()), { recursive: true });\n await mkdir(logsDir(), { recursive: true });\n\n stateMap = await loadState(stateFilePath(), stateDeps());\n systemTasks.length = 0;\n systemTasks.push(...tasks);\n taskManagerRef = taskManager;\n\n const plan = computeCatchUpPlan(catchUpTasksFor(tasks), stateMap, Date.now());\n\n for (const skip of plan.skipped) {\n logger().info(\"catch-up skipped\", { taskId: skip.taskId, windows: skip.windowCount });\n await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });\n }\n\n if (plan.runs.length > 0) {\n logger().info(\"catch-up enqueued\", { runs: plan.runs.length });\n for (const run of plan.runs) {\n const task = tasks.find((taskDef) => taskDef.id === run.taskId);\n if (!task) continue;\n await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);\n }\n }\n\n // Register with task-manager for ongoing ticks\n for (const task of tasks) {\n taskManager.registerTask({\n id: task.id,\n description: task.description,\n schedule: task.schedule,\n run: async (ctx) => {\n const windowIso = computeCurrentWindow(task, ctx.now.getTime());\n await executeAndLog(task, windowIso, TASK_TRIGGERS.scheduled);\n },\n });\n }\n\n logger().info(\"initialized\", { tasks: tasks.map((taskDef) => taskDef.id), stateEntries: stateMap.size });\n}\n\n/** The catch-up input, with the tasks that can never fire left out. Their\n * windows come back NaN rather than null, which `listMissedWindows` cannot\n * see the end of — it fills to its cap and the first `new Date(NaN)` throws\n * `RangeError`, taking every other task's catch-up down with it at startup.\n * Registration still happens, so the task-manager reports the bad schedule. */\nfunction catchUpTasksFor(tasks: SystemTaskDef[]): CatchUpTask[] {\n return tasks\n .filter((taskDef) => {\n const reason = unfireableScheduleReason(taskDef.schedule);\n if (reason === null) return true;\n logger().error(`schedule has an unusable ${reason.field} — skipping catch-up, this task will never run`, {\n taskId: taskDef.id,\n [reason.field]: reason.value,\n });\n return false;\n })\n .map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n schedule: toLibrarySchedule(taskDef.schedule),\n missedRunPolicy: taskDef.missedRunPolicy,\n enabled: true,\n }));\n}\n\n/** Apply a schedule override to a running system task. Updates the\n * in-memory task definition, the task-manager, and recalculates\n * nextScheduledAt in persisted state. */\nexport async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef[\"schedule\"]): Promise<boolean> {\n const task = systemTasks.find((taskDef) => taskDef.id === taskId);\n if (!task || !taskManagerRef) return false;\n if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;\n task.schedule = schedule;\n\n // Recalculate next window so the UI reflects the new schedule\n const nextScheduledAt = computeNextScheduledFor(task.schedule);\n await safeUpdateState(taskId, { nextScheduledAt });\n\n return true;\n}\n\n/** Query execution logs — used by API routes. */\nexport async function getSchedulerLogs(opts: { since?: string | undefined; taskId?: string | undefined; limit?: number | undefined }): Promise<TaskLogEntry[]> {\n return queryLog(logsDir(), opts, logDeps);\n}\n\n/** Get all task states — used by API routes. */\nexport function getSchedulerTasks(): {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: string;\n state: TaskExecutionState;\n}[] {\n return systemTasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n description: taskDef.description,\n schedule: taskDef.schedule,\n missedRunPolicy: taskDef.missedRunPolicy,\n state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),\n }));\n}\n\n/** Read the persisted execution state for any task id (system, user, or\n * skill). Returns an empty state when the id has never run — used by the API\n * route to attach last-run / next-run / history state to user + skill tasks\n * it lists from other sources. */\nexport function getSchedulerTaskState(taskId: string): TaskExecutionState {\n return stateMap.get(taskId) ?? emptyState(taskId);\n}\n\n/** Record a run of an EXTERNAL (skill / user) task — one registered directly\n * on the task-manager rather than through `initScheduler`. Persists a log\n * entry + updates state so these tasks get the same history / last-run /\n * next-run as system tasks. Because they are fire-and-forget (`startChat`\n * spawns an async chat), `errorMessage` reflects whether the *dispatch*\n * succeeded, not the chat's eventual outcome; `chatSessionId` links the run\n * to the spawned session. */\nexport async function recordExternalRun(params: {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errorMessage: string | null;\n chatSessionId?: string | undefined;\n}): Promise<void> {\n await persistRun({\n meta: { id: params.id, name: params.name, schedule: params.schedule },\n scheduledFor: params.scheduledFor,\n startedAt: params.startedAt,\n durationMs: params.durationMs,\n trigger: params.trigger,\n errMsg: params.errorMessage,\n chatSessionId: params.chatSessionId,\n });\n}\n\n/** Test-only: clear config + in-memory state. */\nexport function resetSchedulerForTesting(): void {\n config = null;\n stateMap = new Map();\n systemTasks.length = 0;\n taskManagerRef = null;\n}\n\n// ── Internal ──────────────────────────────────────────────────────\n\nasync function executeAndLog(task: SystemTaskDef, scheduledFor: string, trigger: TaskTrigger): Promise<void> {\n const startedAt = new Date().toISOString();\n const startMs = Date.now();\n let errMsg: string | null = null;\n try {\n await task.run();\n } catch (err) {\n errMsg = errorMessage(err);\n logger().error(\"task failed\", { taskId: task.id, error: errMsg });\n }\n const durationMs = Date.now() - startMs;\n // Persistence is best-effort — never let disk failures propagate to the\n // tick loop or abort startup catch-up.\n await persistRun({ meta: { id: task.id, name: task.name, schedule: task.schedule }, scheduledFor, startedAt, durationMs, trigger, errMsg });\n}\n\n/** The minimum a run needs to persist state + a log entry — shared by system\n * tasks (`executeAndLog`) and external skill/user runs (`recordExternalRun`). */\ninterface TaskRunMeta {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n}\n\ninterface RunRecord {\n meta: TaskRunMeta;\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errMsg: string | null;\n chatSessionId?: string | undefined;\n}\n\n/** Best-effort persistence — state and log are independent, so one failing\n * never blocks the other and neither propagates upward. */\nasync function persistRun(run: RunRecord): Promise<void> {\n await writeRunState(run);\n await writeRunLog(run);\n}\n\nasync function writeRunState(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, durationMs, errMsg } = run;\n const isSuccess = errMsg === null;\n const currentState = stateMap.get(meta.id);\n try {\n await updateAndSave(\n stateFilePath(),\n stateMap,\n meta.id,\n {\n lastRunAt: scheduledFor,\n lastRunResult: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n lastRunDurationMs: durationMs,\n lastErrorMessage: errMsg,\n consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,\n totalRuns: (currentState?.totalRuns ?? 0) + 1,\n nextScheduledAt: computeNextScheduledFor(meta.schedule),\n },\n stateDeps(),\n );\n } catch (err) {\n logger().warn(\"state persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\nasync function writeRunLog(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, startedAt, durationMs, trigger, errMsg, chatSessionId } = run;\n const isSuccess = errMsg === null;\n try {\n await appendLogEntry(\n logsDir(),\n {\n taskId: meta.id,\n taskName: meta.name,\n scheduledFor,\n startedAt,\n completedAt: new Date().toISOString(),\n result: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n durationMs,\n trigger,\n ...(errMsg !== null && { errorMessage: errMsg }),\n ...(chatSessionId !== undefined && { chatSessionId }),\n },\n logDeps,\n );\n } catch (err) {\n logger().warn(\"log persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\n/** Safe state update — swallows errors. */\nasync function safeUpdateState(taskId: string, patch: Partial<TaskExecutionState>): Promise<void> {\n try {\n await updateAndSave(stateFilePath(), stateMap, taskId, patch, stateDeps());\n } catch (err) {\n logger().warn(\"state update failed\", { taskId, error: String(err) });\n }\n}\n\n/** Compute the window boundary that the current tick belongs to. For\n * scheduled runs, this is the epoch-aligned window — not the wall-clock\n * time of execution. This keeps lastRunAt consistent with catch-up's\n * window-based accounting. `tickMs` is the tick's own clock, not\n * `Date.now()`: the two drift apart by however long the tick took to reach\n * this task, and under an injected clock they are unrelated. */\nfunction computeCurrentWindow(task: SystemTaskDef, tickMs: number): string {\n return windowToIso(latestWindowAtOrBefore(task.schedule, tickMs)) ?? new Date(tickMs).toISOString();\n}\n\nfunction computeNextScheduledFor(schedule: TaskDefinition[\"schedule\"]): string | null {\n // The engine will never fire this one, so promising a next run in the UI\n // would be a lie — and the library still hands back a window for some of\n // them (`\"24:00\"` resolves to tomorrow's midnight).\n if (unfireableScheduleReason(schedule) !== null) return null;\n // `windowToIso` is what keeps a window that is not a date (NaN from a\n // degenerate schedule, or an interval so large the next window is past the\n // end of `Date`) from throwing here and taking the whole state write down.\n return windowToIso(nextWindowAfter(toLibrarySchedule(schedule), Date.now() + 1));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/scheduler/schedule-window.ts","../../src/scheduler/task-manager.ts","../../src/scheduler/adapter.ts"],"sourcesContent":["// One definition of \"when does this schedule fire\". The host expresses\n// intervals in milliseconds and the library (@receptron/task-scheduler) in\n// seconds, so every place that answers \"is it due now?\", \"when is the next\n// run?\" or \"which windows were missed?\" has to cross that boundary. Doing it\n// twice is what let the tick engine drift away from `nextWindowAfter`: its own\n// interval check counted from UTC midnight, so anything longer than a day\n// collapsed to \"every day at 00:00\" while the UI kept showing the real,\n// epoch-anchored next run (#2937).\n\nimport { SCHEDULE_TYPES, isDueAt, nextWindowAfter, parseTimeToMs, type TaskSchedule as LibrarySchedule } from \"@receptron/task-scheduler\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_DAY_MS = 24 * 60 * 60 * ONE_SECOND_MS;\n\n/** A schedule as the host states it: intervals in milliseconds, or a daily\n * `HH:MM` UTC time. */\nexport type TaskSchedule = { type: typeof SCHEDULE_TYPES.interval; intervalMs: number } | { type: typeof SCHEDULE_TYPES.daily; time: string };\n\n/** An interval this engine can never turn into a window. Zero divides by zero\n * inside the library's `Math.ceil(afterMs / intervalMs)`; a negative or\n * non-finite one poisons the arithmetic with NaN, which compares false\n * everywhere and so hides itself. */\nfunction isUsableInterval(intervalMs: number): boolean {\n return Number.isFinite(intervalMs) && intervalMs > 0;\n}\n\n/** Milliseconds past UTC midnight for a daily `HH:MM`, or null when the string\n * does not name a time of day. Out-of-range values are rejected, not clamped\n * and not wrapped: `\"24:00\"` would otherwise land on the NEXT day's midnight\n * and `\"-1:00\"` on an hour before it, so a typo that used to be inert would\n * start firing — at 00:00 daily, or 61 times a day. */\nfunction dailyOffsetMs(time: string): number | null {\n const offsetMs = parseTimeToMs(time);\n return Number.isFinite(offsetMs) && offsetMs >= 0 && offsetMs < ONE_DAY_MS ? offsetMs : null;\n}\n\n/** Convert to the library's schedule shape. Milliseconds are divided, never\n * rounded: rounding a sub-second interval to `intervalSec: 0` is what made\n * `nextWindowAfter` answer NaN and `new Date(NaN).toISOString()` throw. */\nexport function toLibrarySchedule(schedule: TaskSchedule): LibrarySchedule {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return { type: SCHEDULE_TYPES.interval, intervalSec: schedule.intervalMs / ONE_SECOND_MS };\n }\n return { type: SCHEDULE_TYPES.daily, time: schedule.time };\n}\n\n/** True when a scheduled window falls inside the tick ending at `nowMs`.\n * Windows are anchored to the epoch, not to the current UTC day, so an\n * interval longer than 24h keeps its real period. */\nexport function isScheduleDueAt(schedule: TaskSchedule, nowMs: number, tickMs: number): boolean {\n if (unfireableScheduleReason(schedule) !== null) return false;\n return isDueAt(toLibrarySchedule(schedule), nowMs, tickMs);\n}\n\n/** The furthest instant `Date` can represent. A window past it is finite\n * arithmetic but not a date, and `toISOString()` answers that with a throw —\n * which, inside the state writer, silently loses the whole run record. */\nconst MAX_DATE_MS = 8.64e15;\n\n/** A window as an ISO timestamp, or null when the number is not a date. */\nexport function windowToIso(windowMs: number | null): string | null {\n if (windowMs === null || !Number.isFinite(windowMs) || Math.abs(windowMs) > MAX_DATE_MS) return null;\n return new Date(windowMs).toISOString();\n}\n\n/** The most recent window at or before `nowMs`, or null when the schedule has\n * none. `nextWindowAfter` only ever looks FORWARD, so asking it from\n * `nowMs - one period` answers the previous window whenever `nowMs` sits\n * exactly on a boundary, and a daily schedule read after its minute answers\n * TOMORROW. Both schedule kinds have a fixed period, so stepping one period\n * back off the next window is exact. */\nexport function latestWindowAtOrBefore(schedule: TaskSchedule, nowMs: number): number | null {\n if (unfireableScheduleReason(schedule) !== null) return null;\n const next = nextWindowAfter(toLibrarySchedule(schedule), nowMs);\n if (next === null || !Number.isFinite(next)) return null;\n if (next <= nowMs) return next;\n const periodMs = schedule.type === SCHEDULE_TYPES.interval ? schedule.intervalMs : ONE_DAY_MS;\n return next - periodMs;\n}\n\n/** Name the field that makes a schedule unfireable, or null when it is fine.\n * Returns the offending value (not a boolean) so the caller can put it in the\n * log — a task that silently never runs leaves no evidence to start from\n * (#2765). */\nexport function unfireableScheduleReason(schedule: TaskSchedule): { field: string; value: string } | null {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return isUsableInterval(schedule.intervalMs) ? null : { field: \"intervalMs\", value: String(schedule.intervalMs) };\n }\n return dailyOffsetMs(schedule.time) !== null ? null : { field: \"time\", value: schedule.time };\n}\n","// Generic dependency-ordered cron tick engine. Host-agnostic: the only\n// host coupling (a logger) is injected via options. Schedules are either\n// fixed intervals or a daily UTC time; tasks may declare a `dependsOn`\n// edge so an ordering like \"news fetch → journal → memory extraction\"\n// runs in sequence within one tick.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { isScheduleDueAt, unfireableScheduleReason, type TaskSchedule } from \"./schedule-window.js\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\n\n// Gap between the START of each independently-due task within one tick. When\n// many tasks come due at the same minute (system journal + feed-refresh + a few\n// user tasks all at 20:00 UTC), firing every chat in the same event-loop turn\n// floods the machine; the mulmoclaude MCP broker each chat spawns then boots\n// under contention and can lose the startup race to the CLI's first tool call,\n// so that turn fails with `handlePermission not found` (#2057). Spacing the\n// launches by a second gives each broker room to connect. Far under one tick\n// (a handful of tasks spread over a few seconds), so nothing is delayed past\n// its window.\nconst DEFAULT_FIRING_STAGGER_MS = ONE_SECOND_MS;\n\n// The total stagger must stay well inside one tick, or the tail tasks start in a\n// LATER tick window and two ticks overlap. Cap all starts to this fraction of\n// `tickMs` regardless of task count or the configured gap — this keeps a debug\n// `tickMs === firingStaggerMs` (see server boot) from degenerating.\nconst MAX_STAGGER_FRACTION_OF_TICK = 0.5;\n\nconst realSleep = (delayMs: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, delayMs));\n\n// Per-task stagger step: the configured gap, shrunk so the last of `count` tasks\n// still starts within MAX_STAGGER_FRACTION_OF_TICK of the tick. 0 disables it.\nfunction staggerStepMs(staggerMs: number, tickMs: number, count: number): number {\n if (staggerMs <= 0 || count <= 1) return 0;\n const maxStepMs = (tickMs * MAX_STAGGER_FRACTION_OF_TICK) / (count - 1);\n return Math.min(staggerMs, maxStepMs);\n}\n\n/** Minimal logger the engine logs through. Absent one, runs silent. */\nexport type SchedulerLogger = MinimalLogger;\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\n// Re-exported, not redefined: the schedule shape belongs with the window\n// arithmetic that interprets it, and hosts have always imported it from here.\nexport type { TaskSchedule } from \"./schedule-window.js\";\n\nexport interface TaskRunContext {\n taskId: string;\n now: Date;\n}\n\nexport interface TaskDefinition {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n enabled?: boolean; // default: true\n /** If set, this task only fires after the named task has completed\n * successfully in the current tick cycle. Enforces ordering like\n * \"news fetch → journal → memory extraction\". */\n dependsOn?: string;\n run: (ctx: TaskRunContext) => Promise<void>;\n}\n\nexport interface TaskSummary {\n id: string;\n description?: string | undefined;\n schedule: TaskSchedule;\n dependsOn?: string | undefined;\n}\n\nexport interface ITaskManager {\n registerTask: (def: TaskDefinition) => void;\n removeTask: (taskId: string) => void;\n /** Update the schedule of an existing task. Returns false if not found. */\n updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;\n start: () => void;\n stop: () => void;\n /** Run one tick manually (for testing). */\n tick: () => Promise<void>;\n listTasks: () => TaskSummary[];\n}\n\nexport interface TaskManagerOptions {\n tickMs?: number; // default: ONE_MINUTE_MS\n now?: () => Date; // default: () => new Date()\n log?: SchedulerLogger; // default: noop\n /** Gap between the start of each independently-due task in a tick (#2057).\n * Default DEFAULT_FIRING_STAGGER_MS; 0 fires them all at once. */\n firingStaggerMs?: number;\n /** Injected so tests advance time without real timers. Default setTimeout. */\n sleep?: (delayMs: number) => Promise<void>;\n}\n\nfunction isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {\n return isScheduleDueAt(schedule, now.getTime(), tickMs);\n}\n\n/** Say out loud that this schedule can never fire. The task is still accepted —\n * `isDue` answers false forever, which is the safe outcome — but silence here\n * is what made \"the task I scheduled has never run once\" a bug with no\n * evidence anywhere to start from (#2765). Deliberately not a throw: a\n * consumer whose task has been quietly dead would get a boot crash instead. */\nfunction reportUnfireable(log: SchedulerLogger, taskId: string, schedule: TaskSchedule): void {\n const reason = unfireableScheduleReason(schedule);\n if (reason === null) return;\n log.error(`schedule has an unusable ${reason.field} — this task will never run`, { id: taskId, [reason.field]: reason.value });\n}\n\n/** Split the due tasks into those that may run immediately and those gated\n * behind a `dependsOn` edge (resolved later in the same tick cycle). */\nexport function collectDueTasks(\n currentTime: Date,\n registry: Map<string, TaskDefinition>,\n tickMs: number,\n): { independent: TaskDefinition[]; dependent: TaskDefinition[] } {\n const independent: TaskDefinition[] = [];\n const dependent: TaskDefinition[] = [];\n for (const def of registry.values()) {\n if (def.enabled === false) continue;\n if (!isDue(currentTime, def.schedule, tickMs)) continue;\n if (def.dependsOn) {\n dependent.push(def);\n } else {\n independent.push(def);\n }\n }\n return { independent, dependent };\n}\n\nasync function runAndTrack(def: TaskDefinition, currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n try {\n await def.run({ taskId: def.id, now: currentTime });\n succeeded.add(def.id);\n } catch (err) {\n log.error(\"task failed\", {\n id: def.id,\n error: String(err),\n });\n }\n}\n\nasync function runDependentChain(dependent: TaskDefinition[], currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n let remaining = [...dependent];\n let progress = true;\n while (remaining.length > 0 && progress) {\n progress = false;\n const next: TaskDefinition[] = [];\n for (const def of remaining) {\n const dep = def.dependsOn;\n if (!dep || !succeeded.has(dep)) {\n next.push(def);\n continue;\n }\n await runAndTrack(def, currentTime, succeeded, log);\n progress = true;\n }\n remaining = next;\n }\n}\n\ninterface TickConfig {\n tickMs: number;\n staggerMs: number;\n sleep: (delayMs: number) => Promise<void>;\n log: SchedulerLogger;\n}\n\nasync function runTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime, registry, cfg.tickMs);\n\n // Per-invocation set — success does not leak across tick() calls.\n const succeeded = new Set<string>();\n\n // Staggered start (#2057), still concurrent: each fires after its own capped\n // delay but the tick awaits them all, preserving the previous \"all due tasks\n // ran this tick\" contract.\n const stepMs = staggerStepMs(cfg.staggerMs, cfg.tickMs, independent.length);\n await Promise.all(\n independent.map(async (def, index) => {\n if (stepMs > 0 && index > 0) await cfg.sleep(index * stepMs);\n await runAndTrack(def, currentTime, succeeded, cfg.log);\n }),\n );\n\n await runDependentChain(dependent, currentTime, succeeded, cfg.log);\n}\n\nexport function listTaskSummaries(registry: Map<string, TaskDefinition>): TaskSummary[] {\n return [...registry.values()].map((taskDef) => ({\n id: taskDef.id,\n description: taskDef.description,\n schedule: taskDef.schedule,\n dependsOn: taskDef.dependsOn,\n }));\n}\n\n// A tick runner that skips re-entry: if the previous tick is still draining its\n// staggered starts, drop this one so a slow tick + short tickMs can never run\n// two ticks concurrently.\nfunction makeGuardedTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): () => Promise<void> {\n let ticking = false;\n return async () => {\n if (ticking) return;\n ticking = true;\n try {\n await runTick(now, registry, cfg);\n } finally {\n ticking = false;\n }\n };\n}\n\nfunction resolveTickConfig(options?: TaskManagerOptions): { tickMs: number; now: () => Date; cfg: TickConfig } {\n const tickMs = options?.tickMs ?? ONE_MINUTE_MS;\n return {\n tickMs,\n now: options?.now ?? (() => new Date()),\n cfg: {\n tickMs,\n staggerMs: options?.firingStaggerMs ?? DEFAULT_FIRING_STAGGER_MS,\n sleep: options?.sleep ?? realSleep,\n log: options?.log ?? NOOP_LOG,\n },\n };\n}\n\nexport function createTaskManager(options?: TaskManagerOptions): ITaskManager {\n const { tickMs, now, cfg } = resolveTickConfig(options);\n const { log } = cfg;\n const registry = new Map<string, TaskDefinition>();\n let timer: ReturnType<typeof setInterval> | null = null;\n const onTick = makeGuardedTick(now, registry, cfg);\n\n return {\n async tick() {\n await onTick();\n },\n\n registerTask(def: TaskDefinition) {\n if (registry.has(def.id)) {\n throw new Error(`[task-manager] Task \"${def.id}\" is already registered`);\n }\n reportUnfireable(log, def.id, def.schedule);\n registry.set(def.id, def);\n log.info(\"registered\", { id: def.id });\n },\n\n updateSchedule(taskId: string, schedule: TaskSchedule): boolean {\n const def = registry.get(taskId);\n if (!def) return false;\n // Checked here as well as at registration: an override applied at run\n // time (`applyScheduleOverride`) never passes through `registerTask`.\n reportUnfireable(log, taskId, schedule);\n def.schedule = schedule;\n log.info(\"schedule updated\", { id: taskId });\n return true;\n },\n\n removeTask(taskId: string) {\n if (registry.delete(taskId)) {\n log.info(\"removed\", { id: taskId });\n }\n },\n\n start() {\n if (timer) return;\n // `onTick` guards re-entry but not failure — it has a `finally`, no\n // `catch` — so handing it straight to setInterval let a rejected tick\n // escape as an unhandled rejection, which the host turns into a process\n // exit. The scheduler must outlive one bad tick.\n timer = setInterval(() => {\n onTick().catch((err: unknown) => log.error(\"tick failed\", { error: String(err) }));\n }, tickMs);\n log.info(\"started\", { tickMs });\n },\n\n stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n log.info(\"stopped\");\n }\n },\n\n listTasks() {\n return listTaskSummaries(registry);\n },\n };\n}\n","// Adapter that wires the pure scheduler library (@receptron/task-scheduler)\n// to a host's task-manager + workspace. Registers system tasks, runs\n// catch-up on startup, and persists execution state + logs.\n//\n// Host-agnostic: the workspace root, the atomic file writer, and the\n// logger are injected via `configureScheduler`. The host supplies its OWN\n// system tasks (journal / feeds / user-cron in MulmoClaude) to\n// `initScheduler` — the package owns no task definitions. Deliberately\n// thin: all complex scheduling logic lives in @receptron/task-scheduler.\n\nimport { existsSync } from \"node:fs\";\nimport { readFile, appendFile, mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n type TaskExecutionState,\n type TaskLogEntry,\n type CatchUpTask,\n type TaskTrigger,\n emptyState,\n computeCatchUpPlan,\n nextWindowAfter,\n loadState,\n updateAndSave,\n appendLogEntry,\n queryLog,\n TASK_RESULTS,\n TASK_TRIGGERS,\n type MISSED_RUN_POLICIES,\n type StateMap,\n type StateDeps,\n type LogDeps,\n} from \"@receptron/task-scheduler\";\nimport type { ITaskManager, TaskDefinition, SchedulerLogger } from \"./task-manager.js\";\nimport { latestWindowAtOrBefore, toLibrarySchedule, unfireableScheduleReason, windowToIso } from \"./schedule-window.js\";\nimport { errorMessage } from \"../utils/errors.js\";\n\nconst SCHEDULER_CONFIG_DIR = \"config/scheduler\";\nconst SCHEDULER_DATA_DIR = \"data/scheduler/logs\";\n\n// ── Host injection ────────────────────────────────────────────────\n\nexport interface SchedulerConfig {\n /** Absolute workspace root — state.json + logs hang off it. */\n workspaceRoot: string;\n /** Host atomic file writer (used with `uniqueTmp` for the state file). */\n writeFileAtomic: (filePath: string, content: string, opts: { uniqueTmp: boolean }) => Promise<void>;\n /** Optional logger. */\n log?: SchedulerLogger;\n}\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nlet config: SchedulerConfig | null = null;\n\n/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */\nexport function configureScheduler(injected: SchedulerConfig): void {\n config = injected;\n}\n\nfunction requireConfig(): SchedulerConfig {\n if (!config) throw new Error(\"scheduler: configureScheduler() not called\");\n return config;\n}\n\nfunction logger(): SchedulerLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n// ── Paths ─────────────────────────────────────────────────────────\n\nfunction stateFilePath(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, \"state.json\");\n}\n\nfunction logsDir(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);\n}\n\n// ── I/O deps (real filesystem) ────────────────────────────────────\n\nfunction stateDeps(): StateDeps {\n return {\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n writeFileAtomic: (filePath: string, content: string) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),\n exists: existsSync,\n };\n}\n\nconst logDeps: LogDeps = {\n appendFile: (filePath: string, content: string) => appendFile(filePath, content),\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n exists: existsSync,\n ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),\n};\n\n// ── System task registry ──────────────────────────────────────────\n\nexport interface SystemTaskDef {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: typeof MISSED_RUN_POLICIES.skip | typeof MISSED_RUN_POLICIES.runOnce | typeof MISSED_RUN_POLICIES.runAll;\n run: () => Promise<void>;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nlet stateMap: StateMap = new Map();\nconst systemTasks: SystemTaskDef[] = [];\nlet taskManagerRef: ITaskManager | null = null;\n\n/**\n * Initialize the scheduler adapter. Call once at server startup AFTER the\n * task-manager is created but BEFORE `taskManager.start()`.\n */\nexport async function initScheduler(taskManager: ITaskManager, tasks: SystemTaskDef[]): Promise<void> {\n await mkdir(path.dirname(stateFilePath()), { recursive: true });\n await mkdir(logsDir(), { recursive: true });\n\n stateMap = await loadState(stateFilePath(), stateDeps());\n systemTasks.length = 0;\n systemTasks.push(...tasks);\n taskManagerRef = taskManager;\n\n const plan = computeCatchUpPlan(catchUpTasksFor(tasks), stateMap, Date.now());\n\n for (const skip of plan.skipped) {\n logger().info(\"catch-up skipped\", { taskId: skip.taskId, windows: skip.windowCount });\n await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });\n }\n\n if (plan.runs.length > 0) {\n logger().info(\"catch-up enqueued\", { runs: plan.runs.length });\n for (const run of plan.runs) {\n const task = tasks.find((taskDef) => taskDef.id === run.taskId);\n if (!task) continue;\n await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);\n }\n }\n\n // Register with task-manager for ongoing ticks\n for (const task of tasks) {\n taskManager.registerTask({\n id: task.id,\n description: task.description,\n schedule: task.schedule,\n run: async (ctx) => {\n const windowIso = computeCurrentWindow(task, ctx.now.getTime());\n await executeAndLog(task, windowIso, TASK_TRIGGERS.scheduled);\n },\n });\n }\n\n logger().info(\"initialized\", { tasks: tasks.map((taskDef) => taskDef.id), stateEntries: stateMap.size });\n}\n\n/** The catch-up input, with the tasks that can never fire left out. Their\n * windows come back NaN rather than null, which `listMissedWindows` cannot\n * see the end of — it fills to its cap and the first `new Date(NaN)` throws\n * `RangeError`, taking every other task's catch-up down with it at startup.\n * Registration still happens, so the task-manager reports the bad schedule. */\nfunction catchUpTasksFor(tasks: SystemTaskDef[]): CatchUpTask[] {\n return tasks\n .filter((taskDef) => {\n const reason = unfireableScheduleReason(taskDef.schedule);\n if (reason === null) return true;\n logger().error(`schedule has an unusable ${reason.field} — skipping catch-up, this task will never run`, {\n taskId: taskDef.id,\n [reason.field]: reason.value,\n });\n return false;\n })\n .map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n schedule: toLibrarySchedule(taskDef.schedule),\n missedRunPolicy: taskDef.missedRunPolicy,\n enabled: true,\n }));\n}\n\n/** Apply a schedule override to a running system task. Updates the\n * in-memory task definition, the task-manager, and recalculates\n * nextScheduledAt in persisted state. */\nexport async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef[\"schedule\"]): Promise<boolean> {\n const task = systemTasks.find((taskDef) => taskDef.id === taskId);\n if (!task || !taskManagerRef) return false;\n if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;\n task.schedule = schedule;\n\n // Recalculate next window so the UI reflects the new schedule\n const nextScheduledAt = computeNextScheduledFor(task.schedule);\n await safeUpdateState(taskId, { nextScheduledAt });\n\n return true;\n}\n\n/** Query execution logs — used by API routes. */\nexport async function getSchedulerLogs(opts: { since?: string | undefined; taskId?: string | undefined; limit?: number | undefined }): Promise<TaskLogEntry[]> {\n return queryLog(logsDir(), opts, logDeps);\n}\n\n/** Get all task states — used by API routes. */\nexport function getSchedulerTasks(): {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: string;\n state: TaskExecutionState;\n}[] {\n return systemTasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n description: taskDef.description,\n schedule: taskDef.schedule,\n missedRunPolicy: taskDef.missedRunPolicy,\n state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),\n }));\n}\n\n/** Read the persisted execution state for any task id (system, user, or\n * skill). Returns an empty state when the id has never run — used by the API\n * route to attach last-run / next-run / history state to user + skill tasks\n * it lists from other sources. */\nexport function getSchedulerTaskState(taskId: string): TaskExecutionState {\n return stateMap.get(taskId) ?? emptyState(taskId);\n}\n\n/** Record a run of an EXTERNAL (skill / user) task — one registered directly\n * on the task-manager rather than through `initScheduler`. Persists a log\n * entry + updates state so these tasks get the same history / last-run /\n * next-run as system tasks. Because they are fire-and-forget (`startChat`\n * spawns an async chat), `errorMessage` reflects whether the *dispatch*\n * succeeded, not the chat's eventual outcome; `chatSessionId` links the run\n * to the spawned session. */\nexport async function recordExternalRun(params: {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errorMessage: string | null;\n chatSessionId?: string | undefined;\n}): Promise<void> {\n await persistRun({\n meta: { id: params.id, name: params.name, schedule: params.schedule },\n scheduledFor: params.scheduledFor,\n startedAt: params.startedAt,\n durationMs: params.durationMs,\n trigger: params.trigger,\n errMsg: params.errorMessage,\n chatSessionId: params.chatSessionId,\n });\n}\n\n/** Test-only: clear config + in-memory state. */\nexport function resetSchedulerForTesting(): void {\n config = null;\n stateMap = new Map();\n systemTasks.length = 0;\n taskManagerRef = null;\n}\n\n// ── Internal ──────────────────────────────────────────────────────\n\nasync function executeAndLog(task: SystemTaskDef, scheduledFor: string, trigger: TaskTrigger): Promise<void> {\n const startedAt = new Date().toISOString();\n const startMs = Date.now();\n let errMsg: string | null = null;\n try {\n await task.run();\n } catch (err) {\n errMsg = errorMessage(err);\n logger().error(\"task failed\", { taskId: task.id, error: errMsg });\n }\n const durationMs = Date.now() - startMs;\n // Persistence is best-effort — never let disk failures propagate to the\n // tick loop or abort startup catch-up.\n await persistRun({ meta: { id: task.id, name: task.name, schedule: task.schedule }, scheduledFor, startedAt, durationMs, trigger, errMsg });\n}\n\n/** The minimum a run needs to persist state + a log entry — shared by system\n * tasks (`executeAndLog`) and external skill/user runs (`recordExternalRun`). */\ninterface TaskRunMeta {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n}\n\ninterface RunRecord {\n meta: TaskRunMeta;\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errMsg: string | null;\n chatSessionId?: string | undefined;\n}\n\n/** Best-effort persistence — state and log are independent, so one failing\n * never blocks the other and neither propagates upward. */\nasync function persistRun(run: RunRecord): Promise<void> {\n await writeRunState(run);\n await writeRunLog(run);\n}\n\nasync function writeRunState(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, durationMs, errMsg } = run;\n const isSuccess = errMsg === null;\n const currentState = stateMap.get(meta.id);\n try {\n await updateAndSave(\n stateFilePath(),\n stateMap,\n meta.id,\n {\n lastRunAt: scheduledFor,\n lastRunResult: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n lastRunDurationMs: durationMs,\n lastErrorMessage: errMsg,\n consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,\n totalRuns: (currentState?.totalRuns ?? 0) + 1,\n nextScheduledAt: computeNextScheduledFor(meta.schedule),\n },\n stateDeps(),\n );\n } catch (err) {\n logger().warn(\"state persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\nasync function writeRunLog(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, startedAt, durationMs, trigger, errMsg, chatSessionId } = run;\n const isSuccess = errMsg === null;\n try {\n await appendLogEntry(\n logsDir(),\n {\n taskId: meta.id,\n taskName: meta.name,\n scheduledFor,\n startedAt,\n completedAt: new Date().toISOString(),\n result: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n durationMs,\n trigger,\n ...(errMsg !== null && { errorMessage: errMsg }),\n ...(chatSessionId !== undefined && { chatSessionId }),\n },\n logDeps,\n );\n } catch (err) {\n logger().warn(\"log persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\n/** Safe state update — swallows errors. */\nasync function safeUpdateState(taskId: string, patch: Partial<TaskExecutionState>): Promise<void> {\n try {\n await updateAndSave(stateFilePath(), stateMap, taskId, patch, stateDeps());\n } catch (err) {\n logger().warn(\"state update failed\", { taskId, error: String(err) });\n }\n}\n\n/** Compute the window boundary that the current tick belongs to. For\n * scheduled runs, this is the epoch-aligned window — not the wall-clock\n * time of execution. This keeps lastRunAt consistent with catch-up's\n * window-based accounting. `tickMs` is the tick's own clock, not\n * `Date.now()`: the two drift apart by however long the tick took to reach\n * this task, and under an injected clock they are unrelated. */\nfunction computeCurrentWindow(task: SystemTaskDef, tickMs: number): string {\n return windowToIso(latestWindowAtOrBefore(task.schedule, tickMs)) ?? new Date(tickMs).toISOString();\n}\n\nfunction computeNextScheduledFor(schedule: TaskDefinition[\"schedule\"]): string | null {\n // The engine will never fire this one, so promising a next run in the UI\n // would be a lie — and the library still hands back a window for some of\n // them (`\"24:00\"` resolves to tomorrow's midnight).\n if (unfireableScheduleReason(schedule) !== null) return null;\n // `windowToIso` is what keeps a window that is not a date (NaN from a\n // degenerate schedule, or an interval so large the next window is past the\n // end of `Date`) from throwing here and taking the whole state write down.\n return windowToIso(nextWindowAfter(toLibrarySchedule(schedule), Date.now() + 1));\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
|
|
2
|
-
const require_dist = require("./dist-
|
|
2
|
+
const require_dist = require("./dist-Gj7ygaW2.cjs");
|
|
3
|
+
require("./errors-CdXsfFCF.cjs");
|
|
3
4
|
const require_root = require("./root-BvwBUcol.cjs");
|
|
4
5
|
const require_relPath = require("./relPath-CTAjGdCL.cjs");
|
|
5
6
|
const require_itemId = require("./itemId-CGT2J7YK.cjs");
|
|
6
|
-
const require_promptSafety = require("./promptSafety-
|
|
7
|
-
const require_discovery = require("./discovery-
|
|
7
|
+
const require_promptSafety = require("./promptSafety-caJWd1ZQ.cjs");
|
|
8
|
+
const require_discovery = require("./discovery-tvFo-bUJ.cjs");
|
|
8
9
|
const require_feeds_paths = require("./feeds/paths.cjs");
|
|
9
10
|
const require_skill_bridge_index = require("./skill-bridge/index.cjs");
|
|
10
11
|
let node_fs = require("node:fs");
|
|
@@ -2672,4 +2673,4 @@ Object.defineProperty(exports, "validateRecordObject", {
|
|
|
2672
2673
|
}
|
|
2673
2674
|
});
|
|
2674
2675
|
|
|
2675
|
-
//# sourceMappingURL=server-
|
|
2676
|
+
//# sourceMappingURL=server-Div1i_k5.cjs.map
|