@hybridaione/hybridclaw 0.2.2 → 0.2.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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +19 -3
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +16 -8
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +563 -73
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +179 -24
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +48 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +29 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/dist/scheduler.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scheduler — timer-based, arms for exact next-fire time.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* sets a single setTimeout for that moment. Re-arms after every tick
|
|
6
|
-
* and whenever a task is added/removed via rearmScheduler().
|
|
4
|
+
* Runs both legacy DB-backed tasks and config-backed scheduler.jobs.
|
|
7
5
|
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
8
|
import { CronExpressionParser } from 'cron-parser';
|
|
9
|
-
import {
|
|
9
|
+
import { DATA_DIR, getConfigSnapshot } from './config.js';
|
|
10
|
+
import { deleteTask, getAllEnabledTasks, markTaskFailure, markTaskSuccess, updateTaskLastRun, } from './db.js';
|
|
10
11
|
import { logger } from './logger.js';
|
|
11
12
|
const MAX_TIMER_DELAY_MS = 300_000; // 5 min safety net for clock drift
|
|
13
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
14
|
+
const CONFIG_ONESHOT_RETRY_MS = 60_000;
|
|
15
|
+
const SCHEDULER_STATE_VERSION = 1;
|
|
16
|
+
const SCHEDULER_STATE_PATH = path.join(DATA_DIR, 'scheduler-jobs-state.json');
|
|
12
17
|
let timer = null;
|
|
13
18
|
let taskRunner = null;
|
|
14
19
|
let ticking = false;
|
|
15
|
-
|
|
20
|
+
const schedulerState = loadSchedulerState();
|
|
21
|
+
// --- Prompt framing ---
|
|
16
22
|
function formatFireTime() {
|
|
17
23
|
return new Date().toLocaleString('en-US', {
|
|
18
24
|
weekday: 'short',
|
|
@@ -23,38 +29,236 @@ function formatFireTime() {
|
|
|
23
29
|
timeZoneName: 'short',
|
|
24
30
|
});
|
|
25
31
|
}
|
|
26
|
-
export function wrapCronPrompt(
|
|
32
|
+
export function wrapCronPrompt(jobLabel, message) {
|
|
27
33
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
28
|
-
return `[cron
|
|
34
|
+
return `[cron:${jobLabel}] ${message}\nCurrent time: ${formatFireTime()} (${tz})\n\nReturn your response as plain text; it will be delivered automatically. If the task explicitly calls for messaging a specific external recipient, note who/where it should go instead of sending it yourself.`;
|
|
29
35
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
function defaultConfigJobMeta() {
|
|
37
|
+
return {
|
|
38
|
+
lastRun: null,
|
|
39
|
+
lastStatus: null,
|
|
40
|
+
nextRunAt: null,
|
|
41
|
+
consecutiveErrors: 0,
|
|
42
|
+
disabled: false,
|
|
43
|
+
oneShotCompleted: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
function normalizeConfigJobMeta(value) {
|
|
50
|
+
if (!isRecord(value))
|
|
51
|
+
return defaultConfigJobMeta();
|
|
52
|
+
const lastRun = typeof value.lastRun === 'string' && value.lastRun.trim()
|
|
53
|
+
? value.lastRun.trim()
|
|
54
|
+
: null;
|
|
55
|
+
const lastStatus = value.lastStatus === 'success' || value.lastStatus === 'error'
|
|
56
|
+
? value.lastStatus
|
|
57
|
+
: null;
|
|
58
|
+
const nextRunAt = typeof value.nextRunAt === 'string' && value.nextRunAt.trim()
|
|
59
|
+
? value.nextRunAt.trim()
|
|
60
|
+
: null;
|
|
61
|
+
const consecutiveErrors = typeof value.consecutiveErrors === 'number' &&
|
|
62
|
+
Number.isFinite(value.consecutiveErrors)
|
|
63
|
+
? Math.max(0, Math.floor(value.consecutiveErrors))
|
|
64
|
+
: 0;
|
|
65
|
+
return {
|
|
66
|
+
lastRun,
|
|
67
|
+
lastStatus,
|
|
68
|
+
nextRunAt,
|
|
69
|
+
consecutiveErrors,
|
|
70
|
+
disabled: Boolean(value.disabled),
|
|
71
|
+
oneShotCompleted: Boolean(value.oneShotCompleted),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function loadSchedulerState() {
|
|
75
|
+
try {
|
|
76
|
+
if (!fs.existsSync(SCHEDULER_STATE_PATH)) {
|
|
77
|
+
return {
|
|
78
|
+
version: SCHEDULER_STATE_VERSION,
|
|
79
|
+
updatedAt: new Date(0).toISOString(),
|
|
80
|
+
configJobs: {},
|
|
81
|
+
};
|
|
42
82
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
const raw = fs.readFileSync(SCHEDULER_STATE_PATH, 'utf-8');
|
|
84
|
+
const parsed = JSON.parse(raw);
|
|
85
|
+
if (!isRecord(parsed))
|
|
86
|
+
throw new Error('state file root must be object');
|
|
87
|
+
const rawJobs = isRecord(parsed.configJobs) ? parsed.configJobs : {};
|
|
88
|
+
const configJobs = {};
|
|
89
|
+
for (const [id, meta] of Object.entries(rawJobs)) {
|
|
90
|
+
const key = id.trim();
|
|
91
|
+
if (!key)
|
|
92
|
+
continue;
|
|
93
|
+
configJobs[key] = normalizeConfigJobMeta(meta);
|
|
49
94
|
}
|
|
50
|
-
|
|
95
|
+
return {
|
|
96
|
+
version: SCHEDULER_STATE_VERSION,
|
|
97
|
+
updatedAt: typeof parsed.updatedAt === 'string' && parsed.updatedAt.trim()
|
|
98
|
+
? parsed.updatedAt
|
|
99
|
+
: new Date(0).toISOString(),
|
|
100
|
+
configJobs,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
logger.warn({ error }, 'Failed to load scheduler state file; starting with defaults');
|
|
105
|
+
return {
|
|
106
|
+
version: SCHEDULER_STATE_VERSION,
|
|
107
|
+
updatedAt: new Date(0).toISOString(),
|
|
108
|
+
configJobs: {},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function persistSchedulerState() {
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(path.dirname(SCHEDULER_STATE_PATH), { recursive: true });
|
|
115
|
+
schedulerState.updatedAt = new Date().toISOString();
|
|
116
|
+
const payload = `${JSON.stringify(schedulerState, null, 2)}\n`;
|
|
117
|
+
const tmpPath = `${SCHEDULER_STATE_PATH}.tmp-${process.pid}-${Date.now()}`;
|
|
118
|
+
fs.writeFileSync(tmpPath, payload, 'utf-8');
|
|
119
|
+
fs.renameSync(tmpPath, SCHEDULER_STATE_PATH);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger.warn({ error }, 'Failed to persist scheduler state file');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function getConfigJobMeta(jobId) {
|
|
126
|
+
const existing = schedulerState.configJobs[jobId];
|
|
127
|
+
if (existing)
|
|
128
|
+
return existing;
|
|
129
|
+
const created = defaultConfigJobMeta();
|
|
130
|
+
schedulerState.configJobs[jobId] = created;
|
|
131
|
+
return created;
|
|
132
|
+
}
|
|
133
|
+
function pruneConfigJobMeta(activeJobs) {
|
|
134
|
+
const activeIds = new Set(activeJobs.map((job) => job.id));
|
|
135
|
+
let changed = false;
|
|
136
|
+
for (const id of Object.keys(schedulerState.configJobs)) {
|
|
137
|
+
if (activeIds.has(id))
|
|
51
138
|
continue;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
139
|
+
delete schedulerState.configJobs[id];
|
|
140
|
+
changed = true;
|
|
141
|
+
}
|
|
142
|
+
if (changed)
|
|
143
|
+
persistSchedulerState();
|
|
144
|
+
}
|
|
145
|
+
function resolveConfigJobLabel(job) {
|
|
146
|
+
const candidate = typeof job.name === 'string' ? job.name.trim() : '';
|
|
147
|
+
return candidate || job.id;
|
|
148
|
+
}
|
|
149
|
+
function parseCronExpression(expr, tz) {
|
|
150
|
+
const trimmedTz = tz?.trim();
|
|
151
|
+
if (trimmedTz) {
|
|
152
|
+
return CronExpressionParser.parse(expr, { tz: trimmedTz });
|
|
153
|
+
}
|
|
154
|
+
return CronExpressionParser.parse(expr);
|
|
155
|
+
}
|
|
156
|
+
function nextFireMsForDbTask(task, nowMs) {
|
|
157
|
+
if (task.run_at) {
|
|
158
|
+
if (task.last_run)
|
|
159
|
+
return null;
|
|
160
|
+
const ms = new Date(task.run_at).getTime();
|
|
161
|
+
return Number.isFinite(ms) ? ms : null;
|
|
162
|
+
}
|
|
163
|
+
if (task.every_ms) {
|
|
164
|
+
const lastRunMs = task.last_run ? new Date(task.last_run).getTime() : 0;
|
|
165
|
+
return lastRunMs > 0 ? lastRunMs + task.every_ms : nowMs;
|
|
166
|
+
}
|
|
167
|
+
if (!task.cron_expr)
|
|
168
|
+
return null;
|
|
169
|
+
try {
|
|
170
|
+
const ms = CronExpressionParser.parse(task.cron_expr)
|
|
171
|
+
.next()
|
|
172
|
+
.toDate()
|
|
173
|
+
.getTime();
|
|
174
|
+
return Number.isFinite(ms) ? ms : null;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function toIsoTimestamp(ms) {
|
|
181
|
+
if (ms == null || !Number.isFinite(ms))
|
|
182
|
+
return null;
|
|
183
|
+
return new Date(ms).toISOString();
|
|
184
|
+
}
|
|
185
|
+
function syncConfigJobNextRunAt(job, nowMs) {
|
|
186
|
+
const meta = getConfigJobMeta(job.id);
|
|
187
|
+
const nextRunAt = toIsoTimestamp(nextFireMsForConfigJob(job, nowMs));
|
|
188
|
+
if (meta.nextRunAt === nextRunAt)
|
|
189
|
+
return false;
|
|
190
|
+
meta.nextRunAt = nextRunAt;
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
function syncConfigJobsNextRunAt(jobs, nowMs) {
|
|
194
|
+
let changed = false;
|
|
195
|
+
for (const job of jobs) {
|
|
196
|
+
if (syncConfigJobNextRunAt(job, nowMs))
|
|
197
|
+
changed = true;
|
|
198
|
+
}
|
|
199
|
+
return changed;
|
|
200
|
+
}
|
|
201
|
+
function nextFireMsForConfigJob(job, nowMs) {
|
|
202
|
+
if (!job.enabled)
|
|
203
|
+
return null;
|
|
204
|
+
const meta = getConfigJobMeta(job.id);
|
|
205
|
+
if (meta.disabled)
|
|
206
|
+
return null;
|
|
207
|
+
if (job.schedule.kind === 'at') {
|
|
208
|
+
if (meta.oneShotCompleted)
|
|
209
|
+
return null;
|
|
210
|
+
if (!job.schedule.at)
|
|
211
|
+
return null;
|
|
212
|
+
const atMs = new Date(job.schedule.at).getTime();
|
|
213
|
+
if (!Number.isFinite(atMs))
|
|
214
|
+
return null;
|
|
215
|
+
const lastRunMs = meta.lastRun ? new Date(meta.lastRun).getTime() : 0;
|
|
216
|
+
if (atMs > nowMs)
|
|
217
|
+
return atMs;
|
|
218
|
+
if (lastRunMs <= 0)
|
|
219
|
+
return atMs;
|
|
220
|
+
return lastRunMs + CONFIG_ONESHOT_RETRY_MS;
|
|
221
|
+
}
|
|
222
|
+
if (job.schedule.kind === 'every') {
|
|
223
|
+
if (!job.schedule.everyMs)
|
|
224
|
+
return null;
|
|
225
|
+
const lastRunMs = meta.lastRun ? new Date(meta.lastRun).getTime() : 0;
|
|
226
|
+
return lastRunMs > 0 ? lastRunMs + job.schedule.everyMs : nowMs;
|
|
227
|
+
}
|
|
228
|
+
if (!job.schedule.expr)
|
|
229
|
+
return null;
|
|
230
|
+
try {
|
|
231
|
+
const ms = parseCronExpression(job.schedule.expr, job.schedule.tz || undefined)
|
|
232
|
+
.next()
|
|
233
|
+
.toDate()
|
|
234
|
+
.getTime();
|
|
235
|
+
return Number.isFinite(ms) ? ms : null;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function computeNextFireMs(nowMs = Date.now()) {
|
|
242
|
+
const dbTasks = getAllEnabledTasks();
|
|
243
|
+
const cfgJobs = getConfigSnapshot().scheduler.jobs;
|
|
244
|
+
pruneConfigJobMeta(cfgJobs);
|
|
245
|
+
if (syncConfigJobsNextRunAt(cfgJobs, nowMs)) {
|
|
246
|
+
persistSchedulerState();
|
|
247
|
+
}
|
|
248
|
+
let earliest = null;
|
|
249
|
+
for (const task of dbTasks) {
|
|
250
|
+
const fireMs = nextFireMsForDbTask(task, nowMs);
|
|
251
|
+
if (fireMs === null)
|
|
252
|
+
continue;
|
|
253
|
+
if (earliest === null || fireMs < earliest)
|
|
254
|
+
earliest = fireMs;
|
|
255
|
+
}
|
|
256
|
+
for (const job of cfgJobs) {
|
|
257
|
+
const fireMs = nextFireMsForConfigJob(job, nowMs);
|
|
258
|
+
if (fireMs === null)
|
|
259
|
+
continue;
|
|
260
|
+
if (earliest === null || fireMs < earliest)
|
|
261
|
+
earliest = fireMs;
|
|
58
262
|
}
|
|
59
263
|
return earliest;
|
|
60
264
|
}
|
|
@@ -64,58 +268,141 @@ function arm() {
|
|
|
64
268
|
timer = null;
|
|
65
269
|
const nextFireMs = computeNextFireMs();
|
|
66
270
|
if (nextFireMs === null)
|
|
67
|
-
return;
|
|
271
|
+
return;
|
|
68
272
|
const delay = Math.max(nextFireMs - Date.now(), 0);
|
|
69
273
|
const clamped = Math.min(delay, MAX_TIMER_DELAY_MS);
|
|
70
274
|
logger.debug({ delayMs: clamped, nextFire: new Date(nextFireMs).toISOString() }, 'Scheduler armed');
|
|
71
275
|
timer = setTimeout(() => {
|
|
72
276
|
void tick().catch((err) => {
|
|
73
277
|
logger.error({ err }, 'Scheduler tick failed');
|
|
74
|
-
arm();
|
|
278
|
+
arm();
|
|
75
279
|
});
|
|
76
280
|
}, clamped);
|
|
77
281
|
}
|
|
282
|
+
async function dispatchDbTask(task) {
|
|
283
|
+
if (!taskRunner)
|
|
284
|
+
return;
|
|
285
|
+
const prompt = wrapCronPrompt(`#${task.id}`, task.prompt);
|
|
286
|
+
await taskRunner({
|
|
287
|
+
source: 'db-task',
|
|
288
|
+
taskId: task.id,
|
|
289
|
+
sessionId: task.session_id,
|
|
290
|
+
channelId: task.channel_id,
|
|
291
|
+
prompt,
|
|
292
|
+
actionKind: 'agent_turn',
|
|
293
|
+
delivery: {
|
|
294
|
+
kind: 'channel',
|
|
295
|
+
channelId: task.channel_id,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function dispatchConfigJob(job) {
|
|
300
|
+
if (!taskRunner)
|
|
301
|
+
return;
|
|
302
|
+
const jobLabel = resolveConfigJobLabel(job);
|
|
303
|
+
const contextChannelId = job.delivery.kind === 'channel' ? job.delivery.to : 'scheduler';
|
|
304
|
+
const prompt = job.action.kind === 'agent_turn'
|
|
305
|
+
? wrapCronPrompt(jobLabel, job.action.message)
|
|
306
|
+
: job.action.message;
|
|
307
|
+
await taskRunner({
|
|
308
|
+
source: 'config-job',
|
|
309
|
+
jobId: job.id,
|
|
310
|
+
sessionId: `scheduler:${job.id}`,
|
|
311
|
+
channelId: contextChannelId,
|
|
312
|
+
prompt,
|
|
313
|
+
actionKind: job.action.kind,
|
|
314
|
+
delivery: job.delivery.kind === 'channel'
|
|
315
|
+
? { kind: 'channel', channelId: job.delivery.to }
|
|
316
|
+
: job.delivery.kind === 'last-channel'
|
|
317
|
+
? { kind: 'last-channel' }
|
|
318
|
+
: { kind: 'webhook', webhookUrl: job.delivery.webhookUrl },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
function markConfigJobSuccess(job, markOneShotDone = false) {
|
|
322
|
+
const meta = getConfigJobMeta(job.id);
|
|
323
|
+
meta.lastStatus = 'success';
|
|
324
|
+
meta.consecutiveErrors = 0;
|
|
325
|
+
if (markOneShotDone)
|
|
326
|
+
meta.oneShotCompleted = true;
|
|
327
|
+
syncConfigJobNextRunAt(job, Date.now());
|
|
328
|
+
persistSchedulerState();
|
|
329
|
+
}
|
|
330
|
+
function markConfigJobFailure(job) {
|
|
331
|
+
const meta = getConfigJobMeta(job.id);
|
|
332
|
+
meta.lastStatus = 'error';
|
|
333
|
+
meta.consecutiveErrors = Math.max(0, meta.consecutiveErrors) + 1;
|
|
334
|
+
if (meta.consecutiveErrors >= MAX_CONSECUTIVE_FAILURES) {
|
|
335
|
+
meta.disabled = true;
|
|
336
|
+
}
|
|
337
|
+
syncConfigJobNextRunAt(job, Date.now());
|
|
338
|
+
persistSchedulerState();
|
|
339
|
+
return {
|
|
340
|
+
disabled: meta.disabled,
|
|
341
|
+
consecutiveErrors: meta.consecutiveErrors,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
78
344
|
async function tick() {
|
|
79
345
|
if (ticking) {
|
|
80
|
-
arm();
|
|
346
|
+
arm();
|
|
81
347
|
return;
|
|
82
348
|
}
|
|
83
349
|
ticking = true;
|
|
84
350
|
try {
|
|
85
|
-
const
|
|
351
|
+
const dbTasks = getAllEnabledTasks();
|
|
352
|
+
const cfgJobs = getConfigSnapshot().scheduler.jobs;
|
|
353
|
+
pruneConfigJobMeta(cfgJobs);
|
|
86
354
|
const now = new Date();
|
|
87
|
-
|
|
355
|
+
const nowMs = now.getTime();
|
|
356
|
+
for (const task of dbTasks) {
|
|
88
357
|
try {
|
|
89
|
-
// --- One-shot task ---
|
|
90
358
|
if (task.run_at) {
|
|
91
359
|
const runAt = new Date(task.run_at);
|
|
92
|
-
if (runAt.getTime() <=
|
|
360
|
+
if (runAt.getTime() <= nowMs && !task.last_run) {
|
|
93
361
|
logger.info({ taskId: task.id, runAt: task.run_at, prompt: task.prompt }, 'One-shot task firing');
|
|
94
|
-
updateTaskLastRun(task.id);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
362
|
+
updateTaskLastRun(task.id);
|
|
363
|
+
dispatchDbTask(task)
|
|
364
|
+
.then(() => {
|
|
365
|
+
markTaskSuccess(task.id);
|
|
366
|
+
deleteTask(task.id);
|
|
367
|
+
})
|
|
98
368
|
.catch((err) => {
|
|
369
|
+
const failure = markTaskFailure(task.id, MAX_CONSECUTIVE_FAILURES);
|
|
99
370
|
logger.error({ taskId: task.id, err }, 'One-shot task failed (task preserved)');
|
|
371
|
+
if (failure.disabled) {
|
|
372
|
+
logger.warn({
|
|
373
|
+
taskId: task.id,
|
|
374
|
+
consecutiveErrors: failure.consecutiveErrors,
|
|
375
|
+
}, 'Scheduled task auto-disabled after repeated failures');
|
|
376
|
+
}
|
|
100
377
|
});
|
|
101
378
|
}
|
|
102
379
|
continue;
|
|
103
380
|
}
|
|
104
|
-
// --- Interval task ---
|
|
105
381
|
if (task.every_ms) {
|
|
106
|
-
const lastRunMs = task.last_run
|
|
107
|
-
|
|
108
|
-
|
|
382
|
+
const lastRunMs = task.last_run
|
|
383
|
+
? new Date(task.last_run).getTime()
|
|
384
|
+
: 0;
|
|
385
|
+
const dueAt = lastRunMs > 0 ? lastRunMs + task.every_ms : 0;
|
|
386
|
+
if (dueAt <= nowMs) {
|
|
109
387
|
logger.info({ taskId: task.id, everyMs: task.every_ms, prompt: task.prompt }, 'Interval task firing');
|
|
110
388
|
updateTaskLastRun(task.id);
|
|
111
|
-
|
|
112
|
-
|
|
389
|
+
dispatchDbTask(task)
|
|
390
|
+
.then(() => {
|
|
391
|
+
markTaskSuccess(task.id);
|
|
392
|
+
})
|
|
393
|
+
.catch((err) => {
|
|
394
|
+
const failure = markTaskFailure(task.id, MAX_CONSECUTIVE_FAILURES);
|
|
113
395
|
logger.error({ taskId: task.id, err }, 'Interval task failed');
|
|
396
|
+
if (failure.disabled) {
|
|
397
|
+
logger.warn({
|
|
398
|
+
taskId: task.id,
|
|
399
|
+
consecutiveErrors: failure.consecutiveErrors,
|
|
400
|
+
}, 'Scheduled task auto-disabled after repeated failures');
|
|
401
|
+
}
|
|
114
402
|
});
|
|
115
403
|
}
|
|
116
404
|
continue;
|
|
117
405
|
}
|
|
118
|
-
// --- Recurring cron task ---
|
|
119
406
|
if (!task.cron_expr)
|
|
120
407
|
continue;
|
|
121
408
|
const cron = CronExpressionParser.parse(task.cron_expr);
|
|
@@ -124,22 +411,210 @@ async function tick() {
|
|
|
124
411
|
if (prev.toDate() > lastRun) {
|
|
125
412
|
logger.info({ taskId: task.id, cron: task.cron_expr, prompt: task.prompt }, 'Cron task firing');
|
|
126
413
|
updateTaskLastRun(task.id);
|
|
127
|
-
|
|
128
|
-
|
|
414
|
+
dispatchDbTask(task)
|
|
415
|
+
.then(() => {
|
|
416
|
+
markTaskSuccess(task.id);
|
|
417
|
+
})
|
|
418
|
+
.catch((err) => {
|
|
419
|
+
const failure = markTaskFailure(task.id, MAX_CONSECUTIVE_FAILURES);
|
|
129
420
|
logger.error({ taskId: task.id, err }, 'Cron task failed');
|
|
421
|
+
if (failure.disabled) {
|
|
422
|
+
logger.warn({
|
|
423
|
+
taskId: task.id,
|
|
424
|
+
consecutiveErrors: failure.consecutiveErrors,
|
|
425
|
+
}, 'Scheduled task auto-disabled after repeated failures');
|
|
426
|
+
}
|
|
130
427
|
});
|
|
131
428
|
}
|
|
132
429
|
}
|
|
133
430
|
catch (err) {
|
|
134
|
-
logger.error({ taskId: task.id, cron: task.cron_expr, err }, 'Scheduler error for task');
|
|
431
|
+
logger.error({ taskId: task.id, cron: task.cron_expr, err }, 'Scheduler error for DB task');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (const job of cfgJobs) {
|
|
435
|
+
if (!job.enabled)
|
|
436
|
+
continue;
|
|
437
|
+
const meta = getConfigJobMeta(job.id);
|
|
438
|
+
if (meta.disabled)
|
|
439
|
+
continue;
|
|
440
|
+
const jobLabel = resolveConfigJobLabel(job);
|
|
441
|
+
try {
|
|
442
|
+
if (job.schedule.kind === 'at') {
|
|
443
|
+
if (meta.oneShotCompleted || !job.schedule.at)
|
|
444
|
+
continue;
|
|
445
|
+
const runAtMs = new Date(job.schedule.at).getTime();
|
|
446
|
+
if (!Number.isFinite(runAtMs) || runAtMs > nowMs)
|
|
447
|
+
continue;
|
|
448
|
+
const lastRunMs = meta.lastRun ? new Date(meta.lastRun).getTime() : 0;
|
|
449
|
+
if (lastRunMs > 0 && nowMs - lastRunMs < CONFIG_ONESHOT_RETRY_MS)
|
|
450
|
+
continue;
|
|
451
|
+
meta.lastRun = now.toISOString();
|
|
452
|
+
persistSchedulerState();
|
|
453
|
+
logger.info({ jobId: job.id, jobLabel, runAt: job.schedule.at }, 'Config one-shot job firing');
|
|
454
|
+
dispatchConfigJob(job)
|
|
455
|
+
.then(() => {
|
|
456
|
+
markConfigJobSuccess(job, true);
|
|
457
|
+
})
|
|
458
|
+
.catch((err) => {
|
|
459
|
+
const failure = markConfigJobFailure(job);
|
|
460
|
+
logger.error({ jobId: job.id, jobLabel, err }, 'Config one-shot job failed');
|
|
461
|
+
if (failure.disabled) {
|
|
462
|
+
logger.warn({
|
|
463
|
+
jobId: job.id,
|
|
464
|
+
jobLabel,
|
|
465
|
+
consecutiveErrors: failure.consecutiveErrors,
|
|
466
|
+
}, 'Config scheduler job auto-disabled after repeated failures');
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (job.schedule.kind === 'every') {
|
|
472
|
+
const everyMs = job.schedule.everyMs;
|
|
473
|
+
if (!everyMs)
|
|
474
|
+
continue;
|
|
475
|
+
const lastRunMs = meta.lastRun ? new Date(meta.lastRun).getTime() : 0;
|
|
476
|
+
const dueAt = lastRunMs > 0 ? lastRunMs + everyMs : 0;
|
|
477
|
+
if (dueAt > nowMs)
|
|
478
|
+
continue;
|
|
479
|
+
meta.lastRun = now.toISOString();
|
|
480
|
+
persistSchedulerState();
|
|
481
|
+
logger.info({ jobId: job.id, jobLabel, everyMs }, 'Config interval job firing');
|
|
482
|
+
dispatchConfigJob(job)
|
|
483
|
+
.then(() => {
|
|
484
|
+
markConfigJobSuccess(job, false);
|
|
485
|
+
})
|
|
486
|
+
.catch((err) => {
|
|
487
|
+
const failure = markConfigJobFailure(job);
|
|
488
|
+
logger.error({ jobId: job.id, jobLabel, err }, 'Config interval job failed');
|
|
489
|
+
if (failure.disabled) {
|
|
490
|
+
logger.warn({
|
|
491
|
+
jobId: job.id,
|
|
492
|
+
jobLabel,
|
|
493
|
+
consecutiveErrors: failure.consecutiveErrors,
|
|
494
|
+
}, 'Config scheduler job auto-disabled after repeated failures');
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (!job.schedule.expr)
|
|
500
|
+
continue;
|
|
501
|
+
const cron = parseCronExpression(job.schedule.expr, job.schedule.tz || undefined);
|
|
502
|
+
const prev = cron.prev().toDate();
|
|
503
|
+
const lastRun = meta.lastRun ? new Date(meta.lastRun) : new Date(0);
|
|
504
|
+
if (prev <= lastRun)
|
|
505
|
+
continue;
|
|
506
|
+
meta.lastRun = now.toISOString();
|
|
507
|
+
persistSchedulerState();
|
|
508
|
+
logger.info({
|
|
509
|
+
jobId: job.id,
|
|
510
|
+
jobLabel,
|
|
511
|
+
expr: job.schedule.expr,
|
|
512
|
+
tz: job.schedule.tz,
|
|
513
|
+
}, 'Config cron job firing');
|
|
514
|
+
dispatchConfigJob(job)
|
|
515
|
+
.then(() => {
|
|
516
|
+
markConfigJobSuccess(job, false);
|
|
517
|
+
})
|
|
518
|
+
.catch((err) => {
|
|
519
|
+
const failure = markConfigJobFailure(job);
|
|
520
|
+
logger.error({ jobId: job.id, jobLabel, err }, 'Config cron job failed');
|
|
521
|
+
if (failure.disabled) {
|
|
522
|
+
logger.warn({
|
|
523
|
+
jobId: job.id,
|
|
524
|
+
jobLabel,
|
|
525
|
+
consecutiveErrors: failure.consecutiveErrors,
|
|
526
|
+
}, 'Config scheduler job auto-disabled after repeated failures');
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
logger.error({ jobId: job.id, jobLabel, err }, 'Scheduler error for config job');
|
|
135
532
|
}
|
|
136
533
|
}
|
|
137
534
|
}
|
|
138
535
|
finally {
|
|
139
536
|
ticking = false;
|
|
140
|
-
arm();
|
|
537
|
+
arm();
|
|
141
538
|
}
|
|
142
539
|
}
|
|
540
|
+
function toRuntimeState(meta) {
|
|
541
|
+
return {
|
|
542
|
+
lastRun: meta.lastRun,
|
|
543
|
+
lastStatus: meta.lastStatus,
|
|
544
|
+
nextRunAt: meta.nextRunAt,
|
|
545
|
+
disabled: meta.disabled,
|
|
546
|
+
consecutiveErrors: meta.consecutiveErrors,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
export function getConfigJobState(jobId) {
|
|
550
|
+
const normalizedJobId = jobId.trim();
|
|
551
|
+
if (!normalizedJobId)
|
|
552
|
+
return null;
|
|
553
|
+
const jobs = getConfigSnapshot().scheduler.jobs;
|
|
554
|
+
pruneConfigJobMeta(jobs);
|
|
555
|
+
const job = jobs.find((candidate) => candidate.id === normalizedJobId);
|
|
556
|
+
if (!job)
|
|
557
|
+
return null;
|
|
558
|
+
if (syncConfigJobNextRunAt(job, Date.now())) {
|
|
559
|
+
persistSchedulerState();
|
|
560
|
+
}
|
|
561
|
+
return toRuntimeState(getConfigJobMeta(normalizedJobId));
|
|
562
|
+
}
|
|
563
|
+
export function getSchedulerStatus() {
|
|
564
|
+
const jobs = getConfigSnapshot().scheduler.jobs;
|
|
565
|
+
pruneConfigJobMeta(jobs);
|
|
566
|
+
if (syncConfigJobsNextRunAt(jobs, Date.now())) {
|
|
567
|
+
persistSchedulerState();
|
|
568
|
+
}
|
|
569
|
+
return jobs.map((job) => {
|
|
570
|
+
const meta = getConfigJobMeta(job.id);
|
|
571
|
+
const description = typeof job.description === 'string' && job.description.trim()
|
|
572
|
+
? job.description.trim()
|
|
573
|
+
: null;
|
|
574
|
+
return {
|
|
575
|
+
id: job.id,
|
|
576
|
+
name: resolveConfigJobLabel(job),
|
|
577
|
+
description,
|
|
578
|
+
enabled: job.enabled,
|
|
579
|
+
...toRuntimeState(meta),
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
export function pauseConfigJob(jobId) {
|
|
584
|
+
const normalizedJobId = jobId.trim();
|
|
585
|
+
if (!normalizedJobId)
|
|
586
|
+
return false;
|
|
587
|
+
const jobs = getConfigSnapshot().scheduler.jobs;
|
|
588
|
+
pruneConfigJobMeta(jobs);
|
|
589
|
+
const job = jobs.find((candidate) => candidate.id === normalizedJobId);
|
|
590
|
+
if (!job)
|
|
591
|
+
return false;
|
|
592
|
+
const meta = getConfigJobMeta(normalizedJobId);
|
|
593
|
+
meta.disabled = true;
|
|
594
|
+
meta.nextRunAt = null;
|
|
595
|
+
persistSchedulerState();
|
|
596
|
+
rearmScheduler();
|
|
597
|
+
logger.info({ jobId: normalizedJobId, jobLabel: resolveConfigJobLabel(job) }, 'Config scheduler job paused');
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
export function resumeConfigJob(jobId) {
|
|
601
|
+
const normalizedJobId = jobId.trim();
|
|
602
|
+
if (!normalizedJobId)
|
|
603
|
+
return false;
|
|
604
|
+
const jobs = getConfigSnapshot().scheduler.jobs;
|
|
605
|
+
pruneConfigJobMeta(jobs);
|
|
606
|
+
const job = jobs.find((candidate) => candidate.id === normalizedJobId);
|
|
607
|
+
if (!job)
|
|
608
|
+
return false;
|
|
609
|
+
const meta = getConfigJobMeta(normalizedJobId);
|
|
610
|
+
meta.disabled = false;
|
|
611
|
+
meta.consecutiveErrors = 0;
|
|
612
|
+
syncConfigJobNextRunAt(job, Date.now());
|
|
613
|
+
persistSchedulerState();
|
|
614
|
+
rearmScheduler();
|
|
615
|
+
logger.info({ jobId: normalizedJobId, jobLabel: resolveConfigJobLabel(job) }, 'Config scheduler job resumed');
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
143
618
|
// --- Public API ---
|
|
144
619
|
export function startScheduler(runner) {
|
|
145
620
|
logger.info('Scheduler started');
|
|
@@ -147,8 +622,7 @@ export function startScheduler(runner) {
|
|
|
147
622
|
arm();
|
|
148
623
|
}
|
|
149
624
|
/**
|
|
150
|
-
* Re-arm the scheduler timer.
|
|
151
|
-
* so newly scheduled work is picked up immediately.
|
|
625
|
+
* Re-arm the scheduler timer. Call after creating/deleting tasks or updating config scheduler jobs.
|
|
152
626
|
*/
|
|
153
627
|
export function rearmScheduler() {
|
|
154
628
|
if (taskRunner)
|