@ixo/oracle-runtime-workers 0.2.0

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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * When the user object may rebuild its working copy without free pages.
3
+ *
4
+ * `VACUUM` (via `DoSqliteDatabase.compact`) holds the transaction lock for
5
+ * the whole rewrite and transiently doubles the file in DO storage, so it
6
+ * only runs on a quiet object with a file that is actually worth it. The
7
+ * predicate is pure so the thresholds are testable; the object supplies the
8
+ * numbers from its alarm tick.
9
+ */
10
+
11
+ /** No turn for this long before the object counts as quiet. */
12
+ export const VACUUM_IDLE_MS = 10 * 60_000;
13
+ /** Files below this are cheap enough to leave alone. */
14
+ export const VACUUM_MIN_FILE_BYTES = 4 * 1024 * 1024;
15
+ /** Free-page share that makes the rewrite worth its cost. */
16
+ export const VACUUM_MIN_FREE_SHARE = 0.2;
17
+ /** At most one rebuild per object per this interval. */
18
+ export const VACUUM_MIN_INTERVAL_MS = 6 * 60 * 60_000;
19
+ /** DO SQLite storage cap; the rewrite needs 2× the file below it. */
20
+ export const DO_STORAGE_CAP_BYTES = 10 * 1024 * 1024 * 1024;
21
+
22
+ export interface VacuumPolicyInput {
23
+ now: number;
24
+ lastAccessAt: number;
25
+ lastVacuumAt: number | undefined;
26
+ /** Unflushed changes pending for the owner store. */
27
+ dirty: boolean;
28
+ /** An owner-store flush is reading the file right now. */
29
+ exporting: boolean;
30
+ fileBytes: number;
31
+ pageCount: number;
32
+ freelistCount: number;
33
+ }
34
+
35
+ export type VacuumVerdict =
36
+ | { vacuum: true; freeShare: number }
37
+ | { vacuum: false; reason: string; freeShare: number };
38
+
39
+ export function shouldVacuum(input: VacuumPolicyInput): VacuumVerdict {
40
+ const freeShare =
41
+ input.pageCount > 0 ? input.freelistCount / input.pageCount : 0;
42
+ const no = (reason: string): VacuumVerdict => ({
43
+ vacuum: false,
44
+ reason,
45
+ freeShare,
46
+ });
47
+ if (input.exporting) return no('owner-store flush in progress');
48
+ if (input.dirty) return no('working copy dirty (flush first)');
49
+ if (input.now - input.lastAccessAt < VACUUM_IDLE_MS)
50
+ return no('object active within the idle window');
51
+ if (input.fileBytes < VACUUM_MIN_FILE_BYTES) return no('file too small');
52
+ if (freeShare <= VACUUM_MIN_FREE_SHARE)
53
+ return no('free share below threshold');
54
+ if (
55
+ input.lastVacuumAt !== undefined &&
56
+ input.now - input.lastVacuumAt < VACUUM_MIN_INTERVAL_MS
57
+ )
58
+ return no('vacuumed recently');
59
+ if (input.fileBytes * 2 >= DO_STORAGE_CAP_BYTES * 0.9)
60
+ return no('not enough storage headroom for the rewrite');
61
+ return { vacuum: true, freeShare };
62
+ }
63
+
64
+ /**
65
+ * Whether the numbers say a rebuild is wanted at all (ignoring quiet-time
66
+ * and interval gates) — used to re-arm the alarm for a later check.
67
+ */
68
+ export function vacuumWanted(input: VacuumPolicyInput): boolean {
69
+ const verdict = shouldVacuum({
70
+ ...input,
71
+ dirty: false,
72
+ exporting: false,
73
+ lastAccessAt: input.now - VACUUM_IDLE_MS,
74
+ lastVacuumAt: undefined,
75
+ });
76
+ return verdict.vacuum;
77
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Instantiates wa-sqlite (SQLite compiled to WebAssembly) on workerd.
3
+ *
4
+ * Why this file exists:
5
+ * - workerd disallows runtime wasm compilation (`WebAssembly.compile`,
6
+ * `new WebAssembly.Module(bytes)`, `instantiateStreaming`). The Emscripten
7
+ * factory in `wa-sqlite/dist/wa-sqlite.mjs` would normally fetch + compile
8
+ * `wa-sqlite.wasm`; instead we import the `.wasm` as a build-time
9
+ * `CompiledWasm` module (a ready `WebAssembly.Module`) and hand it to the
10
+ * factory through its `instantiateWasm(imports, receiveInstance)` hook,
11
+ * which only needs the synchronous `new WebAssembly.Instance(module, imports)`.
12
+ * - We use the SYNC build (`wa-sqlite.mjs`, not the Asyncify `-async` build):
13
+ * Durable Object storage (`ctx.storage.sql.exec`) is synchronous, so the VFS
14
+ * can be fully synchronous — faster and far simpler than Asyncify.
15
+ *
16
+ * Contract verified against `wa-sqlite@1.0.0`:
17
+ * - `dist/wa-sqlite.mjs` default export: `(moduleArg?) => Promise<Module>`.
18
+ * If `moduleArg.instantiateWasm` is set it is called as
19
+ * `instantiateWasm(imports, receiveInstance)` and must return the exports.
20
+ * Without `moduleArg.locateFile` the factory evaluates
21
+ * `new URL('wa-sqlite.wasm', import.meta.url)` eagerly — we set `locateFile`
22
+ * to skip that (it's never fetched anyway).
23
+ * - `src/sqlite-api.js` `Factory(module)` builds the JS API; `vfs_register`
24
+ * delegates to `module.registerVFS(vfs, makeDefault)` which throws when a VFS
25
+ * name is already registered (there is no unregister), hence the registry
26
+ * below.
27
+ *
28
+ * One Emscripten module instance is cached per isolate. Many Durable Objects in
29
+ * the same isolate share it; each DO registers its own VFS (named after the DO
30
+ * id) so different objects open different databases on the same instance.
31
+ */
32
+ import './wasm.d';
33
+ import * as SQLite from 'wa-sqlite';
34
+ import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite.mjs';
35
+ import wasmModule from 'wa-sqlite/dist/wa-sqlite.wasm';
36
+
37
+ /** The subset of the Emscripten module surface this runtime relies on. */
38
+ export interface WaSqliteModule {
39
+ /** Current wasm heap view (re-created by Emscripten when memory grows). */
40
+ HEAPU8: Uint8Array;
41
+ registerVFS(vfs: SQLiteVFS, makeDefault?: boolean): number;
42
+ }
43
+
44
+ export interface SqliteRuntime {
45
+ sqlite3: SQLiteAPI;
46
+ module: WaSqliteModule;
47
+ }
48
+
49
+ interface ModuleFactoryArgs {
50
+ locateFile(path: string, scriptDirectory: string): string;
51
+ instantiateWasm(
52
+ imports: WebAssembly.Imports,
53
+ receiveInstance: (instance: WebAssembly.Instance) => void,
54
+ ): WebAssembly.Exports;
55
+ }
56
+
57
+ let runtimePromise: Promise<SqliteRuntime> | undefined;
58
+
59
+ /**
60
+ * Returns the per-isolate SQLite runtime, instantiating it on first use.
61
+ * Safe to call concurrently; all callers share one promise.
62
+ */
63
+ export function loadSqlite(): Promise<SqliteRuntime> {
64
+ runtimePromise ??= instantiate().catch((error: unknown) => {
65
+ // Let a later call retry instead of caching a rejected promise forever.
66
+ runtimePromise = undefined;
67
+ throw error;
68
+ });
69
+ return runtimePromise;
70
+ }
71
+
72
+ async function instantiate(): Promise<SqliteRuntime> {
73
+ const args: ModuleFactoryArgs = {
74
+ locateFile: (path) => path,
75
+ instantiateWasm: (imports, receiveInstance) => {
76
+ const instance = new WebAssembly.Instance(wasmModule, imports);
77
+ receiveInstance(instance);
78
+ return instance.exports;
79
+ },
80
+ };
81
+ const module: WaSqliteModule = await SQLiteESMFactory(args);
82
+ const sqlite3 = SQLite.Factory(module);
83
+ return { sqlite3, module };
84
+ }
85
+
86
+ /**
87
+ * VFS objects registered on the shared module, keyed by VFS name. SQLite has
88
+ * no "unregister" in the wa-sqlite glue, so a Durable Object that is evicted
89
+ * and re-created in the same isolate re-uses (and re-binds) its VFS object
90
+ * rather than registering a second one under the same name.
91
+ */
92
+ const vfsRegistry = new Map<string, SQLiteVFS>();
93
+
94
+ /**
95
+ * Register `create()`'s VFS under `name` unless one is already registered, in
96
+ * which case the existing object is returned and `create` is not called.
97
+ */
98
+ export function getOrRegisterVfs<V extends SQLiteVFS>(
99
+ runtime: SqliteRuntime,
100
+ name: string,
101
+ create: () => V,
102
+ isInstance: (vfs: SQLiteVFS) => vfs is V,
103
+ ): V {
104
+ const existing = vfsRegistry.get(name);
105
+ if (existing !== undefined) {
106
+ if (!isInstance(existing)) {
107
+ throw new Error(
108
+ `VFS '${name}' is registered with a different implementation`,
109
+ );
110
+ }
111
+ return existing;
112
+ }
113
+ const vfs = create();
114
+ runtime.sqlite3.vfs_register(vfs, false);
115
+ vfsRegistry.set(name, vfs);
116
+ return vfs;
117
+ }
118
+
119
+ /** SQLite library version string of the loaded build (e.g. `3.45.1`). */
120
+ export async function sqliteVersion(): Promise<string> {
121
+ const { sqlite3 } = await loadSqlite();
122
+ return sqlite3.libversion();
123
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `.wasm` imports are `CompiledWasm` modules on workerd (wrangler's default
3
+ * module rule and the Workers vitest pool both apply it). The import yields a
4
+ * ready `WebAssembly.Module`, so no runtime compilation happens — which is
5
+ * required, because workerd forbids `WebAssembly.compile` / `new Module(bytes)`.
6
+ */
7
+ declare module '*.wasm' {
8
+ const wasmModule: WebAssembly.Module;
9
+ export default wasmModule;
10
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Pure scheduling math for the tasks subsystem — next-fire computation for
3
+ * the three trigger kinds (`once` / `cron` / `interval`), schedule
4
+ * validation, run previews and the consecutive-failure backoff curve.
5
+ *
6
+ * Everything here is a pure function of its inputs so it can be unit-tested
7
+ * without a Durable Object; the scheduler (`scheduler.ts`) supplies the
8
+ * clock.
9
+ */
10
+ import { CronExpressionParser } from 'cron-parser';
11
+ import type { OracleTaskSchedule } from '../plugin-api/types';
12
+
13
+ /** Consecutive failed runs before a recurring task is stopped as `failed`. */
14
+ export const MAX_CONSECUTIVE_FAILURES = 3;
15
+
16
+ /** First-retry delay after a failed run; doubles per consecutive failure. */
17
+ export const FAILURE_BACKOFF_BASE_MS = 60_000;
18
+
19
+ /** Backoff ceiling — a retry is never pushed further out than this. */
20
+ export const FAILURE_BACKOFF_MAX_MS = 60 * 60_000;
21
+
22
+ /** Default for `TASKS_MAX_PER_USER` (live = active or paused tasks). */
23
+ export const DEFAULT_MAX_TASKS_PER_USER = 50;
24
+
25
+ /** Default for `TASKS_MIN_CRON_INTERVAL_SEC` (applies to `interval` too). */
26
+ export const DEFAULT_MIN_CRON_INTERVAL_SEC = 300;
27
+
28
+ /**
29
+ * Next fire time (ms epoch) strictly after `fromMs`, or null when the
30
+ * schedule has none (one-shot in the past, unparseable cron, bad interval).
31
+ */
32
+ export function computeNextRunAtMs(
33
+ schedule: OracleTaskSchedule,
34
+ fromMs: number,
35
+ ): number | null {
36
+ switch (schedule.kind) {
37
+ case 'once': {
38
+ const at = Date.parse(schedule.at);
39
+ if (Number.isNaN(at)) return null;
40
+ return at > fromMs ? at : null;
41
+ }
42
+ case 'cron': {
43
+ try {
44
+ return CronExpressionParser.parse(schedule.cron, {
45
+ currentDate: new Date(fromMs),
46
+ ...(schedule.timezone ? { tz: schedule.timezone } : {}),
47
+ })
48
+ .next()
49
+ .toDate()
50
+ .getTime();
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ case 'interval': {
56
+ if (
57
+ !Number.isInteger(schedule.everySeconds) ||
58
+ schedule.everySeconds <= 0
59
+ ) {
60
+ return null;
61
+ }
62
+ return fromMs + schedule.everySeconds * 1000;
63
+ }
64
+ }
65
+ }
66
+
67
+ /** The next `count` fire times as ISO strings (fewer when the schedule ends). */
68
+ export function previewRuns(
69
+ schedule: OracleTaskSchedule,
70
+ count: number,
71
+ fromMs: number,
72
+ ): string[] {
73
+ const runs: string[] = [];
74
+ let cursor = fromMs;
75
+ for (let i = 0; i < count; i++) {
76
+ const next = computeNextRunAtMs(schedule, cursor);
77
+ if (next === null || next <= cursor) break;
78
+ runs.push(new Date(next).toISOString());
79
+ cursor = next;
80
+ }
81
+ return runs;
82
+ }
83
+
84
+ /**
85
+ * Milliseconds between two consecutive fires of a cron pattern, or null when
86
+ * the pattern does not parse. Drives the minimum-frequency check.
87
+ */
88
+ export function cronIntervalMs(cron: string, timezone?: string): number | null {
89
+ try {
90
+ const it = CronExpressionParser.parse(cron, {
91
+ ...(timezone ? { tz: timezone } : {}),
92
+ });
93
+ const a = it.next().toDate().getTime();
94
+ const b = it.next().toDate().getTime();
95
+ return b - a;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Human-readable problems with a schedule; empty = valid. The minimum
103
+ * interval floor applies to cron patterns AND `interval` schedules — a
104
+ * one-shot only needs to be in the future.
105
+ */
106
+ export function validateSchedule(
107
+ schedule: OracleTaskSchedule,
108
+ minIntervalSec: number,
109
+ fromMs = Date.now(),
110
+ ): string[] {
111
+ const problems: string[] = [];
112
+ switch (schedule.kind) {
113
+ case 'once': {
114
+ const at = Date.parse(schedule.at);
115
+ if (Number.isNaN(at)) {
116
+ problems.push(`"${schedule.at}" is not a valid ISO timestamp.`);
117
+ } else if (at <= fromMs) {
118
+ problems.push(
119
+ `The one-shot time ${schedule.at} is in the past — pick a future time.`,
120
+ );
121
+ }
122
+ break;
123
+ }
124
+ case 'cron': {
125
+ const interval = cronIntervalMs(schedule.cron, schedule.timezone);
126
+ if (interval === null) {
127
+ problems.push(`Cron pattern "${schedule.cron}" is not valid.`);
128
+ } else if (interval < minIntervalSec * 1000) {
129
+ problems.push(
130
+ `That cron schedule fires every ${Math.round(interval / 1000)}s — the minimum interval between runs is ${minIntervalSec}s. Pick a less frequent schedule.`,
131
+ );
132
+ }
133
+ break;
134
+ }
135
+ case 'interval': {
136
+ if (
137
+ !Number.isInteger(schedule.everySeconds) ||
138
+ schedule.everySeconds <= 0
139
+ ) {
140
+ problems.push('everySeconds must be a positive integer.');
141
+ } else if (schedule.everySeconds < minIntervalSec) {
142
+ problems.push(
143
+ `An interval of ${schedule.everySeconds}s is below the minimum of ${minIntervalSec}s between runs.`,
144
+ );
145
+ }
146
+ break;
147
+ }
148
+ }
149
+ return problems;
150
+ }
151
+
152
+ /**
153
+ * Delay before the next attempt after `consecutiveFailures` failed runs in a
154
+ * row: base × 2^(n−1), capped. The scheduler takes the LATER of this and the
155
+ * schedule's own next fire, so backoff can only push runs out, never pull
156
+ * them in.
157
+ */
158
+ export function backoffDelayMs(consecutiveFailures: number): number {
159
+ if (consecutiveFailures <= 0) return 0;
160
+ const exp = Math.min(consecutiveFailures - 1, 30);
161
+ return Math.min(FAILURE_BACKOFF_BASE_MS * 2 ** exp, FAILURE_BACKOFF_MAX_MS);
162
+ }
163
+
164
+ /** One-line human summary of a schedule (tool responses, log lines). */
165
+ export function summarizeSchedule(schedule: OracleTaskSchedule): string {
166
+ switch (schedule.kind) {
167
+ case 'once':
168
+ return `once at ${schedule.at}`;
169
+ case 'cron':
170
+ return `cron \`${schedule.cron}\`${schedule.timezone ? ` (${schedule.timezone})` : ''}`;
171
+ case 'interval':
172
+ return `every ${schedule.everySeconds}s`;
173
+ }
174
+ }