@johpaz/hive-sdk 0.1.4 → 0.1.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/CHANGELOG.md +129 -0
- package/README.md +78 -23
- package/bun.lock +55 -29
- package/bunfig.toml +4 -2
- package/docs/API-AGENTS.md +78 -27
- package/docs/API-CONTEXT-COMPILER.md +31 -34
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
- package/docs/HIVE-HARNESS.md +1 -1
- package/docs/INDEX.md +4 -4
- package/docs/TEMPLATE-HIVE-APP.md +10 -10
- package/package.json +17 -12
- package/packages/cli/package.json +2 -2
- package/packages/cli/src/commands/create-app.test.ts +36 -7
- package/packages/cli/src/commands/init.ts +3 -3
- package/packages/cli/src/commands/run.ts +1 -1
- package/packages/cli/src/commands/test.ts +37 -25
- package/packages/cli/src/commands/trace.ts +30 -28
- package/packages/cli/templates/hive-app/.env.example +10 -2
- package/packages/cli/templates/hive-app/README.md +103 -0
- package/packages/cli/templates/hive-app/hive.config.ts +9 -3
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
- package/packages/cli/templates/hive-app/src/main.ts +12 -19
- package/packages/core/package.json +13 -12
- package/packages/core/src/agent/acceptance-checks.ts +172 -0
- package/packages/core/src/agent/agent-catalog.ts +348 -0
- package/packages/core/src/agent/agent-loop.ts +1373 -0
- package/packages/core/src/agent/capability-search.ts +186 -0
- package/packages/core/src/agent/catalog-selector.ts +103 -0
- package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
- package/packages/core/src/agent/context-compiler.ts +689 -0
- package/packages/core/src/agent/conversation-store.ts +381 -0
- package/packages/core/src/agent/curator.ts +276 -0
- package/packages/core/src/agent/delegation-runtime.ts +241 -0
- package/packages/core/src/agent/goal-runner.ts +323 -0
- package/packages/core/src/agent/index.ts +17 -12
- package/packages/core/src/agent/llm-client.ts +266 -0
- package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
- package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
- package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
- package/packages/core/src/agent/llm-providers/groq.ts +5 -0
- package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
- package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
- package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
- package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
- package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
- package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
- package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
- package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
- package/packages/core/src/agent/llm-providers/openai.ts +5 -0
- package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
- package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
- package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
- package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
- package/packages/core/src/agent/minimal-loadout.ts +47 -0
- package/packages/core/src/agent/playbook-selector.ts +119 -0
- package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
- package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
- package/packages/core/src/agent/providers/index.ts +35 -16
- package/packages/core/src/agent/reflector.ts +320 -0
- package/packages/core/src/agent/routing-intent.ts +22 -0
- package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
- package/packages/core/src/{harness → agent}/run-store.ts +142 -81
- package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
- package/packages/core/src/agent/skill-selector.ts +374 -0
- package/packages/core/src/agent/stuck-loop.ts +209 -0
- package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
- package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
- package/packages/core/src/api/createAgent.test.ts +139 -27
- package/packages/core/src/api/createAgent.ts +232 -44
- package/packages/core/src/artifacts/store.ts +162 -0
- package/packages/core/src/canvas/canvas-manager.ts +161 -0
- package/packages/core/src/canvas/canvas.test.ts +8 -4
- package/packages/core/src/canvas/emitter.ts +131 -80
- package/packages/core/src/canvas/index.ts +1 -3
- package/packages/core/src/channels/base.ts +9 -1
- package/packages/core/src/channels/discord.ts +5 -4
- package/packages/core/src/channels/manager.ts +122 -30
- package/packages/core/src/channels/slack.ts +5 -4
- package/packages/core/src/channels/telegram.ts +36 -6
- package/packages/core/src/channels/webchat.ts +11 -10
- package/packages/core/src/channels/whatsapp.ts +23 -7
- package/packages/core/src/config/index.ts +13 -2
- package/packages/core/src/config/loader.ts +76 -29
- package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
- package/packages/core/src/ethics/EthicsGuard.ts +51 -47
- package/packages/core/src/events/agent-bus.ts +44 -68
- package/packages/core/src/events/channel-narration.ts +150 -0
- package/packages/core/src/events/narration.ts +82 -0
- package/packages/core/src/events/tool-narration.ts +62 -0
- package/packages/core/src/gateway/delegation-groups.ts +258 -0
- package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
- package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
- package/packages/core/src/gateway/lane-queue.ts +173 -0
- package/packages/core/src/gateway/notification-inbox.ts +57 -0
- package/packages/core/src/gateway/server.ts +1 -1
- package/packages/core/src/harness/index.ts +46 -27
- package/packages/core/src/index.ts +33 -27
- package/packages/core/src/mcp/hot-reload.ts +32 -23
- package/packages/core/src/mcp/index.ts +6 -3
- package/packages/core/src/mcp/singleton.ts +1 -4
- package/packages/core/src/mcp/tool-sync.ts +138 -0
- package/packages/core/src/memory/Scratchpad.test.ts +39 -20
- package/packages/core/src/memory/Scratchpad.ts +27 -34
- package/packages/core/src/multimodal/vision-service.ts +44 -38
- package/packages/core/src/resilience/retry.ts +95 -0
- package/packages/core/src/scheduler/CronScheduler.ts +334 -287
- package/packages/core/src/scheduler/index.ts +9 -7
- package/packages/core/src/scheduler/integration.ts +46 -26
- package/packages/core/src/scheduler/scheduler.test.ts +9 -13
- package/packages/core/src/scheduler/types.ts +7 -2
- package/packages/core/src/security/Pairing.ts +1 -1
- package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
- package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
- package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
- package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
- package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
- package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
- package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
- package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
- package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
- package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
- package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
- package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
- package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
- package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
- package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
- package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
- package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
- package/packages/core/src/skills/skills.test.ts +52 -11
- package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
- package/packages/core/src/storage/bootstrap.ts +151 -0
- package/packages/core/src/storage/causal-events.ts +84 -0
- package/packages/core/src/storage/collections.ts +680 -0
- package/packages/core/src/storage/crypto.ts +205 -74
- package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
- package/packages/core/src/storage/hivedb.ts +61 -0
- package/packages/core/src/storage/index.ts +111 -18
- package/packages/core/src/storage/model-id.ts +53 -0
- package/packages/core/src/storage/onboarding.ts +540 -972
- package/packages/core/src/storage/reconcile.ts +238 -0
- package/packages/core/src/storage/seed.ts +572 -406
- package/packages/core/src/storage/usage.ts +285 -225
- package/packages/core/src/storage/user-email.ts +11 -0
- package/packages/core/src/swarm/AgentExecutor.ts +1 -1
- package/packages/core/src/swarm/EventBridge.ts +1 -1
- package/packages/core/src/swarm/index.ts +12 -9
- package/packages/core/src/tool-runtime/index.ts +146 -23
- package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
- package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
- package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
- package/packages/core/src/tools/agents/get-available-models.ts +36 -54
- package/packages/core/src/tools/agents/index.ts +784 -292
- package/packages/core/src/tools/api/api-request.test.ts +164 -0
- package/packages/core/src/tools/api/api-request.ts +174 -0
- package/packages/core/src/tools/api/index.ts +16 -0
- package/packages/core/src/tools/cli/index.ts +4 -0
- package/packages/core/src/tools/core/index.ts +281 -112
- package/packages/core/src/tools/cron/index.ts +121 -124
- package/packages/core/src/tools/index.ts +63 -78
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
- package/packages/core/src/tools/types.ts +3 -1
- package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
- package/packages/core/src/tools/web/browser-backend.ts +129 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +80 -35
- package/packages/core/src/tools/web/browser-type.ts +3 -8
- package/packages/core/src/tools/web/index.ts +4 -4
- package/packages/core/src/tools/web/webview-backend.ts +412 -0
- package/packages/core/src/voice/index.ts +89 -63
- package/packages/core/src/workers/agent.worker.ts +2 -2
- package/packages/core/src/workers/workers.test.ts +3 -10
- package/scripts/bump-version.ts +248 -0
- package/scripts/generate-skill-bundle.ts +108 -0
- package/test/acceptance-checks.test.ts +403 -0
- package/test/agent-loop-terminal-synthesis.test.ts +32 -0
- package/test/browser-backend.test.ts +308 -0
- package/test/catalog-agents-stay-enabled.test.ts +117 -0
- package/test/causal-events.test.ts +117 -0
- package/test/compaction.test.ts +105 -0
- package/test/context-compiler.test.ts +269 -0
- package/test/curator.test.ts +130 -0
- package/test/durable-queue.test.ts +114 -0
- package/test/harness-barrel.test.ts +64 -0
- package/test/hive-helpers.test.ts +130 -0
- package/test/hivedb-search.test.ts +189 -0
- package/test/internal-turns.test.ts +166 -0
- package/test/job-idempotency.test.ts +68 -0
- package/test/job-retry-backoff.test.ts +184 -0
- package/test/job-store.test.ts +381 -0
- package/test/llm-retry.test.ts +97 -0
- package/test/memory-perf.test.ts +774 -0
- package/test/minimal-loadout.test.ts +78 -0
- package/test/model-catalog.test.ts +105 -0
- package/test/preload.ts +12 -0
- package/test/reflector.test.ts +320 -0
- package/test/retention-cap.test.ts +91 -0
- package/test/retired-capabilities-pruned.test.ts +192 -0
- package/test/run-store.test.ts +355 -0
- package/test/scratchpad.test.ts +74 -0
- package/test/secrets-durability.test.ts +119 -0
- package/test/seed-model-reseed.test.ts +155 -0
- package/test/setup-agent-seed.test.ts +264 -0
- package/test/tool-inventory.test.ts +65 -0
- package/test/tool-runtime.test.ts +258 -0
- package/test/tool-selector-runtime-tools.test.ts +117 -0
- package/test/toon.test.ts +429 -0
- package/tsconfig.json +2 -0
- package/packages/core/src/ace/Curator.ts +0 -158
- package/packages/core/src/ace/Reflector.ts +0 -200
- package/packages/core/src/ace/index.ts +0 -4
- package/packages/core/src/agent/AgentRunner.ts +0 -711
- package/packages/core/src/agent/ContextCompiler.ts +0 -567
- package/packages/core/src/agent/ContextGuard.ts +0 -91
- package/packages/core/src/agent/ConversationStore.ts +0 -254
- package/packages/core/src/agent/Hooks.ts +0 -166
- package/packages/core/src/agent/StuckLoop.ts +0 -133
- package/packages/core/src/agent/providers/LLMClient.ts +0 -149
- package/packages/core/src/agent/providers/anthropic.ts +0 -212
- package/packages/core/src/agent/providers/openai-compat.ts +0 -231
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
- package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
- package/packages/core/src/agent/selectors/index.ts +0 -6
- package/packages/core/src/auth/auth.ts +0 -121
- package/packages/core/src/auth/index.ts +0 -1
- package/packages/core/src/canvas/CanvasManager.ts +0 -390
- package/packages/core/src/canvas/canvas-tools.ts +0 -448
- package/packages/core/src/harness/collections.ts +0 -98
- package/packages/core/src/harness/goal-verifier.ts +0 -141
- package/packages/core/src/harness/harness.test.ts +0 -236
- package/packages/core/src/harness/reconcile.ts +0 -149
- package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
- package/packages/core/src/multimodal/VisionService.ts +0 -293
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
- package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
- package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
- package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
- package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
- package/packages/core/src/scheduler/dag/errors.ts +0 -37
- package/packages/core/src/scheduler/dag/index.ts +0 -26
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
- package/packages/core/src/storage/HiveDBStorage.ts +0 -64
- package/packages/core/src/storage/SQLiteStorage.ts +0 -414
- package/packages/core/src/storage/hiveSeed.ts +0 -308
- package/packages/core/src/storage/hiveStorage.test.ts +0 -38
- package/packages/core/src/storage/schema.ts +0 -689
- package/packages/core/src/storage/storage.test.ts +0 -37
- package/packages/core/src/swarm/AgentBus.ts +0 -460
- package/packages/core/src/swarm/EventBus.ts +0 -169
- package/packages/core/src/swarm/WorkerPool.ts +0 -236
- package/packages/core/src/tools/bridge-events.ts +0 -26
- package/packages/core/src/tools/canvas/index.ts +0 -375
- package/packages/core/src/tools/codebridge/index.ts +0 -342
- package/packages/core/src/tools/meeting/index.ts +0 -353
- package/packages/core/src/tools/projects/index.ts +0 -37
- package/packages/core/src/tools/projects/project-create.ts +0 -94
- package/packages/core/src/tools/projects/project-done.ts +0 -66
- package/packages/core/src/tools/projects/project-fail.ts +0 -66
- package/packages/core/src/tools/projects/project-list.ts +0 -96
- package/packages/core/src/tools/projects/project-update.ts +0 -72
- package/packages/core/src/tools/projects/task-create.ts +0 -68
- package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
- package/packages/core/src/tools/projects/task-update.ts +0 -93
- package/packages/core/src/tools/voice/index.ts +0 -104
- package/packages/core/src/tools/web/api-request.test.ts +0 -170
- package/packages/core/src/tools/web/api-request.ts +0 -239
- package/test/setup-db.ts +0 -216
- /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hive CronScheduler
|
|
3
|
-
*
|
|
4
|
-
* Croner-based scheduler for Hive with
|
|
3
|
+
*
|
|
4
|
+
* Croner-based scheduler for Hive with HiveDB persistence.
|
|
5
5
|
* Manages recurring and one-shot cron jobs that execute through the agent pipeline.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Cron } from "croner";
|
|
9
|
-
import type { Database } from "bun:sqlite";
|
|
10
9
|
import { logger } from "../utils/logger";
|
|
11
10
|
import { notifyTaskCompletion } from "./integration";
|
|
11
|
+
import { col, toIndexable, fromIndexable } from "../storage/hive";
|
|
12
|
+
import type { CronJobDoc, TaskRunDoc } from "../storage/collections";
|
|
13
|
+
import { expireArtifacts } from "../artifacts/store";
|
|
12
14
|
import type {
|
|
13
15
|
CronJob,
|
|
14
16
|
TaskRun,
|
|
@@ -20,38 +22,107 @@ import type {
|
|
|
20
22
|
|
|
21
23
|
const log = logger.child("CronScheduler");
|
|
22
24
|
|
|
25
|
+
function fromDoc(doc: CronJobDoc): CronJob {
|
|
26
|
+
return { ...doc, agent_id: fromIndexable(doc.agent_id) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toDoc(job: CronJob): CronJobDoc {
|
|
30
|
+
return { ...job, agent_id: toIndexable(job.agent_id) };
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
export class CronScheduler {
|
|
24
34
|
private jobs: Map<string, Cron> = new Map();
|
|
25
|
-
private db: Database;
|
|
26
35
|
private handler: CronJobExecutionHandler;
|
|
27
36
|
private cleanupTaskId: string | null = null;
|
|
28
37
|
|
|
29
|
-
constructor(
|
|
30
|
-
this.db = db;
|
|
38
|
+
constructor(handler: CronJobExecutionHandler) {
|
|
31
39
|
this.handler = handler;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
* Boot the scheduler - load all active jobs from DB and activate them
|
|
43
|
+
* Boot the scheduler - load all active jobs from DB and activate them.
|
|
44
|
+
* Also detects misfires (next_run_at/fire_at in the past) and handles
|
|
45
|
+
* them according to misfire_policy.
|
|
36
46
|
*/
|
|
37
|
-
boot(): void {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
async boot(): Promise<void> {
|
|
48
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
49
|
+
const tasks = (await cronJobsCol.findBy("status", "active")).map(e => fromDoc(e.doc));
|
|
50
|
+
|
|
51
|
+
const now = new Date();
|
|
52
|
+
let misfireCount = 0;
|
|
41
53
|
|
|
42
54
|
for (const task of tasks) {
|
|
43
|
-
|
|
55
|
+
// ── Misfire detection ──────────────────────────────────────────────
|
|
56
|
+
const misfirePolicy = task.misfire_policy ?? "skip";
|
|
57
|
+
const graceMin = task.misfire_grace_min ?? 60;
|
|
58
|
+
const graceCutoff = new Date(now.getTime() - graceMin * 60 * 1000);
|
|
59
|
+
|
|
60
|
+
let misfireTime: Date | null = null;
|
|
61
|
+
if (task.task_type === "recurring" && task.next_run_at) {
|
|
62
|
+
const nextRun = new Date(task.next_run_at);
|
|
63
|
+
if (nextRun < now) {
|
|
64
|
+
misfireTime = nextRun;
|
|
65
|
+
}
|
|
66
|
+
} else if (task.task_type === "one_shot" && task.fire_at) {
|
|
67
|
+
const fireAt = new Date(task.fire_at);
|
|
68
|
+
if (fireAt < now) {
|
|
69
|
+
misfireTime = fireAt;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (misfireTime) {
|
|
74
|
+
misfireCount++;
|
|
75
|
+
const withinGrace = misfireTime >= graceCutoff;
|
|
76
|
+
|
|
77
|
+
if (misfirePolicy === "fire_once" && withinGrace) {
|
|
78
|
+
log.info(`[boot:misfire] Job "${task.name}" (${task.id}) misfired at ${misfireTime.toISOString()} — executing now (fire_once, within grace)`);
|
|
79
|
+
// Activate the job first (so the Croner handle exists for future runs)
|
|
80
|
+
await this.activate(task);
|
|
81
|
+
// Then execute it immediately
|
|
82
|
+
this.execute(task.id).catch((err) => {
|
|
83
|
+
log.error(`[boot:misfire] Catch-up execution failed for "${task.name}": ${(err as Error).message}`);
|
|
84
|
+
});
|
|
85
|
+
} else if (task.task_type === "one_shot") {
|
|
86
|
+
// A missed one-shot that won't be caught up can never fire again
|
|
87
|
+
// (its fire_at is in the past); activating it would leave a zombie
|
|
88
|
+
// "active" job forever.
|
|
89
|
+
log.warn(`[boot:misfire] One-shot "${task.name}" (${task.id}) missed at ${misfireTime.toISOString()} (policy=${misfirePolicy}${misfirePolicy === "fire_once" ? ", outside grace" : ""}) → failed`);
|
|
90
|
+
await this.updateJob(task.id, {
|
|
91
|
+
status: "failed",
|
|
92
|
+
last_error: `Missed while down (fire_at=${misfireTime.toISOString()}, policy=${misfirePolicy})`,
|
|
93
|
+
updated_at: now.toISOString(),
|
|
94
|
+
});
|
|
95
|
+
} else if (misfirePolicy === "fire_once" && !withinGrace) {
|
|
96
|
+
log.warn(`[boot:misfire] Job "${task.name}" (${task.id}) misfired at ${misfireTime.toISOString()} — outside grace window (${graceMin}min), skipping catch-up`);
|
|
97
|
+
await this.updateJob(task.id, {
|
|
98
|
+
last_error: `Missed run at ${misfireTime.toISOString()} (outside grace)`,
|
|
99
|
+
updated_at: now.toISOString(),
|
|
100
|
+
});
|
|
101
|
+
await this.activate(task);
|
|
102
|
+
} else {
|
|
103
|
+
// skip policy — recurring re-schedules its next occurrence via Croner
|
|
104
|
+
log.info(`[boot:misfire] Job "${task.name}" (${task.id}) misfired at ${misfireTime.toISOString()} — skipping (misfire_policy=skip)`);
|
|
105
|
+
await this.updateJob(task.id, {
|
|
106
|
+
last_error: `Missed run at ${misfireTime.toISOString()} (policy: skip)`,
|
|
107
|
+
updated_at: now.toISOString(),
|
|
108
|
+
});
|
|
109
|
+
await this.activate(task);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// No misfire — activate normally
|
|
113
|
+
await this.activate(task);
|
|
114
|
+
}
|
|
44
115
|
}
|
|
45
116
|
|
|
46
|
-
log.info(`[boot] Loaded ${tasks.length} active job(s)`);
|
|
117
|
+
log.info(`[boot] Loaded ${tasks.length} active job(s)${misfireCount > 0 ? `, ${misfireCount} misfire(s) detected` : ""}`);
|
|
47
118
|
|
|
48
|
-
this.ensureCleanupTask();
|
|
119
|
+
await this.ensureCleanupTask();
|
|
49
120
|
}
|
|
50
121
|
|
|
51
122
|
/**
|
|
52
123
|
* Activate a cron job - create or recreate its Croner instance
|
|
53
124
|
*/
|
|
54
|
-
activate(task: CronJob): void {
|
|
125
|
+
async activate(task: CronJob): Promise<void> {
|
|
55
126
|
const existingJob = this.jobs.get(task.id);
|
|
56
127
|
if (existingJob) {
|
|
57
128
|
existingJob.stop();
|
|
@@ -66,10 +137,13 @@ export class CronScheduler {
|
|
|
66
137
|
|
|
67
138
|
// Fix 2A: auto-pause jobs that exceeded the error threshold
|
|
68
139
|
const MAX_ERRORS = 5;
|
|
140
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
69
141
|
if (task.error_count >= MAX_ERRORS) {
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
142
|
+
await this.updateJob(task.id, {
|
|
143
|
+
status: "paused",
|
|
144
|
+
last_error: `Auto-paused after ${MAX_ERRORS} consecutive errors`,
|
|
145
|
+
updated_at: new Date().toISOString(),
|
|
146
|
+
});
|
|
73
147
|
log.warn(`[activate] Job "${task.name}" (${task.id}) auto-paused (error_count=${task.error_count})`);
|
|
74
148
|
return;
|
|
75
149
|
}
|
|
@@ -134,9 +208,7 @@ export class CronScheduler {
|
|
|
134
208
|
if (nextRun) {
|
|
135
209
|
const nextRunIso = nextRun.toISOString();
|
|
136
210
|
// Fix 4: also update updated_at when writing next_run_at
|
|
137
|
-
this.
|
|
138
|
-
"UPDATE cron_jobs SET next_run_at = ?, updated_at = ? WHERE id = ?"
|
|
139
|
-
).run(nextRunIso, new Date().toISOString(), task.id);
|
|
211
|
+
await this.updateJob(task.id, { next_run_at: nextRunIso, updated_at: new Date().toISOString() });
|
|
140
212
|
log.info(`[activate] Job "${task.name}" (${task.id}) scheduled - next: ${nextRunIso}`);
|
|
141
213
|
} else {
|
|
142
214
|
log.warn(`[activate] Job "${task.name}" (${task.id}) has no next run date`);
|
|
@@ -146,16 +218,35 @@ export class CronScheduler {
|
|
|
146
218
|
}
|
|
147
219
|
}
|
|
148
220
|
|
|
221
|
+
/** Read-modify-write helper for cron_jobs partial updates, retrying on OCC conflict. */
|
|
222
|
+
private async updateJob(taskId: string, patch: Partial<CronJobDoc>): Promise<CronJobDoc | null> {
|
|
223
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
224
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
225
|
+
const existing = await cronJobsCol.get(taskId);
|
|
226
|
+
if (!existing) return null;
|
|
227
|
+
const merged = { ...existing.doc, ...patch };
|
|
228
|
+
try {
|
|
229
|
+
await cronJobsCol.put(taskId, merged, { expectedVersion: existing.version });
|
|
230
|
+
return merged;
|
|
231
|
+
} catch {
|
|
232
|
+
// Version conflict — retry with a fresh read.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
throw new Error(`CronScheduler.updateJob: too much contention on cronJobs/${taskId}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
149
238
|
/**
|
|
150
239
|
* Execute a cron job - run it through the agent pipeline
|
|
151
240
|
*/
|
|
152
241
|
private async execute(taskId: string): Promise<void> {
|
|
153
242
|
// Fix 1: read fresh task data from DB to avoid stale closure snapshots
|
|
154
|
-
const
|
|
155
|
-
|
|
243
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
244
|
+
const taskEntry = await cronJobsCol.get(taskId);
|
|
245
|
+
if (!taskEntry) {
|
|
156
246
|
log.warn(`[execute] Job "${taskId}" not found in DB — skipping`);
|
|
157
247
|
return;
|
|
158
248
|
}
|
|
249
|
+
const task = fromDoc(taskEntry.doc);
|
|
159
250
|
|
|
160
251
|
const runId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
161
252
|
const startedAt = new Date().toISOString();
|
|
@@ -163,11 +254,19 @@ export class CronScheduler {
|
|
|
163
254
|
|
|
164
255
|
log.info(`[execute] Starting job "${task.name}" (${task.id}) run #${runId}`);
|
|
165
256
|
|
|
257
|
+
const taskRunsCol = await col<TaskRunDoc>("taskRuns");
|
|
166
258
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
259
|
+
await taskRunsCol.put(runId, {
|
|
260
|
+
id: runId,
|
|
261
|
+
task_id: task.id,
|
|
262
|
+
status: "running",
|
|
263
|
+
started_at: startedAt,
|
|
264
|
+
finished_at: null,
|
|
265
|
+
duration_ms: null,
|
|
266
|
+
error_message: null,
|
|
267
|
+
payload_snapshot: task.payload,
|
|
268
|
+
agent_response: null,
|
|
269
|
+
}, { expectedVersion: 0 });
|
|
171
270
|
} catch (err) {
|
|
172
271
|
log.error(`[execute] Failed to create task_run record: ${(err as Error).message}`);
|
|
173
272
|
}
|
|
@@ -178,36 +277,31 @@ export class CronScheduler {
|
|
|
178
277
|
const finishedAt = new Date().toISOString();
|
|
179
278
|
|
|
180
279
|
if (result.success) {
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
280
|
+
await this.updateTaskRun(runId, {
|
|
281
|
+
status: "success",
|
|
282
|
+
finished_at: finishedAt,
|
|
283
|
+
duration_ms: Math.round(duration),
|
|
284
|
+
agent_response: result.response?.slice(0, 1000) || null,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const refreshed = await this.updateJob(task.id, {
|
|
288
|
+
run_count: task.run_count + 1,
|
|
289
|
+
last_run_at: finishedAt,
|
|
290
|
+
last_error: null,
|
|
291
|
+
});
|
|
192
292
|
|
|
193
293
|
const job = this.jobs.get(task.id);
|
|
194
294
|
if (job) {
|
|
195
295
|
const nextRun = job.nextRun();
|
|
196
296
|
if (nextRun) {
|
|
197
|
-
this.
|
|
198
|
-
"UPDATE cron_jobs SET next_run_at = ? WHERE id = ?"
|
|
199
|
-
).run(nextRun.toISOString(), task.id);
|
|
297
|
+
await this.updateJob(task.id, { next_run_at: nextRun.toISOString() });
|
|
200
298
|
}
|
|
201
299
|
}
|
|
202
300
|
|
|
203
301
|
await notifyTaskCompletion(task.id, task.name, true, result.response);
|
|
204
302
|
|
|
205
303
|
if (task.task_type === "one_shot") {
|
|
206
|
-
this.
|
|
207
|
-
UPDATE cron_jobs
|
|
208
|
-
SET status = 'completed', completed_at = ?
|
|
209
|
-
WHERE id = ?
|
|
210
|
-
`).run(finishedAt, task.id);
|
|
304
|
+
await this.updateJob(task.id, { status: "completed", completed_at: finishedAt });
|
|
211
305
|
this.deactivate(task.id);
|
|
212
306
|
log.info(`[execute] One-shot job "${task.name}" (${task.id}) completed`);
|
|
213
307
|
} else {
|
|
@@ -221,30 +315,24 @@ export class CronScheduler {
|
|
|
221
315
|
const finishedAt = new Date().toISOString();
|
|
222
316
|
const errorMessage = (err as Error).message;
|
|
223
317
|
|
|
224
|
-
this.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
318
|
+
await this.updateTaskRun(runId, {
|
|
319
|
+
status: "failed",
|
|
320
|
+
finished_at: finishedAt,
|
|
321
|
+
duration_ms: Math.round(duration),
|
|
322
|
+
error_message: errorMessage,
|
|
323
|
+
});
|
|
229
324
|
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
`).run(errorMessage, task.id);
|
|
325
|
+
const updated = await this.updateJob(task.id, {
|
|
326
|
+
error_count: task.error_count + 1,
|
|
327
|
+
last_error: errorMessage,
|
|
328
|
+
});
|
|
235
329
|
|
|
236
330
|
log.error(`[execute] Job "${task.name}" (${task.id}) failed: ${errorMessage}`);
|
|
237
331
|
|
|
238
332
|
// Fix 2B: auto-pause if error threshold reached
|
|
239
333
|
const MAX_ERRORS = 5;
|
|
240
|
-
const updated = this.db.query(
|
|
241
|
-
"SELECT error_count FROM cron_jobs WHERE id = ?"
|
|
242
|
-
).get(task.id) as { error_count: number } | null;
|
|
243
|
-
|
|
244
334
|
if (updated && updated.error_count >= MAX_ERRORS) {
|
|
245
|
-
this.
|
|
246
|
-
"UPDATE cron_jobs SET status = 'paused', updated_at = ? WHERE id = ?"
|
|
247
|
-
).run(new Date().toISOString(), task.id);
|
|
335
|
+
await this.updateJob(task.id, { status: "paused", updated_at: new Date().toISOString() });
|
|
248
336
|
this.deactivate(task.id);
|
|
249
337
|
log.warn(`[execute] Job "${task.name}" (${task.id}) auto-paused after ${MAX_ERRORS} errors`);
|
|
250
338
|
}
|
|
@@ -253,6 +341,22 @@ export class CronScheduler {
|
|
|
253
341
|
}
|
|
254
342
|
}
|
|
255
343
|
|
|
344
|
+
/** Read-modify-write helper for task_runs partial updates, retrying on OCC conflict. */
|
|
345
|
+
private async updateTaskRun(runId: string, patch: Partial<TaskRunDoc>): Promise<void> {
|
|
346
|
+
const taskRunsCol = await col<TaskRunDoc>("taskRuns");
|
|
347
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
348
|
+
const existing = await taskRunsCol.get(runId);
|
|
349
|
+
if (!existing) return;
|
|
350
|
+
try {
|
|
351
|
+
await taskRunsCol.put(runId, { ...existing.doc, ...patch }, { expectedVersion: existing.version });
|
|
352
|
+
return;
|
|
353
|
+
} catch {
|
|
354
|
+
// Version conflict — retry with a fresh read.
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
log.warn(`[updateTaskRun] Too much contention on taskRuns/${runId}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
256
360
|
/**
|
|
257
361
|
* Handle errors from Croner
|
|
258
362
|
*/
|
|
@@ -260,38 +364,45 @@ export class CronScheduler {
|
|
|
260
364
|
log.error(`[error] Job "${task.name}" (${task.id}) error: ${error.message}`);
|
|
261
365
|
|
|
262
366
|
// Fix 3: record Croner-level errors in task_runs for full history
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
367
|
+
Promise.resolve().then(async () => {
|
|
368
|
+
const runId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
369
|
+
const now = new Date().toISOString();
|
|
370
|
+
try {
|
|
371
|
+
const taskRunsCol = await col<TaskRunDoc>("taskRuns");
|
|
372
|
+
await taskRunsCol.put(runId, {
|
|
373
|
+
id: runId,
|
|
374
|
+
task_id: task.id,
|
|
375
|
+
status: "failed",
|
|
376
|
+
started_at: now,
|
|
377
|
+
finished_at: now,
|
|
378
|
+
duration_ms: 0,
|
|
379
|
+
error_message: error.message,
|
|
380
|
+
payload_snapshot: null,
|
|
381
|
+
agent_response: null,
|
|
382
|
+
}, { expectedVersion: 0 });
|
|
383
|
+
} catch (e) {
|
|
384
|
+
log.warn(`[handleError] Failed to insert task_run: ${(e as Error).message}`);
|
|
385
|
+
}
|
|
273
386
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
387
|
+
await this.updateJob(task.id, {
|
|
388
|
+
error_count: task.error_count + 1,
|
|
389
|
+
last_error: error.message,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
279
392
|
}
|
|
280
393
|
|
|
281
394
|
/**
|
|
282
395
|
* Pause a cron job
|
|
283
396
|
*/
|
|
284
|
-
pause(taskId: string): boolean {
|
|
397
|
+
async pause(taskId: string): Promise<boolean> {
|
|
285
398
|
const job = this.jobs.get(taskId);
|
|
286
399
|
if (job) {
|
|
287
400
|
job.pause();
|
|
288
401
|
}
|
|
289
402
|
|
|
290
|
-
const
|
|
291
|
-
"UPDATE cron_jobs SET status = 'paused' WHERE id = ?"
|
|
292
|
-
).run(taskId);
|
|
403
|
+
const updated = await this.updateJob(taskId, { status: "paused" });
|
|
293
404
|
|
|
294
|
-
if (
|
|
405
|
+
if (updated) {
|
|
295
406
|
log.info(`[pause] Job "${taskId}" paused`);
|
|
296
407
|
return true;
|
|
297
408
|
}
|
|
@@ -303,21 +414,18 @@ export class CronScheduler {
|
|
|
303
414
|
/**
|
|
304
415
|
* Resume a paused cron job
|
|
305
416
|
*/
|
|
306
|
-
resume(taskId: string): boolean {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
).get(taskId) as CronJob | undefined;
|
|
417
|
+
async resume(taskId: string): Promise<boolean> {
|
|
418
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
419
|
+
const taskEntry = await cronJobsCol.get(taskId);
|
|
310
420
|
|
|
311
|
-
if (!
|
|
421
|
+
if (!taskEntry) {
|
|
312
422
|
log.warn(`[resume] Job "${taskId}" not found`);
|
|
313
423
|
return false;
|
|
314
424
|
}
|
|
315
425
|
|
|
316
|
-
this.
|
|
317
|
-
"UPDATE cron_jobs SET status = 'active' WHERE id = ?"
|
|
318
|
-
).run(taskId);
|
|
426
|
+
const updated = await this.updateJob(taskId, { status: "active" });
|
|
319
427
|
|
|
320
|
-
this.activate(
|
|
428
|
+
await this.activate(fromDoc(updated!));
|
|
321
429
|
log.info(`[resume] Job "${taskId}" resumed`);
|
|
322
430
|
return true;
|
|
323
431
|
}
|
|
@@ -337,26 +445,25 @@ export class CronScheduler {
|
|
|
337
445
|
/**
|
|
338
446
|
* Delete a cron job - deactivate and remove from DB
|
|
339
447
|
*/
|
|
340
|
-
delete(taskId: string): boolean {
|
|
448
|
+
async delete(taskId: string): Promise<boolean> {
|
|
341
449
|
this.deactivate(taskId);
|
|
342
450
|
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
log.info(`[delete] Job "${taskId}" deleted`);
|
|
349
|
-
return true;
|
|
451
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
452
|
+
const existing = await cronJobsCol.get(taskId);
|
|
453
|
+
if (!existing) {
|
|
454
|
+
log.warn(`[delete] Job "${taskId}" not found`);
|
|
455
|
+
return false;
|
|
350
456
|
}
|
|
351
457
|
|
|
352
|
-
|
|
353
|
-
|
|
458
|
+
await cronJobsCol.delete(taskId);
|
|
459
|
+
log.info(`[delete] Job "${taskId}" deleted`);
|
|
460
|
+
return true;
|
|
354
461
|
}
|
|
355
462
|
|
|
356
463
|
/**
|
|
357
464
|
* Create a new cron job
|
|
358
465
|
*/
|
|
359
|
-
create(input: CreateCronJobInput): { id: string; nextRun?: string } {
|
|
466
|
+
async create(input: CreateCronJobInput): Promise<{ id: string; nextRun?: string }> {
|
|
360
467
|
const id = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
361
468
|
const now = new Date().toISOString();
|
|
362
469
|
|
|
@@ -394,40 +501,41 @@ export class CronScheduler {
|
|
|
394
501
|
throw new Error("Invalid payload JSON");
|
|
395
502
|
}
|
|
396
503
|
|
|
397
|
-
|
|
398
|
-
INSERT INTO cron_jobs (
|
|
399
|
-
id, name, task, task_type, cron_expression, fire_at, timezone,
|
|
400
|
-
start_at, stop_at, dom_and_dow,
|
|
401
|
-
max_runs, protect, interval_sec, agent_id, channel, payload, tool_name,
|
|
402
|
-
status, created_at, updated_at
|
|
403
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
|
|
404
|
-
`).run(
|
|
504
|
+
const doc: CronJobDoc = {
|
|
405
505
|
id,
|
|
406
|
-
input.name,
|
|
407
|
-
input.task,
|
|
408
|
-
input.task_type,
|
|
409
|
-
input.cron_expression || null,
|
|
410
|
-
input.fire_at || null,
|
|
411
|
-
input.timezone,
|
|
412
|
-
input.start_at || null,
|
|
413
|
-
input.stop_at || null,
|
|
414
|
-
input.dom_and_dow ? 1 : 0,
|
|
415
|
-
input.max_runs || null,
|
|
416
|
-
input.protect !== false ? 1 : 0,
|
|
417
|
-
input.interval_sec || null,
|
|
418
|
-
input.agent_id || null,
|
|
419
|
-
input.channel || "system",
|
|
420
|
-
payloadJson,
|
|
421
|
-
input.tool_name || null,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
506
|
+
name: input.name,
|
|
507
|
+
task: input.task,
|
|
508
|
+
task_type: input.task_type,
|
|
509
|
+
cron_expression: input.cron_expression || null,
|
|
510
|
+
fire_at: input.fire_at || null,
|
|
511
|
+
timezone: input.timezone,
|
|
512
|
+
start_at: input.start_at || null,
|
|
513
|
+
stop_at: input.stop_at || null,
|
|
514
|
+
dom_and_dow: input.dom_and_dow ? 1 : 0,
|
|
515
|
+
max_runs: input.max_runs || null,
|
|
516
|
+
protect: input.protect !== false ? 1 : 0,
|
|
517
|
+
interval_sec: input.interval_sec || null,
|
|
518
|
+
agent_id: toIndexable(input.agent_id || null),
|
|
519
|
+
channel: input.channel || "system",
|
|
520
|
+
payload: payloadJson,
|
|
521
|
+
tool_name: input.tool_name || null,
|
|
522
|
+
status: "active",
|
|
523
|
+
run_count: 0,
|
|
524
|
+
error_count: 0,
|
|
525
|
+
last_error: null,
|
|
526
|
+
misfire_policy: input.misfire_policy ?? "skip",
|
|
527
|
+
misfire_grace_min: input.misfire_grace_min ?? 60,
|
|
528
|
+
created_at: now,
|
|
529
|
+
updated_at: now,
|
|
530
|
+
last_run_at: null,
|
|
531
|
+
next_run_at: null,
|
|
532
|
+
completed_at: null,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
536
|
+
await cronJobsCol.put(id, doc, { expectedVersion: 0 });
|
|
537
|
+
|
|
538
|
+
await this.activate(fromDoc(doc));
|
|
431
539
|
|
|
432
540
|
const job = this.jobs.get(id);
|
|
433
541
|
const nextRun = job?.nextRun()?.toISOString();
|
|
@@ -440,100 +548,42 @@ export class CronScheduler {
|
|
|
440
548
|
/**
|
|
441
549
|
* Update an existing cron job
|
|
442
550
|
*/
|
|
443
|
-
update(taskId: string, changes: UpdateCronJobInput): boolean {
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
).get(taskId) as CronJob | undefined;
|
|
551
|
+
async update(taskId: string, changes: UpdateCronJobInput): Promise<boolean> {
|
|
552
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
553
|
+
const taskEntry = await cronJobsCol.get(taskId);
|
|
447
554
|
|
|
448
|
-
if (!
|
|
555
|
+
if (!taskEntry) {
|
|
449
556
|
log.warn(`[update] Job "${taskId}" not found`);
|
|
450
557
|
return false;
|
|
451
558
|
}
|
|
452
559
|
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (changes.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (changes.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (changes.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (changes.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
values.push(changes.fire_at);
|
|
475
|
-
}
|
|
476
|
-
if (changes.timezone !== undefined) {
|
|
477
|
-
fields.push("timezone = ?");
|
|
478
|
-
values.push(changes.timezone);
|
|
479
|
-
}
|
|
480
|
-
if (changes.start_at !== undefined) {
|
|
481
|
-
fields.push("start_at = ?");
|
|
482
|
-
values.push(changes.start_at);
|
|
483
|
-
}
|
|
484
|
-
if (changes.stop_at !== undefined) {
|
|
485
|
-
fields.push("stop_at = ?");
|
|
486
|
-
values.push(changes.stop_at);
|
|
487
|
-
}
|
|
488
|
-
if (changes.dom_and_dow !== undefined) {
|
|
489
|
-
fields.push("dom_and_dow = ?");
|
|
490
|
-
values.push(changes.dom_and_dow ? 1 : 0);
|
|
491
|
-
}
|
|
492
|
-
if (changes.agent_id !== undefined) {
|
|
493
|
-
fields.push("agent_id = ?");
|
|
494
|
-
values.push(changes.agent_id);
|
|
495
|
-
}
|
|
496
|
-
if (changes.channel !== undefined) {
|
|
497
|
-
fields.push("channel = ?");
|
|
498
|
-
values.push(changes.channel);
|
|
499
|
-
}
|
|
500
|
-
if (changes.payload !== undefined) {
|
|
501
|
-
fields.push("payload = ?");
|
|
502
|
-
values.push(JSON.stringify(changes.payload));
|
|
503
|
-
}
|
|
504
|
-
if (changes.tool_name !== undefined) {
|
|
505
|
-
fields.push("tool_name = ?");
|
|
506
|
-
values.push(changes.tool_name);
|
|
507
|
-
}
|
|
508
|
-
if (changes.max_runs !== undefined) {
|
|
509
|
-
fields.push("max_runs = ?");
|
|
510
|
-
values.push(changes.max_runs);
|
|
511
|
-
}
|
|
512
|
-
if (changes.protect !== undefined) {
|
|
513
|
-
fields.push("protect = ?");
|
|
514
|
-
values.push(changes.protect ? 1 : 0);
|
|
515
|
-
}
|
|
516
|
-
if (changes.interval_sec !== undefined) {
|
|
517
|
-
fields.push("interval_sec = ?");
|
|
518
|
-
values.push(changes.interval_sec);
|
|
519
|
-
}
|
|
520
|
-
if (changes.status !== undefined) {
|
|
521
|
-
fields.push("status = ?");
|
|
522
|
-
values.push(changes.status);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (fields.length === 0) {
|
|
560
|
+
const patch: Partial<CronJobDoc> = {};
|
|
561
|
+
|
|
562
|
+
if (changes.name !== undefined) patch.name = changes.name;
|
|
563
|
+
if (changes.task !== undefined) patch.task = changes.task;
|
|
564
|
+
if (changes.task_type !== undefined) patch.task_type = changes.task_type;
|
|
565
|
+
if (changes.cron_expression !== undefined) patch.cron_expression = changes.cron_expression;
|
|
566
|
+
if (changes.fire_at !== undefined) patch.fire_at = changes.fire_at;
|
|
567
|
+
if (changes.timezone !== undefined) patch.timezone = changes.timezone;
|
|
568
|
+
if (changes.start_at !== undefined) patch.start_at = changes.start_at;
|
|
569
|
+
if (changes.stop_at !== undefined) patch.stop_at = changes.stop_at;
|
|
570
|
+
if (changes.dom_and_dow !== undefined) patch.dom_and_dow = changes.dom_and_dow ? 1 : 0;
|
|
571
|
+
if (changes.agent_id !== undefined) patch.agent_id = toIndexable(changes.agent_id);
|
|
572
|
+
if (changes.channel !== undefined) patch.channel = changes.channel;
|
|
573
|
+
if (changes.payload !== undefined) patch.payload = JSON.stringify(changes.payload);
|
|
574
|
+
if (changes.tool_name !== undefined) patch.tool_name = changes.tool_name;
|
|
575
|
+
if (changes.max_runs !== undefined) patch.max_runs = changes.max_runs;
|
|
576
|
+
if (changes.protect !== undefined) patch.protect = changes.protect ? 1 : 0;
|
|
577
|
+
if (changes.interval_sec !== undefined) patch.interval_sec = changes.interval_sec;
|
|
578
|
+
if (changes.status !== undefined) patch.status = changes.status;
|
|
579
|
+
|
|
580
|
+
if (Object.keys(patch).length === 0) {
|
|
526
581
|
return true;
|
|
527
582
|
}
|
|
528
583
|
|
|
529
|
-
|
|
530
|
-
this.db.query(`UPDATE cron_jobs SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
531
|
-
|
|
532
|
-
const updatedTask = this.db.query(
|
|
533
|
-
"SELECT * FROM cron_jobs WHERE id = ?"
|
|
534
|
-
).get(taskId) as CronJob;
|
|
584
|
+
const updatedDoc = await this.updateJob(taskId, patch);
|
|
535
585
|
|
|
536
|
-
this.activate(
|
|
586
|
+
await this.activate(fromDoc(updatedDoc!));
|
|
537
587
|
|
|
538
588
|
log.info(`[update] Job "${taskId}" updated`);
|
|
539
589
|
return true;
|
|
@@ -542,10 +592,9 @@ export class CronScheduler {
|
|
|
542
592
|
/**
|
|
543
593
|
* Get status of all cron jobs
|
|
544
594
|
*/
|
|
545
|
-
getStatus(): CronJobStatus[] {
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
).all() as Array<{ id: string; name: string; status: string }>;
|
|
595
|
+
async getStatus(): Promise<CronJobStatus[]> {
|
|
596
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
597
|
+
const tasks = (await cronJobsCol.scan({})).map(e => e.doc).sort((a, b) => a.id.localeCompare(b.id));
|
|
549
598
|
|
|
550
599
|
return tasks.map((task) => {
|
|
551
600
|
const job = this.jobs.get(task.id);
|
|
@@ -563,15 +612,6 @@ export class CronScheduler {
|
|
|
563
612
|
* Manually trigger a cron job execution
|
|
564
613
|
*/
|
|
565
614
|
trigger(taskId: string): boolean {
|
|
566
|
-
const task = this.db.query(
|
|
567
|
-
"SELECT * FROM cron_jobs WHERE id = ?"
|
|
568
|
-
).get(taskId) as CronJob | undefined;
|
|
569
|
-
|
|
570
|
-
if (!task) {
|
|
571
|
-
log.warn(`[trigger] Job "${taskId}" not found`);
|
|
572
|
-
return false;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
615
|
const job = this.jobs.get(taskId);
|
|
576
616
|
if (!job) {
|
|
577
617
|
log.warn(`[trigger] Job "${taskId}" has no active job`);
|
|
@@ -597,10 +637,9 @@ export class CronScheduler {
|
|
|
597
637
|
/**
|
|
598
638
|
* Ensure the cleanup job exists
|
|
599
639
|
*/
|
|
600
|
-
private ensureCleanupTask(): void {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
).get() as { id: string } | undefined;
|
|
640
|
+
private async ensureCleanupTask(): Promise<void> {
|
|
641
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
642
|
+
const existing = (await cronJobsCol.scan({})).find(e => e.doc.name === "_hive_cleanup_runs");
|
|
604
643
|
|
|
605
644
|
if (existing) {
|
|
606
645
|
this.cleanupTaskId = existing.id;
|
|
@@ -609,7 +648,7 @@ export class CronScheduler {
|
|
|
609
648
|
}
|
|
610
649
|
|
|
611
650
|
try {
|
|
612
|
-
const result = this.create({
|
|
651
|
+
const result = await this.create({
|
|
613
652
|
name: "_hive_cleanup_runs",
|
|
614
653
|
task: "Automatic cleanup of old task_runs and completed one_shot jobs",
|
|
615
654
|
task_type: "recurring",
|
|
@@ -629,71 +668,79 @@ export class CronScheduler {
|
|
|
629
668
|
/**
|
|
630
669
|
* Run cleanup - called by the internal cleanup job
|
|
631
670
|
*/
|
|
632
|
-
runCleanup(): void {
|
|
671
|
+
async runCleanup(): Promise<void> {
|
|
633
672
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
634
|
-
this.db.query(`
|
|
635
|
-
DELETE FROM task_runs
|
|
636
|
-
WHERE status IN ('success', 'failed') AND started_at < ?
|
|
637
|
-
`).run(thirtyDaysAgo);
|
|
638
|
-
|
|
639
673
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
for (const { task_id } of tasks) {
|
|
651
|
-
this.db.query(`
|
|
652
|
-
DELETE FROM task_runs
|
|
653
|
-
WHERE task_id = ? AND id NOT IN (
|
|
654
|
-
SELECT id FROM task_runs
|
|
655
|
-
WHERE task_id = ?
|
|
656
|
-
ORDER BY started_at DESC
|
|
657
|
-
LIMIT 1000
|
|
658
|
-
)
|
|
659
|
-
`).run(task_id, task_id);
|
|
674
|
+
|
|
675
|
+
const taskRunsCol = await col<TaskRunDoc>("taskRuns");
|
|
676
|
+
const allRuns = await taskRunsCol.scan({});
|
|
677
|
+
|
|
678
|
+
const oldRuns = allRuns.filter(e =>
|
|
679
|
+
(e.doc.status === "success" || e.doc.status === "failed") && e.doc.started_at < thirtyDaysAgo
|
|
680
|
+
);
|
|
681
|
+
for (const run of oldRuns) {
|
|
682
|
+
await taskRunsCol.delete(run.id);
|
|
660
683
|
}
|
|
661
684
|
|
|
662
|
-
|
|
685
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
686
|
+
const allJobs = await cronJobsCol.scan({});
|
|
687
|
+
const staleOneShots = allJobs.filter(e =>
|
|
688
|
+
e.doc.task_type === "one_shot" && e.doc.status === "completed" &&
|
|
689
|
+
e.doc.completed_at !== null && e.doc.completed_at < sevenDaysAgo
|
|
690
|
+
);
|
|
691
|
+
for (const job of staleOneShots) {
|
|
692
|
+
await this.updateJob(job.id, { status: "cancelled" });
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Keep only the most recent 1000 runs per task
|
|
696
|
+
const remainingRuns = allRuns.filter(r => !oldRuns.includes(r));
|
|
697
|
+
const runsByTask = new Map<string, typeof remainingRuns>();
|
|
698
|
+
for (const run of remainingRuns) {
|
|
699
|
+
const list = runsByTask.get(run.doc.task_id) ?? [];
|
|
700
|
+
list.push(run);
|
|
701
|
+
runsByTask.set(run.doc.task_id, list);
|
|
702
|
+
}
|
|
703
|
+
for (const [, runs] of runsByTask) {
|
|
704
|
+
if (runs.length <= 1000) continue;
|
|
705
|
+
const toDelete = runs.sort((a, b) => b.doc.started_at.localeCompare(a.doc.started_at)).slice(1000);
|
|
706
|
+
for (const run of toDelete) {
|
|
707
|
+
await taskRunsCol.delete(run.id);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const artifacts = await expireArtifacts();
|
|
712
|
+
log.info(`[runCleanup] Cleanup completed (expired artifacts=${artifacts.expired})`);
|
|
663
713
|
}
|
|
664
714
|
|
|
665
715
|
/**
|
|
666
716
|
* Get task run history
|
|
667
717
|
*/
|
|
668
|
-
getHistory(taskId: string, limit = 50): TaskRun[] {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
LIMIT ?
|
|
674
|
-
`).all(taskId, limit) as TaskRun[];
|
|
718
|
+
async getHistory(taskId: string, limit = 50): Promise<TaskRun[]> {
|
|
719
|
+
const taskRunsCol = await col<TaskRunDoc>("taskRuns");
|
|
720
|
+
const runs = (await taskRunsCol.scan({})).map(e => e.doc).filter(r => r.task_id === taskId);
|
|
721
|
+
runs.sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
722
|
+
return runs.slice(0, limit);
|
|
675
723
|
}
|
|
676
724
|
|
|
677
725
|
/**
|
|
678
726
|
* Get a single cron job by ID
|
|
679
727
|
*/
|
|
680
|
-
getTask(taskId: string): CronJob | null {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
728
|
+
async getTask(taskId: string): Promise<CronJob | null> {
|
|
729
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
730
|
+
const entry = await cronJobsCol.get(taskId);
|
|
731
|
+
return entry ? fromDoc(entry.doc) : null;
|
|
684
732
|
}
|
|
685
733
|
|
|
686
734
|
/**
|
|
687
735
|
* List all cron jobs
|
|
688
736
|
*/
|
|
689
|
-
listTasks(status?: string): CronJob[] {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
).all() as CronJob[];
|
|
737
|
+
async listTasks(status?: string): Promise<CronJob[]> {
|
|
738
|
+
const cronJobsCol = await col<CronJobDoc>("cronJobs");
|
|
739
|
+
const entries = status
|
|
740
|
+
? await cronJobsCol.findBy("status", status)
|
|
741
|
+
: await cronJobsCol.scan({});
|
|
742
|
+
const tasks = entries.map(e => fromDoc(e.doc));
|
|
743
|
+
tasks.sort((a, b) => (a.next_run_at ?? "").localeCompare(b.next_run_at ?? ""));
|
|
744
|
+
return tasks;
|
|
698
745
|
}
|
|
699
|
-
}
|
|
746
|
+
}
|