ofiere-openclaw-plugin 4.56.4 → 4.56.6
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/src/tools.js +156 -3
- package/package.json +1 -1
- package/src/tools.ts +171 -3
package/dist/src/tools.js
CHANGED
|
@@ -244,7 +244,18 @@ function registerTaskOps(api, supabase, userId, resolveAgent, timezone) {
|
|
|
244
244
|
`For simple tasks, just provide title and optionally description.\n` +
|
|
245
245
|
`agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
|
|
246
246
|
`subagent_id: When delegating to one of your own staff, pass the staff UUID here AND set agent_id to yourself (or the chief). The staff's chief_agent_id MUST match agent_id or the call rejects with subagent_chief_mismatch. Use OFIERE_AGENT_OPS list_subagents to discover available staff under a chief.\n` +
|
|
247
|
-
|
|
247
|
+
`\n` +
|
|
248
|
+
`For recurring tasks: set start_date + recurrence_type + recurrence_interval.\n` +
|
|
249
|
+
`⚠️ CADENCE MAPPING — match the user's spoken phrase EXACTLY (server-side cadence-intent guard rejects mismatches):\n` +
|
|
250
|
+
` • "every N seconds" → recurrence_type:"minutely" + recurrence_interval:max(1, ceil(N/60)) ← cron resolution = 1 minute\n` +
|
|
251
|
+
` • "every N minutes" → recurrence_type:"minutely" + recurrence_interval:N ← e.g. every 35 min → minutely+35\n` +
|
|
252
|
+
` • "every N hours" → recurrence_type:"hourly" + recurrence_interval:N\n` +
|
|
253
|
+
` • "every N days" → recurrence_type:"daily" + recurrence_interval:N ← daily+1 = 24h gap!\n` +
|
|
254
|
+
` • "every N weeks" → recurrence_type:"weekly" + recurrence_interval:N\n` +
|
|
255
|
+
` • "every N months" → recurrence_type:"monthly" + recurrence_interval:N\n` +
|
|
256
|
+
`🛑 NEVER use daily+1 (or hourly+24, etc.) when the user said a minute-level cadence — the task will fire ONCE per 24h instead of every N minutes. This was a real prod bug (v1 smoke 2026-05-17).\n` +
|
|
257
|
+
`🚦 BURST GUARD (Fix #3, 2026-05-17): when you create >3 recurring tasks within 60s for the same user, the server auto-staggers each new task's first fire by (count × min(recurrence_interval_secs, 1800)) so 6+ tasks with a shared T0 anchor do NOT all burst on the same pg_cron tick. Pass distinct start_date values per task if you need precise first-fire times.\n` +
|
|
258
|
+
`\n` +
|
|
248
259
|
`Approvals: Use add_approval to request sign-off from humans or agents. Approvals are separate from workflow gate nodes.\n` +
|
|
249
260
|
`Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
|
|
250
261
|
parameters: {
|
|
@@ -561,10 +572,119 @@ function normalizeStartDate(startDate, explicitScheduledTime, tzOffsetHours) {
|
|
|
561
572
|
scheduledDateLocal: `${year}-${pad(month)}-${pad(day)}`,
|
|
562
573
|
};
|
|
563
574
|
}
|
|
575
|
+
// ── Fix #10 (2026-05-17): Cadence-intent guard ──────────────────────────────
|
|
576
|
+
// v1 smoke incident: LLM picked recurrence_type='daily' + recurrence_interval=1
|
|
577
|
+
// for a task whose title said "every 35 minutes" — so it fired ONCE per 24h
|
|
578
|
+
// instead of every 35 min. Silent test failure. This guard inspects the task
|
|
579
|
+
// text for an explicit "every N <unit>" phrase and rejects the create when the
|
|
580
|
+
// chosen recurrence_type/interval doesn't match. Combined with the tightened
|
|
581
|
+
// OFIERE_TASK_OPS description (cadence-mapping table), prevents the drift at
|
|
582
|
+
// both the LLM-prompt layer and the server-runtime layer.
|
|
583
|
+
const RECURRENCE_TYPES = ["none", "minutely", "hourly", "daily", "weekly", "monthly"];
|
|
584
|
+
function humanizeCadence(type, interval) {
|
|
585
|
+
if (type === "none")
|
|
586
|
+
return "one-shot";
|
|
587
|
+
const unitMap = {
|
|
588
|
+
minutely: "minute",
|
|
589
|
+
hourly: "hour",
|
|
590
|
+
daily: "day",
|
|
591
|
+
weekly: "week",
|
|
592
|
+
monthly: "month",
|
|
593
|
+
};
|
|
594
|
+
const unit = unitMap[type] || type;
|
|
595
|
+
return interval === 1 ? `every ${unit}` : `every ${interval} ${unit}s`;
|
|
596
|
+
}
|
|
597
|
+
// Fix #3 (2026-05-17): bulk-create stagger helper. Maps recurrence_type +
|
|
598
|
+
// interval to cadence in seconds; used to compute the per-slot offset
|
|
599
|
+
// applied when a user bulk-creates >3 recurring scheduler_events within a
|
|
600
|
+
// 60s window (see handleCreateTask below). monthly approximated as 30 days.
|
|
601
|
+
function recurrenceIntervalSeconds(rtype, ri) {
|
|
602
|
+
const safe = Math.max(1, ri || 1);
|
|
603
|
+
switch (rtype) {
|
|
604
|
+
case "minutely": return 60 * safe;
|
|
605
|
+
case "hourly": return 3600 * safe;
|
|
606
|
+
case "daily": return 86400 * safe;
|
|
607
|
+
case "weekly": return 604800 * safe;
|
|
608
|
+
case "monthly": return 2592000 * safe;
|
|
609
|
+
default: return 60;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function validateRecurrence(params) {
|
|
613
|
+
const rtRaw = params.recurrence_type ?? "none";
|
|
614
|
+
const rt = rtRaw.toLowerCase();
|
|
615
|
+
const riRaw = params.recurrence_interval;
|
|
616
|
+
const ri = typeof riRaw === "number" ? riRaw : (riRaw === undefined ? 1 : Number(riRaw));
|
|
617
|
+
if (!RECURRENCE_TYPES.includes(rt)) {
|
|
618
|
+
return {
|
|
619
|
+
ok: false,
|
|
620
|
+
reason: `Invalid recurrence_type "${rtRaw}". Must be one of: ${RECURRENCE_TYPES.join(", ")}.`,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (rt === "none")
|
|
624
|
+
return { ok: true };
|
|
625
|
+
if (!Number.isFinite(ri) || !Number.isInteger(ri) || ri < 1 || ri > 9999) {
|
|
626
|
+
return {
|
|
627
|
+
ok: false,
|
|
628
|
+
reason: `Invalid recurrence_interval ${riRaw}. Must be a positive integer between 1 and 9999.`,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
// Cadence-intent guard: scan title + description for an "every N <unit>"
|
|
632
|
+
// phrase. If present, the chosen recurrence_type+interval MUST match.
|
|
633
|
+
const text = `${params.title || ""} ${params.description || ""}`.toLowerCase();
|
|
634
|
+
const cadenceMatch = text.match(/every\s+(\d+)\s*(seconds?|secs?|minutes?|mins?|hours?|hrs?|days?|weeks?|months?)\b/);
|
|
635
|
+
if (!cadenceMatch)
|
|
636
|
+
return { ok: true };
|
|
637
|
+
const n = parseInt(cadenceMatch[1], 10);
|
|
638
|
+
const unitRaw = cadenceMatch[2];
|
|
639
|
+
let expectedType;
|
|
640
|
+
let expectedInterval;
|
|
641
|
+
if (/^sec/.test(unitRaw)) {
|
|
642
|
+
expectedType = "minutely";
|
|
643
|
+
expectedInterval = Math.max(1, Math.ceil(n / 60));
|
|
644
|
+
}
|
|
645
|
+
else if (/^min/.test(unitRaw)) {
|
|
646
|
+
expectedType = "minutely";
|
|
647
|
+
expectedInterval = n;
|
|
648
|
+
}
|
|
649
|
+
else if (/^(hour|hr)/.test(unitRaw)) {
|
|
650
|
+
expectedType = "hourly";
|
|
651
|
+
expectedInterval = n;
|
|
652
|
+
}
|
|
653
|
+
else if (/^day/.test(unitRaw)) {
|
|
654
|
+
expectedType = "daily";
|
|
655
|
+
expectedInterval = n;
|
|
656
|
+
}
|
|
657
|
+
else if (/^week/.test(unitRaw)) {
|
|
658
|
+
expectedType = "weekly";
|
|
659
|
+
expectedInterval = n;
|
|
660
|
+
}
|
|
661
|
+
else if (/^month/.test(unitRaw)) {
|
|
662
|
+
expectedType = "monthly";
|
|
663
|
+
expectedInterval = n;
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
return { ok: true };
|
|
667
|
+
}
|
|
668
|
+
if (rt !== expectedType || ri !== expectedInterval) {
|
|
669
|
+
return {
|
|
670
|
+
ok: false,
|
|
671
|
+
reason: `Recurrence mismatch: task text says "every ${n} ${unitRaw}" but you passed ` +
|
|
672
|
+
`recurrence_type="${rt}", recurrence_interval=${ri} (= ${humanizeCadence(rt, ri)}). ` +
|
|
673
|
+
`Correct combo: recurrence_type="${expectedType}", recurrence_interval=${expectedInterval} ` +
|
|
674
|
+
`(= ${humanizeCadence(expectedType, expectedInterval)}). ` +
|
|
675
|
+
`Re-call with the matching values, OR remove the "every N ${unitRaw}" phrase from the title/description if you intended a different cadence.`,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
return { ok: true };
|
|
679
|
+
}
|
|
564
680
|
async function handleCreateTask(supabase, userId, resolveAgent, params, fallbackTimezone = "Asia/Jakarta") {
|
|
565
681
|
try {
|
|
566
682
|
if (!params.title)
|
|
567
683
|
return err("Missing required field: title");
|
|
684
|
+
// Fix #10 (2026-05-17): cadence-intent guard — see helper comment above.
|
|
685
|
+
const recurCheck = validateRecurrence(params);
|
|
686
|
+
if (!recurCheck.ok)
|
|
687
|
+
return err(recurCheck.reason);
|
|
568
688
|
const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
569
689
|
const now = new Date().toISOString();
|
|
570
690
|
// BUG 5 fix: resolve user's IANA timezone from their profile row. Plugin
|
|
@@ -740,6 +860,32 @@ async function handleCreateTask(supabase, userId, resolveAgent, params, fallback
|
|
|
740
860
|
if (nextRunAtEpoch <= nowEpoch) {
|
|
741
861
|
nextRunAtEpoch = nowEpoch + 60 + Math.floor(Math.random() * 300);
|
|
742
862
|
}
|
|
863
|
+
// Fix #3 (2026-05-17): bulk-recurring stagger safety net. Fix #2
|
|
864
|
+
// only jitters past-time bumps; an LLM that intentionally seeds
|
|
865
|
+
// 6+ recurring tasks with a shared FUTURE T0 anchor still bursts
|
|
866
|
+
// every tick. Detect the burst by counting recent recurring
|
|
867
|
+
// scheduler_events for this user (last 60s); once >3 exist, push
|
|
868
|
+
// the new task's first fire by (recent × per-slot) seconds where
|
|
869
|
+
// per-slot = min(recurrence_interval_secs, 1800). For 6 minutely
|
|
870
|
+
// creates this spreads first-fires across ~5 min instead of one
|
|
871
|
+
// pg_cron tick. Cap per-slot at 30min so long-interval cadences
|
|
872
|
+
// (hourly/daily) don't push first fires hours out.
|
|
873
|
+
const rTypeStr = params.recurrence_type || "none";
|
|
874
|
+
const rIntervalNum = params.recurrence_interval || 1;
|
|
875
|
+
if (rTypeStr !== "none" && rIntervalNum > 0) {
|
|
876
|
+
const sinceISO = new Date(Date.now() - 60_000).toISOString();
|
|
877
|
+
const { count: recentCount } = await supabase.from("scheduler_events")
|
|
878
|
+
.select("id", { count: "exact", head: true })
|
|
879
|
+
.eq("user_id", userId)
|
|
880
|
+
.neq("recurrence_type", "none")
|
|
881
|
+
.gte("created_at", sinceISO);
|
|
882
|
+
const recent = recentCount ?? 0;
|
|
883
|
+
if (recent >= 3) {
|
|
884
|
+
const ivSecs = recurrenceIntervalSeconds(rTypeStr, rIntervalNum);
|
|
885
|
+
const perSlotSecs = Math.min(ivSecs, 1800);
|
|
886
|
+
nextRunAtEpoch += recent * perSlotSecs;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
743
889
|
await supabase.from("scheduler_events").insert({
|
|
744
890
|
id: crypto.randomUUID(),
|
|
745
891
|
user_id: userId,
|
|
@@ -774,9 +920,16 @@ async function handleCreateTask(supabase, userId, resolveAgent, params, fallback
|
|
|
774
920
|
extras.push(`${cf.constraints.length} constraints`);
|
|
775
921
|
if (cf.system_prompt)
|
|
776
922
|
extras.push("custom system prompt");
|
|
777
|
-
// BUG 6 fix: surface recurrence fields in create response
|
|
923
|
+
// BUG 6 fix: surface recurrence fields in create response.
|
|
924
|
+
// Fix #10 (2026-05-17): also surface effective_cadence so the LLM sees the
|
|
925
|
+
// human-readable interpretation (e.g. "every 35 minutes") of the values it
|
|
926
|
+
// just passed — defense-in-depth alongside validateRecurrence().
|
|
778
927
|
const recurrenceInfo = (params.recurrence_type && params.recurrence_type !== "none")
|
|
779
|
-
? {
|
|
928
|
+
? {
|
|
929
|
+
recurrence_type: params.recurrence_type,
|
|
930
|
+
recurrence_interval: params.recurrence_interval || 1,
|
|
931
|
+
effective_cadence: humanizeCadence(params.recurrence_type, params.recurrence_interval || 1),
|
|
932
|
+
}
|
|
780
933
|
: undefined;
|
|
781
934
|
// BUG 7 fix: only claim scheduling when bridge actually fired
|
|
782
935
|
const didSchedule = !!(startDate && effectiveAgentId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.56.
|
|
3
|
+
"version": "4.56.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM - 18 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, agent brain, talent management, corporate frameworks, agent office canvas, and PM gate approvals",
|
|
6
6
|
"keywords": [
|
package/src/tools.ts
CHANGED
|
@@ -282,7 +282,18 @@ function registerTaskOps(
|
|
|
282
282
|
`For simple tasks, just provide title and optionally description.\n` +
|
|
283
283
|
`agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
|
|
284
284
|
`subagent_id: When delegating to one of your own staff, pass the staff UUID here AND set agent_id to yourself (or the chief). The staff's chief_agent_id MUST match agent_id or the call rejects with subagent_chief_mismatch. Use OFIERE_AGENT_OPS list_subagents to discover available staff under a chief.\n` +
|
|
285
|
-
|
|
285
|
+
`\n` +
|
|
286
|
+
`For recurring tasks: set start_date + recurrence_type + recurrence_interval.\n` +
|
|
287
|
+
`⚠️ CADENCE MAPPING — match the user's spoken phrase EXACTLY (server-side cadence-intent guard rejects mismatches):\n` +
|
|
288
|
+
` • "every N seconds" → recurrence_type:"minutely" + recurrence_interval:max(1, ceil(N/60)) ← cron resolution = 1 minute\n` +
|
|
289
|
+
` • "every N minutes" → recurrence_type:"minutely" + recurrence_interval:N ← e.g. every 35 min → minutely+35\n` +
|
|
290
|
+
` • "every N hours" → recurrence_type:"hourly" + recurrence_interval:N\n` +
|
|
291
|
+
` • "every N days" → recurrence_type:"daily" + recurrence_interval:N ← daily+1 = 24h gap!\n` +
|
|
292
|
+
` • "every N weeks" → recurrence_type:"weekly" + recurrence_interval:N\n` +
|
|
293
|
+
` • "every N months" → recurrence_type:"monthly" + recurrence_interval:N\n` +
|
|
294
|
+
`🛑 NEVER use daily+1 (or hourly+24, etc.) when the user said a minute-level cadence — the task will fire ONCE per 24h instead of every N minutes. This was a real prod bug (v1 smoke 2026-05-17).\n` +
|
|
295
|
+
`🚦 BURST GUARD (Fix #3, 2026-05-17): when you create >3 recurring tasks within 60s for the same user, the server auto-staggers each new task's first fire by (count × min(recurrence_interval_secs, 1800)) so 6+ tasks with a shared T0 anchor do NOT all burst on the same pg_cron tick. Pass distinct start_date values per task if you need precise first-fire times.\n` +
|
|
296
|
+
`\n` +
|
|
286
297
|
`Approvals: Use add_approval to request sign-off from humans or agents. Approvals are separate from workflow gate nodes.\n` +
|
|
287
298
|
`Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
|
|
288
299
|
parameters: {
|
|
@@ -625,6 +636,122 @@ function normalizeStartDate(
|
|
|
625
636
|
};
|
|
626
637
|
}
|
|
627
638
|
|
|
639
|
+
// ── Fix #10 (2026-05-17): Cadence-intent guard ──────────────────────────────
|
|
640
|
+
// v1 smoke incident: LLM picked recurrence_type='daily' + recurrence_interval=1
|
|
641
|
+
// for a task whose title said "every 35 minutes" — so it fired ONCE per 24h
|
|
642
|
+
// instead of every 35 min. Silent test failure. This guard inspects the task
|
|
643
|
+
// text for an explicit "every N <unit>" phrase and rejects the create when the
|
|
644
|
+
// chosen recurrence_type/interval doesn't match. Combined with the tightened
|
|
645
|
+
// OFIERE_TASK_OPS description (cadence-mapping table), prevents the drift at
|
|
646
|
+
// both the LLM-prompt layer and the server-runtime layer.
|
|
647
|
+
|
|
648
|
+
const RECURRENCE_TYPES = ["none", "minutely", "hourly", "daily", "weekly", "monthly"] as const;
|
|
649
|
+
type RecurrenceType = (typeof RECURRENCE_TYPES)[number];
|
|
650
|
+
|
|
651
|
+
function humanizeCadence(type: string, interval: number): string {
|
|
652
|
+
if (type === "none") return "one-shot";
|
|
653
|
+
const unitMap: Record<string, string> = {
|
|
654
|
+
minutely: "minute",
|
|
655
|
+
hourly: "hour",
|
|
656
|
+
daily: "day",
|
|
657
|
+
weekly: "week",
|
|
658
|
+
monthly: "month",
|
|
659
|
+
};
|
|
660
|
+
const unit = unitMap[type] || type;
|
|
661
|
+
return interval === 1 ? `every ${unit}` : `every ${interval} ${unit}s`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Fix #3 (2026-05-17): bulk-create stagger helper. Maps recurrence_type +
|
|
665
|
+
// interval to cadence in seconds; used to compute the per-slot offset
|
|
666
|
+
// applied when a user bulk-creates >3 recurring scheduler_events within a
|
|
667
|
+
// 60s window (see handleCreateTask below). monthly approximated as 30 days.
|
|
668
|
+
function recurrenceIntervalSeconds(rtype: string, ri: number): number {
|
|
669
|
+
const safe = Math.max(1, ri || 1);
|
|
670
|
+
switch (rtype) {
|
|
671
|
+
case "minutely": return 60 * safe;
|
|
672
|
+
case "hourly": return 3600 * safe;
|
|
673
|
+
case "daily": return 86400 * safe;
|
|
674
|
+
case "weekly": return 604800 * safe;
|
|
675
|
+
case "monthly": return 2592000 * safe;
|
|
676
|
+
default: return 60;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function validateRecurrence(
|
|
681
|
+
params: Record<string, unknown>,
|
|
682
|
+
): { ok: true } | { ok: false; reason: string } {
|
|
683
|
+
const rtRaw = (params.recurrence_type as string | undefined) ?? "none";
|
|
684
|
+
const rt = rtRaw.toLowerCase() as RecurrenceType;
|
|
685
|
+
const riRaw = params.recurrence_interval;
|
|
686
|
+
const ri = typeof riRaw === "number" ? riRaw : (riRaw === undefined ? 1 : Number(riRaw));
|
|
687
|
+
|
|
688
|
+
if (!RECURRENCE_TYPES.includes(rt)) {
|
|
689
|
+
return {
|
|
690
|
+
ok: false,
|
|
691
|
+
reason:
|
|
692
|
+
`Invalid recurrence_type "${rtRaw}". Must be one of: ${RECURRENCE_TYPES.join(", ")}.`,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (rt === "none") return { ok: true };
|
|
697
|
+
|
|
698
|
+
if (!Number.isFinite(ri) || !Number.isInteger(ri) || ri < 1 || ri > 9999) {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
reason:
|
|
702
|
+
`Invalid recurrence_interval ${riRaw}. Must be a positive integer between 1 and 9999.`,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Cadence-intent guard: scan title + description for an "every N <unit>"
|
|
707
|
+
// phrase. If present, the chosen recurrence_type+interval MUST match.
|
|
708
|
+
const text = `${(params.title as string) || ""} ${(params.description as string) || ""}`.toLowerCase();
|
|
709
|
+
const cadenceMatch = text.match(
|
|
710
|
+
/every\s+(\d+)\s*(seconds?|secs?|minutes?|mins?|hours?|hrs?|days?|weeks?|months?)\b/,
|
|
711
|
+
);
|
|
712
|
+
if (!cadenceMatch) return { ok: true };
|
|
713
|
+
|
|
714
|
+
const n = parseInt(cadenceMatch[1], 10);
|
|
715
|
+
const unitRaw = cadenceMatch[2];
|
|
716
|
+
let expectedType: RecurrenceType;
|
|
717
|
+
let expectedInterval: number;
|
|
718
|
+
if (/^sec/.test(unitRaw)) {
|
|
719
|
+
expectedType = "minutely";
|
|
720
|
+
expectedInterval = Math.max(1, Math.ceil(n / 60));
|
|
721
|
+
} else if (/^min/.test(unitRaw)) {
|
|
722
|
+
expectedType = "minutely";
|
|
723
|
+
expectedInterval = n;
|
|
724
|
+
} else if (/^(hour|hr)/.test(unitRaw)) {
|
|
725
|
+
expectedType = "hourly";
|
|
726
|
+
expectedInterval = n;
|
|
727
|
+
} else if (/^day/.test(unitRaw)) {
|
|
728
|
+
expectedType = "daily";
|
|
729
|
+
expectedInterval = n;
|
|
730
|
+
} else if (/^week/.test(unitRaw)) {
|
|
731
|
+
expectedType = "weekly";
|
|
732
|
+
expectedInterval = n;
|
|
733
|
+
} else if (/^month/.test(unitRaw)) {
|
|
734
|
+
expectedType = "monthly";
|
|
735
|
+
expectedInterval = n;
|
|
736
|
+
} else {
|
|
737
|
+
return { ok: true };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (rt !== expectedType || ri !== expectedInterval) {
|
|
741
|
+
return {
|
|
742
|
+
ok: false,
|
|
743
|
+
reason:
|
|
744
|
+
`Recurrence mismatch: task text says "every ${n} ${unitRaw}" but you passed ` +
|
|
745
|
+
`recurrence_type="${rt}", recurrence_interval=${ri} (= ${humanizeCadence(rt, ri)}). ` +
|
|
746
|
+
`Correct combo: recurrence_type="${expectedType}", recurrence_interval=${expectedInterval} ` +
|
|
747
|
+
`(= ${humanizeCadence(expectedType, expectedInterval)}). ` +
|
|
748
|
+
`Re-call with the matching values, OR remove the "every N ${unitRaw}" phrase from the title/description if you intended a different cadence.`,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return { ok: true };
|
|
753
|
+
}
|
|
754
|
+
|
|
628
755
|
async function handleCreateTask(
|
|
629
756
|
supabase: SupabaseClient,
|
|
630
757
|
userId: string,
|
|
@@ -635,6 +762,10 @@ async function handleCreateTask(
|
|
|
635
762
|
try {
|
|
636
763
|
if (!params.title) return err("Missing required field: title");
|
|
637
764
|
|
|
765
|
+
// Fix #10 (2026-05-17): cadence-intent guard — see helper comment above.
|
|
766
|
+
const recurCheck = validateRecurrence(params);
|
|
767
|
+
if (!recurCheck.ok) return err(recurCheck.reason);
|
|
768
|
+
|
|
638
769
|
const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
639
770
|
const now = new Date().toISOString();
|
|
640
771
|
|
|
@@ -827,6 +958,33 @@ async function handleCreateTask(
|
|
|
827
958
|
nextRunAtEpoch = nowEpoch + 60 + Math.floor(Math.random() * 300);
|
|
828
959
|
}
|
|
829
960
|
|
|
961
|
+
// Fix #3 (2026-05-17): bulk-recurring stagger safety net. Fix #2
|
|
962
|
+
// only jitters past-time bumps; an LLM that intentionally seeds
|
|
963
|
+
// 6+ recurring tasks with a shared FUTURE T0 anchor still bursts
|
|
964
|
+
// every tick. Detect the burst by counting recent recurring
|
|
965
|
+
// scheduler_events for this user (last 60s); once >3 exist, push
|
|
966
|
+
// the new task's first fire by (recent × per-slot) seconds where
|
|
967
|
+
// per-slot = min(recurrence_interval_secs, 1800). For 6 minutely
|
|
968
|
+
// creates this spreads first-fires across ~5 min instead of one
|
|
969
|
+
// pg_cron tick. Cap per-slot at 30min so long-interval cadences
|
|
970
|
+
// (hourly/daily) don't push first fires hours out.
|
|
971
|
+
const rTypeStr = (params.recurrence_type as string) || "none";
|
|
972
|
+
const rIntervalNum = (params.recurrence_interval as number) || 1;
|
|
973
|
+
if (rTypeStr !== "none" && rIntervalNum > 0) {
|
|
974
|
+
const sinceISO = new Date(Date.now() - 60_000).toISOString();
|
|
975
|
+
const { count: recentCount } = await supabase.from("scheduler_events")
|
|
976
|
+
.select("id", { count: "exact", head: true })
|
|
977
|
+
.eq("user_id", userId)
|
|
978
|
+
.neq("recurrence_type", "none")
|
|
979
|
+
.gte("created_at", sinceISO);
|
|
980
|
+
const recent = recentCount ?? 0;
|
|
981
|
+
if (recent >= 3) {
|
|
982
|
+
const ivSecs = recurrenceIntervalSeconds(rTypeStr, rIntervalNum);
|
|
983
|
+
const perSlotSecs = Math.min(ivSecs, 1800);
|
|
984
|
+
nextRunAtEpoch += recent * perSlotSecs;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
830
988
|
await supabase.from("scheduler_events").insert({
|
|
831
989
|
id: crypto.randomUUID(),
|
|
832
990
|
user_id: userId,
|
|
@@ -858,9 +1016,19 @@ async function handleCreateTask(
|
|
|
858
1016
|
if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
|
|
859
1017
|
if (cf.system_prompt) extras.push("custom system prompt");
|
|
860
1018
|
|
|
861
|
-
// BUG 6 fix: surface recurrence fields in create response
|
|
1019
|
+
// BUG 6 fix: surface recurrence fields in create response.
|
|
1020
|
+
// Fix #10 (2026-05-17): also surface effective_cadence so the LLM sees the
|
|
1021
|
+
// human-readable interpretation (e.g. "every 35 minutes") of the values it
|
|
1022
|
+
// just passed — defense-in-depth alongside validateRecurrence().
|
|
862
1023
|
const recurrenceInfo = (params.recurrence_type && params.recurrence_type !== "none")
|
|
863
|
-
? {
|
|
1024
|
+
? {
|
|
1025
|
+
recurrence_type: params.recurrence_type,
|
|
1026
|
+
recurrence_interval: (params.recurrence_interval as number) || 1,
|
|
1027
|
+
effective_cadence: humanizeCadence(
|
|
1028
|
+
params.recurrence_type as string,
|
|
1029
|
+
(params.recurrence_interval as number) || 1,
|
|
1030
|
+
),
|
|
1031
|
+
}
|
|
864
1032
|
: undefined;
|
|
865
1033
|
|
|
866
1034
|
// BUG 7 fix: only claim scheduling when bridge actually fired
|