@trevonistrevon/pi-loop 0.3.0 → 0.4.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/src/index.ts CHANGED
@@ -22,6 +22,7 @@ import { parseInterval } from "./loop-parse.js";
22
22
  import { MonitorManager } from "./monitor-manager.js";
23
23
  import { CronScheduler } from "./scheduler.js";
24
24
  import { LoopStore } from "./store.js";
25
+ import { TaskStore } from "./task-store.js";
25
26
  import { TriggerSystem } from "./trigger-system.js";
26
27
  import type { LoopEntry, Trigger } from "./types.js";
27
28
  import { LoopWidget } from "./ui/widget.js";
@@ -68,19 +69,36 @@ export default function (pi: ExtensionAPI) {
68
69
  return join(process.cwd(), ".pi", "loops", "loops.json");
69
70
  }
70
71
 
72
+ function resolveTaskStorePath(): string | undefined {
73
+ if (loopScope === "memory") return undefined;
74
+ return join(process.cwd(), ".pi", "tasks", "tasks.json");
75
+ }
76
+
71
77
  let store = new LoopStore(resolveStorePath());
72
78
  const monitorManager = new MonitorManager(pi);
73
79
  let scheduler: CronScheduler;
74
80
  let triggerSystem: TriggerSystem;
75
- const widget = new LoopWidget(store, undefined, monitorManager);
81
+ const widget = new LoopWidget(store, monitorManager);
82
+ widget.setTaskSummaryProvider(() => {
83
+ if (!nativeTaskStore) return { count: 0 };
84
+ const tasks = nativeTaskStore.list().filter(t => t.status === "pending" || t.status === "in_progress");
85
+ const active = tasks.find(t => t.status === "in_progress");
86
+ const next = tasks.find(t => t.status === "pending");
87
+ const focus = active
88
+ ? `active: ${active.subject.slice(0, 50)}`
89
+ : next
90
+ ? `next: ${next.subject.slice(0, 50)}`
91
+ : undefined;
92
+ return { count: tasks.length, focusText: focus };
93
+ });
76
94
 
77
95
  scheduler = new CronScheduler(store, onLoopFire);
78
- widget.setScheduler(scheduler);
79
- triggerSystem = new TriggerSystem(pi, scheduler, store);
96
+ triggerSystem = new TriggerSystem(pi, scheduler, store, onLoopFire);
80
97
 
81
98
  // ── pi-tasks integration ──
82
99
  let tasksAvailable = false;
83
- const _PROTOCOL_VERSION = 1;
100
+ let nativeTaskStore: TaskStore | undefined;
101
+ let nativeTasksRegistered = false;
84
102
 
85
103
  function checkTasksVersion() {
86
104
  const requestId = randomUUID();
@@ -97,46 +115,79 @@ export default function (pi: ExtensionAPI) {
97
115
  pi.events.on("tasks:ready", () => checkTasksVersion());
98
116
 
99
117
  async function autoCreateTask(entry: LoopEntry): Promise<string | undefined> {
100
- if (!tasksAvailable || !entry.autoTask) return undefined;
101
- try {
102
- const requestId = randomUUID();
103
- const taskId = await new Promise<string | undefined>((resolve, _reject) => {
104
- const timer = setTimeout(() => { unsub(); resolve(undefined); }, 5000);
105
- const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw: unknown) => {
106
- unsub(); clearTimeout(timer);
107
- const reply = raw as { success: boolean; data?: { id: string }; error?: string };
108
- if (reply.success && reply.data) resolve(reply.data.id);
109
- else resolve(undefined);
118
+ if (!entry.autoTask) return undefined;
119
+ if (tasksAvailable) {
120
+ try {
121
+ const requestId = randomUUID();
122
+ const taskId = await new Promise<string | undefined>((resolve, _reject) => {
123
+ const timer = setTimeout(() => { unsub(); resolve(undefined); }, 5000);
124
+ const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw: unknown) => {
125
+ unsub(); clearTimeout(timer);
126
+ const reply = raw as { success: boolean; data?: { id: string }; error?: string };
127
+ if (reply.success && reply.data) resolve(reply.data.id);
128
+ else resolve(undefined);
129
+ });
130
+ pi.events.emit("tasks:rpc:create", {
131
+ requestId,
132
+ subject: entry.prompt.slice(0, 80),
133
+ description: `Auto-created from loop #${entry.id}`,
134
+ metadata: { loopId: entry.id, trigger: entry.trigger },
135
+ });
110
136
  });
111
- pi.events.emit("tasks:rpc:create", {
112
- requestId,
113
- subject: entry.prompt.slice(0, 80),
114
- description: `Auto-created from loop #${entry.id}`,
115
- metadata: { loopId: entry.id, trigger: entry.trigger },
116
- });
117
- });
118
- return taskId;
119
- } catch {
120
- return undefined;
137
+ return taskId;
138
+ } catch {
139
+ return undefined;
140
+ }
121
141
  }
142
+ if (!nativeTaskStore) return undefined;
143
+ const task = nativeTaskStore.create(entry.prompt.slice(0, 80), `Auto-created from loop #${entry.id}`, {
144
+ loopId: entry.id,
145
+ trigger: entry.trigger,
146
+ });
147
+ widget.update();
148
+ return task.id;
122
149
  }
123
150
 
124
151
  async function hasPendingTasks(): Promise<number> {
125
- if (!tasksAvailable) return -1;
126
- try {
127
- const requestId = randomUUID();
128
- const count = await new Promise<number>((resolve) => {
129
- const timer = setTimeout(() => { unsub(); resolve(-1); }, 3000);
130
- const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw: unknown) => {
131
- unsub(); clearTimeout(timer);
132
- const reply = raw as { success: boolean; data?: { pending: number }; error?: string };
133
- resolve(reply.success && reply.data ? reply.data.pending : -1);
152
+ if (tasksAvailable) {
153
+ try {
154
+ const requestId = randomUUID();
155
+ const count = await new Promise<number>((resolve) => {
156
+ const timer = setTimeout(() => { unsub(); resolve(-1); }, 3000);
157
+ const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw: unknown) => {
158
+ unsub(); clearTimeout(timer);
159
+ const reply = raw as { success: boolean; data?: { pending: number }; error?: string };
160
+ resolve(reply.success && reply.data ? reply.data.pending : -1);
161
+ });
162
+ pi.events.emit("tasks:rpc:pending", { requestId });
134
163
  });
135
- pi.events.emit("tasks:rpc:pending", { requestId });
136
- });
137
- return count;
138
- } catch {
139
- return -1;
164
+ return count;
165
+ } catch {
166
+ return -1;
167
+ }
168
+ }
169
+ return nativeTaskStore ? nativeTaskStore.pendingCount() : -1;
170
+ }
171
+
172
+ async function cleanDoneTasks(): Promise<void> {
173
+ if (tasksAvailable) {
174
+ try {
175
+ const requestId = randomUUID();
176
+ await new Promise<void>((resolve) => {
177
+ const timer = setTimeout(() => { unsub(); resolve(); }, 3000);
178
+ const unsub = pi.events.on(`tasks:rpc:clean:reply:${requestId}`, () => {
179
+ unsub(); clearTimeout(timer);
180
+ debug("tasks:rpc:clean — done tasks swept");
181
+ resolve();
182
+ });
183
+ pi.events.emit("tasks:rpc:clean", { requestId });
184
+ });
185
+ } catch { /* timeout or error, ignore */ }
186
+ return;
187
+ }
188
+ if (nativeTaskStore) {
189
+ nativeTaskStore.sweepCompleted();
190
+ widget.update();
140
191
  }
141
192
  }
142
193
 
@@ -147,7 +198,7 @@ export default function (pi: ExtensionAPI) {
147
198
 
148
199
  if (entry.maxFires && (entry.fireCount ?? 0) >= entry.maxFires) {
149
200
  debug(`loop #${entry.id} — reached maxFires ${entry.maxFires}, expiring`);
150
- store.update(entry.id, { status: "expired" });
201
+ store.delete(entry.id);
151
202
  return;
152
203
  }
153
204
  store.update(entry.id, { fireCount: (entry.fireCount ?? 0) + 1 });
@@ -183,8 +234,7 @@ export default function (pi: ExtensionAPI) {
183
234
  store = new LoopStore(path);
184
235
  widget.setStore(store);
185
236
  scheduler = new CronScheduler(store, onLoopFire);
186
- widget.setScheduler(scheduler);
187
- triggerSystem = new TriggerSystem(pi, scheduler, store);
237
+ triggerSystem = new TriggerSystem(pi, scheduler, store, onLoopFire);
188
238
  }
189
239
  storeUpgraded = true;
190
240
  }
@@ -206,6 +256,7 @@ export default function (pi: ExtensionAPI) {
206
256
  _latestCtx = ctx;
207
257
  widget.setUICtx(ctx.ui);
208
258
  upgradeStoreIfNeeded(ctx);
259
+ widget.update();
209
260
  });
210
261
 
211
262
  pi.on("before_agent_start", async (_event, ctx) => {
@@ -213,6 +264,7 @@ export default function (pi: ExtensionAPI) {
213
264
  widget.setUICtx(ctx.ui);
214
265
  upgradeStoreIfNeeded(ctx);
215
266
  showPersistedLoops();
267
+ widget.update();
216
268
  });
217
269
 
218
270
  pi.on("session_switch" as any, async (event: SessionSwitchEvent, ctx: ExtensionContext) => {
@@ -230,6 +282,7 @@ export default function (pi: ExtensionAPI) {
230
282
 
231
283
  upgradeStoreIfNeeded(ctx);
232
284
  showPersistedLoops(isResume);
285
+ widget.update();
233
286
  });
234
287
 
235
288
  // ── Loop fire handler — sends a user message to re-wake the agent ──
@@ -245,7 +298,8 @@ export default function (pi: ExtensionAPI) {
245
298
  if (data.autoTask) {
246
299
  const pending = await hasPendingTasks();
247
300
  if (pending === 0) {
248
- debug(`loop:fire #${data.loopId} — no pending tasks, skipping`);
301
+ debug(`loop:fire #${data.loopId} — no pending tasks, skipping, requesting cleanup`);
302
+ cleanDoneTasks();
249
303
  return;
250
304
  }
251
305
  }
@@ -382,7 +436,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
382
436
  if (monitor && monitor.status !== "running") {
383
437
  debug(`loop #${entry.id} — monitor #${monitorId} already ${monitor.status}, expiring`);
384
438
  triggerSystem.remove(entry.id);
385
- store.update(entry.id, { status: "expired" });
439
+ store.delete(entry.id);
386
440
  }
387
441
  }
388
442
  } catch { /* filter parse failure, ignore */ }
@@ -413,7 +467,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
413
467
  if (monitor && monitor.status !== "running") {
414
468
  debug(`onDone loop #${doneLoop.id} — monitor #${monitorId} already ${monitor.status}, expiring`);
415
469
  triggerSystem.remove(doneLoop.id);
416
- store.update(doneLoop.id, { status: "expired" });
470
+ store.delete(doneLoop.id);
417
471
  }
418
472
  }
419
473
 
@@ -474,7 +528,7 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
474
528
  ? scheduler.nextFire(entry.id)
475
529
  : undefined;
476
530
 
477
- const statusIcon = entry.status === "active" ? "" : entry.status === "paused" ? "" : "";
531
+ const statusIcon = entry.status === "active" ? "*" : entry.status === "paused" ? "-" : "x";
478
532
  let line = `${statusIcon} #${entry.id} [${entry.status}] ${entry.prompt.slice(0, 60)}`;
479
533
  line += ` (${triggerDesc})`;
480
534
  if (nextFire) {
@@ -607,7 +661,7 @@ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fire
607
661
 
608
662
  const lines: string[] = [];
609
663
  for (const m of monitors) {
610
- const icon = m.status === "running" ? "" : m.status === "completed" ? "" : "";
664
+ const icon = m.status === "running" ? ">" : m.status === "completed" ? "ok" : "!!";
611
665
  const age = Date.now() - m.startedAt;
612
666
  const ageStr = formatRemaining(age);
613
667
  let line = `${icon} #${m.id} [${m.status}] ${m.command.slice(0, 60)} — ${m.outputLines} lines (${ageStr})`;
@@ -744,45 +798,45 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
744
798
  async function viewLoops(ui: ExtensionUIContext) {
745
799
  const loops = store.list();
746
800
  if (loops.length === 0) {
747
- await ui.select("No active loops", [" Back"]);
801
+ await ui.select("No active loops", ["< Back"]);
748
802
  return;
749
803
  }
750
804
 
751
805
  const choices = loops.map((l: LoopEntry) => {
752
- const icon = l.status === "active" ? "" : l.status === "paused" ? "" : "";
806
+ const icon = l.status === "active" ? "*" : l.status === "paused" ? "-" : "x";
753
807
  const triggerDesc = l.trigger.type === "cron" ? `cron: ${l.trigger.schedule}` : l.trigger.type === "event" ? `event: ${l.trigger.source}` : `hybrid: ${l.trigger.cron}`;
754
808
  return `${icon} #${l.id} [${l.status}] ${l.prompt.slice(0, 50)} (${triggerDesc})`;
755
809
  });
756
- choices.push(" Back");
810
+ choices.push("< Back");
757
811
 
758
812
  const selected = await ui.select("Active Loops", choices);
759
- if (!selected || selected === " Back") return;
813
+ if (!selected || selected === "< Back") return;
760
814
 
761
815
  const match = selected.match(/#(\d+)/);
762
816
  if (match) {
763
817
  const entry = store.get(match[1]);
764
818
  if (entry) {
765
- const actions = [" Delete"];
766
- if (entry.status === "active") actions.unshift(" Pause");
767
- else if (entry.status === "paused") actions.unshift(" Resume");
768
- actions.push(" Back");
819
+ const actions = ["x Delete"];
820
+ if (entry.status === "active") actions.unshift("- Pause");
821
+ else if (entry.status === "paused") actions.unshift("* Resume");
822
+ actions.push("< Back");
769
823
 
770
824
  const action = await ui.select(
771
825
  `#${entry.id}: ${entry.prompt}\nTrigger: ${JSON.stringify(entry.trigger)}`,
772
826
  actions,
773
827
  );
774
828
 
775
- if (action === " Delete") {
829
+ if (action === "x Delete") {
776
830
  triggerSystem.remove(entry.id);
777
831
  store.delete(entry.id);
778
832
  widget.update();
779
833
  ui.notify(`Loop #${entry.id} deleted`, "info");
780
- } else if (action === " Pause") {
834
+ } else if (action === "- Pause") {
781
835
  store.update(entry.id, { status: "paused" });
782
836
  triggerSystem.remove(entry.id);
783
837
  widget.update();
784
838
  ui.notify(`Loop #${entry.id} paused`, "info");
785
- } else if (action === " Resume") {
839
+ } else if (action === "* Resume") {
786
840
  store.update(entry.id, { status: "active" });
787
841
  triggerSystem.add(entry);
788
842
  widget.update();
@@ -799,4 +853,193 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
799
853
  const active = loops.filter(l => l.status === "active").length;
800
854
  ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
801
855
  }
856
+
857
+ async function createNativeTaskInteractively(ui: ExtensionUIContext) {
858
+ if (!nativeTaskStore) {
859
+ ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
860
+ return;
861
+ }
862
+
863
+ const subject = await ui.input("Task subject");
864
+ if (!subject) return;
865
+ const description = await ui.input("Task description") || subject;
866
+ const entry = nativeTaskStore.create(subject, description);
867
+ widget.update();
868
+ ui.notify(`Task #${entry.id} created`, "info");
869
+ }
870
+
871
+ async function viewNativeTasks(ui: ExtensionUIContext): Promise<void> {
872
+ if (!nativeTaskStore) {
873
+ ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
874
+ return;
875
+ }
876
+
877
+ const tasks = nativeTaskStore.list();
878
+ const choices = tasks.map((task) => {
879
+ const icon = task.status === "in_progress" ? ">" : task.status === "completed" ? "ok" : "*";
880
+ return `${icon} #${task.id} [${task.status}] ${task.subject.slice(0, 60)}`;
881
+ });
882
+ choices.unshift("+ Create task");
883
+ choices.push("< Back");
884
+
885
+ const selected = await ui.select("Native Tasks", choices);
886
+ if (!selected || selected === "< Back") return;
887
+ if (selected === "+ Create task") {
888
+ await createNativeTaskInteractively(ui);
889
+ return viewNativeTasks(ui);
890
+ }
891
+
892
+ const match = selected.match(/#(\d+)/);
893
+ if (!match) return viewNativeTasks(ui);
894
+
895
+ const task = nativeTaskStore.get(match[1]);
896
+ if (!task) return viewNativeTasks(ui);
897
+
898
+ const actions = ["x Delete"];
899
+ if (task.status === "pending") {
900
+ actions.unshift("ok Complete");
901
+ actions.unshift("> Start");
902
+ } else if (task.status === "in_progress") {
903
+ actions.unshift("ok Complete");
904
+ actions.unshift("* Return to pending");
905
+ } else {
906
+ actions.unshift("* Reopen");
907
+ }
908
+ actions.push("< Back");
909
+
910
+ const action = await ui.select(`#${task.id}: ${task.subject}\n\n${task.description}`, actions);
911
+ if (!action || action === "< Back") return viewNativeTasks(ui);
912
+
913
+ if (action === "x Delete") {
914
+ nativeTaskStore.delete(task.id);
915
+ ui.notify(`Task #${task.id} deleted`, "info");
916
+ } else if (action === "> Start") {
917
+ nativeTaskStore.update(task.id, { status: "in_progress" });
918
+ ui.notify(`Task #${task.id} started`, "info");
919
+ } else if (action === "ok Complete") {
920
+ nativeTaskStore.update(task.id, { status: "completed" });
921
+ ui.notify(`Task #${task.id} completed`, "info");
922
+ } else if (action === "* Return to pending" || action === "* Reopen") {
923
+ nativeTaskStore.update(task.id, { status: "pending" });
924
+ ui.notify(`Task #${task.id} reopened`, "info");
925
+ }
926
+
927
+ widget.update();
928
+ return viewNativeTasks(ui);
929
+ }
930
+
931
+ // ── Native task tools (only when pi-tasks is absent) ──
932
+
933
+ setTimeout(async () => {
934
+ if (tasksAvailable || nativeTasksRegistered) return;
935
+ nativeTaskStore = new TaskStore(resolveTaskStorePath());
936
+ nativeTasksRegistered = true;
937
+ const taskStore = nativeTaskStore;
938
+
939
+ pi.registerCommand("tasks", {
940
+ description: "View or manage native pi-loop tasks when pi-tasks is not installed",
941
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
942
+ const trimmed = args.trim();
943
+ if (!nativeTaskStore) {
944
+ ctx.ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
945
+ return;
946
+ }
947
+ if (trimmed) {
948
+ const entry = nativeTaskStore.create(trimmed.slice(0, 80), trimmed);
949
+ widget.update();
950
+ ctx.ui.notify(`Task #${entry.id} created`, "info");
951
+ return;
952
+ }
953
+ await viewNativeTasks(ctx.ui);
954
+ },
955
+ });
956
+
957
+ pi.registerTool({
958
+ name: "TaskCreate",
959
+ label: "TaskCreate",
960
+ description: `Create a task for tracking work across turns. Use when you need to track progress on complex multi-step tasks.
961
+
962
+ Fields:
963
+ - subject: brief actionable title
964
+ - description: detailed requirements
965
+ - metadata: optional tags/metadata`,
966
+ parameters: Type.Object({
967
+ subject: Type.String({ description: "Brief actionable title for the task" }),
968
+ description: Type.String({ description: "Detailed description of what needs to be done" }),
969
+ }),
970
+ execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
971
+ const entry = taskStore.create(params.subject, params.description);
972
+ widget.update();
973
+ return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}`));
974
+ },
975
+ });
976
+
977
+ pi.registerTool({
978
+ name: "TaskList",
979
+ label: "TaskList",
980
+ description: `List all tasks with status. Use to check progress and find available work.`,
981
+ parameters: Type.Object({}),
982
+ execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
983
+ const tasks = taskStore.list();
984
+ if (tasks.length === 0) return Promise.resolve(textResult("No tasks."));
985
+
986
+ const lines: string[] = [];
987
+ const statuses: Record<"pending" | "in_progress" | "completed", number> = {
988
+ pending: 0,
989
+ in_progress: 0,
990
+ completed: 0,
991
+ };
992
+ for (const t of tasks) {
993
+ statuses[t.status]++;
994
+ const icon = t.status === "completed" ? "ok" : t.status === "in_progress" ? ">" : "*";
995
+ lines.push(`${icon} #${t.id} [${t.status}] ${t.subject.slice(0, 80)}`);
996
+ }
997
+ lines.unshift(`${tasks.length} tasks (${statuses.pending} pending, ${statuses.in_progress} in progress, ${statuses.completed} done)`);
998
+ return Promise.resolve(textResult(lines.join("\n")));
999
+ },
1000
+ });
1001
+
1002
+ pi.registerTool({
1003
+ name: "TaskUpdate",
1004
+ label: "TaskUpdate",
1005
+ description: `Update task status or details. Set status to "in_progress" before starting work, "completed" when done.
1006
+
1007
+ Statuses: pending → in_progress → completed`,
1008
+ parameters: Type.Object({
1009
+ id: Type.String({ description: "Task ID to update" }),
1010
+ status: Type.Optional(Type.String({ description: "New status", enum: ["pending", "in_progress", "completed"] })),
1011
+ subject: Type.Optional(Type.String({ description: "New title" })),
1012
+ description: Type.Optional(Type.String({ description: "New description" })),
1013
+ }),
1014
+ execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1015
+ const { id, status, subject, description } = params;
1016
+ const entry = taskStore.update(id, {
1017
+ status: status as "pending" | "in_progress" | "completed" | undefined,
1018
+ subject,
1019
+ description,
1020
+ });
1021
+ if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
1022
+ widget.update();
1023
+ const statusMsg = status ? ` → ${status}` : "";
1024
+ return Promise.resolve(textResult(`Task #${id} updated${statusMsg}`));
1025
+ },
1026
+ });
1027
+
1028
+ pi.registerTool({
1029
+ name: "TaskDelete",
1030
+ label: "TaskDelete",
1031
+ description: `Delete a task by ID. Use for cleaning up completed or irrelevant tasks.`,
1032
+ parameters: Type.Object({
1033
+ id: Type.String({ description: "Task ID to delete" }),
1034
+ }),
1035
+ execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1036
+ const deleted = taskStore.delete(params.id);
1037
+ widget.update();
1038
+ if (deleted) return Promise.resolve(textResult(`Task #${params.id} deleted`));
1039
+ return Promise.resolve(textResult(`Task #${params.id} not found`));
1040
+ },
1041
+ });
1042
+
1043
+ debug("native task tools registered (pi-tasks not detected)");
1044
+ }, 6000);
802
1045
  }
package/src/scheduler.ts CHANGED
@@ -65,7 +65,7 @@ export class CronScheduler {
65
65
  const now = Date.now();
66
66
 
67
67
  if (fireTime > entry.expiresAt) {
68
- this.store.update(entry.id, { status: "expired" });
68
+ this.store.delete(entry.id);
69
69
  return;
70
70
  }
71
71
 
@@ -85,7 +85,7 @@ export class CronScheduler {
85
85
 
86
86
  const now2 = Date.now();
87
87
  if (now2 >= current.expiresAt) {
88
- this.store.update(entry.id, { status: "expired" });
88
+ this.store.delete(entry.id);
89
89
  this.timers.delete(entry.id);
90
90
  this.fireTimes.delete(entry.id);
91
91
  return;
@@ -96,7 +96,7 @@ export class CronScheduler {
96
96
  if (current.recurring) {
97
97
  const fresh = this.store.get(entry.id);
98
98
  if (fresh?.maxFires && (fresh.fireCount ?? 0) >= fresh.maxFires) {
99
- this.store.update(entry.id, { status: "expired" });
99
+ this.store.delete(entry.id);
100
100
  this.timers.delete(entry.id);
101
101
  this.fireTimes.delete(entry.id);
102
102
  return;