@pi-agents/loop 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArtemisAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @pi-agents/loop
2
+
3
+ Recurring prompt scheduling and cron job management for [pi-coding-agent](https://github.com/badlogic/pi-mono).
4
+
5
+ ## Features
6
+
7
+ - **`/loop` command** — Quick recurring prompts: `/loop 5m check the deploy`
8
+ - **Cron tools** — LLM-callable `cron_create`, `cron_delete`, `cron_list` for programmatic scheduling
9
+ - **Idle gating** — Prompts only fire when the agent is idle, never interrupting in-progress work
10
+ - **Durable tasks** — Optionally persist schedules across sessions via `.pi-loop.json`
11
+ - **Anti-thundering-herd** — Deterministic jitter prevents multiple tasks from firing simultaneously
12
+ - **Auto-expiry** — Recurring tasks expire after 7 days by default
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @pi-agents/loop
18
+ ```
19
+
20
+ Add to your pi-agent settings (`~/.pi/agent/settings.json`):
21
+
22
+ ```json
23
+ {
24
+ "extensions": ["npm:@pi-agents/loop"]
25
+ }
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Slash Commands
31
+
32
+ | Command | Description |
33
+ |---------|-------------|
34
+ | `/loop [interval] <prompt>` | Start a recurring loop (default 10m) |
35
+ | `/loop-list` | List all active loops |
36
+ | `/loop-kill <id>` | Cancel a loop by ID |
37
+
38
+ **Examples:**
39
+ ```
40
+ /loop 5m check the deploy
41
+ /loop check tests every 15m
42
+ /loop-kill a1b2c3d4
43
+ ```
44
+
45
+ ### LLM Tools
46
+
47
+ The extension registers three tools the LLM can call directly:
48
+
49
+ | Tool | Description |
50
+ |------|-------------|
51
+ | `cron_create` | Schedule a prompt on a 5-field cron expression |
52
+ | `cron_delete` | Cancel a scheduled task by ID |
53
+ | `cron_list` | List all active scheduled tasks |
54
+
55
+ ### Cron Format
56
+
57
+ Standard 5-field cron in local time: `minute hour day-of-month month day-of-week`
58
+
59
+ | Pattern | Meaning |
60
+ |---------|---------|
61
+ | `*/5 * * * *` | Every 5 minutes |
62
+ | `0 */2 * * *` | Every 2 hours |
63
+ | `30 14 * * *` | Daily at 2:30 PM |
64
+ | `0 9 * * 1-5` | Weekdays at 9 AM |
65
+
66
+ ### Durable vs Session Tasks
67
+
68
+ - **Session tasks** (default) — Live only for the current session. Lost when the agent exits.
69
+ - **Durable tasks** (`durable: true`) — Persisted to `.pi-loop.json` and restored on next session start.
70
+
71
+ ## Configuration
72
+
73
+ Default configuration in `config/default.json`:
74
+
75
+ | Setting | Default | Description |
76
+ |---------|---------|-------------|
77
+ | `maxJobs` | 50 | Maximum concurrent scheduled tasks |
78
+ | `recurringMaxAgeDays` | 7 | Auto-expiry for recurring tasks |
79
+ | `recurringJitterFrac` | 0.1 | Jitter as fraction of cron gap |
80
+ | `recurringJitterCapMinutes` | 15 | Maximum jitter cap |
81
+ | `checkIntervalMs` | 1000 | Scheduler tick interval |
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ npm run build # Compile TypeScript
87
+ npm run lint # Type-check without emitting
88
+ npm run dev # Run with pi-agent in dev mode
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,11 @@
1
+ {
2
+ "maxJobs": 50,
3
+ "recurringMaxAgeDays": 7,
4
+ "recurringJitterFrac": 0.1,
5
+ "recurringJitterCapMinutes": 15,
6
+ "oneShotJitterMaxSeconds": 90,
7
+ "oneShotJitterFloorSeconds": 0,
8
+ "oneShotJitterMinuteMod": 30,
9
+ "checkIntervalMs": 1000,
10
+ "durableFilePath": ".pi-loop.json"
11
+ }
package/dist/cron.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 5-field cron parser and next-run calculator.
3
+ * All evaluation is in the user's local timezone.
4
+ *
5
+ * Format: minute hour day-of-month month day-of-week
6
+ */
7
+ interface CronField {
8
+ values: Set<number>;
9
+ }
10
+ interface ParsedCron {
11
+ minute: CronField;
12
+ hour: CronField;
13
+ dayOfMonth: CronField;
14
+ month: CronField;
15
+ dayOfWeek: CronField;
16
+ }
17
+ export declare function parseCronExpression(cron: string): ParsedCron | null;
18
+ /**
19
+ * Compute next cron run after `from`. Returns null if no match within 1 year.
20
+ */
21
+ export declare function computeNextCronRun(parsed: ParsedCron, from: Date): Date | null;
22
+ /**
23
+ * Convenience: parse cron string and compute next run in epoch ms.
24
+ */
25
+ export declare function nextCronRunMs(cron: string, fromMs: number): number | null;
26
+ /**
27
+ * Convert cron expression to human-readable string.
28
+ */
29
+ export declare function cronToHuman(cron: string): string;
30
+ /**
31
+ * Convert a human interval string (e.g. "5m", "2h", "1d") to a cron expression.
32
+ */
33
+ export declare function intervalToCron(interval: string): string | null;
34
+ /**
35
+ * Compute the gap between consecutive fires for a cron expression (in ms).
36
+ * Used for jitter calculation.
37
+ */
38
+ export declare function cronGapMs(cron: string, fromMs: number): number | null;
39
+ export {};
package/dist/cron.js ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * 5-field cron parser and next-run calculator.
3
+ * All evaluation is in the user's local timezone.
4
+ *
5
+ * Format: minute hour day-of-month month day-of-week
6
+ */
7
+ const FIELD_RANGES = [
8
+ [0, 59], // minute
9
+ [0, 23], // hour
10
+ [1, 31], // day of month
11
+ [1, 12], // month
12
+ [0, 6], // day of week (0=Sunday)
13
+ ];
14
+ function parseField(field, min, max) {
15
+ const values = new Set();
16
+ for (const part of field.split(",")) {
17
+ const stepMatch = part.match(/^(.+)\/(\d+)$/);
18
+ const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
19
+ const range = stepMatch ? stepMatch[1] : part;
20
+ if (step < 1)
21
+ return null;
22
+ if (range === "*") {
23
+ for (let i = min; i <= max; i += step)
24
+ values.add(i);
25
+ }
26
+ else {
27
+ const dashMatch = range.match(/^(\d+)-(\d+)$/);
28
+ if (dashMatch) {
29
+ const lo = parseInt(dashMatch[1], 10);
30
+ const hi = parseInt(dashMatch[2], 10);
31
+ if (lo < min || hi > max || lo > hi)
32
+ return null;
33
+ for (let i = lo; i <= hi; i += step)
34
+ values.add(i);
35
+ }
36
+ else {
37
+ const val = parseInt(range, 10);
38
+ if (isNaN(val) || val < min || val > max)
39
+ return null;
40
+ values.add(val);
41
+ }
42
+ }
43
+ }
44
+ return values.size > 0 ? { values } : null;
45
+ }
46
+ export function parseCronExpression(cron) {
47
+ const parts = cron.trim().split(/\s+/);
48
+ if (parts.length !== 5)
49
+ return null;
50
+ const fields = [];
51
+ for (let i = 0; i < 5; i++) {
52
+ const field = parseField(parts[i], FIELD_RANGES[i][0], FIELD_RANGES[i][1]);
53
+ if (!field)
54
+ return null;
55
+ fields.push(field);
56
+ }
57
+ return {
58
+ minute: fields[0],
59
+ hour: fields[1],
60
+ dayOfMonth: fields[2],
61
+ month: fields[3],
62
+ dayOfWeek: fields[4],
63
+ };
64
+ }
65
+ /**
66
+ * Compute next cron run after `from`. Returns null if no match within 1 year.
67
+ */
68
+ export function computeNextCronRun(parsed, from) {
69
+ const d = new Date(from.getTime());
70
+ // Start from the next minute
71
+ d.setSeconds(0, 0);
72
+ d.setMinutes(d.getMinutes() + 1);
73
+ const limit = new Date(from.getTime() + 366 * 24 * 60 * 60 * 1000);
74
+ while (d < limit) {
75
+ if (!parsed.month.values.has(d.getMonth() + 1)) {
76
+ d.setMonth(d.getMonth() + 1, 1);
77
+ d.setHours(0, 0, 0, 0);
78
+ continue;
79
+ }
80
+ if (!parsed.dayOfMonth.values.has(d.getDate()) ||
81
+ !parsed.dayOfWeek.values.has(d.getDay())) {
82
+ d.setDate(d.getDate() + 1);
83
+ d.setHours(0, 0, 0, 0);
84
+ continue;
85
+ }
86
+ if (!parsed.hour.values.has(d.getHours())) {
87
+ d.setHours(d.getHours() + 1, 0, 0, 0);
88
+ continue;
89
+ }
90
+ if (!parsed.minute.values.has(d.getMinutes())) {
91
+ d.setMinutes(d.getMinutes() + 1, 0, 0);
92
+ continue;
93
+ }
94
+ return d;
95
+ }
96
+ return null;
97
+ }
98
+ /**
99
+ * Convenience: parse cron string and compute next run in epoch ms.
100
+ */
101
+ export function nextCronRunMs(cron, fromMs) {
102
+ const parsed = parseCronExpression(cron);
103
+ if (!parsed)
104
+ return null;
105
+ const next = computeNextCronRun(parsed, new Date(fromMs));
106
+ return next ? next.getTime() : null;
107
+ }
108
+ /**
109
+ * Convert cron expression to human-readable string.
110
+ */
111
+ export function cronToHuman(cron) {
112
+ const parts = cron.trim().split(/\s+/);
113
+ if (parts.length !== 5)
114
+ return cron;
115
+ const [min, hour, dom, mon, dow] = parts;
116
+ // Every N minutes
117
+ const everyMinMatch = min.match(/^\*\/(\d+)$/);
118
+ if (everyMinMatch && hour === "*" && dom === "*" && mon === "*" && dow === "*") {
119
+ const n = parseInt(everyMinMatch[1], 10);
120
+ return n === 1 ? "every minute" : `every ${n} minutes`;
121
+ }
122
+ // Every N hours
123
+ const everyHourMatch = hour.match(/^\*\/(\d+)$/);
124
+ if (min === "0" && everyHourMatch && dom === "*" && mon === "*" && dow === "*") {
125
+ const n = parseInt(everyHourMatch[1], 10);
126
+ return n === 1 ? "every hour" : `every ${n} hours`;
127
+ }
128
+ // Every N days
129
+ const everyDayMatch = dom.match(/^\*\/(\d+)$/);
130
+ if (min === "0" && hour === "0" && everyDayMatch && mon === "*" && dow === "*") {
131
+ const n = parseInt(everyDayMatch[1], 10);
132
+ return n === 1 ? "every day at midnight" : `every ${n} days at midnight`;
133
+ }
134
+ // Specific time
135
+ if (/^\d+$/.test(min) && /^\d+$/.test(hour) && dom === "*" && mon === "*" && dow === "*") {
136
+ const h = parseInt(hour, 10);
137
+ const m = parseInt(min, 10);
138
+ const period = h >= 12 ? "PM" : "AM";
139
+ const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
140
+ return `daily at ${h12}:${m.toString().padStart(2, "0")} ${period}`;
141
+ }
142
+ return cron;
143
+ }
144
+ /**
145
+ * Convert a human interval string (e.g. "5m", "2h", "1d") to a cron expression.
146
+ */
147
+ export function intervalToCron(interval) {
148
+ const match = interval.match(/^(\d+)([smhd])$/);
149
+ if (!match)
150
+ return null;
151
+ let n = parseInt(match[1], 10);
152
+ const unit = match[2];
153
+ if (n <= 0)
154
+ return null;
155
+ switch (unit) {
156
+ case "s":
157
+ // Cron minimum is 1 minute; round up
158
+ n = Math.max(1, Math.ceil(n / 60));
159
+ return `*/${n} * * * *`;
160
+ case "m":
161
+ if (n <= 59)
162
+ return `*/${n} * * * *`;
163
+ // Round to hours
164
+ const hours = Math.max(1, Math.round(n / 60));
165
+ return `0 */${hours} * * *`;
166
+ case "h":
167
+ if (n <= 23)
168
+ return `0 */${n} * * *`;
169
+ return `0 0 */${Math.ceil(n / 24)} * *`;
170
+ case "d":
171
+ return `0 0 */${n} * *`;
172
+ default:
173
+ return null;
174
+ }
175
+ }
176
+ /**
177
+ * Compute the gap between consecutive fires for a cron expression (in ms).
178
+ * Used for jitter calculation.
179
+ */
180
+ export function cronGapMs(cron, fromMs) {
181
+ const first = nextCronRunMs(cron, fromMs);
182
+ if (first === null)
183
+ return null;
184
+ const second = nextCronRunMs(cron, first);
185
+ if (second === null)
186
+ return null;
187
+ return second - first;
188
+ }
189
+ //# sourceMappingURL=cron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.js","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * pi-loop — recurring prompt execution and cron scheduling for pi-agent.
3
+ *
4
+ * Registers:
5
+ * - /loop, /loop-list, /loop-kill commands
6
+ * - cron_create, cron_delete, cron_list LLM-callable tools
7
+ * - Scheduler engine with idle gating
8
+ */
9
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
+ export default function piLoop(pi: ExtensionAPI): void;
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * pi-loop — recurring prompt execution and cron scheduling for pi-agent.
3
+ *
4
+ * Registers:
5
+ * - /loop, /loop-list, /loop-kill commands
6
+ * - cron_create, cron_delete, cron_list LLM-callable tools
7
+ * - Scheduler engine with idle gating
8
+ */
9
+ import { intervalToCron, cronToHuman, nextCronRunMs } from "./cron.js";
10
+ import { parseLoopArgs } from "./parse-args.js";
11
+ import { LoopScheduler } from "./scheduler.js";
12
+ import { addTask, removeTask, getAllTasks, getTaskCount, generateTaskId, loadDurableTasks, writeDurableTasks, acquireLock, releaseLock, } from "./store.js";
13
+ import { registerCronTools } from "./tools/cron-tools.js";
14
+ import { DEFAULT_CONFIG } from "./types.js";
15
+ export default function piLoop(pi) {
16
+ const config = { ...DEFAULT_CONFIG };
17
+ let cwd = process.cwd();
18
+ let scheduler = null;
19
+ let hasLock = false;
20
+ // --- Commands ---
21
+ pi.registerCommand("loop", {
22
+ description: "Run a prompt on a recurring interval (e.g. /loop 5m check the deploy). Defaults to 10m.",
23
+ getArgumentCompletions(prefix) {
24
+ const suggestions = ["5m ", "10m ", "15m ", "30m ", "1h ", "2h "];
25
+ return suggestions
26
+ .filter((s) => s.startsWith(prefix))
27
+ .map((s) => ({ value: s, label: s.trim() }));
28
+ },
29
+ async handler(args, ctx) {
30
+ const parsed = parseLoopArgs(args);
31
+ if (!parsed) {
32
+ if (ctx.hasUI) {
33
+ ctx.ui.notify("Usage: /loop [interval] <prompt>\n" +
34
+ "Examples: /loop 5m check the deploy, /loop check tests every 15m", "warning");
35
+ }
36
+ return;
37
+ }
38
+ const cron = intervalToCron(parsed.interval);
39
+ if (!cron) {
40
+ if (ctx.hasUI) {
41
+ ctx.ui.notify(`Invalid interval: "${parsed.interval}". Use format: 5s, 10m, 2h, 1d`, "error");
42
+ }
43
+ return;
44
+ }
45
+ if (getTaskCount() >= config.maxJobs) {
46
+ if (ctx.hasUI) {
47
+ ctx.ui.notify(`Maximum of ${config.maxJobs} loops reached. Use /loop-kill to remove some.`, "error");
48
+ }
49
+ return;
50
+ }
51
+ const task = {
52
+ id: generateTaskId(),
53
+ cron,
54
+ prompt: parsed.prompt,
55
+ createdAt: Date.now(),
56
+ recurring: true,
57
+ durable: false,
58
+ };
59
+ addTask(task);
60
+ scheduler?.refreshStatus();
61
+ const human = cronToHuman(cron);
62
+ const nextRun = nextCronRunMs(cron, Date.now());
63
+ const nextStr = nextRun ? new Date(nextRun).toLocaleString() : "soon";
64
+ const expiryDays = Math.round(config.recurringMaxAgeMs / (24 * 60 * 60 * 1000));
65
+ if (ctx.hasUI) {
66
+ ctx.ui.notify(`Loop ${task.id} created: ${human}\n` +
67
+ `Next fire: ${nextStr}\n` +
68
+ `Auto-expires in ${expiryDays} days. Cancel: /loop-kill ${task.id}`, "info");
69
+ }
70
+ // Immediately execute the prompt (don't wait for first cron fire)
71
+ pi.sendUserMessage(parsed.prompt);
72
+ },
73
+ });
74
+ pi.registerCommand("loop-list", {
75
+ description: "List all active loop tasks",
76
+ async handler(_args, ctx) {
77
+ const tasks = getAllTasks();
78
+ if (tasks.length === 0) {
79
+ if (ctx.hasUI) {
80
+ ctx.ui.notify("No active loops.", "info");
81
+ }
82
+ return;
83
+ }
84
+ const now = Date.now();
85
+ const lines = tasks.map((t) => {
86
+ const human = cronToHuman(t.cron);
87
+ const next = nextCronRunMs(t.cron, t.lastFiredAt ?? t.createdAt);
88
+ const nextStr = next ? new Date(next).toLocaleString() : "unknown";
89
+ const flags = [
90
+ t.recurring ? "recurring" : "one-shot",
91
+ t.durable ? "durable" : "session",
92
+ ].join(", ");
93
+ return ` [${t.id}] ${human} — ${t.prompt.slice(0, 50)} (next: ${nextStr}, ${flags})`;
94
+ });
95
+ if (ctx.hasUI) {
96
+ ctx.ui.notify(`${tasks.length} active loop${tasks.length === 1 ? "" : "s"}:\n${lines.join("\n")}`, "info");
97
+ }
98
+ },
99
+ });
100
+ pi.registerCommand("loop-kill", {
101
+ description: "Cancel a loop task by ID",
102
+ getArgumentCompletions(prefix) {
103
+ return getAllTasks()
104
+ .filter((t) => t.id.startsWith(prefix))
105
+ .map((t) => {
106
+ const label = `${t.id} — ${cronToHuman(t.cron)}: ${t.prompt.slice(0, 30)}`;
107
+ return { value: t.id, label };
108
+ });
109
+ },
110
+ async handler(args, ctx) {
111
+ const id = args.trim();
112
+ if (!id) {
113
+ if (ctx.hasUI) {
114
+ ctx.ui.notify("Usage: /loop-kill <task-id>", "warning");
115
+ }
116
+ return;
117
+ }
118
+ const removed = removeTask(id);
119
+ if (!removed) {
120
+ if (ctx.hasUI) {
121
+ ctx.ui.notify(`No loop found with ID "${id}".`, "error");
122
+ }
123
+ return;
124
+ }
125
+ // Persist if we had durable tasks
126
+ writeDurableTasks(cwd, config).catch(() => { });
127
+ scheduler?.refreshStatus();
128
+ if (ctx.hasUI) {
129
+ ctx.ui.notify(`Loop ${id} cancelled.`, "info");
130
+ }
131
+ },
132
+ });
133
+ // --- LLM-callable tools ---
134
+ // Deferred to session_start so scheduler is initialized
135
+ // --- Lifecycle events ---
136
+ pi.on("session_start", async (_event, ctx) => {
137
+ cwd = ctx.cwd;
138
+ // Initialize scheduler
139
+ scheduler = new LoopScheduler(pi, config, cwd);
140
+ scheduler.setContext(ctx);
141
+ // Register LLM tools (needs scheduler reference)
142
+ registerCronTools(pi, scheduler, config, () => cwd);
143
+ // Load durable tasks
144
+ hasLock = await acquireLock(cwd, config);
145
+ if (hasLock) {
146
+ const durableTasks = await loadDurableTasks(cwd, config);
147
+ for (const task of durableTasks) {
148
+ addTask(task);
149
+ }
150
+ }
151
+ // Start the scheduler tick loop
152
+ scheduler.start();
153
+ scheduler.refreshStatus();
154
+ if (ctx.hasUI) {
155
+ const count = getTaskCount();
156
+ if (count > 0) {
157
+ ctx.ui.notify(`pi-loop: ${count} task${count === 1 ? "" : "s"} loaded`, "info");
158
+ }
159
+ }
160
+ });
161
+ pi.on("session_shutdown", async () => {
162
+ scheduler?.stop();
163
+ if (hasLock) {
164
+ await releaseLock(cwd, config);
165
+ }
166
+ });
167
+ // --- Idle gate ---
168
+ pi.on("agent_start", () => {
169
+ scheduler?.setBusy();
170
+ });
171
+ pi.on("agent_end", () => {
172
+ scheduler?.setIdle();
173
+ });
174
+ }
175
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Deterministic jitter system.
3
+ *
4
+ * Ported from Claude Code's cron jitter design:
5
+ * - Recurring tasks: forward delay (spread fires over a window)
6
+ * - One-shot tasks: backward lead (fire slightly early to avoid pile-ups)
7
+ */
8
+ import type { LoopConfig, LoopTask } from "./types.js";
9
+ /**
10
+ * Deterministic fraction [0, 1) from task ID.
11
+ * The ID is an 8-hex-char string; we parse it as a u32 and divide.
12
+ */
13
+ export declare function jitterFrac(taskId: string): number;
14
+ /**
15
+ * Compute jitter delay for a recurring task (forward delay in ms).
16
+ *
17
+ * nextFire = baseFire + frac * jitterFrac * gap
18
+ * Capped at recurringJitterCapMs.
19
+ */
20
+ export declare function recurringJitterMs(task: LoopTask, gapMs: number, config: LoopConfig): number;
21
+ /**
22
+ * Compute jitter for a one-shot task (backward lead in ms).
23
+ *
24
+ * Only applies if fire minute is on a boundary (e.g. :00 or :30).
25
+ * Returns a negative offset (subtract from fire time).
26
+ * Never fires before task creation time.
27
+ */
28
+ export declare function oneShotJitterMs(task: LoopTask, fireTimeMs: number, config: LoopConfig): number;
package/dist/jitter.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Deterministic jitter system.
3
+ *
4
+ * Ported from Claude Code's cron jitter design:
5
+ * - Recurring tasks: forward delay (spread fires over a window)
6
+ * - One-shot tasks: backward lead (fire slightly early to avoid pile-ups)
7
+ */
8
+ /**
9
+ * Deterministic fraction [0, 1) from task ID.
10
+ * The ID is an 8-hex-char string; we parse it as a u32 and divide.
11
+ */
12
+ export function jitterFrac(taskId) {
13
+ const frac = parseInt(taskId.slice(0, 8), 16) / 0x100000000;
14
+ return Number.isFinite(frac) ? frac : 0;
15
+ }
16
+ /**
17
+ * Compute jitter delay for a recurring task (forward delay in ms).
18
+ *
19
+ * nextFire = baseFire + frac * jitterFrac * gap
20
+ * Capped at recurringJitterCapMs.
21
+ */
22
+ export function recurringJitterMs(task, gapMs, config) {
23
+ const frac = jitterFrac(task.id);
24
+ const raw = frac * config.recurringJitterFrac * gapMs;
25
+ return Math.min(raw, config.recurringJitterCapMs);
26
+ }
27
+ /**
28
+ * Compute jitter for a one-shot task (backward lead in ms).
29
+ *
30
+ * Only applies if fire minute is on a boundary (e.g. :00 or :30).
31
+ * Returns a negative offset (subtract from fire time).
32
+ * Never fires before task creation time.
33
+ */
34
+ export function oneShotJitterMs(task, fireTimeMs, config) {
35
+ const fireDate = new Date(fireTimeMs);
36
+ const fireMinute = fireDate.getMinutes();
37
+ // Only jitter fires on minute boundaries
38
+ if (fireMinute % config.oneShotJitterMinuteMod !== 0)
39
+ return 0;
40
+ const frac = jitterFrac(task.id);
41
+ const lead = config.oneShotJitterFloorMs +
42
+ frac * (config.oneShotJitterMaxMs - config.oneShotJitterFloorMs);
43
+ // Never fire before creation
44
+ const earliest = task.createdAt;
45
+ const jittered = fireTimeMs - lead;
46
+ if (jittered < earliest)
47
+ return fireTimeMs - earliest;
48
+ return lead;
49
+ }
50
+ //# sourceMappingURL=jitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jitter.js","sourceRoot":"","sources":["../src/jitter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,WAAW,CAAC;IAC5D,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAc,EACd,KAAa,EACb,MAAkB;IAElB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC,mBAAmB,GAAG,KAAK,CAAC;IACtD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,oBAAoB,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAc,EACd,UAAkB,EAClB,MAAkB;IAElB,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,QAAQ,CAAC,UAAU,EAAE,CAAC;IAEzC,yCAAyC;IACzC,IAAI,UAAU,GAAG,MAAM,CAAC,sBAAsB,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAE/D,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,IAAI,GACR,MAAM,CAAC,oBAAoB;QAC3B,IAAI,GAAG,CAAC,MAAM,CAAC,kBAAkB,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAEnE,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;IAChC,MAAM,QAAQ,GAAG,UAAU,GAAG,IAAI,CAAC;IACnC,IAAI,QAAQ,GAAG,QAAQ;QAAE,OAAO,UAAU,GAAG,QAAQ,CAAC;IAEtD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * /loop argument parser.
3
+ *
4
+ * Priority:
5
+ * 1. Leading interval token: "5m check the deploy"
6
+ * 2. Trailing "every" clause: "check the deploy every 20m"
7
+ * 3. Default 10m
8
+ */
9
+ export interface ParsedLoopArgs {
10
+ interval: string;
11
+ prompt: string;
12
+ }
13
+ export declare function parseLoopArgs(args: string): ParsedLoopArgs | null;