amalgm 0.1.48 → 0.1.50

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.
@@ -1,18 +1,27 @@
1
1
  /**
2
- * Task scheduler — 30s poll loop.
2
+ * Task scheduler — 60s poll loop.
3
3
  *
4
- * Checks every enabled task each tick; if it's due and not already running,
5
- * kicks off an execution via tasks/executor.js. Scheduler is stateless;
6
- * currently-running tasks live in executor.js's `runningTasks` map.
4
+ * Claims due rows from SQLite, then kicks off execution via tasks/executor.js.
5
+ * The SQLite task_runs table is the durable receipt/claim log; executor.js
6
+ * keeps only abort controllers for live in-process cancellation.
7
7
  */
8
8
 
9
9
  const { SCHEDULER_INTERVAL_MS } = require('../config');
10
- const { loadTasks } = require('./store');
10
+ const { claimDueTaskRuns, expireStaleTaskRuns } = require('./store');
11
11
  const { executeTask, isRunning } = require('./executor');
12
12
  const { loadCronParser } = require('../deps');
13
13
 
14
14
  let schedulerTimer = null;
15
15
  let _cronParser = null;
16
+ let isPolling = false;
17
+
18
+ const schedulerStatus = {
19
+ startedAt: null,
20
+ lastTickAt: null,
21
+ lastError: null,
22
+ lastClaimed: 0,
23
+ lastExpired: 0,
24
+ };
16
25
 
17
26
  function cron() {
18
27
  if (!_cronParser) _cronParser = loadCronParser();
@@ -79,23 +88,45 @@ function isTaskDue(task, now) {
79
88
 
80
89
  function startScheduler() {
81
90
  console.log(`[AmalgmMCP:Scheduler] Starting (interval: ${SCHEDULER_INTERVAL_MS}ms)`);
82
- schedulerTimer = setInterval(() => {
83
- try {
84
- const data = loadTasks();
85
- const now = new Date();
86
- for (const task of data.tasks) {
87
- if (!task.enabled) continue;
88
- if (isRunning(task.id)) continue;
89
- if (isTaskDue(task, now)) {
90
- executeTask(task).catch((err) => {
91
- console.error(`[AmalgmMCP:Scheduler] Task ${task.id} error:`, err.message);
92
- });
93
- }
94
- }
95
- } catch (err) {
96
- console.error('[AmalgmMCP:Scheduler] Poll error:', err.message);
91
+ if (schedulerTimer) clearInterval(schedulerTimer);
92
+ schedulerStatus.startedAt = new Date().toISOString();
93
+ pollScheduler();
94
+ schedulerTimer = setInterval(pollScheduler, SCHEDULER_INTERVAL_MS);
95
+ }
96
+
97
+ function pollScheduler() {
98
+ if (isPolling) return;
99
+ isPolling = true;
100
+ try {
101
+ const now = new Date();
102
+ schedulerStatus.lastTickAt = now.toISOString();
103
+ schedulerStatus.lastExpired = expireStaleTaskRuns(now, { source: 'scheduler:expire' });
104
+ const claims = claimDueTaskRuns(now, { source: 'scheduler' });
105
+ schedulerStatus.lastClaimed = claims.length;
106
+ schedulerStatus.lastError = null;
107
+
108
+ for (const claim of claims) {
109
+ if (!claim?.task || !claim?.run) continue;
110
+ if (isRunning(claim.task.id)) continue;
111
+ executeTask(claim.task, claim.run).catch((err) => {
112
+ console.error(`[AmalgmMCP:Scheduler] Task ${claim.task.id} error:`, err.message);
113
+ });
97
114
  }
98
- }, SCHEDULER_INTERVAL_MS);
115
+ } catch (err) {
116
+ schedulerStatus.lastError = err.message;
117
+ console.error('[AmalgmMCP:Scheduler] Poll error:', err.message);
118
+ } finally {
119
+ isPolling = false;
120
+ }
121
+ }
122
+
123
+ function getSchedulerStatus() {
124
+ return {
125
+ intervalMs: SCHEDULER_INTERVAL_MS,
126
+ running: Boolean(schedulerTimer),
127
+ polling: isPolling,
128
+ ...schedulerStatus,
129
+ };
99
130
  }
100
131
 
101
132
  function stopScheduler() {
@@ -105,4 +136,11 @@ function stopScheduler() {
105
136
  }
106
137
  }
107
138
 
108
- module.exports = { validateCronExpr, isTaskDue, startScheduler, stopScheduler };
139
+ module.exports = {
140
+ getSchedulerStatus,
141
+ isTaskDue,
142
+ pollScheduler,
143
+ startScheduler,
144
+ stopScheduler,
145
+ validateCronExpr,
146
+ };