@mulmoclaude/core 5.7.1 → 5.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifacts/paths.cjs +38 -75
- package/dist/artifacts/paths.cjs.map +1 -1
- package/dist/artifacts/paths.d.ts +2 -59
- package/dist/artifacts/paths.js +2 -69
- package/dist/artifacts/paths.js.map +1 -1
- package/dist/collection/firestore.cjs.map +1 -1
- package/dist/collection/firestore.js.map +1 -1
- package/dist/collection/index.cjs +3 -3
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +3 -3
- package/dist/collection/registry/server/index.cjs +3 -3
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +3 -3
- package/dist/collection/registry/server/index.js.map +1 -1
- 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.cjs.map +1 -1
- package/dist/collection-watchers/index.js +4 -4
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/{discovery-XoYJI2Ot.js → discovery-CbwHtcwk.js} +3 -3
- package/dist/{discovery-XoYJI2Ot.js.map → discovery-CbwHtcwk.js.map} +1 -1
- package/dist/{discovery-CwBXvtyn.cjs → discovery-DgZRmO7F.cjs} +3 -3
- package/dist/discovery-DgZRmO7F.cjs.map +1 -0
- package/dist/{dist-Gj7ygaW2.cjs → dist-BZ62pKuU.cjs} +11 -1
- package/dist/dist-BZ62pKuU.cjs.map +1 -0
- package/dist/{dist-BzoA9pDR.js → dist-MFUV9FYo.js} +6 -2
- package/dist/dist-MFUV9FYo.js.map +1 -0
- package/dist/errorMessage-OEQzRwsi.cjs +1 -0
- package/dist/errorMessage-VEiXHRnX.js +1 -0
- package/dist/{errors-CdXsfFCF.cjs → errors-B0xloJ3E.cjs} +2 -2
- package/dist/errors-B0xloJ3E.cjs.map +1 -0
- package/dist/{errors-C7TI_t_4.js → errors-zTAsW7dS.js} +2 -2
- package/dist/{errors-C7TI_t_4.js.map → errors-zTAsW7dS.js.map} +1 -1
- package/dist/feeds/paths.cjs.map +1 -1
- package/dist/feeds/server/index.cjs +4 -4
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +4 -4
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/file-change/index.cjs.map +1 -1
- package/dist/files/index.cjs.map +1 -1
- package/dist/files/index.js.map +1 -1
- package/dist/global-config/index.cjs.map +1 -1
- package/dist/google/index.cjs +3 -3
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +3 -3
- package/dist/google/index.js.map +1 -1
- package/dist/graph-CURotEkH.cjs.map +1 -1
- package/dist/ingestTypes-DS9HiR2I.cjs.map +1 -1
- package/dist/{listen-Dca-NoBi.js → listen-DUlzSOzX.js} +2 -2
- package/dist/{listen-Dca-NoBi.js.map → listen-DUlzSOzX.js.map} +1 -1
- package/dist/{listen-BXthb-9C.cjs → listen-N_8XdKz_.cjs} +2 -2
- package/dist/listen-N_8XdKz_.cjs.map +1 -0
- package/dist/notifier/index.cjs +1 -1
- package/dist/notifier/index.js +1 -1
- package/dist/{notifier-B65Mvngb.cjs → notifier-CiemUClt.cjs} +3 -6
- package/dist/notifier-CiemUClt.cjs.map +1 -0
- package/dist/{notifier-vAp5t6wL.js → notifier-dgkC-h80.js} +2 -5
- package/dist/notifier-dgkC-h80.js.map +1 -0
- package/dist/paths-C8-jxR0s.cjs.map +1 -1
- package/dist/plugin-vue/i18n.cjs.map +1 -1
- package/dist/plugin-vue/index.cjs +82 -16
- package/dist/plugin-vue/index.cjs.map +1 -1
- package/dist/plugin-vue/index.js +82 -16
- package/dist/plugin-vue/index.js.map +1 -1
- package/dist/{promptSafety-BGV1wrE6.cjs → promptSafety-auQEatax.cjs} +2 -2
- package/dist/promptSafety-auQEatax.cjs.map +1 -0
- package/dist/{promptSafety-B5uGroik.js → promptSafety-yG60Vof8.js} +2 -2
- package/dist/promptSafety-yG60Vof8.js.map +1 -0
- package/dist/relPath-CTAjGdCL.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 -4
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.js +4 -4
- package/dist/remote-host/server/index.js.map +1 -1
- package/dist/{remote-host-DhAHBYzN.cjs → remote-host-BMjh_F-6.cjs} +2 -2
- package/dist/remote-host-BMjh_F-6.cjs.map +1 -0
- package/dist/{remote-host-Cjox2F2J.js → remote-host-CoNX9JEC.js} +2 -2
- package/dist/remote-host-CoNX9JEC.js.map +1 -0
- package/dist/remote-view/index.cjs +1 -1
- package/dist/remote-view/index.cjs.map +1 -1
- package/dist/remote-view/index.js +1 -1
- package/dist/root-BvwBUcol.cjs.map +1 -1
- package/dist/scheduler/index.cjs +2 -2
- package/dist/scheduler/index.cjs.map +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/scheduler/index.js.map +1 -1
- package/dist/{server-CLhww8NL.cjs → server-RsnFqPt7.cjs} +5 -5
- package/dist/server-RsnFqPt7.cjs.map +1 -0
- package/dist/{server-CTkTHqEL.js → server-iYXYObKb.js} +5 -5
- package/dist/server-iYXYObKb.js.map +1 -0
- package/dist/skill-bridge/index.cjs.map +1 -1
- package/dist/utils/index.cjs +2 -2
- package/dist/utils/index.js +2 -2
- package/dist/whisper/index.cjs +2 -2
- package/dist/whisper/index.cjs.map +1 -1
- package/dist/whisper/index.js +2 -2
- package/dist/whisper/index.js.map +1 -1
- package/dist/wiki/index.cjs +1 -1
- package/dist/wiki/index.cjs.map +1 -1
- package/dist/wiki/index.js +1 -1
- package/dist/wiki/index.js.map +1 -1
- package/dist/wiki/paths.cjs.map +1 -1
- package/dist/wiki/server/index.cjs.map +1 -1
- package/dist/workspace-setup/index.js.map +1 -1
- package/package.json +9 -8
- package/dist/discovery-CwBXvtyn.cjs.map +0 -1
- package/dist/dist-BzoA9pDR.js.map +0 -1
- package/dist/dist-Gj7ygaW2.cjs.map +0 -1
- package/dist/errorMessage-BQNpSYdT.js +0 -1
- package/dist/errorMessage-CT-va_Pv.cjs +0 -1
- package/dist/errors-CdXsfFCF.cjs.map +0 -1
- package/dist/listen-BXthb-9C.cjs.map +0 -1
- package/dist/notifier-B65Mvngb.cjs.map +0 -1
- package/dist/notifier-vAp5t6wL.js.map +0 -1
- package/dist/promptSafety-B5uGroik.js.map +0 -1
- package/dist/promptSafety-BGV1wrE6.cjs.map +0 -1
- package/dist/remote-host-Cjox2F2J.js.map +0 -1
- package/dist/remote-host-DhAHBYzN.cjs.map +0 -1
- package/dist/server-CLhww8NL.cjs.map +0 -1
- package/dist/server-CTkTHqEL.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/remote-view/index.ts"],"sourcesContent":["// The remote custom-view contract (phase 3 — plans/done/feat-remote-custom-view.md).\n//\n// Browser-safe single source of truth shared by the host server (which wraps\n// the view HTML into a sandboxed srcdoc), the desktop phone-frame preview, and\n// the mulmoserver mobile client (post-publish). A remote view runs on a phone\n// that can reach the internet but NOT the host's localhost, so — unlike the\n// desktop custom view (token + fetch to the view-data route) — its records\n// arrive over an async postMessage bridge owned by the parent page, and its\n// CSP locks `connect-src` to 'none' entirely.\n//\n// BACKWARD COMPATIBILITY — this bridge is a frozen public contract. Remote\n// views are LLM-authored HTML files persisted in users' workspaces\n// (`data/skills/*/views/*.html`), written against\n// `packages/core/assets/helps/custom-view-remote.md`; they cannot be\n// migrated centrally and must keep working across host upgrades and any\n// storage-virtualization work underneath. Evolve only by backward-compatible\n// supersets, the way protocol v2 added the mutate pair: bump\n// `REMOTE_VIEW_PROTOCOL`, add new message types / optional fields — never\n// repurpose an existing message type, change the `getItems` page shape\n// (`{ items, total, offset, limit }`), or tighten limits a shipped view may\n// already rely on.\n\nimport { isRecord } from \"@mulmoclaude/common\";\nimport { projectRecordFields } from \"../collection/core/project\";\n\n/** Bump when the bootstrap/message contract changes shape; the bootstrap\n * exposes it as `__MC_VIEW.protocol` so a parent can refuse a stale view.\n * v2 (phase 4) adds the mutate pair below — a backward-compatible superset,\n * so a v1 (read-only) parent still serves get-items/start-chat unchanged. */\nexport const REMOTE_VIEW_PROTOCOL = 2;\n\n/** postMessage types between the sandboxed view and its parent page.\n * `startChat` reuses the desktop custom-view message type on purpose — the\n * desktop parent already understands it. */\nexport const REMOTE_VIEW_MESSAGES = {\n /** view → parent: request one page of records ({ requestId, offset, limit, fields }). */\n getItems: \"mc-remote-get-items\",\n /** parent → view: the reply ({ requestId, ok, page | error }). */\n items: \"mc-remote-items\",\n /** view → parent: mutate one record ({ requestId, op: \"update\"|\"delete\", id, patch? }). */\n mutate: \"mc-remote-mutate\",\n /** parent → view: the mutate reply ({ requestId, ok, result | error }). */\n mutateResult: \"mc-remote-mutate-result\",\n /** view → parent: open a new chat seeded with the prompt. Whether the parent\n * SENDS it or leaves it as an editable draft follows the view's\n * `allowSendChat` declaration, on every surface. Both read it from the\n * collection SCHEMA — never from this message, which the sandbox composes. */\n startChat: \"mc-start-chat\",\n} as const;\n\n/** Pagination defaults — mirrored by the phase-2 record handlers\n * (`server/remoteHost/handlers/collectionPage.ts` imports these) so a view\n * page can never outgrow what the command channel itself serves. */\nexport const DEFAULT_PAGE_LIMIT = 50;\nexport const MAX_PAGE_LIMIT = 200;\n\n/** Hard cap on the wrapped srcdoc: it travels to the phone INSIDE a Firestore\n * command document (1 MiB total), so leave envelope headroom. */\nexport const REMOTE_VIEW_MAX_BYTES = 900_000;\n\n/** Hard cap on ONE `getItems` page (phase 5 — plans/done/feat-remote-view-images.md).\n * Same 1 MiB command-document envelope as the srcdoc: when a view inlines image\n * fields as `data:` URLs, the host stops inlining once the serialized page would\n * exceed this, leaving the remaining image fields as their original path (which\n * the view renders as a placeholder). Guards the doc-write from ever failing. */\nexport const REMOTE_VIEW_ITEMS_MAX_BYTES = 900_000;\n\n/** Default longest-edge (px) a remote view's inlined image thumbnail is\n * downscaled to; a view may override via `imageMaxEdge`. */\nexport const DEFAULT_IMAGE_MAX_EDGE = 512;\n\n/** In-iframe `getItems` timeout — matches the remote client's `callHost`\n * response timeout so the two layers give up together. */\nconst GET_ITEMS_TIMEOUT_MS = 30_000;\n\n// CDN allowlist for sandboxed LLM-authored HTML (script/style/font loads).\n// Shared with the desktop preview + custom-view CSPs\n// (src/utils/html/previewCsp.ts re-exports it as its default) so the two\n// policies can't drift. Keep the list audited — every entry is a potential\n// supply-chain surface; the hosts here are reputable infrastructure that does\n// not expose per-request logs to third parties.\nexport const SANDBOXED_VIEW_CDN_ALLOWLIST: readonly string[] = [\n \"https://cdn.jsdelivr.net\",\n \"https://unpkg.com\",\n \"https://cdnjs.cloudflare.com\",\n \"https://fonts.googleapis.com\",\n \"https://fonts.gstatic.com\",\n // Plotly's first-party CDN — the LLM defaults to it for Plotly charts.\n \"https://cdn.plot.ly\",\n];\n\nconst toInt = (value: unknown): number | null => {\n const num = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : NaN;\n return Number.isFinite(num) ? Math.floor(num) : null;\n};\n\n/** Coerce a channel/postMessage offset (arrives as untyped JSON) to a non-negative int. */\nexport const clampOffset = (value: unknown): number => Math.max(0, toInt(value) ?? 0);\n\n/** Read a channel/postMessage identifier (a slug, a view id) as a string.\n * Params arrive as untyped JSON, so a caller can send anything; `String(...)`\n * turned an object into the literal \"[object Object]\", which then travelled on\n * as if the caller had asked for a collection by that name — surfacing as\n * `collection '[object Object]' not found` rather than a bad-request. A value\n * with no string form is simply absent. */\nexport const readIdParam = (value: unknown): string => (typeof value === \"string\" ? value : \"\");\n\n/** Coerce a channel/postMessage limit to [1, MAX_PAGE_LIMIT] (default 50). */\nexport const clampLimit = (value: unknown): number => {\n const num = toInt(value);\n if (num === null || num <= 0) return DEFAULT_PAGE_LIMIT;\n return Math.min(num, MAX_PAGE_LIMIT);\n};\n\n/** Clamp an `imageMaxEdge` (arrives as untyped schema/JSON) to [64, 1024];\n * default 512. Keeps a runaway edge from defeating the thumbnail's purpose. */\nexport const clampImageMaxEdge = (value: unknown): number => {\n const num = toInt(value);\n if (num === null || num <= 0) return DEFAULT_IMAGE_MAX_EDGE;\n return Math.min(Math.max(num, 64), 1024);\n};\n\n/** Coerce a `fields` projection list from untyped message JSON. */\nexport const normalizeFields = (value: unknown): string[] | undefined => {\n if (!Array.isArray(value)) return undefined;\n const cleaned = value\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n return cleaned.length > 0 ? cleaned : undefined;\n};\n\nexport type RemoteViewItem = Record<string, unknown>;\n\n/** One page of records, the resolved value of the view's `getItems()`. Same\n * shape as the phase-2 `getCollection` page so a parent can pass a channel\n * page straight through. */\nexport interface RemoteViewPage {\n items: RemoteViewItem[];\n total: number;\n offset: number;\n limit: number;\n}\n\n/** A normalized (clamped, fields-cleaned) page request handed to a parent's\n * `getPage` — `handleRemoteViewMessage` does the coercion so every parent\n * answers identical values. */\nexport interface RemoteViewPageRequest {\n offset: number;\n limit: number;\n fields?: string[] | undefined;\n}\n\n/** Keep only `fields` (+ always the primary key) on each record. Parents apply\n * this uniformly — the desktop preview via `pageFromItems`, the phone parent\n * over the page it fetched through the channel — so a view sees the same\n * projection everywhere. No-op without `fields`; an EMPTY `fields` array is\n * also a no-op here (frozen bridge behavior — the shared helper would\n * project down to the primary key alone). */\nexport function projectItems(items: RemoteViewItem[], fields: string[] | undefined, primaryKey: string): RemoteViewItem[] {\n if (!fields || fields.length === 0) return items;\n return projectRecordFields(items, fields, primaryKey);\n}\n\n/** Answer a page request from an already-loaded record array (the desktop\n * preview's data source): slice + project. Observable behavior matches the\n * phone paging over the command channel. */\nexport function pageFromItems(items: RemoteViewItem[], request: RemoteViewPageRequest, primaryKey: string): RemoteViewPage {\n const pageItems = items.slice(request.offset, request.offset + request.limit);\n return { items: projectItems(pageItems, request.fields, primaryKey), total: items.length, offset: request.offset, limit: request.limit };\n}\n\n/**\n * CSP for a remote (mobile) custom view. Stricter than the desktop custom-view\n * policy: the view's data arrives over postMessage, so `connect-src` is\n * `'none'` — no fetch / XHR / WebSocket / sendBeacon to ANY origin, which\n * closes the bidirectional-exfiltration channel completely (there is no token\n * to steal either). Script/style/font keep the curated CDN allowlist (the\n * phone can reach the internet; only the host is unreachable), and\n * `img-src`/`media-src` allow any `https:` host so record image/media URLs\n * render — the same knowingly-accepted one-way GET-exfil tradeoff as the\n * desktop policy (see buildCustomViewCsp in src/utils/html/previewCsp.ts).\n */\nexport function buildRemoteViewCsp(cdns: readonly string[] = SANDBOXED_VIEW_CDN_ALLOWLIST): string {\n const cdnList = cdns.join(\" \");\n return [\n \"default-src 'none'\",\n `script-src 'unsafe-inline' ${cdnList}`,\n `style-src 'unsafe-inline' ${cdnList}`,\n `font-src ${cdnList}`,\n `img-src ${cdnList} data: blob: https:`,\n \"media-src https: data: blob:\",\n \"connect-src 'none'\",\n ].join(\"; \");\n}\n\n/** The in-iframe bootstrap installed before any of the view's own scripts.\n * Owns the fiddly part of the contract — request/response correlation — so an\n * LLM-authored view only ever awaits `__MC_VIEW.getItems(...)` /\n * `.updateItem(...)` / `.deleteItem(...)`:\n *\n * - `getItems({ offset, limit, fields })`: posts an `mc-remote-get-items`\n * with a fresh `requestId`, resolves on the matching `mc-remote-items`\n * reply (validated to come from `window.parent`), rejects on `ok: false`\n * or after 30 s. targetOrigin `'*'` is safe: the request carries no secret\n * and the parent is by construction the party supplying the data.\n * - `updateItem(id, patch)` / `deleteItem(id)` (phase 4): post an\n * `mc-remote-mutate` and resolve on the matching `mc-remote-mutate-result`,\n * sharing the same `call()` correlation as `getItems`. Installed ONLY when\n * the host set `writable` (the view declared `editableFields`/`allowDelete`);\n * otherwise both reject `\"this view is read-only\"` so a mis-declared view\n * fails loudly instead of silently no-op'ing. The HOST still re-derives and\n * enforces the write policy — `writable` only gates the client surface.\n * - `startChat(prompt, role)`: same message type as the desktop bridge — the\n * parent opens a new chat seeded with `prompt`. Whether it runs follows the\n * view's `allowSendChat` declaration on every surface, read from the\n * collection schema — the phone reads it from the schema it already holds, the\n * desktop surfaces through `customViewSendsChat`. Never from this message:\n * the sandbox composes the prompt, so letting it also decide whether the\n * prompt runs would hand it both halves.\n * - `t(key, named)`: the same vue-i18n-compatible dict helper as the desktop\n * bootstrap (named interpolation only), over the host-picked `dict`.\n *\n * Self-contained one-line string (no `<`, no `</script>`, `${}` only for the\n * interpolated constants). */\nfunction remoteViewBootstrap(): string {\n return `(function(){var v=window.__MC_VIEW,seq=0,pend={};window.addEventListener('message',function(e){if(e.source!==window.parent)return;var d=e.data;if(!d)return;if(d.type!=='${REMOTE_VIEW_MESSAGES.items}'&&d.type!=='${REMOTE_VIEW_MESSAGES.mutateResult}')return;var p=pend[d.requestId];if(!p)return;delete pend[d.requestId];clearTimeout(p.timer);if(d.ok)p.resolve(d.type==='${REMOTE_VIEW_MESSAGES.items}'?d.page:d.result);else p.reject(new Error(typeof d.error==='string'?d.error:'request failed'));});function call(type,payload){return new Promise(function(resolve,reject){var id='q'+(++seq);var timer=setTimeout(function(){delete pend[id];reject(new Error(type+' timed out'));},${GET_ITEMS_TIMEOUT_MS});pend[id]={resolve:resolve,reject:reject,timer:timer};var m={type:type,slug:v.slug,requestId:id};for(var k in payload){m[k]=payload[k];}window.parent.postMessage(m,'*');});}v.getItems=function(opts){opts=opts&&typeof opts==='object'?opts:{};return call('${REMOTE_VIEW_MESSAGES.getItems}',{offset:opts.offset,limit:opts.limit,fields:opts.fields});};if(v.writable){v.updateItem=function(id,patch){return call('${REMOTE_VIEW_MESSAGES.mutate}',{op:'update',id:String(id),patch:patch&&typeof patch==='object'?patch:{}});};v.deleteItem=function(id){return call('${REMOTE_VIEW_MESSAGES.mutate}',{op:'delete',id:String(id)});};}else{v.updateItem=v.deleteItem=function(){return Promise.reject(new Error('this view is read-only'));};}v.startChat=function(prompt,role){window.parent.postMessage({type:'${REMOTE_VIEW_MESSAGES.startChat}',slug:v.slug,prompt:String(prompt),role:typeof role==='string'?role:undefined},'*');};v.dict=v.dict||{};v.t=function(key,named){var s=v.dict[key];if(typeof s!=='string')return typeof key==='string'?key:String(key);if(!named||typeof named!=='object')return s;return s.replace(/\\\\{(\\\\w+)\\\\}/g,function(m,n){var x=named[n];return x==null?m:String(x);});};})();`;\n}\n\n/** What the host injects into `window.__MC_VIEW` — note what is ABSENT\n * compared to the desktop boot: no token, no dataUrl, no origin. */\nexport interface RemoteViewBoot {\n slug: string;\n /** Locale the dict was picked for; empty string when no translations. */\n locale?: string;\n /** Host-picked, locale-filtered flat string map (same contract as the\n * desktop custom-view dict). */\n dict?: Record<string, string>;\n /** True when the view declared a mutable surface (`editableFields` and/or\n * `allowDelete`). Gates the client-side `updateItem`/`deleteItem` install\n * only — the host re-enforces the actual policy on every mutate. */\n writable?: boolean;\n}\n\n/** Wrap a view's HTML into the sandboxed srcdoc: CSP meta + `__MC_VIEW` boot +\n * bridge bootstrap injected at the start of `<head>` (before any view\n * script). Runs HOST-side (`getRemoteView`) so the phone and the desktop\n * preview receive the identical finished artifact. */\nexport function buildRemoteViewSrcdoc(html: string, boot: RemoteViewBoot): string {\n const cspMeta = `<meta http-equiv=\"Content-Security-Policy\" content=\"${buildRemoteViewCsp()}\">`;\n // `<`-escape the JSON so a hostile slug/dict string can't break out of the\n // <script> element (same escape as the desktop srcdoc builder).\n const json = JSON.stringify({\n slug: boot.slug,\n locale: boot.locale ?? \"\",\n dict: boot.dict ?? {},\n target: \"mobile\",\n protocol: REMOTE_VIEW_PROTOCOL,\n writable: boot.writable ?? false,\n }).replace(/</g, \"\\\\u003c\");\n const injection = `${cspMeta}<script>window.__MC_VIEW=${json};${remoteViewBootstrap()}</script>`;\n if (/<head\\b[^>]*>/i.test(html)) {\n return html.replace(/(<head\\b[^>]*>)/i, `$1${injection}`);\n }\n return `<!DOCTYPE html><html><head>${injection}</head><body>${html}</body></html>`;\n}\n\n/** A normalized mutate request handed to a parent's `onMutate`\n * (`handleRemoteViewMessage` validates op/id/patch first). `update` carries a\n * partial record; the HOST decides which keys are actually writable. */\nexport type RemoteViewMutateRequest = { op: \"update\"; id: string; patch: Record<string, unknown> } | { op: \"delete\"; id: string };\n\n/** The resolved value of a mutate: the merged record for an update, the removed\n * id for a delete. Sent back to the view as the `mc-remote-mutate-result`\n * `result`. */\nexport interface RemoteViewMutateResult {\n item?: RemoteViewItem;\n id?: string;\n}\n\n/** What a parent page provides to answer the sandboxed view. Deliberately\n * minimal — exactly the phone runtime's capabilities, nothing more, so the\n * desktop preview can never exceed what works on the phone. */\nexport interface RemoteViewBridgeHandlers {\n slug: string;\n /** Answer one normalized page request (already clamped + fields-cleaned). */\n getPage: (request: RemoteViewPageRequest) => Promise<RemoteViewPage> | RemoteViewPage;\n /** Apply one normalized mutate (update/delete). Omit on a read-only parent —\n * the handler then replies `ok: false, \"this view is read-only\"`. The parent\n * forwards to the host (which enforces the write policy authoritatively). */\n onMutate?: (request: RemoteViewMutateRequest) => Promise<RemoteViewMutateResult> | RemoteViewMutateResult;\n /** Relay a `startChat` draft; omit on a parent without a chat surface. */\n onStartChat?: (prompt: string, role?: string) => void;\n}\n\n/** Coerce an untyped `mc-remote-mutate` payload to a normalized request, or\n * null when it is malformed (unknown op, missing id, non-object update patch —\n * the parent then replies with an `\"invalid mutate request\"` error). */\nexport function normalizeMutate(data: { op?: unknown; id?: unknown; patch?: unknown }): RemoteViewMutateRequest | null {\n const itemId = typeof data.id === \"string\" ? data.id : typeof data.id === \"number\" && Number.isFinite(data.id) ? String(data.id) : \"\";\n if (!itemId) return null;\n if (data.op === \"delete\") return { op: \"delete\", id: itemId };\n if (data.op === \"update\") {\n const { patch } = data;\n if (!isRecord(patch)) return null;\n return { op: \"update\", id: itemId, patch };\n }\n return null;\n}\n\nasync function answerGetItems(requestId: string, request: RemoteViewPageRequest, handlers: RemoteViewBridgeHandlers, reply: RemoteViewReply): Promise<void> {\n try {\n const page = await handlers.getPage(request);\n reply({ type: REMOTE_VIEW_MESSAGES.items, requestId, ok: true, page });\n } catch (err) {\n reply({ type: REMOTE_VIEW_MESSAGES.items, requestId, ok: false, error: err instanceof Error ? err.message : String(err) });\n }\n}\n\n/** Validate + dispatch a `mc-remote-mutate` payload, replying on every path\n * (malformed request, read-only parent, handler success/throw). Split out of\n * `handleRemoteViewMessage` so that function stays under the 20-line limit. */\nasync function answerMutate(\n requestId: string,\n msg: { op?: unknown; id?: unknown; patch?: unknown },\n handlers: RemoteViewBridgeHandlers,\n reply: RemoteViewReply,\n): Promise<void> {\n const request = normalizeMutate(msg);\n if (!request) {\n reply({ type: REMOTE_VIEW_MESSAGES.mutateResult, requestId, ok: false, error: \"invalid mutate request\" });\n return;\n }\n if (!handlers.onMutate) {\n reply({ type: REMOTE_VIEW_MESSAGES.mutateResult, requestId, ok: false, error: \"this view is read-only\" });\n return;\n }\n try {\n const result = await handlers.onMutate(request);\n reply({ type: REMOTE_VIEW_MESSAGES.mutateResult, requestId, ok: true, result });\n } catch (err) {\n reply({ type: REMOTE_VIEW_MESSAGES.mutateResult, requestId, ok: false, error: err instanceof Error ? err.message : String(err) });\n }\n}\n\ntype RemoteViewReply = (message: Record<string, unknown>) => void;\n\n/**\n * Handle one message-event payload from a sandboxed remote view. DOM- and\n * framework-free: the caller owns the `message` listener (and MUST verify\n * `event.source === iframe.contentWindow` before calling), `reply` posts the\n * response back into the iframe (targetOrigin `\"*\"` — the sandboxed document's\n * origin is opaque, so nothing else can match). Returns true when the payload\n * was a remote-view request for this slug (callers ignore everything else).\n */\nexport async function handleRemoteViewMessage(data: unknown, handlers: RemoteViewBridgeHandlers, reply: RemoteViewReply): Promise<boolean> {\n if (!isRecord(data)) return false;\n const msg = data;\n if (msg.slug !== handlers.slug) return false;\n if (msg.type === REMOTE_VIEW_MESSAGES.startChat) {\n const prompt = typeof msg.prompt === \"string\" ? msg.prompt.trim() : \"\";\n if (prompt) handlers.onStartChat?.(prompt, typeof msg.role === \"string\" ? msg.role : undefined);\n return true;\n }\n if (msg.type === REMOTE_VIEW_MESSAGES.mutate && typeof msg.requestId === \"string\") {\n await answerMutate(msg.requestId, msg, handlers, reply);\n return true;\n }\n if (msg.type !== REMOTE_VIEW_MESSAGES.getItems || typeof msg.requestId !== \"string\") return false;\n const request: RemoteViewPageRequest = { offset: clampOffset(msg.offset), limit: clampLimit(msg.limit), fields: normalizeFields(msg.fields) };\n await answerGetItems(msg.requestId, request, handlers, reply);\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["projectRecordFields","isRecord"],"sources":["../../src/remote-view/index.ts"],"sourcesContent":["// The remote custom-view contract (phase 3 — plans/done/feat-remote-custom-view.md).\n//\n// Browser-safe single source of truth shared by the host server (which wraps\n// the view HTML into a sandboxed srcdoc), the desktop phone-frame preview, and\n// the mulmoserver mobile client (post-publish). A remote view runs on a phone\n// that can reach the internet but NOT the host's localhost, so — unlike the\n// desktop custom view (token + fetch to the view-data route) — its records\n// arrive over an async postMessage bridge owned by the parent page, and its\n// CSP locks `connect-src` to 'none' entirely.\n//\n// BACKWARD COMPATIBILITY — this bridge is a frozen public contract. Remote\n// views are LLM-authored HTML files persisted in users' workspaces\n// (`data/skills/*/views/*.html`), written against\n// `packages/core/assets/helps/custom-view-remote.md`; they cannot be\n// migrated centrally and must keep working across host upgrades and any\n// storage-virtualization work underneath. Evolve only by backward-compatible\n// supersets, the way protocol v2 added the mutate pair: bump\n// `REMOTE_VIEW_PROTOCOL`, add new message types / optional fields — never\n// repurpose an existing message type, change the `getItems` page shape\n// (`{ items, total, offset, limit }`), or tighten limits a shipped view may\n// already rely on.\n\nimport { isRecord } from \"@mulmoclaude/common\";\nimport { projectRecordFields } from \"../collection/core/project\";\n\n/** Bump when the bootstrap/message contract changes shape; the bootstrap\n * exposes it as `__MC_VIEW.protocol` so a parent can refuse a stale view.\n * v2 (phase 4) adds the mutate pair below — a backward-compatible superset,\n * so a v1 (read-only) parent still serves get-items/start-chat unchanged. */\nexport const REMOTE_VIEW_PROTOCOL = 2;\n\n/** postMessage types between the sandboxed view and its parent page.\n * `startChat` reuses the desktop custom-view message type on purpose — the\n * desktop parent already understands it. */\nexport const REMOTE_VIEW_MESSAGES = {\n /** view → parent: request one page of records ({ requestId, offset, limit, fields }). */\n getItems: \"mc-remote-get-items\",\n /** parent → view: the reply ({ requestId, ok, page | error }). */\n items: \"mc-remote-items\",\n /** view → parent: mutate one record ({ requestId, op: \"update\"|\"delete\", id, patch? }). */\n mutate: \"mc-remote-mutate\",\n /** parent → view: the mutate reply ({ requestId, ok, result | error }). */\n mutateResult: \"mc-remote-mutate-result\",\n /** view → parent: open a new chat seeded with the prompt. Whether the parent\n * SENDS it or leaves it as an editable draft follows the view's\n * `allowSendChat` declaration, on every surface. Both read it from the\n * collection SCHEMA — never from this message, which the sandbox composes. */\n startChat: \"mc-start-chat\",\n} as const;\n\n/** Pagination defaults — mirrored by the phase-2 record handlers\n * (`server/remoteHost/handlers/collectionPage.ts` imports these) so a view\n * page can never outgrow what the command channel itself serves. */\nexport const DEFAULT_PAGE_LIMIT = 50;\nexport const MAX_PAGE_LIMIT = 200;\n\n/** Hard cap on the wrapped srcdoc: it travels to the phone INSIDE a Firestore\n * command document (1 MiB total), so leave envelope headroom. */\nexport const REMOTE_VIEW_MAX_BYTES = 900_000;\n\n/** Hard cap on ONE `getItems` page (phase 5 — plans/done/feat-remote-view-images.md).\n * Same 1 MiB command-document envelope as the srcdoc: when a view inlines image\n * fields as `data:` URLs, the host stops inlining once the serialized page would\n * exceed this, leaving the remaining image fields as their original path (which\n * the view renders as a placeholder). Guards the doc-write from ever failing. */\nexport const REMOTE_VIEW_ITEMS_MAX_BYTES = 900_000;\n\n/** Default longest-edge (px) a remote view's inlined image thumbnail is\n * downscaled to; a view may override via `imageMaxEdge`. */\nexport const DEFAULT_IMAGE_MAX_EDGE = 512;\n\n/** In-iframe `getItems` timeout — matches the remote client's `callHost`\n * response timeout so the two layers give up together. */\nconst GET_ITEMS_TIMEOUT_MS = 30_000;\n\n// CDN allowlist for sandboxed LLM-authored HTML (script/style/font loads).\n// Shared with the desktop preview + custom-view CSPs\n// (src/utils/html/previewCsp.ts re-exports it as its default) so the two\n// policies can't drift. Keep the list audited — every entry is a potential\n// supply-chain surface; the hosts here are reputable infrastructure that does\n// not expose per-request logs to third parties.\nexport const SANDBOXED_VIEW_CDN_ALLOWLIST: readonly string[] = [\n \"https://cdn.jsdelivr.net\",\n \"https://unpkg.com\",\n \"https://cdnjs.cloudflare.com\",\n \"https://fonts.googleapis.com\",\n \"https://fonts.gstatic.com\",\n // Plotly's first-party CDN — the LLM defaults to it for Plotly charts.\n \"https://cdn.plot.ly\",\n];\n\nconst toInt = (value: unknown): number | null => {\n const num = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : NaN;\n return Number.isFinite(num) ? Math.floor(num) : null;\n};\n\n/** Coerce a channel/postMessage offset (arrives as untyped JSON) to a non-negative int. */\nexport const clampOffset = (value: unknown): number => Math.max(0, toInt(value) ?? 0);\n\n/** Read a channel/postMessage identifier (a slug, a view id) as a string.\n * Params arrive as untyped JSON, so a caller can send anything; `String(...)`\n * turned an object into the literal \"[object Object]\", which then travelled on\n * as if the caller had asked for a collection by that name — surfacing as\n * `collection '[object Object]' not found` rather than a bad-request. A value\n * with no string form is simply absent. */\nexport const readIdParam = (value: unknown): string => (typeof value === \"string\" ? value : \"\");\n\n/** Coerce a channel/postMessage limit to [1, MAX_PAGE_LIMIT] (default 50). */\nexport const clampLimit = (value: unknown): number => {\n const num = toInt(value);\n if (num === null || num <= 0) return DEFAULT_PAGE_LIMIT;\n return Math.min(num, MAX_PAGE_LIMIT);\n};\n\n/** Clamp an `imageMaxEdge` (arrives as untyped schema/JSON) to [64, 1024];\n * default 512. Keeps a runaway edge from defeating the thumbnail's purpose. */\nexport const clampImageMaxEdge = (value: unknown): number => {\n const num = toInt(value);\n if (num === null || num <= 0) return DEFAULT_IMAGE_MAX_EDGE;\n return Math.min(Math.max(num, 64), 1024);\n};\n\n/** Coerce a `fields` projection list from untyped message JSON. */\nexport const normalizeFields = (value: unknown): string[] | undefined => {\n if (!Array.isArray(value)) return undefined;\n const cleaned = value\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n return cleaned.length > 0 ? cleaned : undefined;\n};\n\nexport type RemoteViewItem = Record<string, unknown>;\n\n/** One page of records, the resolved value of the view's `getItems()`. Same\n * shape as the phase-2 `getCollection` page so a parent can pass a channel\n * page straight through. */\nexport interface RemoteViewPage {\n items: RemoteViewItem[];\n total: number;\n offset: number;\n limit: number;\n}\n\n/** A normalized (clamped, fields-cleaned) page request handed to a parent's\n * `getPage` — `handleRemoteViewMessage` does the coercion so every parent\n * answers identical values. */\nexport interface RemoteViewPageRequest {\n offset: number;\n limit: number;\n fields?: string[] | undefined;\n}\n\n/** Keep only `fields` (+ always the primary key) on each record. Parents apply\n * this uniformly — the desktop preview via `pageFromItems`, the phone parent\n * over the page it fetched through the channel — so a view sees the same\n * projection everywhere. No-op without `fields`; an EMPTY `fields` array is\n * also a no-op here (frozen bridge behavior — the shared helper would\n * project down to the primary key alone). */\nexport function projectItems(items: RemoteViewItem[], fields: string[] | undefined, primaryKey: string): RemoteViewItem[] {\n if (!fields || fields.length === 0) return items;\n return projectRecordFields(items, fields, primaryKey);\n}\n\n/** Answer a page request from an already-loaded record array (the desktop\n * preview's data source): slice + project. Observable behavior matches the\n * phone paging over the command channel. */\nexport function pageFromItems(items: RemoteViewItem[], request: RemoteViewPageRequest, primaryKey: string): RemoteViewPage {\n const pageItems = items.slice(request.offset, request.offset + request.limit);\n return { items: projectItems(pageItems, request.fields, primaryKey), total: items.length, offset: request.offset, limit: request.limit };\n}\n\n/**\n * CSP for a remote (mobile) custom view. Stricter than the desktop custom-view\n * policy: the view's data arrives over postMessage, so `connect-src` is\n * `'none'` — no fetch / XHR / WebSocket / sendBeacon to ANY origin, which\n * closes the bidirectional-exfiltration channel completely (there is no token\n * to steal either). Script/style/font keep the curated CDN allowlist (the\n * phone can reach the internet; only the host is unreachable), and\n * `img-src`/`media-src` allow any `https:` host so record image/media URLs\n * render — the same knowingly-accepted one-way GET-exfil tradeoff as the\n * desktop policy (see buildCustomViewCsp in src/utils/html/previewCsp.ts).\n */\nexport function buildRemoteViewCsp(cdns: readonly string[] = SANDBOXED_VIEW_CDN_ALLOWLIST): string {\n const cdnList = cdns.join(\" \");\n return [\n \"default-src 'none'\",\n `script-src 'unsafe-inline' ${cdnList}`,\n `style-src 'unsafe-inline' ${cdnList}`,\n `font-src ${cdnList}`,\n `img-src ${cdnList} data: blob: https:`,\n \"media-src https: data: blob:\",\n \"connect-src 'none'\",\n ].join(\"; \");\n}\n\n/** The in-iframe bootstrap installed before any of the view's own scripts.\n * Owns the fiddly part of the contract — request/response correlation — so an\n * LLM-authored view only ever awaits `__MC_VIEW.getItems(...)` /\n * `.updateItem(...)` / `.deleteItem(...)`:\n *\n * - `getItems({ offset, limit, fields })`: posts an `mc-remote-get-items`\n * with a fresh `requestId`, resolves on the matching `mc-remote-items`\n * reply (validated to come from `window.parent`), rejects on `ok: false`\n * or after 30 s. targetOrigin `'*'` is safe: the request carries no secret\n * and the parent is by construction the party supplying the data.\n * - `updateItem(id, patch)` / `deleteItem(id)` (phase 4): post an\n * `mc-remote-mutate` and resolve on the matching `mc-remote-mutate-result`,\n * sharing the same `call()` correlation as `getItems`. Installed ONLY when\n * the host set `writable` (the view declared `editableFields`/`allowDelete`);\n * otherwise both reject `\"this view is read-only\"` so a mis-declared view\n * fails loudly instead of silently no-op'ing. The HOST still re-derives and\n * enforces the write policy — `writable` only gates the client surface.\n * - `startChat(prompt, role)`: same message type as the desktop bridge — the\n * parent opens a new chat seeded with `prompt`. Whether it runs follows the\n * view's `allowSendChat` declaration on every surface, read from the\n * collection schema — the phone reads it from the schema it already holds, the\n * desktop surfaces through `customViewSendsChat`. Never from this message:\n * the sandbox composes the prompt, so letting it also decide whether the\n * prompt runs would hand it both halves.\n * - `t(key, named)`: the same vue-i18n-compatible dict helper as the desktop\n * bootstrap (named interpolation only), over the host-picked `dict`.\n *\n * Self-contained one-line string (no `<`, no `</script>`, `${}` only for the\n * interpolated constants). */\nfunction remoteViewBootstrap(): string {\n return `(function(){var v=window.__MC_VIEW,seq=0,pend={};window.addEventListener('message',function(e){if(e.source!==window.parent)return;var d=e.data;if(!d)return;if(d.type!=='${REMOTE_VIEW_MESSAGES.items}'&&d.type!=='${REMOTE_VIEW_MESSAGES.mutateResult}')return;var p=pend[d.requestId];if(!p)return;delete pend[d.requestId];clearTimeout(p.timer);if(d.ok)p.resolve(d.type==='${REMOTE_VIEW_MESSAGES.items}'?d.page:d.result);else p.reject(new Error(typeof d.error==='string'?d.error:'request failed'));});function call(type,payload){return new Promise(function(resolve,reject){var id='q'+(++seq);var timer=setTimeout(function(){delete pend[id];reject(new Error(type+' timed out'));},${GET_ITEMS_TIMEOUT_MS});pend[id]={resolve:resolve,reject:reject,timer:timer};var m={type:type,slug:v.slug,requestId:id};for(var k in payload){m[k]=payload[k];}window.parent.postMessage(m,'*');});}v.getItems=function(opts){opts=opts&&typeof opts==='object'?opts:{};return call('${REMOTE_VIEW_MESSAGES.getItems}',{offset:opts.offset,limit:opts.limit,fields:opts.fields});};if(v.writable){v.updateItem=function(id,patch){return call('${REMOTE_VIEW_MESSAGES.mutate}',{op:'update',id:String(id),patch:patch&&typeof patch==='object'?patch:{}});};v.deleteItem=function(id){return call('${REMOTE_VIEW_MESSAGES.mutate}',{op:'delete',id:String(id)});};}else{v.updateItem=v.deleteItem=function(){return Promise.reject(new Error('this view is read-only'));};}v.startChat=function(prompt,role){window.parent.postMessage({type:'${REMOTE_VIEW_MESSAGES.startChat}',slug:v.slug,prompt:String(prompt),role:typeof role==='string'?role:undefined},'*');};v.dict=v.dict||{};v.t=function(key,named){var s=v.dict[key];if(typeof s!=='string')return typeof key==='string'?key:String(key);if(!named||typeof named!=='object')return s;return s.replace(/\\\\{(\\\\w+)\\\\}/g,function(m,n){var x=named[n];return x==null?m:String(x);});};})();`;\n}\n\n/** What the host injects into `window.__MC_VIEW` — note what is ABSENT\n * compared to the desktop boot: no token, no dataUrl, no origin. */\nexport interface RemoteViewBoot {\n slug: string;\n /** Locale the dict was picked for; empty string when no translations. */\n locale?: string;\n /** Host-picked, locale-filtered flat string map (same contract as the\n * desktop custom-view dict). */\n dict?: Record<string, string>;\n /** True when the view declared a mutable surface (`editableFields` and/or\n * `allowDelete`). Gates the client-side `updateItem`/`deleteItem` install\n * only — the host re-enforces the actual policy on every mutate. */\n writable?: boolean;\n}\n\n/** Wrap a view's HTML into the sandboxed srcdoc: CSP meta + `__MC_VIEW` boot +\n * bridge bootstrap injected at the start of `<head>` (before any view\n * script). Runs HOST-side (`getRemoteView`) so the phone and the desktop\n * preview receive the identical finished artifact. */\nexport function buildRemoteViewSrcdoc(html: string, boot: RemoteViewBoot): string {\n const cspMeta = `<meta http-equiv=\"Content-Security-Policy\" content=\"${buildRemoteViewCsp()}\">`;\n // `<`-escape the JSON so a hostile slug/dict string can't break out of the\n // <script> element (same escape as the desktop srcdoc builder).\n const json = JSON.stringify({\n slug: boot.slug,\n locale: boot.locale ?? \"\",\n dict: boot.dict ?? {},\n target: \"mobile\",\n protocol: REMOTE_VIEW_PROTOCOL,\n writable: boot.writable ?? false,\n }).replace(/</g, \"\\\\u003c\");\n const injection = `${cspMeta}<script>window.__MC_VIEW=${json};${remoteViewBootstrap()}</script>`;\n if (/<head\\b[^>]*>/i.test(html)) {\n return html.replace(/(<head\\b[^>]*>)/i, `$1${injection}`);\n }\n return `<!DOCTYPE html><html><head>${injection}</head><body>${html}</body></html>`;\n}\n\n/** A normalized mutate request handed to a parent's `onMutate`\n * (`handleRemoteViewMessage` validates op/id/patch first). `update` carries a\n * partial record; the HOST decides which keys are actually writable. */\nexport type RemoteViewMutateRequest = { op: \"update\"; id: string; patch: Record<string, unknown> } | { op: \"delete\"; id: string };\n\n/** The resolved value of a mutate: the merged record for an update, the removed\n * id for a delete. Sent back to the view as the `mc-remote-mutate-result`\n * `result`. */\nexport interface RemoteViewMutateResult {\n item?: RemoteViewItem;\n id?: string;\n}\n\n/** What a parent page provides to answer the sandboxed view. Deliberately\n * minimal — exactly the phone runtime's capabilities, nothing more, so the\n * desktop preview can never exceed what works on the phone. */\nexport interface RemoteViewBridgeHandlers {\n slug: string;\n /** Answer one normalized page request (already clamped + fields-cleaned). */\n getPage: (request: RemoteViewPageRequest) => Promise<RemoteViewPage> | RemoteViewPage;\n /** Apply one normalized mutate (update/delete). Omit on a read-only parent —\n * the handler then replies `ok: false, \"this view is read-only\"`. The parent\n * forwards to the host (which enforces the write policy authoritatively). */\n onMutate?: (request: RemoteViewMutateRequest) => Promise<RemoteViewMutateResult> | RemoteViewMutateResult;\n /** Relay a `startChat` draft; omit on a parent without a chat surface. */\n onStartChat?: (prompt: string, role?: string) => void;\n}\n\n/** Coerce an untyped `mc-remote-mutate` payload to a normalized request, or\n * null when it is malformed (unknown op, missing id, non-object update patch —\n * the parent then replies with an `\"invalid mutate request\"` error). */\nexport function normalizeMutate(data: { op?: unknown; id?: unknown; patch?: unknown }): RemoteViewMutateRequest | null {\n const itemId = typeof data.id === \"string\" ? data.id : typeof data.id === \"number\" && Number.isFinite(data.id) ? String(data.id) : \"\";\n if (!itemId) return null;\n if (data.op === \"delete\") return { op: \"delete\", id: itemId };\n if (data.op === \"update\") {\n const { patch } = data;\n if (!isRecord(patch)) return null;\n return { op: \"update\", id: itemId, patch };\n }\n return null;\n}\n\nasync function answerGetItems(requestId: string, request: RemoteViewPageRequest, handlers: RemoteViewBridgeHandlers, reply: RemoteViewReply): Promise<void> {\n try {\n const page = await handlers.getPage(request);\n reply({ type: REMOTE_VIEW_MESSAGES.items, requestId, ok: true, page });\n } catch (err) {\n reply({ type: REMOTE_VIEW_MESSAGES.items, requestId, ok: false, error: err instanceof Error ? err.message : String(err) });\n }\n}\n\n/** Validate + dispatch a `mc-remote-mutate` payload, replying on every path\n * (malformed request, read-only parent, handler success/throw). Split out of\n * `handleRemoteViewMessage` so that function stays under the 20-line limit. */\nasync function answerMutate(\n requestId: string,\n msg: { op?: unknown; id?: unknown; patch?: unknown },\n handlers: RemoteViewBridgeHandlers,\n reply: RemoteViewReply,\n): Promise<void> {\n const request = normalizeMutate(msg);\n if (!request) {\n reply({ type: REMOTE_VIEW_MESSAGES.mutateResult, requestId, ok: false, error: \"invalid mutate request\" });\n return;\n }\n if (!handlers.onMutate) {\n reply({ type: REMOTE_VIEW_MESSAGES.mutateResult, requestId, ok: false, error: \"this view is read-only\" });\n return;\n }\n try {\n const result = await handlers.onMutate(request);\n reply({ type: REMOTE_VIEW_MESSAGES.mutateResult, requestId, ok: true, result });\n } catch (err) {\n reply({ type: REMOTE_VIEW_MESSAGES.mutateResult, requestId, ok: false, error: err instanceof Error ? err.message : String(err) });\n }\n}\n\ntype RemoteViewReply = (message: Record<string, unknown>) => void;\n\n/**\n * Handle one message-event payload from a sandboxed remote view. DOM- and\n * framework-free: the caller owns the `message` listener (and MUST verify\n * `event.source === iframe.contentWindow` before calling), `reply` posts the\n * response back into the iframe (targetOrigin `\"*\"` — the sandboxed document's\n * origin is opaque, so nothing else can match). Returns true when the payload\n * was a remote-view request for this slug (callers ignore everything else).\n */\nexport async function handleRemoteViewMessage(data: unknown, handlers: RemoteViewBridgeHandlers, reply: RemoteViewReply): Promise<boolean> {\n if (!isRecord(data)) return false;\n const msg = data;\n if (msg.slug !== handlers.slug) return false;\n if (msg.type === REMOTE_VIEW_MESSAGES.startChat) {\n const prompt = typeof msg.prompt === \"string\" ? msg.prompt.trim() : \"\";\n if (prompt) handlers.onStartChat?.(prompt, typeof msg.role === \"string\" ? msg.role : undefined);\n return true;\n }\n if (msg.type === REMOTE_VIEW_MESSAGES.mutate && typeof msg.requestId === \"string\") {\n await answerMutate(msg.requestId, msg, handlers, reply);\n return true;\n }\n if (msg.type !== REMOTE_VIEW_MESSAGES.getItems || typeof msg.requestId !== \"string\") return false;\n const request: RemoteViewPageRequest = { offset: clampOffset(msg.offset), limit: clampLimit(msg.limit), fields: normalizeFields(msg.fields) };\n await answerGetItems(msg.requestId, request, handlers, reply);\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as isRecord } from "../dist-MFUV9FYo.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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-BvwBUcol.cjs","names":[],"sources":["../src/files/atomic.ts","../src/files/root.ts"],"sourcesContent":["// rename(2) is atomic on POSIX; Node's Windows fallback (copy+unlink) is still safer than truncating in place.\n// Readers always see either the old file or the new — never a half-written one.\n//\n// Single source of truth for atomic file writes across the whole monorepo\n// (host, core, plugins). Previously copy-pasted four times, so a Windows retry\n// fix landed in only one (#2399, precedent #2222).\n\nimport { mkdirSync, promises, renameSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { randomBytes } from \"node:crypto\";\n\nexport interface WriteAtomicOptions {\n mode?: number;\n /** Give the staging file a random suffix so concurrent writers to the same\n * destination can't collide at the OS layer.\n *\n * Defaults to `true`, and opting out is almost always wrong. A shared\n * `${filePath}.tmp` means two writers of one file race: the second write\n * overwrites the first's staging file, or one rename/unlink pulls it out\n * from under the other, surfacing as `ENOENT … rename '<file>.tmp'`. That is\n * not theoretical — it fired in production on session meta, where ten\n * distinct callers (`setClaudeSessionId`, `backfillOrigin`,\n * `incrementUserQueryCount`, …) all write the same `<sessionId>.json`\n * (#2222).\n *\n * Pass `false` only when you specifically need the staging path to be\n * predictable (e.g. a single-writer token file, or a test that pre-creates\n * it to force a write failure). */\n uniqueTmp?: boolean;\n}\n\n// Unique staging names are the safe default: the cost is a random suffix, the\n// cost of the alternative is a lost update under concurrency (#2222).\nconst DEFAULT_UNIQUE_TMP = true;\n\nfunction tmpPathFor(filePath: string, uniqueTmp: boolean | undefined): string {\n return (uniqueTmp ?? DEFAULT_UNIQUE_TMP) ? `${filePath}.${randomBytes(6).toString(\"hex\")}.tmp` : `${filePath}.tmp`;\n}\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and rename trips EPERM/EBUSY/EACCES. Retry loop is\n// gated to Windows because POSIX EPERM means a real perm problem (read-only fs, sticky, cross-device) — retrying\n// just adds latency before the inevitable throw.\nconst IS_WINDOWS = process.platform === \"win32\";\n// The ASYNC ladder is generous because waiting is nearly free there — the call\n// yields, and a write that would otherwise throw is worth a few seconds. The\n// previous ~430 ms budget was not enough on a loaded Windows CI runner: the\n// notifier's `active.json` write lost the rename and threw, and the listener\n// waiting on it timed out ten seconds later, so the operation failed while the\n// contention it was retrying through had long since cleared (#2972 follow-up).\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300, 600, 1000, 2000] as const;\n// The SYNC ladder stays short on purpose. `sleepSync` parks the whole thread,\n// so a long budget here does not delay one write — it freezes the process. A\n// caller that cannot afford to yield cannot afford to wait either; failing\n// after ~430 ms and letting the caller decide beats a four-second stall.\nconst RENAME_RETRY_DELAYS_SYNC_MS = [30, 100, 300] as const;\n\nfunction hasErrnoCode(err: unknown): err is { code: string } {\n return typeof err === \"object\" && err !== null && \"code\" in err && typeof err.code === \"string\";\n}\n\n// `isWindows` is a parameter (defaulting to the real platform) so the safety-critical decision is testable on any OS.\n/** The retry budgets, exported so a test can assert what they cost rather than\n * hard-coding a second copy of the numbers. */\nexport const RENAME_RETRY_BUDGET_MS = RENAME_RETRY_DELAYS_MS.reduce((total, delay) => total + delay, 0);\nexport const RENAME_RETRY_BUDGET_SYNC_MS = RENAME_RETRY_DELAYS_SYNC_MS.reduce((total, delay) => total + delay, 0);\n\nexport function isTransientRenameError(err: unknown, isWindows: boolean = IS_WINDOWS): boolean {\n if (!isWindows || !hasErrnoCode(err)) return false;\n return err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\";\n}\n\n// Injectable so a test can drive the retry path (fail-then-succeed rename, no-op sleep, isWindows=true) on any OS.\nexport interface RenameRetryDeps {\n rename: (fromPath: string, toPath: string) => Promise<void>;\n sleep: (millis: number) => Promise<void>;\n isWindows: boolean;\n}\n\nconst defaultRenameRetryDeps: RenameRetryDeps = {\n rename: (fromPath, toPath) => promises.rename(fromPath, toPath),\n sleep: (millis) => new Promise((resolve) => setTimeout(resolve, millis)),\n isWindows: IS_WINDOWS,\n};\n\nexport async function renameWithWindowsRetry(fromPath: string, toPath: string, deps: RenameRetryDeps = defaultRenameRetryDeps): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await deps.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err, deps.isWindows)) throw err;\n await deps.sleep(delayMs);\n }\n }\n // Final attempt — let any error propagate.\n await deps.rename(fromPath, toPath);\n}\n\n// Atomics.wait parks the thread instead of busy-spinning. Only on the Windows-rename retry path, and only on the SYNC\n// ladder, whose budget is capped at ~430ms precisely because this blocks everything.\nconst SYNC_SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));\nfunction sleepSync(millis: number): void {\n Atomics.wait(SYNC_SLEEP_BUF, 0, 0, millis);\n}\n\n// Deliberate async/sync twin of RenameRetryDeps: `sleep` blocks the thread instead of returning a Promise.\nexport interface RenameRetryDepsSync {\n rename: (fromPath: string, toPath: string) => void;\n sleep: (millis: number) => void;\n isWindows: boolean;\n}\n\nconst defaultRenameRetryDepsSync: RenameRetryDepsSync = {\n rename: (fromPath, toPath) => renameSync(fromPath, toPath),\n sleep: sleepSync,\n isWindows: IS_WINDOWS,\n};\n\nexport function renameSyncWithWindowsRetry(fromPath: string, toPath: string, deps: RenameRetryDepsSync = defaultRenameRetryDepsSync): void {\n for (const delayMs of RENAME_RETRY_DELAYS_SYNC_MS) {\n try {\n deps.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err, deps.isWindows)) throw err;\n deps.sleep(delayMs);\n }\n }\n deps.rename(fromPath, toPath);\n}\n\n// Forcing utf-8 on a Uint8Array would re-encode the bytes — wrong for PNGs and other binary blobs.\nfunction writeOptionsFor(content: string | Uint8Array, mode: number | undefined): { encoding?: \"utf-8\" | undefined; mode?: number | undefined } {\n return typeof content === \"string\" ? { encoding: \"utf-8\", mode } : { mode };\n}\n\nexport async function writeFileAtomic(filePath: string, content: string | Uint8Array, opts: WriteAtomicOptions = {}): Promise<void> {\n const tmp = tmpPathFor(filePath, opts.uniqueTmp);\n await promises.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await promises.writeFile(tmp, content, writeOptionsFor(content, opts.mode));\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await promises.unlink(tmp).catch(() => {});\n throw err;\n }\n}\n\nexport function writeFileAtomicSync(filePath: string, content: string | Uint8Array, opts: WriteAtomicOptions = {}): void {\n const tmp = tmpPathFor(filePath, opts.uniqueTmp);\n mkdirSync(path.dirname(filePath), { recursive: true });\n try {\n writeFileSync(tmp, content, writeOptionsFor(content, opts.mode));\n renameSyncWithWindowsRetry(tmp, filePath);\n } catch (err) {\n try {\n unlinkSync(tmp);\n } catch {\n // best-effort cleanup\n }\n throw err;\n }\n}\n","// The canonical form of a workspace / project root.\n//\n// Lives here rather than in the collection engine because a root is an IDENTITY\n// in several subsystems that do not depend on each other — a watcher generation\n// key, a change payload, a completion-bell id, a scheduled task id — and every\n// one of them has to agree on it or the same project registers twice.\n\nimport path from \"node:path\";\n\n/** The canonical form of a root, for every place a root becomes an IDENTITY\n * rather than a path to read.\n *\n * `path.resolve` only — it collapses `.`/`..` and the trailing separator, so\n * `/work/project` and `/work/project/` are one root instead of two watcher\n * generations over the same tree, two bells per record, and two scheduled\n * refresh jobs.\n *\n * Deliberately NOT `realpath`. Resolving symlinks is async (so it could not run\n * on a synchronous claim path), it fails for a root that does not exist yet,\n * and it would put a path the host never named into an id that is written to\n * disk. The policy is therefore lexical: a host that hands the same tree under\n * two different symlink spellings gets two roots, and it is the host's job to\n * name a project the same way every time. */\nexport function canonicalRoot(root: string): string {\n return path.resolve(root);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"root-BvwBUcol.cjs","names":["randomBytes","promises","renameSync","path","path"],"sources":["../src/files/atomic.ts","../src/files/root.ts"],"sourcesContent":["// rename(2) is atomic on POSIX; Node's Windows fallback (copy+unlink) is still safer than truncating in place.\n// Readers always see either the old file or the new — never a half-written one.\n//\n// Single source of truth for atomic file writes across the whole monorepo\n// (host, core, plugins). Previously copy-pasted four times, so a Windows retry\n// fix landed in only one (#2399, precedent #2222).\n\nimport { mkdirSync, promises, renameSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { randomBytes } from \"node:crypto\";\n\nexport interface WriteAtomicOptions {\n mode?: number;\n /** Give the staging file a random suffix so concurrent writers to the same\n * destination can't collide at the OS layer.\n *\n * Defaults to `true`, and opting out is almost always wrong. A shared\n * `${filePath}.tmp` means two writers of one file race: the second write\n * overwrites the first's staging file, or one rename/unlink pulls it out\n * from under the other, surfacing as `ENOENT … rename '<file>.tmp'`. That is\n * not theoretical — it fired in production on session meta, where ten\n * distinct callers (`setClaudeSessionId`, `backfillOrigin`,\n * `incrementUserQueryCount`, …) all write the same `<sessionId>.json`\n * (#2222).\n *\n * Pass `false` only when you specifically need the staging path to be\n * predictable (e.g. a single-writer token file, or a test that pre-creates\n * it to force a write failure). */\n uniqueTmp?: boolean;\n}\n\n// Unique staging names are the safe default: the cost is a random suffix, the\n// cost of the alternative is a lost update under concurrency (#2222).\nconst DEFAULT_UNIQUE_TMP = true;\n\nfunction tmpPathFor(filePath: string, uniqueTmp: boolean | undefined): string {\n return (uniqueTmp ?? DEFAULT_UNIQUE_TMP) ? `${filePath}.${randomBytes(6).toString(\"hex\")}.tmp` : `${filePath}.tmp`;\n}\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and rename trips EPERM/EBUSY/EACCES. Retry loop is\n// gated to Windows because POSIX EPERM means a real perm problem (read-only fs, sticky, cross-device) — retrying\n// just adds latency before the inevitable throw.\nconst IS_WINDOWS = process.platform === \"win32\";\n// The ASYNC ladder is generous because waiting is nearly free there — the call\n// yields, and a write that would otherwise throw is worth a few seconds. The\n// previous ~430 ms budget was not enough on a loaded Windows CI runner: the\n// notifier's `active.json` write lost the rename and threw, and the listener\n// waiting on it timed out ten seconds later, so the operation failed while the\n// contention it was retrying through had long since cleared (#2972 follow-up).\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300, 600, 1000, 2000] as const;\n// The SYNC ladder stays short on purpose. `sleepSync` parks the whole thread,\n// so a long budget here does not delay one write — it freezes the process. A\n// caller that cannot afford to yield cannot afford to wait either; failing\n// after ~430 ms and letting the caller decide beats a four-second stall.\nconst RENAME_RETRY_DELAYS_SYNC_MS = [30, 100, 300] as const;\n\nfunction hasErrnoCode(err: unknown): err is { code: string } {\n return typeof err === \"object\" && err !== null && \"code\" in err && typeof err.code === \"string\";\n}\n\n// `isWindows` is a parameter (defaulting to the real platform) so the safety-critical decision is testable on any OS.\n/** The retry budgets, exported so a test can assert what they cost rather than\n * hard-coding a second copy of the numbers. */\nexport const RENAME_RETRY_BUDGET_MS = RENAME_RETRY_DELAYS_MS.reduce((total, delay) => total + delay, 0);\nexport const RENAME_RETRY_BUDGET_SYNC_MS = RENAME_RETRY_DELAYS_SYNC_MS.reduce((total, delay) => total + delay, 0);\n\nexport function isTransientRenameError(err: unknown, isWindows: boolean = IS_WINDOWS): boolean {\n if (!isWindows || !hasErrnoCode(err)) return false;\n return err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\";\n}\n\n// Injectable so a test can drive the retry path (fail-then-succeed rename, no-op sleep, isWindows=true) on any OS.\nexport interface RenameRetryDeps {\n rename: (fromPath: string, toPath: string) => Promise<void>;\n sleep: (millis: number) => Promise<void>;\n isWindows: boolean;\n}\n\nconst defaultRenameRetryDeps: RenameRetryDeps = {\n rename: (fromPath, toPath) => promises.rename(fromPath, toPath),\n sleep: (millis) => new Promise((resolve) => setTimeout(resolve, millis)),\n isWindows: IS_WINDOWS,\n};\n\nexport async function renameWithWindowsRetry(fromPath: string, toPath: string, deps: RenameRetryDeps = defaultRenameRetryDeps): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await deps.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err, deps.isWindows)) throw err;\n await deps.sleep(delayMs);\n }\n }\n // Final attempt — let any error propagate.\n await deps.rename(fromPath, toPath);\n}\n\n// Atomics.wait parks the thread instead of busy-spinning. Only on the Windows-rename retry path, and only on the SYNC\n// ladder, whose budget is capped at ~430ms precisely because this blocks everything.\nconst SYNC_SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));\nfunction sleepSync(millis: number): void {\n Atomics.wait(SYNC_SLEEP_BUF, 0, 0, millis);\n}\n\n// Deliberate async/sync twin of RenameRetryDeps: `sleep` blocks the thread instead of returning a Promise.\nexport interface RenameRetryDepsSync {\n rename: (fromPath: string, toPath: string) => void;\n sleep: (millis: number) => void;\n isWindows: boolean;\n}\n\nconst defaultRenameRetryDepsSync: RenameRetryDepsSync = {\n rename: (fromPath, toPath) => renameSync(fromPath, toPath),\n sleep: sleepSync,\n isWindows: IS_WINDOWS,\n};\n\nexport function renameSyncWithWindowsRetry(fromPath: string, toPath: string, deps: RenameRetryDepsSync = defaultRenameRetryDepsSync): void {\n for (const delayMs of RENAME_RETRY_DELAYS_SYNC_MS) {\n try {\n deps.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err, deps.isWindows)) throw err;\n deps.sleep(delayMs);\n }\n }\n deps.rename(fromPath, toPath);\n}\n\n// Forcing utf-8 on a Uint8Array would re-encode the bytes — wrong for PNGs and other binary blobs.\nfunction writeOptionsFor(content: string | Uint8Array, mode: number | undefined): { encoding?: \"utf-8\" | undefined; mode?: number | undefined } {\n return typeof content === \"string\" ? { encoding: \"utf-8\", mode } : { mode };\n}\n\nexport async function writeFileAtomic(filePath: string, content: string | Uint8Array, opts: WriteAtomicOptions = {}): Promise<void> {\n const tmp = tmpPathFor(filePath, opts.uniqueTmp);\n await promises.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await promises.writeFile(tmp, content, writeOptionsFor(content, opts.mode));\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await promises.unlink(tmp).catch(() => {});\n throw err;\n }\n}\n\nexport function writeFileAtomicSync(filePath: string, content: string | Uint8Array, opts: WriteAtomicOptions = {}): void {\n const tmp = tmpPathFor(filePath, opts.uniqueTmp);\n mkdirSync(path.dirname(filePath), { recursive: true });\n try {\n writeFileSync(tmp, content, writeOptionsFor(content, opts.mode));\n renameSyncWithWindowsRetry(tmp, filePath);\n } catch (err) {\n try {\n unlinkSync(tmp);\n } catch {\n // best-effort cleanup\n }\n throw err;\n }\n}\n","// The canonical form of a workspace / project root.\n//\n// Lives here rather than in the collection engine because a root is an IDENTITY\n// in several subsystems that do not depend on each other — a watcher generation\n// key, a change payload, a completion-bell id, a scheduled task id — and every\n// one of them has to agree on it or the same project registers twice.\n\nimport path from \"node:path\";\n\n/** The canonical form of a root, for every place a root becomes an IDENTITY\n * rather than a path to read.\n *\n * `path.resolve` only — it collapses `.`/`..` and the trailing separator, so\n * `/work/project` and `/work/project/` are one root instead of two watcher\n * generations over the same tree, two bells per record, and two scheduled\n * refresh jobs.\n *\n * Deliberately NOT `realpath`. Resolving symlinks is async (so it could not run\n * on a synchronous claim path), it fails for a root that does not exist yet,\n * and it would put a path the host never named into an id that is written to\n * disk. The policy is therefore lexical: a host that hands the same tree under\n * two different symlink spellings gets two roots, and it is the host's job to\n * name a project the same way every time. */\nexport function canonicalRoot(root: string): string {\n return path.resolve(root);\n}\n"],"mappings":";;;;;;AAiCA,IAAM,qBAAqB;AAE3B,SAAS,WAAW,UAAkB,WAAwC;CAC5E,OAAQ,aAAa,qBAAsB,GAAG,SAAS,IAAA,GAAGA,YAAAA,YAAAA,CAAY,CAAC,CAAC,CAAC,SAAS,KAAK,EAAE,QAAQ,GAAG,SAAS;AAC/G;AAKA,IAAM,aAAa,QAAQ,aAAa;AAOxC,IAAM,yBAAyB;CAAC;CAAI;CAAK;CAAK;CAAK;CAAM;AAAI;AAK7D,IAAM,8BAA8B;CAAC;CAAI;CAAK;AAAG;AAEjD,SAAS,aAAa,KAAuC;CAC3D,OAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,UAAU,OAAO,OAAO,IAAI,SAAS;AACzF;AAKsC,uBAAuB,QAAQ,OAAO,UAAU,QAAQ,OAAO,CAAC;AAC3D,4BAA4B,QAAQ,OAAO,UAAU,QAAQ,OAAO,CAAC;AAEhH,SAAgB,uBAAuB,KAAc,YAAqB,YAAqB;CAC7F,IAAI,CAAC,aAAa,CAAC,aAAa,GAAG,GAAG,OAAO;CAC7C,OAAO,IAAI,SAAS,WAAW,IAAI,SAAS,WAAW,IAAI,SAAS;AACtE;AASA,IAAM,yBAA0C;CAC9C,SAAS,UAAU,WAAWC,QAAAA,SAAS,OAAO,UAAU,MAAM;CAC9D,QAAQ,WAAW,IAAI,SAAS,YAAY,WAAW,SAAS,MAAM,CAAC;CACvE,WAAW;AACb;AAEA,eAAsB,uBAAuB,UAAkB,QAAgB,OAAwB,wBAAuC;CAC5I,KAAK,MAAM,WAAW,wBACpB,IAAI;EACF,MAAM,KAAK,OAAO,UAAU,MAAM;EAClC;CACF,SAAS,KAAK;EACZ,IAAI,CAAC,uBAAuB,KAAK,KAAK,SAAS,GAAG,MAAM;EACxD,MAAM,KAAK,MAAM,OAAO;CAC1B;CAGF,MAAM,KAAK,OAAO,UAAU,MAAM;AACpC;AAIA,IAAM,iBAAiB,IAAI,WAAW,IAAI,kBAAkB,CAAC,CAAC;AAC9D,SAAS,UAAU,QAAsB;CACvC,QAAQ,KAAK,gBAAgB,GAAG,GAAG,MAAM;AAC3C;AASA,IAAM,6BAAkD;CACtD,SAAS,UAAU,YAAA,GAAWC,QAAAA,WAAAA,CAAW,UAAU,MAAM;CACzD,OAAO;CACP,WAAW;AACb;AAEA,SAAgB,2BAA2B,UAAkB,QAAgB,OAA4B,4BAAkC;CACzI,KAAK,MAAM,WAAW,6BACpB,IAAI;EACF,KAAK,OAAO,UAAU,MAAM;EAC5B;CACF,SAAS,KAAK;EACZ,IAAI,CAAC,uBAAuB,KAAK,KAAK,SAAS,GAAG,MAAM;EACxD,KAAK,MAAM,OAAO;CACpB;CAEF,KAAK,OAAO,UAAU,MAAM;AAC9B;AAGA,SAAS,gBAAgB,SAA8B,MAAyF;CAC9I,OAAO,OAAO,YAAY,WAAW;EAAE,UAAU;EAAS;CAAK,IAAI,EAAE,KAAK;AAC5E;AAEA,eAAsB,gBAAgB,UAAkB,SAA8B,OAA2B,CAAC,GAAkB;CAClI,MAAM,MAAM,WAAW,UAAU,KAAK,SAAS;CAC/C,MAAMD,QAAAA,SAAS,MAAME,UAAAA,QAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;CAChE,IAAI;EACF,MAAMF,QAAAA,SAAS,UAAU,KAAK,SAAS,gBAAgB,SAAS,KAAK,IAAI,CAAC;EAC1E,MAAM,uBAAuB,KAAK,QAAQ;CAC5C,SAAS,KAAK;EACZ,MAAMA,QAAAA,SAAS,OAAO,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;EACzC,MAAM;CACR;AACF;AAEA,SAAgB,oBAAoB,UAAkB,SAA8B,OAA2B,CAAC,GAAS;CACvH,MAAM,MAAM,WAAW,UAAU,KAAK,SAAS;CAC/C,CAAA,GAAA,QAAA,UAAA,CAAUE,UAAAA,QAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;CACrD,IAAI;EACF,CAAA,GAAA,QAAA,cAAA,CAAc,KAAK,SAAS,gBAAgB,SAAS,KAAK,IAAI,CAAC;EAC/D,2BAA2B,KAAK,QAAQ;CAC1C,SAAS,KAAK;EACZ,IAAI;GACF,CAAA,GAAA,QAAA,WAAA,CAAW,GAAG;EAChB,QAAQ,CAER;EACA,MAAM;CACR;AACF;;;;;;;;;;;;;;;;;AC3IA,SAAgB,cAAc,MAAsB;CAClD,OAAOC,UAAAA,QAAK,QAAQ,IAAI;AAC1B"}
|
package/dist/scheduler/index.cjs
CHANGED
|
@@ -1,7 +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-
|
|
4
|
-
require("../errors-
|
|
3
|
+
const require_dist = require("../dist-BZ62pKuU.cjs");
|
|
4
|
+
require("../errors-B0xloJ3E.cjs");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
let node_path = require("node:path");
|
|
7
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":["ONE_SECOND_MS","parseTimeToMs","SCHEDULE_TYPES","isDueAt","nextWindowAfter","NOOP_LOG","path","readFile","existsSync","appendFile","mkdir","loadState","computeCatchUpPlan","TASK_TRIGGERS","queryLog","emptyState","errorMessage","updateAndSave","TASK_RESULTS","appendLogEntry","nextWindowAfter"],"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,5 +1,5 @@
|
|
|
1
|
-
import { t as errorMessage } from "../dist-
|
|
2
|
-
import "../errors-
|
|
1
|
+
import { t as errorMessage } from "../dist-MFUV9FYo.js";
|
|
2
|
+
import "../errors-zTAsW7dS.js";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|