@oka-core/reason 0.2.14 → 0.2.16

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 (163) hide show
  1. package/dist/abort-controller.d.ts +19 -0
  2. package/dist/abort-controller.d.ts.map +1 -0
  3. package/dist/abort-controller.js +53 -0
  4. package/dist/activity-tracker.d.ts +48 -0
  5. package/dist/activity-tracker.d.ts.map +1 -0
  6. package/dist/activity-tracker.js +80 -0
  7. package/dist/analytics.d.ts +49 -0
  8. package/dist/analytics.d.ts.map +1 -0
  9. package/dist/analytics.js +88 -0
  10. package/dist/array.d.ts +12 -0
  11. package/dist/array.d.ts.map +1 -0
  12. package/dist/array.js +20 -0
  13. package/dist/async-context.d.ts +20 -0
  14. package/dist/async-context.d.ts.map +1 -0
  15. package/dist/async-context.js +25 -0
  16. package/dist/auth.d.ts +15 -0
  17. package/dist/auth.d.ts.map +1 -1
  18. package/dist/auth.js +77 -0
  19. package/dist/binary-check.d.ts +16 -0
  20. package/dist/binary-check.d.ts.map +1 -0
  21. package/dist/binary-check.js +43 -0
  22. package/dist/buffered-writer.d.ts +30 -0
  23. package/dist/buffered-writer.d.ts.map +1 -0
  24. package/dist/buffered-writer.js +87 -0
  25. package/dist/circular-buffer.d.ts +28 -0
  26. package/dist/circular-buffer.d.ts.map +1 -0
  27. package/dist/circular-buffer.js +61 -0
  28. package/dist/cleanup-registry.d.ts +23 -0
  29. package/dist/cleanup-registry.d.ts.map +1 -0
  30. package/dist/cleanup-registry.js +34 -0
  31. package/dist/client.d.ts +6 -5
  32. package/dist/client.d.ts.map +1 -1
  33. package/dist/client.js +51 -64
  34. package/dist/combined-abort-signal.d.ts +25 -0
  35. package/dist/combined-abort-signal.d.ts.map +1 -0
  36. package/dist/combined-abort-signal.js +47 -0
  37. package/dist/cron-lock.d.ts +29 -0
  38. package/dist/cron-lock.d.ts.map +1 -0
  39. package/dist/cron-lock.js +127 -0
  40. package/dist/cron-scheduler.d.ts +41 -0
  41. package/dist/cron-scheduler.d.ts.map +1 -0
  42. package/dist/cron-scheduler.js +189 -0
  43. package/dist/cron-tasks.d.ts +86 -0
  44. package/dist/cron-tasks.d.ts.map +1 -0
  45. package/dist/cron-tasks.js +205 -0
  46. package/dist/cron.d.ts +35 -0
  47. package/dist/cron.d.ts.map +1 -0
  48. package/dist/cron.js +215 -0
  49. package/dist/env.d.ts +26 -0
  50. package/dist/env.d.ts.map +1 -0
  51. package/dist/env.js +50 -0
  52. package/dist/errors.d.ts +99 -0
  53. package/dist/errors.d.ts.map +1 -0
  54. package/dist/errors.js +214 -0
  55. package/dist/format.d.ts +21 -0
  56. package/dist/format.d.ts.map +1 -0
  57. package/dist/format.js +48 -0
  58. package/dist/fps-tracker.d.ts +22 -0
  59. package/dist/fps-tracker.d.ts.map +1 -0
  60. package/dist/fps-tracker.js +44 -0
  61. package/dist/graceful-shutdown.d.ts +35 -0
  62. package/dist/graceful-shutdown.d.ts.map +1 -0
  63. package/dist/graceful-shutdown.js +89 -0
  64. package/dist/hash.d.ts +21 -0
  65. package/dist/hash.d.ts.map +1 -0
  66. package/dist/hash.js +31 -0
  67. package/dist/heap-diagnostics.d.ts +68 -0
  68. package/dist/heap-diagnostics.d.ts.map +1 -0
  69. package/dist/heap-diagnostics.js +110 -0
  70. package/dist/idle-timeout.d.ts +21 -0
  71. package/dist/idle-timeout.d.ts.map +1 -0
  72. package/dist/idle-timeout.js +42 -0
  73. package/dist/index.d.ts +2 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +5 -0
  76. package/dist/intl.d.ts +18 -0
  77. package/dist/intl.d.ts.map +1 -0
  78. package/dist/intl.js +75 -0
  79. package/dist/jsonl.d.ts +16 -0
  80. package/dist/jsonl.d.ts.map +1 -0
  81. package/dist/jsonl.js +60 -0
  82. package/dist/lazy-schema.d.ts +6 -0
  83. package/dist/lazy-schema.d.ts.map +1 -0
  84. package/dist/lazy-schema.js +8 -0
  85. package/dist/memo.d.ts +64 -0
  86. package/dist/memo.d.ts.map +1 -0
  87. package/dist/memo.js +162 -0
  88. package/dist/pkce.d.ts +13 -0
  89. package/dist/pkce.d.ts.map +1 -0
  90. package/dist/pkce.js +28 -0
  91. package/dist/priority-queue.d.ts +36 -0
  92. package/dist/priority-queue.d.ts.map +1 -0
  93. package/dist/priority-queue.js +97 -0
  94. package/dist/process-utils.d.ts +20 -0
  95. package/dist/process-utils.d.ts.map +1 -0
  96. package/dist/process-utils.js +54 -0
  97. package/dist/query-guard.d.ts +34 -0
  98. package/dist/query-guard.d.ts.map +1 -0
  99. package/dist/query-guard.js +74 -0
  100. package/dist/retry.d.ts +60 -0
  101. package/dist/retry.d.ts.map +1 -0
  102. package/dist/retry.js +89 -0
  103. package/dist/schemas.d.ts +6 -6
  104. package/dist/secrets.d.ts +44 -0
  105. package/dist/secrets.d.ts.map +1 -0
  106. package/dist/secrets.js +115 -0
  107. package/dist/semantic-types.d.ts +39 -0
  108. package/dist/semantic-types.d.ts.map +1 -0
  109. package/dist/semantic-types.js +49 -0
  110. package/dist/sequential.d.ts +21 -0
  111. package/dist/sequential.d.ts.map +1 -0
  112. package/dist/sequential.js +49 -0
  113. package/dist/signal.d.ts +29 -0
  114. package/dist/signal.d.ts.map +1 -0
  115. package/dist/signal.js +39 -0
  116. package/dist/sleep.d.ts +21 -0
  117. package/dist/sleep.d.ts.map +1 -0
  118. package/dist/sleep.js +58 -0
  119. package/dist/slow-ops.d.ts +41 -0
  120. package/dist/slow-ops.d.ts.map +1 -0
  121. package/dist/slow-ops.js +133 -0
  122. package/dist/store.d.ts +20 -0
  123. package/dist/store.d.ts.map +1 -0
  124. package/dist/store.js +34 -0
  125. package/dist/stream.d.ts +29 -0
  126. package/dist/stream.d.ts.map +1 -0
  127. package/dist/stream.js +92 -0
  128. package/dist/string-utils.d.ts +46 -0
  129. package/dist/string-utils.d.ts.map +1 -0
  130. package/dist/string-utils.js +69 -0
  131. package/dist/strip-bom.d.ts +8 -0
  132. package/dist/strip-bom.d.ts.map +1 -0
  133. package/dist/strip-bom.js +10 -0
  134. package/dist/subprocess-env.d.ts +25 -0
  135. package/dist/subprocess-env.d.ts.map +1 -0
  136. package/dist/subprocess-env.js +55 -0
  137. package/dist/temp-file.d.ts +18 -0
  138. package/dist/temp-file.d.ts.map +1 -0
  139. package/dist/temp-file.js +26 -0
  140. package/dist/tool-contract.d.ts +85 -0
  141. package/dist/tool-contract.d.ts.map +1 -0
  142. package/dist/tool-contract.js +101 -0
  143. package/dist/tools/auth.d.ts.map +1 -1
  144. package/dist/tools/auth.js +8 -7
  145. package/dist/tools/read.d.ts +2 -10
  146. package/dist/tools/read.d.ts.map +1 -1
  147. package/dist/tools/read.js +662 -537
  148. package/dist/tools/write.d.ts +3 -2
  149. package/dist/tools/write.d.ts.map +1 -1
  150. package/dist/tools/write.js +329 -177
  151. package/dist/uuid.d.ts +20 -0
  152. package/dist/uuid.d.ts.map +1 -0
  153. package/dist/uuid.js +28 -0
  154. package/dist/validation.d.ts +64 -0
  155. package/dist/validation.d.ts.map +1 -0
  156. package/dist/validation.js +236 -0
  157. package/dist/with-resolvers.d.ts +12 -0
  158. package/dist/with-resolvers.d.ts.map +1 -0
  159. package/dist/with-resolvers.js +14 -0
  160. package/dist/xml-escape.d.ts +12 -0
  161. package/dist/xml-escape.d.ts.map +1 -0
  162. package/dist/xml-escape.js +15 -0
  163. package/package.json +1 -1
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Scheduler lease lock for cron task files.
3
+ *
4
+ * When multiple processes share the same task directory, only one should
5
+ * drive the cron scheduler. Uses O_EXCL atomic create, PID liveness probe,
6
+ * and stale-lock recovery.
7
+ */
8
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
9
+ import { dirname, join } from "path";
10
+ const LOCK_FILE_NAME = "scheduled_tasks.lock";
11
+ function getLockPath(dir) {
12
+ return join(dir, LOCK_FILE_NAME);
13
+ }
14
+ function isProcessRunning(pid) {
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ function getErrnoCode(e) {
24
+ if (e && typeof e === "object" && "code" in e) {
25
+ return e.code;
26
+ }
27
+ return undefined;
28
+ }
29
+ async function readLock(dir) {
30
+ let raw;
31
+ try {
32
+ raw = await readFile(getLockPath(dir), "utf8");
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ try {
38
+ const parsed = JSON.parse(raw);
39
+ if (parsed &&
40
+ typeof parsed.ownerId === "string" &&
41
+ typeof parsed.pid === "number" &&
42
+ typeof parsed.acquiredAt === "number") {
43
+ return parsed;
44
+ }
45
+ }
46
+ catch {
47
+ // Corrupt file
48
+ }
49
+ return undefined;
50
+ }
51
+ async function tryCreateExclusive(lock, dir) {
52
+ const path = getLockPath(dir);
53
+ const body = JSON.stringify(lock);
54
+ try {
55
+ await writeFile(path, body, { flag: "wx" });
56
+ return true;
57
+ }
58
+ catch (e) {
59
+ const code = getErrnoCode(e);
60
+ if (code === "EEXIST")
61
+ return false;
62
+ if (code === "ENOENT") {
63
+ await mkdir(dirname(path), { recursive: true });
64
+ try {
65
+ await writeFile(path, body, { flag: "wx" });
66
+ return true;
67
+ }
68
+ catch (retryErr) {
69
+ if (getErrnoCode(retryErr) === "EEXIST")
70
+ return false;
71
+ throw retryErr;
72
+ }
73
+ }
74
+ throw e;
75
+ }
76
+ }
77
+ /**
78
+ * Try to acquire the scheduler lock.
79
+ *
80
+ * Returns true on success, false if another live process holds it.
81
+ *
82
+ * Uses O_EXCL for atomic test-and-set. If the file exists:
83
+ * - Already ours → true (idempotent)
84
+ * - Another live PID → false
85
+ * - Stale (PID dead / corrupt) → unlink and retry once
86
+ */
87
+ export async function tryAcquireSchedulerLock(opts) {
88
+ const { dir, ownerId } = opts;
89
+ const lock = {
90
+ ownerId,
91
+ pid: process.pid,
92
+ acquiredAt: Date.now(),
93
+ };
94
+ if (await tryCreateExclusive(lock, dir)) {
95
+ return true;
96
+ }
97
+ const existing = await readLock(dir);
98
+ // Already ours (idempotent). Update PID if changed (e.g. after resume).
99
+ if (existing?.ownerId === ownerId) {
100
+ if (existing.pid !== process.pid) {
101
+ await writeFile(getLockPath(dir), JSON.stringify(lock));
102
+ }
103
+ return true;
104
+ }
105
+ // Another live process holds it.
106
+ if (existing && isProcessRunning(existing.pid)) {
107
+ return false;
108
+ }
109
+ // Stale — unlink and retry exclusive create once.
110
+ await unlink(getLockPath(dir)).catch(() => { });
111
+ return tryCreateExclusive(lock, dir);
112
+ }
113
+ /**
114
+ * Release the scheduler lock if we own it.
115
+ */
116
+ export async function releaseSchedulerLock(opts) {
117
+ const { dir, ownerId } = opts;
118
+ const existing = await readLock(dir);
119
+ if (!existing || existing.ownerId !== ownerId)
120
+ return;
121
+ try {
122
+ await unlink(getLockPath(dir));
123
+ }
124
+ catch {
125
+ // Already gone.
126
+ }
127
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Polling-based cron scheduler.
3
+ *
4
+ * Simplified extraction from CC's cronScheduler — strips chokidar file watching
5
+ * (uses polling), bootstrap state, analytics, and session tasks. Designed for
6
+ * standalone use with file-backed cron tasks.
7
+ */
8
+ import { type CronJitterConfig, type CronTask } from "./cron-tasks.js";
9
+ /**
10
+ * True when a recurring task was created more than maxAgeMs ago.
11
+ * Permanent tasks never age. maxAgeMs === 0 means unlimited.
12
+ */
13
+ export declare function isRecurringTaskAged(t: CronTask, nowMs: number, maxAgeMs: number): boolean;
14
+ export type CronSchedulerOptions = {
15
+ /** Directory containing scheduled_tasks.json. */
16
+ dir: string;
17
+ /** Stable owner identity for the scheduler lock. */
18
+ ownerId: string;
19
+ /** Called when a task fires. */
20
+ onFire: (prompt: string) => void;
21
+ /** Called with the full CronTask on fire (bypasses onFire). */
22
+ onFireTask?: (task: CronTask) => void;
23
+ /** Called with missed one-shot tasks on initial load. */
24
+ onMissed?: (tasks: CronTask[]) => void;
25
+ /** Per-task gate: tasks returning false are invisible to this scheduler. */
26
+ filter?: (t: CronTask) => boolean;
27
+ /** Jitter config (defaults to DEFAULT_CRON_JITTER_CONFIG). */
28
+ jitterConfig?: CronJitterConfig;
29
+ /** Killswitch: polled each tick. When true, check() bails. */
30
+ isKilled?: () => boolean;
31
+ /** Check interval in ms (default: 1000). */
32
+ checkIntervalMs?: number;
33
+ };
34
+ export type CronScheduler = {
35
+ start: () => void;
36
+ stop: () => void;
37
+ /** Epoch ms of the soonest scheduled fire, or null if nothing pending. */
38
+ getNextFireTime: () => number | null;
39
+ };
40
+ export declare function createCronScheduler(options: CronSchedulerOptions): CronScheduler;
41
+ //# sourceMappingURL=cron-scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-scheduler.d.ts","sourceRoot":"","sources":["../src/cron-scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,QAAQ,EAUd,MAAM,iBAAiB,CAAC;AAMzB;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,QAAQ,EACX,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAKT;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACtC,yDAAyD;IACzD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACvC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC;IAClC,8DAA8D;IAC9D,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC;IACzB,4CAA4C;IAC5C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACtC,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CAqMf"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Polling-based cron scheduler.
3
+ *
4
+ * Simplified extraction from CC's cronScheduler — strips chokidar file watching
5
+ * (uses polling), bootstrap state, analytics, and session tasks. Designed for
6
+ * standalone use with file-backed cron tasks.
7
+ */
8
+ import { DEFAULT_CRON_JITTER_CONFIG, buildMissedTaskNotification, findMissedTasks, jitteredNextCronRunMs, oneShotJitteredNextCronRunMs, readCronTasks, removeCronTasks, markCronTasksFired, } from "./cron-tasks.js";
9
+ import { tryAcquireSchedulerLock, releaseSchedulerLock } from "./cron-lock.js";
10
+ const CHECK_INTERVAL_MS = 1000;
11
+ const LOCK_PROBE_INTERVAL_MS = 5000;
12
+ /**
13
+ * True when a recurring task was created more than maxAgeMs ago.
14
+ * Permanent tasks never age. maxAgeMs === 0 means unlimited.
15
+ */
16
+ export function isRecurringTaskAged(t, nowMs, maxAgeMs) {
17
+ if (maxAgeMs === 0)
18
+ return false;
19
+ return Boolean(t.recurring && !t.permanent && nowMs - t.createdAt >= maxAgeMs);
20
+ }
21
+ export function createCronScheduler(options) {
22
+ const { dir, ownerId, onFire, onFireTask, onMissed, filter, isKilled, checkIntervalMs = CHECK_INTERVAL_MS, } = options;
23
+ const jitterCfg = options.jitterConfig ?? DEFAULT_CRON_JITTER_CONFIG;
24
+ const lockOpts = { dir, ownerId };
25
+ let tasks = [];
26
+ const nextFireAt = new Map();
27
+ const missedAsked = new Set();
28
+ const inFlight = new Set();
29
+ let checkTimer = null;
30
+ let lockProbeTimer = null;
31
+ let stopped = false;
32
+ let isOwner = false;
33
+ async function load(initial) {
34
+ const next = await readCronTasks(dir);
35
+ if (stopped)
36
+ return;
37
+ tasks = next;
38
+ if (!initial)
39
+ return;
40
+ const now = Date.now();
41
+ const missed = findMissedTasks(next, now).filter((t) => !t.recurring && !missedAsked.has(t.id) && (!filter || filter(t)));
42
+ if (missed.length > 0) {
43
+ for (const t of missed) {
44
+ missedAsked.add(t.id);
45
+ nextFireAt.set(t.id, Infinity);
46
+ }
47
+ if (onMissed) {
48
+ onMissed(missed);
49
+ }
50
+ else {
51
+ onFire(buildMissedTaskNotification(missed));
52
+ }
53
+ void removeCronTasks(missed.map((t) => t.id), dir).catch(() => { });
54
+ }
55
+ }
56
+ function check() {
57
+ if (isKilled?.())
58
+ return;
59
+ if (!isOwner)
60
+ return;
61
+ const now = Date.now();
62
+ const seen = new Set();
63
+ const firedFileRecurring = [];
64
+ for (const t of tasks) {
65
+ if (filter && !filter(t))
66
+ continue;
67
+ seen.add(t.id);
68
+ if (inFlight.has(t.id))
69
+ continue;
70
+ let next = nextFireAt.get(t.id);
71
+ if (next === undefined) {
72
+ next = t.recurring
73
+ ? (jitteredNextCronRunMs(t.cron, t.lastFiredAt ?? t.createdAt, t.id, jitterCfg) ?? Infinity)
74
+ : (oneShotJitteredNextCronRunMs(t.cron, t.createdAt, t.id, jitterCfg) ?? Infinity);
75
+ nextFireAt.set(t.id, next);
76
+ }
77
+ if (now < next)
78
+ continue;
79
+ // Fire
80
+ if (onFireTask) {
81
+ onFireTask(t);
82
+ }
83
+ else {
84
+ onFire(t.prompt);
85
+ }
86
+ const aged = isRecurringTaskAged(t, now, jitterCfg.recurringMaxAgeMs);
87
+ if (t.recurring && !aged) {
88
+ const newNext = jitteredNextCronRunMs(t.cron, now, t.id, jitterCfg) ?? Infinity;
89
+ nextFireAt.set(t.id, newNext);
90
+ firedFileRecurring.push(t.id);
91
+ }
92
+ else {
93
+ // One-shot or aged-out recurring: delete
94
+ inFlight.add(t.id);
95
+ void removeCronTasks([t.id], dir)
96
+ .catch(() => { })
97
+ .finally(() => inFlight.delete(t.id));
98
+ nextFireAt.delete(t.id);
99
+ }
100
+ }
101
+ // Batch lastFiredAt write
102
+ if (firedFileRecurring.length > 0) {
103
+ for (const id of firedFileRecurring)
104
+ inFlight.add(id);
105
+ void markCronTasksFired(firedFileRecurring, now, dir)
106
+ .catch(() => { })
107
+ .finally(() => {
108
+ for (const id of firedFileRecurring)
109
+ inFlight.delete(id);
110
+ });
111
+ }
112
+ if (seen.size === 0) {
113
+ nextFireAt.clear();
114
+ return;
115
+ }
116
+ for (const id of nextFireAt.keys()) {
117
+ if (!seen.has(id))
118
+ nextFireAt.delete(id);
119
+ }
120
+ }
121
+ async function enable() {
122
+ if (stopped)
123
+ return;
124
+ isOwner = await tryAcquireSchedulerLock(lockOpts).catch(() => false);
125
+ if (stopped) {
126
+ if (isOwner) {
127
+ isOwner = false;
128
+ void releaseSchedulerLock(lockOpts);
129
+ }
130
+ return;
131
+ }
132
+ if (!isOwner) {
133
+ lockProbeTimer = setInterval(() => {
134
+ void tryAcquireSchedulerLock(lockOpts)
135
+ .then((owned) => {
136
+ if (stopped) {
137
+ if (owned)
138
+ void releaseSchedulerLock(lockOpts);
139
+ return;
140
+ }
141
+ if (owned) {
142
+ isOwner = true;
143
+ if (lockProbeTimer) {
144
+ clearInterval(lockProbeTimer);
145
+ lockProbeTimer = null;
146
+ }
147
+ // Load tasks now that we own the lock
148
+ void load(true);
149
+ }
150
+ })
151
+ .catch(() => { });
152
+ }, LOCK_PROBE_INTERVAL_MS);
153
+ lockProbeTimer.unref?.();
154
+ return;
155
+ }
156
+ void load(true);
157
+ checkTimer = setInterval(check, checkIntervalMs);
158
+ checkTimer.unref?.();
159
+ }
160
+ return {
161
+ start() {
162
+ stopped = false;
163
+ void enable();
164
+ },
165
+ stop() {
166
+ stopped = true;
167
+ if (checkTimer) {
168
+ clearInterval(checkTimer);
169
+ checkTimer = null;
170
+ }
171
+ if (lockProbeTimer) {
172
+ clearInterval(lockProbeTimer);
173
+ lockProbeTimer = null;
174
+ }
175
+ if (isOwner) {
176
+ isOwner = false;
177
+ void releaseSchedulerLock(lockOpts);
178
+ }
179
+ },
180
+ getNextFireTime() {
181
+ let min = Infinity;
182
+ for (const t of nextFireAt.values()) {
183
+ if (t < min)
184
+ min = t;
185
+ }
186
+ return min === Infinity ? null : min;
187
+ },
188
+ };
189
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * File-backed cron task persistence.
3
+ *
4
+ * Tasks are stored in `<dir>/scheduled_tasks.json` with the shape:
5
+ * { "tasks": [{ id, cron, prompt, createdAt, lastFiredAt?, recurring?, permanent? }] }
6
+ *
7
+ * Provides CRUD operations, next-run computation with jitter, and missed-task detection.
8
+ */
9
+ export type CronTask = {
10
+ id: string;
11
+ /** 5-field cron string (local time). */
12
+ cron: string;
13
+ /** Prompt or payload to deliver when the task fires. */
14
+ prompt: string;
15
+ /** Epoch ms when the task was created. */
16
+ createdAt: number;
17
+ /** Epoch ms of the most recent fire (recurring tasks only). */
18
+ lastFiredAt?: number;
19
+ /** When true, reschedules after firing instead of being deleted. */
20
+ recurring?: boolean;
21
+ /** When true, exempt from recurringMaxAgeMs auto-expiry. */
22
+ permanent?: boolean;
23
+ };
24
+ /**
25
+ * Path to the cron tasks file within `dir`.
26
+ */
27
+ export declare function getCronFilePath(dir: string): string;
28
+ /**
29
+ * Read and parse scheduled_tasks.json. Returns empty list if missing or malformed.
30
+ * Tasks with invalid cron strings are silently dropped.
31
+ */
32
+ export declare function readCronTasks(dir: string): Promise<CronTask[]>;
33
+ /**
34
+ * Overwrite the tasks file. Creates parent directory if missing.
35
+ */
36
+ export declare function writeCronTasks(tasks: CronTask[], dir: string): Promise<void>;
37
+ /**
38
+ * Append a task. Returns the generated id.
39
+ */
40
+ export declare function addCronTask(dir: string, cron: string, prompt: string, recurring: boolean): Promise<string>;
41
+ /**
42
+ * Remove tasks by id. No-op if none match.
43
+ */
44
+ export declare function removeCronTasks(ids: string[], dir: string): Promise<void>;
45
+ /**
46
+ * Stamp `lastFiredAt` on the given recurring tasks and write back.
47
+ */
48
+ export declare function markCronTasksFired(ids: string[], firedAt: number, dir: string): Promise<void>;
49
+ /**
50
+ * Next fire time in epoch ms for a cron string, strictly after `fromMs`.
51
+ */
52
+ export declare function nextCronRunMs(cron: string, fromMs: number): number | null;
53
+ export type CronJitterConfig = {
54
+ /** Recurring forward delay as fraction of interval between fires. */
55
+ recurringFrac: number;
56
+ /** Upper bound on recurring forward delay. */
57
+ recurringCapMs: number;
58
+ /** One-shot backward lead: max ms a task may fire early. */
59
+ oneShotMaxMs: number;
60
+ /** One-shot backward lead: min ms to fire early. */
61
+ oneShotFloorMs: number;
62
+ /** Jitter fires on minutes where minute % N === 0. */
63
+ oneShotMinuteMod: number;
64
+ /** Recurring tasks auto-expire after this many ms (0 = unlimited). */
65
+ recurringMaxAgeMs: number;
66
+ };
67
+ export declare const DEFAULT_CRON_JITTER_CONFIG: CronJitterConfig;
68
+ /**
69
+ * Next fire time for recurring tasks, with deterministic forward jitter
70
+ * proportional to the interval between fires.
71
+ */
72
+ export declare function jitteredNextCronRunMs(cron: string, fromMs: number, taskId: string, cfg?: CronJitterConfig): number | null;
73
+ /**
74
+ * Next fire time for one-shot tasks, with deterministic backward jitter
75
+ * on round minutes (mod oneShotMinuteMod).
76
+ */
77
+ export declare function oneShotJitteredNextCronRunMs(cron: string, fromMs: number, taskId: string, cfg?: CronJitterConfig): number | null;
78
+ /**
79
+ * Tasks whose next scheduled run (from createdAt) is in the past.
80
+ */
81
+ export declare function findMissedTasks(tasks: CronTask[], nowMs: number): CronTask[];
82
+ /**
83
+ * Build a notification message for missed tasks.
84
+ */
85
+ export declare function buildMissedTaskNotification(missed: CronTask[]): string;
86
+ //# sourceMappingURL=cron-tasks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-tasks.d.ts","sourceRoot":"","sources":["../src/cron-tasks.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAMF;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA4CpE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,QAAQ,EAAE,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,MAAM,CAAC,CAajB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EAAE,EACb,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EAAE,EACb,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAaf;AAID;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKzE;AAID,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,gBAOxC,CAAC;AAUF;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,gBAA6C,GACjD,MAAM,GAAG,IAAI,CAUf;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,gBAA6C,GACjD,MAAM,GAAG,IAAI,CAQf;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAK5E;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAmBtE"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * File-backed cron task persistence.
3
+ *
4
+ * Tasks are stored in `<dir>/scheduled_tasks.json` with the shape:
5
+ * { "tasks": [{ id, cron, prompt, createdAt, lastFiredAt?, recurring?, permanent? }] }
6
+ *
7
+ * Provides CRUD operations, next-run computation with jitter, and missed-task detection.
8
+ */
9
+ import { randomUUID } from "crypto";
10
+ import { mkdir, readFile, writeFile } from "fs/promises";
11
+ import { dirname, join } from "path";
12
+ import { computeNextCronRun, cronToHuman, parseCronExpression, } from "./cron.js";
13
+ const DEFAULT_FILE_NAME = "scheduled_tasks.json";
14
+ /**
15
+ * Path to the cron tasks file within `dir`.
16
+ */
17
+ export function getCronFilePath(dir) {
18
+ return join(dir, DEFAULT_FILE_NAME);
19
+ }
20
+ /**
21
+ * Read and parse scheduled_tasks.json. Returns empty list if missing or malformed.
22
+ * Tasks with invalid cron strings are silently dropped.
23
+ */
24
+ export async function readCronTasks(dir) {
25
+ let raw;
26
+ try {
27
+ raw = await readFile(getCronFilePath(dir), { encoding: "utf-8" });
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ if (!parsed || typeof parsed !== "object")
40
+ return [];
41
+ const file = parsed;
42
+ if (!Array.isArray(file.tasks))
43
+ return [];
44
+ const out = [];
45
+ for (const t of file.tasks) {
46
+ if (!t ||
47
+ typeof t.id !== "string" ||
48
+ typeof t.cron !== "string" ||
49
+ typeof t.prompt !== "string" ||
50
+ typeof t.createdAt !== "number") {
51
+ continue;
52
+ }
53
+ if (!parseCronExpression(t.cron))
54
+ continue;
55
+ out.push({
56
+ id: t.id,
57
+ cron: t.cron,
58
+ prompt: t.prompt,
59
+ createdAt: t.createdAt,
60
+ ...(typeof t.lastFiredAt === "number"
61
+ ? { lastFiredAt: t.lastFiredAt }
62
+ : {}),
63
+ ...(t.recurring ? { recurring: true } : {}),
64
+ ...(t.permanent ? { permanent: true } : {}),
65
+ });
66
+ }
67
+ return out;
68
+ }
69
+ /**
70
+ * Overwrite the tasks file. Creates parent directory if missing.
71
+ */
72
+ export async function writeCronTasks(tasks, dir) {
73
+ await mkdir(dirname(getCronFilePath(dir)), { recursive: true });
74
+ const body = { tasks };
75
+ await writeFile(getCronFilePath(dir), JSON.stringify(body, null, 2) + "\n", "utf-8");
76
+ }
77
+ /**
78
+ * Append a task. Returns the generated id.
79
+ */
80
+ export async function addCronTask(dir, cron, prompt, recurring) {
81
+ const id = randomUUID().slice(0, 8);
82
+ const task = {
83
+ id,
84
+ cron,
85
+ prompt,
86
+ createdAt: Date.now(),
87
+ ...(recurring ? { recurring: true } : {}),
88
+ };
89
+ const tasks = await readCronTasks(dir);
90
+ tasks.push(task);
91
+ await writeCronTasks(tasks, dir);
92
+ return id;
93
+ }
94
+ /**
95
+ * Remove tasks by id. No-op if none match.
96
+ */
97
+ export async function removeCronTasks(ids, dir) {
98
+ if (ids.length === 0)
99
+ return;
100
+ const idSet = new Set(ids);
101
+ const tasks = await readCronTasks(dir);
102
+ const remaining = tasks.filter((t) => !idSet.has(t.id));
103
+ if (remaining.length === tasks.length)
104
+ return;
105
+ await writeCronTasks(remaining, dir);
106
+ }
107
+ /**
108
+ * Stamp `lastFiredAt` on the given recurring tasks and write back.
109
+ */
110
+ export async function markCronTasksFired(ids, firedAt, dir) {
111
+ if (ids.length === 0)
112
+ return;
113
+ const idSet = new Set(ids);
114
+ const tasks = await readCronTasks(dir);
115
+ let changed = false;
116
+ for (const t of tasks) {
117
+ if (idSet.has(t.id)) {
118
+ t.lastFiredAt = firedAt;
119
+ changed = true;
120
+ }
121
+ }
122
+ if (!changed)
123
+ return;
124
+ await writeCronTasks(tasks, dir);
125
+ }
126
+ // --- Next-run computation ---
127
+ /**
128
+ * Next fire time in epoch ms for a cron string, strictly after `fromMs`.
129
+ */
130
+ export function nextCronRunMs(cron, fromMs) {
131
+ const fields = parseCronExpression(cron);
132
+ if (!fields)
133
+ return null;
134
+ const next = computeNextCronRun(fields, new Date(fromMs));
135
+ return next ? next.getTime() : null;
136
+ }
137
+ export const DEFAULT_CRON_JITTER_CONFIG = {
138
+ recurringFrac: 0.1,
139
+ recurringCapMs: 15 * 60 * 1000,
140
+ oneShotMaxMs: 90 * 1000,
141
+ oneShotFloorMs: 0,
142
+ oneShotMinuteMod: 30,
143
+ recurringMaxAgeMs: 7 * 24 * 60 * 60 * 1000,
144
+ };
145
+ /**
146
+ * Parse taskId (8-hex UUID slice) to a [0, 1) fraction for deterministic jitter.
147
+ */
148
+ function jitterFrac(taskId) {
149
+ const frac = parseInt(taskId.slice(0, 8), 16) / 0x1_0000_0000;
150
+ return Number.isFinite(frac) ? frac : 0;
151
+ }
152
+ /**
153
+ * Next fire time for recurring tasks, with deterministic forward jitter
154
+ * proportional to the interval between fires.
155
+ */
156
+ export function jitteredNextCronRunMs(cron, fromMs, taskId, cfg = DEFAULT_CRON_JITTER_CONFIG) {
157
+ const t1 = nextCronRunMs(cron, fromMs);
158
+ if (t1 === null)
159
+ return null;
160
+ const t2 = nextCronRunMs(cron, t1);
161
+ if (t2 === null)
162
+ return t1;
163
+ const jitter = Math.min(jitterFrac(taskId) * cfg.recurringFrac * (t2 - t1), cfg.recurringCapMs);
164
+ return t1 + jitter;
165
+ }
166
+ /**
167
+ * Next fire time for one-shot tasks, with deterministic backward jitter
168
+ * on round minutes (mod oneShotMinuteMod).
169
+ */
170
+ export function oneShotJitteredNextCronRunMs(cron, fromMs, taskId, cfg = DEFAULT_CRON_JITTER_CONFIG) {
171
+ const t1 = nextCronRunMs(cron, fromMs);
172
+ if (t1 === null)
173
+ return null;
174
+ if (new Date(t1).getMinutes() % cfg.oneShotMinuteMod !== 0)
175
+ return t1;
176
+ const lead = cfg.oneShotFloorMs +
177
+ jitterFrac(taskId) * (cfg.oneShotMaxMs - cfg.oneShotFloorMs);
178
+ return Math.max(t1 - lead, fromMs);
179
+ }
180
+ /**
181
+ * Tasks whose next scheduled run (from createdAt) is in the past.
182
+ */
183
+ export function findMissedTasks(tasks, nowMs) {
184
+ return tasks.filter((t) => {
185
+ const next = nextCronRunMs(t.cron, t.createdAt);
186
+ return next !== null && next < nowMs;
187
+ });
188
+ }
189
+ /**
190
+ * Build a notification message for missed tasks.
191
+ */
192
+ export function buildMissedTaskNotification(missed) {
193
+ const plural = missed.length > 1;
194
+ const header = `The following one-shot scheduled task${plural ? "s were" : " was"} missed. ` +
195
+ `${plural ? "They have" : "It has"} been removed from the task file.\n\n` +
196
+ `Do NOT execute ${plural ? "these prompts" : "this prompt"} yet. ` +
197
+ `First confirm with the user whether to run ${plural ? "each one" : "it"} now.`;
198
+ const blocks = missed.map((t) => {
199
+ const meta = `[${cronToHuman(t.cron)}, created ${new Date(t.createdAt).toLocaleString()}]`;
200
+ const longestRun = (t.prompt.match(/`+/g) ?? []).reduce((max, run) => Math.max(max, run.length), 0);
201
+ const fence = "`".repeat(Math.max(3, longestRun + 1));
202
+ return `${meta}\n${fence}\n${t.prompt}\n${fence}`;
203
+ });
204
+ return `${header}\n\n${blocks.join("\n\n")}`;
205
+ }
package/dist/cron.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Minimal 5-field cron expression parser and next-run calculator.
3
+ *
4
+ * Field syntax: wildcard (*), step ( * /N), range (N-M), range-step (N-M/S),
5
+ * list (N,M,...), plain value (N). Sunday alias: 7 → 0.
6
+ *
7
+ * All times are local timezone. DST-safe: spring-forward gaps are skipped,
8
+ * fall-back duplicates fire once (vixie-cron semantics).
9
+ */
10
+ export type CronFields = {
11
+ minute: number[];
12
+ hour: number[];
13
+ dayOfMonth: number[];
14
+ month: number[];
15
+ dayOfWeek: number[];
16
+ };
17
+ /**
18
+ * Parse a 5-field cron expression into expanded number arrays.
19
+ * Returns null if invalid or unsupported syntax.
20
+ */
21
+ export declare function parseCronExpression(expr: string): CronFields | null;
22
+ /**
23
+ * Compute the next Date strictly after `from` that matches the cron fields.
24
+ * Uses local timezone. Walks forward minute-by-minute, bounded at 366 days.
25
+ *
26
+ * Standard cron: when both dayOfMonth and dayOfWeek are constrained
27
+ * (neither is the full range), a date matches if EITHER matches.
28
+ */
29
+ export declare function computeNextCronRun(fields: CronFields, from: Date): Date | null;
30
+ /**
31
+ * Convert a cron expression to a human-readable description.
32
+ * Covers common patterns; falls through to the raw string for complex ones.
33
+ */
34
+ export declare function cronToHuman(cron: string): string;
35
+ //# sourceMappingURL=cron.d.ts.map