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 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
- `For recurring tasks: set start_date + recurrence_type + recurrence_interval. Example: every 2 minutes = recurrence_type: "minutely", recurrence_interval: 2.\n` +
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
- ? { recurrence_type: params.recurrence_type, recurrence_interval: params.recurrence_interval || 1 }
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.4",
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
- `For recurring tasks: set start_date + recurrence_type + recurrence_interval. Example: every 2 minutes = recurrence_type: "minutely", recurrence_interval: 2.\n` +
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
- ? { recurrence_type: params.recurrence_type, recurrence_interval: (params.recurrence_interval as number) || 1 }
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