@johpaz/hive-sdk 0.1.3 → 0.1.5
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 +97 -0
- package/README.md +78 -23
- 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 +9 -4
- 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 +5 -4
- package/packages/core/src/agent/acceptance-checks.ts +166 -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 +71 -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 -20
- 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 -17
- 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-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +5 -0
- 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/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/agent-loop-terminal-synthesis.test.ts +32 -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/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,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DurableLaneQueue — persistent work queue backed by the
|
|
3
|
-
*
|
|
4
|
-
* generalized for SDK consumers (job `type` is a plain string; the set of
|
|
5
|
-
* "never auto-retry a logical failure" types is configurable instead of a
|
|
6
|
-
* hardcoded `chat_turn` check — default matches `hive`'s convention).
|
|
2
|
+
* DurableLaneQueue — persistent work queue backed by the `jobQueue` HiveDB
|
|
3
|
+
* collection.
|
|
7
4
|
*
|
|
8
5
|
* Guarantees:
|
|
9
|
-
* - FIFO + priority per lane
|
|
6
|
+
* - FIFO + priority per lane (same semantics as LaneQueue)
|
|
10
7
|
* - 1 concurrent running job per lane
|
|
11
8
|
* - maxGlobalConcurrency across all lanes (default 4)
|
|
12
9
|
* - JobDoc persists every status transition → survives crashes
|
|
13
10
|
* - Expired leases (crashed worker) are reclaimed or interrupted on next dispatch
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
*
|
|
12
|
+
* The live LaneQueue instance is used as the in-memory dispatch layer:
|
|
13
|
+
* DurableLaneQueue wraps it so that the existing per-session serial behavior
|
|
14
|
+
* is preserved, while all queue state is mirrored to the DB.
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
import { logger } from "../utils/logger";
|
|
@@ -27,18 +26,21 @@ import {
|
|
|
27
26
|
findPendingJobsByLane,
|
|
28
27
|
findAllPendingJobs,
|
|
29
28
|
findExpiredLeases,
|
|
29
|
+
getJob,
|
|
30
|
+
loadJobRetryPolicy,
|
|
30
31
|
DEFAULT_JOB_RETRY_POLICY,
|
|
31
32
|
type JobRetryPolicy,
|
|
32
33
|
} from "./job-store";
|
|
33
|
-
import type { JobDoc } from "
|
|
34
|
-
import { getBootId } from "
|
|
34
|
+
import type { JobDoc } from "../storage/collections";
|
|
35
|
+
import { getBootId } from "../storage/boot-id";
|
|
35
36
|
|
|
36
|
-
const log = logger.child("
|
|
37
|
+
const log = logger.child("durable-queue");
|
|
37
38
|
|
|
38
39
|
const LEASE_CHECK_INTERVAL_MS = 10_000;
|
|
39
40
|
const DEFAULT_MAX_GLOBAL_CONCURRENCY = 4;
|
|
40
41
|
const DEFAULT_TASK_TIMEOUT_MS = 30 * 60 * 1000;
|
|
41
|
-
|
|
42
|
+
|
|
43
|
+
export type JobType = JobDoc["type"];
|
|
42
44
|
|
|
43
45
|
export interface JobPayload {
|
|
44
46
|
[key: string]: unknown;
|
|
@@ -57,6 +59,7 @@ export interface JobExecutorResult {
|
|
|
57
59
|
export interface JobLiveCallbacks {
|
|
58
60
|
onToken?: (token: string) => void;
|
|
59
61
|
onStep?: (step: unknown) => Promise<void>;
|
|
62
|
+
/** Sends an already-serialized frame to the live socket (webchat turns). */
|
|
60
63
|
sendRaw?: (payload: string) => void;
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -66,52 +69,74 @@ export type JobExecutor = (
|
|
|
66
69
|
callbacks?: JobLiveCallbacks
|
|
67
70
|
) => Promise<JobExecutorResult>;
|
|
68
71
|
|
|
69
|
-
const executors = new Map<
|
|
72
|
+
const executors = new Map<JobType, JobExecutor>();
|
|
70
73
|
|
|
71
|
-
export function registerExecutor(type:
|
|
74
|
+
export function registerExecutor(type: JobType, executor: JobExecutor): void {
|
|
72
75
|
executors.set(type, executor);
|
|
73
76
|
log.info(`[registerExecutor] Registered executor for type=${type}`);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
export interface
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
export interface JobTerminalOutcome {
|
|
80
|
+
ok: boolean;
|
|
81
|
+
result?: unknown;
|
|
82
|
+
error?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Fires exactly once when a job of `type` reaches a terminal state (completed, or failed with no retries left) — never on a retryable failure that gets requeued. */
|
|
86
|
+
export type JobTerminalHook = (job: JobDoc, outcome: JobTerminalOutcome) => void | Promise<void>;
|
|
87
|
+
|
|
88
|
+
const terminalHooks = new Map<JobType, JobTerminalHook>();
|
|
89
|
+
|
|
90
|
+
/** Register a side-effect to run once a job type finishes for good. Kept decoupled from executors so this module doesn't need to import webchat-turn.ts (circular: webchat-turn.ts already imports durable-queue.ts for enqueueChatTurn). */
|
|
91
|
+
export function registerTerminalHook(type: JobType, hook: JobTerminalHook): void {
|
|
92
|
+
terminalHooks.set(type, hook);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function runTerminalHook(job: JobDoc, outcome: JobTerminalOutcome): Promise<void> {
|
|
96
|
+
const hook = terminalHooks.get(job.type);
|
|
97
|
+
if (!hook) return;
|
|
98
|
+
try {
|
|
99
|
+
await hook(job, outcome);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
log.warn(`[runTerminalHook] Hook for type=${job.type} failed: ${(err as Error).message}`);
|
|
102
|
+
}
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
export class DurableLaneQueue {
|
|
85
106
|
private maxGlobalConcurrency: number;
|
|
86
107
|
private taskTimeoutMs: number;
|
|
87
108
|
private jobRetryPolicy: JobRetryPolicy;
|
|
88
|
-
private nonRetryableTypes: Set<string>;
|
|
89
109
|
private runningCount = 0;
|
|
90
110
|
private runningAborts = new Map<string, { lane: string; controller: AbortController }>();
|
|
91
111
|
private dispatchTimers = new Map<string, ReturnType<typeof setInterval>>();
|
|
92
112
|
private leaseCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
93
113
|
private bootId: string;
|
|
94
114
|
|
|
95
|
-
constructor(options:
|
|
115
|
+
constructor(options: {
|
|
116
|
+
maxGlobalConcurrency?: number;
|
|
117
|
+
taskTimeoutMs?: number;
|
|
118
|
+
jobRetryPolicy?: JobRetryPolicy;
|
|
119
|
+
} = {}) {
|
|
96
120
|
this.maxGlobalConcurrency = options.maxGlobalConcurrency ?? DEFAULT_MAX_GLOBAL_CONCURRENCY;
|
|
97
121
|
this.taskTimeoutMs = options.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
|
|
98
122
|
this.jobRetryPolicy = options.jobRetryPolicy ?? DEFAULT_JOB_RETRY_POLICY;
|
|
99
|
-
this.nonRetryableTypes = new Set(options.nonRetryableTypes ?? DEFAULT_NON_RETRYABLE_TYPES);
|
|
100
123
|
this.bootId = getBootId();
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
/**
|
|
104
|
-
* Enqueue a durable job. Returns the JobDoc.
|
|
105
|
-
* instead of
|
|
127
|
+
* Enqueue a durable job. Returns the JobDoc.
|
|
128
|
+
* Callers should use `enqueue` instead of the in-memory `laneQueue.enqueue`
|
|
129
|
+
* for any work that must survive crashes.
|
|
106
130
|
*/
|
|
107
131
|
async enqueue(input: {
|
|
108
132
|
lane: string;
|
|
109
|
-
type:
|
|
133
|
+
type: JobType;
|
|
110
134
|
payload: JobPayload;
|
|
111
135
|
run_id: string;
|
|
112
136
|
priority?: number;
|
|
113
137
|
max_attempts?: number;
|
|
114
138
|
not_before?: number;
|
|
139
|
+
/** Client-supplied dedup key: a repeated key returns the existing job instead of creating a new one. */
|
|
115
140
|
idempotency_key?: string | null;
|
|
116
141
|
callbacks?: JobLiveCallbacks;
|
|
117
142
|
}): Promise<JobDoc> {
|
|
@@ -126,6 +151,7 @@ export class DurableLaneQueue {
|
|
|
126
151
|
idempotency_key: input.idempotency_key,
|
|
127
152
|
});
|
|
128
153
|
|
|
154
|
+
// Stash live callbacks in memory (not serializable)
|
|
129
155
|
if (input.callbacks) {
|
|
130
156
|
liveCallbacks.set(job.id, input.callbacks);
|
|
131
157
|
}
|
|
@@ -134,14 +160,19 @@ export class DurableLaneQueue {
|
|
|
134
160
|
return job;
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Cancel a job by id (pending or running). A running job also gets its
|
|
165
|
+
* AbortSignal fired so the executor stops streaming/working.
|
|
166
|
+
*/
|
|
138
167
|
async cancel(jobId: string): Promise<boolean> {
|
|
139
168
|
const running = this.runningAborts.get(jobId);
|
|
140
169
|
if (running) running.controller.abort();
|
|
141
170
|
return cancelJob(jobId);
|
|
142
171
|
}
|
|
143
172
|
|
|
144
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Cancel all pending and running jobs in a lane.
|
|
175
|
+
*/
|
|
145
176
|
async cancelLane(lane: string): Promise<number> {
|
|
146
177
|
let count = 0;
|
|
147
178
|
for (const [jobId, entry] of this.runningAborts) {
|
|
@@ -158,13 +189,15 @@ export class DurableLaneQueue {
|
|
|
158
189
|
}
|
|
159
190
|
|
|
160
191
|
/**
|
|
161
|
-
* Start the
|
|
162
|
-
*
|
|
192
|
+
* Start the lease expiry checker and dispatch any jobs left pending by a
|
|
193
|
+
* previous boot. Called once during boot.
|
|
163
194
|
*/
|
|
164
195
|
start(): void {
|
|
165
196
|
if (this.leaseCheckTimer) return;
|
|
166
197
|
this.leaseCheckTimer = setInterval(() => this.runMaintenanceTick(), LEASE_CHECK_INTERVAL_MS);
|
|
167
198
|
log.info(`[start] Maintenance tick running every ${LEASE_CHECK_INTERVAL_MS}ms`);
|
|
199
|
+
// Without this, jobs reclaimed to "pending" by reconcileOnBoot (or enqueued
|
|
200
|
+
// right before a crash) would sit until a new enqueue touched their lane.
|
|
168
201
|
this.dispatchPendingLanes().catch((err) => {
|
|
169
202
|
log.error(`[start] Failed to dispatch pending lanes: ${(err as Error).message}`);
|
|
170
203
|
});
|
|
@@ -173,7 +206,9 @@ export class DurableLaneQueue {
|
|
|
173
206
|
/**
|
|
174
207
|
* Periodic tick: reclaim crashed jobs (expired leases) AND re-dispatch
|
|
175
208
|
* lanes with jobs whose `not_before` (e.g. a backoff delay from
|
|
176
|
-
* `failJobOrRetry`) has now elapsed.
|
|
209
|
+
* `failJobOrRetry`) has now elapsed. `dispatchPendingLanes` is safe to call
|
|
210
|
+
* repeatedly — `scheduleDispatch` debounces per lane and a lane with
|
|
211
|
+
* nothing due is a cheap no-op.
|
|
177
212
|
*/
|
|
178
213
|
private async runMaintenanceTick(): Promise<void> {
|
|
179
214
|
await this.checkExpiredLeases();
|
|
@@ -200,6 +235,9 @@ export class DurableLaneQueue {
|
|
|
200
235
|
this.dispatchTimers.clear();
|
|
201
236
|
}
|
|
202
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Schedule dispatch for a lane (debounced — coalesce rapid enqueues).
|
|
240
|
+
*/
|
|
203
241
|
private scheduleDispatch(lane: string): void {
|
|
204
242
|
if (this.dispatchTimers.has(lane)) return;
|
|
205
243
|
const timer = setTimeout(() => {
|
|
@@ -211,29 +249,36 @@ export class DurableLaneQueue {
|
|
|
211
249
|
this.dispatchTimers.set(lane, timer);
|
|
212
250
|
}
|
|
213
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Dispatch pending jobs for a single lane, respecting global concurrency.
|
|
254
|
+
*/
|
|
214
255
|
private async dispatchLane(lane: string): Promise<void> {
|
|
215
256
|
for (;;) {
|
|
216
257
|
const pending = await findPendingJobsByLane(lane, 1);
|
|
217
258
|
if (pending.length === 0) break;
|
|
218
259
|
|
|
219
260
|
const job = pending[0];
|
|
220
|
-
// Interactive
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
if (!this.nonRetryableTypes.has(job.type) && this.runningCount >= this.maxGlobalConcurrency) break;
|
|
261
|
+
// Interactive chat turns bypass the global cap: a busy batch of
|
|
262
|
+
// workers/goals must not make the webchat stop responding.
|
|
263
|
+
if (job.type !== "chat_turn" && this.runningCount >= this.maxGlobalConcurrency) break;
|
|
224
264
|
|
|
225
265
|
const claimed = await claimJob(job.id, this.bootId);
|
|
226
|
-
if (!claimed) continue;
|
|
266
|
+
if (!claimed) continue; // someone else won the claim — re-read the lane
|
|
227
267
|
|
|
228
268
|
this.runningCount++;
|
|
229
269
|
this.executeJob(claimed, lane).catch((err) => {
|
|
230
270
|
log.error(`[dispatchLane] Unhandled error in job ${claimed.id}: ${(err as Error).message}`);
|
|
231
271
|
});
|
|
232
272
|
|
|
273
|
+
// Only 1 running per lane — break after dispatching
|
|
233
274
|
break;
|
|
234
275
|
}
|
|
235
276
|
}
|
|
236
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Execute a claimed job: look up executor, run with timeout + abort,
|
|
280
|
+
* persist result.
|
|
281
|
+
*/
|
|
237
282
|
private async executeJob(job: JobDoc, lane: string): Promise<void> {
|
|
238
283
|
const abortController = new AbortController();
|
|
239
284
|
this.runningAborts.set(job.id, { lane, controller: abortController });
|
|
@@ -261,12 +306,17 @@ export class DurableLaneQueue {
|
|
|
261
306
|
|
|
262
307
|
if (result.ok) {
|
|
263
308
|
await completeJob(job.id, result.result ?? null, this.bootId);
|
|
309
|
+
await runTerminalHook(job, { ok: true, result: result.result });
|
|
264
310
|
} else {
|
|
265
311
|
const error = result.error ?? "Unknown error";
|
|
266
|
-
|
|
267
|
-
|
|
312
|
+
// chat_turn is user-facing — never auto-retry a logical failure, the
|
|
313
|
+
// user just sees the error and can re-ask.
|
|
314
|
+
if (job.type !== "chat_turn" && result.retryable !== false) {
|
|
315
|
+
const updated = await failJobOrRetry(job.id, error, this.bootId, this.jobRetryPolicy);
|
|
316
|
+
if (updated?.status === "failed") await runTerminalHook(job, { ok: false, error });
|
|
268
317
|
} else {
|
|
269
318
|
await failJob(job.id, error, this.bootId);
|
|
319
|
+
await runTerminalHook(job, { ok: false, error });
|
|
270
320
|
}
|
|
271
321
|
}
|
|
272
322
|
} catch (err) {
|
|
@@ -274,10 +324,12 @@ export class DurableLaneQueue {
|
|
|
274
324
|
await cancelJob(job.id);
|
|
275
325
|
} else {
|
|
276
326
|
const error = (err as Error).message;
|
|
277
|
-
if (
|
|
278
|
-
await failJobOrRetry(job.id, error, this.bootId, this.jobRetryPolicy);
|
|
327
|
+
if (job.type !== "chat_turn") {
|
|
328
|
+
const updated = await failJobOrRetry(job.id, error, this.bootId, this.jobRetryPolicy);
|
|
329
|
+
if (updated?.status === "failed") await runTerminalHook(job, { ok: false, error });
|
|
279
330
|
} else {
|
|
280
331
|
await failJob(job.id, error, this.bootId);
|
|
332
|
+
await runTerminalHook(job, { ok: false, error });
|
|
281
333
|
}
|
|
282
334
|
}
|
|
283
335
|
} finally {
|
|
@@ -286,10 +338,14 @@ export class DurableLaneQueue {
|
|
|
286
338
|
leasedHere = false;
|
|
287
339
|
this.runningAborts.delete(job.id);
|
|
288
340
|
this.runningCount--;
|
|
341
|
+
// Try to dispatch next job in this lane
|
|
289
342
|
this.scheduleDispatch(lane);
|
|
290
343
|
}
|
|
291
344
|
}
|
|
292
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Periodically check for jobs with expired leases (crashed workers).
|
|
348
|
+
*/
|
|
293
349
|
private async checkExpiredLeases(): Promise<void> {
|
|
294
350
|
try {
|
|
295
351
|
const expired = await findExpiredLeases();
|
|
@@ -330,8 +386,12 @@ export function getDurableQueue(): DurableLaneQueue {
|
|
|
330
386
|
return _durableQueue;
|
|
331
387
|
}
|
|
332
388
|
|
|
333
|
-
export function initDurableQueue(options?:
|
|
389
|
+
export function initDurableQueue(options?: {
|
|
390
|
+
maxGlobalConcurrency?: number;
|
|
391
|
+
taskTimeoutMs?: number;
|
|
392
|
+
jobRetryPolicy?: JobRetryPolicy;
|
|
393
|
+
}): DurableLaneQueue {
|
|
334
394
|
_durableQueue = new DurableLaneQueue(options);
|
|
335
395
|
_durableQueue.start();
|
|
336
396
|
return _durableQueue;
|
|
337
|
-
}
|
|
397
|
+
}
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* job-store — durable persistence + lease/claim for the
|
|
3
|
-
* collection. Ported from `hive`'s gateway/job-store.ts.
|
|
2
|
+
* job-store — durable persistence + lease/claim for the jobQueue collection.
|
|
4
3
|
*
|
|
5
4
|
* All claim transitions use OCC (expectedVersion). The "claim pending→running"
|
|
6
5
|
* path guarantees only one process wins the race for the same job.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import { col, nextId, toIndexable } from "
|
|
10
|
-
import type { JobDoc } from "
|
|
11
|
-
import { getBootId } from "
|
|
8
|
+
import { col, nextId, updateDoc, toIndexable } from "../storage/hive";
|
|
9
|
+
import type { JobDoc } from "../storage/collections";
|
|
10
|
+
import { getBootId } from "../storage/boot-id";
|
|
12
11
|
import { logger } from "../utils/logger";
|
|
12
|
+
import { loadConfig } from "../config/loader";
|
|
13
13
|
|
|
14
|
-
const log = logger.child("
|
|
14
|
+
const log = logger.child("job-store");
|
|
15
15
|
|
|
16
|
-
const COLLECTION = "harness_jobQueue";
|
|
17
|
-
const DEFAULT_LEASE_DURATION_MS = 30 * 60 * 1000;
|
|
18
16
|
const MAX_RETRIES = 5;
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/** Override the job lease duration (default 30 minutes). Affects future claims/renewals. */
|
|
23
|
-
export function setJobLeaseDurationMs(ms: number): void {
|
|
24
|
-
leaseDurationMs = ms;
|
|
18
|
+
function jobLeaseDurationMs(): number {
|
|
19
|
+
return loadConfig().harness?.jobLeaseMs ?? 30 * 60 * 1000;
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
export interface JobRetryPolicy {
|
|
@@ -40,6 +35,17 @@ export const DEFAULT_JOB_RETRY_POLICY: JobRetryPolicy = {
|
|
|
40
35
|
jitter: 0.2,
|
|
41
36
|
};
|
|
42
37
|
|
|
38
|
+
export function loadJobRetryPolicy(): JobRetryPolicy {
|
|
39
|
+
const cfg = loadConfig().harness?.jobRetry;
|
|
40
|
+
return {
|
|
41
|
+
maxRetries: cfg?.maxRetries ?? DEFAULT_JOB_RETRY_POLICY.maxRetries,
|
|
42
|
+
initialDelayMs: cfg?.initialDelayMs ?? DEFAULT_JOB_RETRY_POLICY.initialDelayMs,
|
|
43
|
+
backoffMultiplier: cfg?.backoffMultiplier ?? DEFAULT_JOB_RETRY_POLICY.backoffMultiplier,
|
|
44
|
+
maxDelayMs: cfg?.maxDelayMs ?? DEFAULT_JOB_RETRY_POLICY.maxDelayMs,
|
|
45
|
+
jitter: cfg?.jitter ?? DEFAULT_JOB_RETRY_POLICY.jitter,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
/** Exponential backoff with full jitter, capped at policy.maxDelayMs. */
|
|
44
50
|
export function computeBackoffDelay(retryCount: number, policy: JobRetryPolicy): number {
|
|
45
51
|
const base = Math.min(policy.maxDelayMs, policy.initialDelayMs * Math.pow(policy.backoffMultiplier, retryCount));
|
|
@@ -54,14 +60,14 @@ function occRetryDelay(attempt: number): Promise<void> {
|
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export async function findByIdempotencyKey(key: string): Promise<JobDoc | null> {
|
|
57
|
-
const c = await col<JobDoc>(
|
|
63
|
+
const c = await col<JobDoc>("jobQueue");
|
|
58
64
|
const entries = await c.findBy("idempotency_key", key);
|
|
59
65
|
return entries.length > 0 ? entries[0].doc : null;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
export async function createJob(input: {
|
|
63
69
|
lane: string;
|
|
64
|
-
type:
|
|
70
|
+
type: JobDoc["type"];
|
|
65
71
|
payload: unknown;
|
|
66
72
|
run_id: string;
|
|
67
73
|
priority?: number;
|
|
@@ -78,7 +84,7 @@ export async function createJob(input: {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
const id = await nextId(
|
|
87
|
+
const id = await nextId("jobQueue");
|
|
82
88
|
const now = Date.now();
|
|
83
89
|
const doc: JobDoc = {
|
|
84
90
|
id,
|
|
@@ -102,14 +108,19 @@ export async function createJob(input: {
|
|
|
102
108
|
last_error: null,
|
|
103
109
|
idempotency_key: toIndexable(input.idempotency_key ?? null),
|
|
104
110
|
};
|
|
105
|
-
const c = await col<JobDoc>(
|
|
111
|
+
const c = await col<JobDoc>("jobQueue");
|
|
106
112
|
await c.put(id, doc, { expectedVersion: 0 });
|
|
107
113
|
log.info(`[createJob] Job ${id} created (lane=${input.lane} type=${input.type})`);
|
|
108
114
|
return doc;
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Atomically claim a pending job: transitions status pending→running only if
|
|
119
|
+
* the version hasn't changed since the read. Returns the claimed doc or null
|
|
120
|
+
* if another writer won the race.
|
|
121
|
+
*/
|
|
111
122
|
export async function claimJob(jobId: string, bootId: string = getBootId()): Promise<JobDoc | null> {
|
|
112
|
-
const c = await col<JobDoc>(
|
|
123
|
+
const c = await col<JobDoc>("jobQueue");
|
|
113
124
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
114
125
|
const entry = await c.get(jobId);
|
|
115
126
|
if (!entry) return null;
|
|
@@ -123,7 +134,7 @@ export async function claimJob(jobId: string, bootId: string = getBootId()): Pro
|
|
|
123
134
|
status: "running",
|
|
124
135
|
attempts: doc.attempts + 1,
|
|
125
136
|
boot_id: bootId,
|
|
126
|
-
lease_expires_at: now +
|
|
137
|
+
lease_expires_at: now + jobLeaseDurationMs(),
|
|
127
138
|
started_at: doc.started_at ?? now,
|
|
128
139
|
};
|
|
129
140
|
try {
|
|
@@ -131,6 +142,7 @@ export async function claimJob(jobId: string, bootId: string = getBootId()): Pro
|
|
|
131
142
|
log.info(`[claimJob] Job ${jobId} claimed by boot ${bootId}`);
|
|
132
143
|
return updated;
|
|
133
144
|
} catch {
|
|
145
|
+
// OCC conflict — retry
|
|
134
146
|
await occRetryDelay(attempt);
|
|
135
147
|
}
|
|
136
148
|
}
|
|
@@ -138,8 +150,12 @@ export async function claimJob(jobId: string, bootId: string = getBootId()): Pro
|
|
|
138
150
|
return null;
|
|
139
151
|
}
|
|
140
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Renew the lease on a running job to prevent it from being reclaimed by
|
|
155
|
+
* another process. Uses OCC.
|
|
156
|
+
*/
|
|
141
157
|
export async function renewLease(jobId: string, bootId: string = getBootId()): Promise<boolean> {
|
|
142
|
-
const c = await col<JobDoc>(
|
|
158
|
+
const c = await col<JobDoc>("jobQueue");
|
|
143
159
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
144
160
|
const entry = await c.get(jobId);
|
|
145
161
|
if (!entry) return false;
|
|
@@ -147,11 +163,15 @@ export async function renewLease(jobId: string, bootId: string = getBootId()): P
|
|
|
147
163
|
if (doc.status !== "running") return false;
|
|
148
164
|
if (doc.boot_id !== bootId) return false;
|
|
149
165
|
|
|
150
|
-
const updated: JobDoc = {
|
|
166
|
+
const updated: JobDoc = {
|
|
167
|
+
...doc,
|
|
168
|
+
lease_expires_at: Date.now() + jobLeaseDurationMs(),
|
|
169
|
+
};
|
|
151
170
|
try {
|
|
152
171
|
await c.put(jobId, updated, { expectedVersion: entry.version });
|
|
153
172
|
return true;
|
|
154
173
|
} catch {
|
|
174
|
+
// OCC conflict — retry
|
|
155
175
|
await occRetryDelay(attempt);
|
|
156
176
|
}
|
|
157
177
|
}
|
|
@@ -160,7 +180,7 @@ export async function renewLease(jobId: string, bootId: string = getBootId()): P
|
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
export async function completeJob(jobId: string, result: unknown, bootId: string = getBootId()): Promise<void> {
|
|
163
|
-
const c = await col<JobDoc>(
|
|
183
|
+
const c = await col<JobDoc>("jobQueue");
|
|
164
184
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
165
185
|
const entry = await c.get(jobId);
|
|
166
186
|
if (!entry) return;
|
|
@@ -182,6 +202,7 @@ export async function completeJob(jobId: string, result: unknown, bootId: string
|
|
|
182
202
|
log.info(`[completeJob] Job ${jobId} completed`);
|
|
183
203
|
return;
|
|
184
204
|
} catch {
|
|
205
|
+
// OCC conflict — retry
|
|
185
206
|
await occRetryDelay(attempt);
|
|
186
207
|
}
|
|
187
208
|
}
|
|
@@ -189,7 +210,7 @@ export async function completeJob(jobId: string, result: unknown, bootId: string
|
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
export async function failJob(jobId: string, error: string, bootId: string = getBootId()): Promise<void> {
|
|
192
|
-
const c = await col<JobDoc>(
|
|
213
|
+
const c = await col<JobDoc>("jobQueue");
|
|
193
214
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
194
215
|
const entry = await c.get(jobId);
|
|
195
216
|
if (!entry) return;
|
|
@@ -211,6 +232,7 @@ export async function failJob(jobId: string, error: string, bootId: string = get
|
|
|
211
232
|
log.info(`[failJob] Job ${jobId} failed: ${error}`);
|
|
212
233
|
return;
|
|
213
234
|
} catch {
|
|
235
|
+
// OCC conflict — retry
|
|
214
236
|
await occRetryDelay(attempt);
|
|
215
237
|
}
|
|
216
238
|
}
|
|
@@ -221,7 +243,8 @@ export async function failJob(jobId: string, error: string, bootId: string = get
|
|
|
221
243
|
* Fail a job that returned a LOGICAL failure ({ok:false}), retrying with
|
|
222
244
|
* exponential backoff + jitter up to `policy.maxRetries` before giving up.
|
|
223
245
|
* Distinct from `attempts`/`reclaimOrInterrupt`, which only handle crash /
|
|
224
|
-
* lease-expiry recovery.
|
|
246
|
+
* lease-expiry recovery. `chat_turn` jobs must never be routed here (caller's
|
|
247
|
+
* responsibility) — a user-facing turn should not silently retry later.
|
|
225
248
|
*/
|
|
226
249
|
export async function failJobOrRetry(
|
|
227
250
|
jobId: string,
|
|
@@ -229,7 +252,7 @@ export async function failJobOrRetry(
|
|
|
229
252
|
bootId: string = getBootId(),
|
|
230
253
|
policy: JobRetryPolicy = DEFAULT_JOB_RETRY_POLICY
|
|
231
254
|
): Promise<JobDoc | null> {
|
|
232
|
-
const c = await col<JobDoc>(
|
|
255
|
+
const c = await col<JobDoc>("jobQueue");
|
|
233
256
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
234
257
|
const entry = await c.get(jobId);
|
|
235
258
|
if (!entry) return null;
|
|
@@ -275,6 +298,7 @@ export async function failJobOrRetry(
|
|
|
275
298
|
log.info(`[failJobOrRetry] Job ${jobId} scheduled for retry ${retryCount + 1}/${policy.maxRetries} in ${delay}ms: ${error}`);
|
|
276
299
|
return updated;
|
|
277
300
|
} catch {
|
|
301
|
+
// OCC conflict — retry
|
|
278
302
|
await occRetryDelay(attempt);
|
|
279
303
|
}
|
|
280
304
|
}
|
|
@@ -287,11 +311,12 @@ export async function failJobOrRetry(
|
|
|
287
311
|
* attempt count (already bumped at claim time). If attempts >= max_attempts,
|
|
288
312
|
* marks it as interrupted instead.
|
|
289
313
|
*
|
|
290
|
-
* `force` skips the lease-expiry check
|
|
291
|
-
*
|
|
314
|
+
* `force` skips the lease-expiry check: at boot every "running" row belongs to
|
|
315
|
+
* a dead process (HiveDB is single-process), so waiting out a 30-min lease
|
|
316
|
+
* would only delay recovery.
|
|
292
317
|
*/
|
|
293
318
|
export async function reclaimOrInterrupt(jobId: string, opts?: { force?: boolean }): Promise<JobDoc | null> {
|
|
294
|
-
const c = await col<JobDoc>(
|
|
319
|
+
const c = await col<JobDoc>("jobQueue");
|
|
295
320
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
296
321
|
const entry = await c.get(jobId);
|
|
297
322
|
if (!entry) return null;
|
|
@@ -319,12 +344,18 @@ export async function reclaimOrInterrupt(jobId: string, opts?: { force?: boolean
|
|
|
319
344
|
}
|
|
320
345
|
}
|
|
321
346
|
|
|
322
|
-
const updated: JobDoc = {
|
|
347
|
+
const updated: JobDoc = {
|
|
348
|
+
...doc,
|
|
349
|
+
status: "pending",
|
|
350
|
+
boot_id: null,
|
|
351
|
+
lease_expires_at: null,
|
|
352
|
+
};
|
|
323
353
|
try {
|
|
324
354
|
await c.put(jobId, updated, { expectedVersion: entry.version });
|
|
325
355
|
log.info(`[reclaimOrInterrupt] Job ${jobId} back to pending (attempt ${doc.attempts}/${doc.max_attempts})`);
|
|
326
356
|
return updated;
|
|
327
357
|
} catch {
|
|
358
|
+
// OCC conflict — retry
|
|
328
359
|
await occRetryDelay(attempt);
|
|
329
360
|
}
|
|
330
361
|
}
|
|
@@ -333,28 +364,38 @@ export async function reclaimOrInterrupt(jobId: string, opts?: { force?: boolean
|
|
|
333
364
|
}
|
|
334
365
|
|
|
335
366
|
export async function cancelJob(jobId: string): Promise<boolean> {
|
|
336
|
-
const c = await col<JobDoc>(
|
|
367
|
+
const c = await col<JobDoc>("jobQueue");
|
|
337
368
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
338
369
|
const entry = await c.get(jobId);
|
|
339
370
|
if (!entry) return false;
|
|
340
371
|
const doc = entry.doc;
|
|
341
372
|
if (doc.status === "completed" || doc.status === "failed" || doc.status === "cancelled") return false;
|
|
342
373
|
|
|
343
|
-
const updated: JobDoc = {
|
|
374
|
+
const updated: JobDoc = {
|
|
375
|
+
...doc,
|
|
376
|
+
status: "cancelled",
|
|
377
|
+
finished_at: Date.now(),
|
|
378
|
+
boot_id: null,
|
|
379
|
+
lease_expires_at: null,
|
|
380
|
+
};
|
|
344
381
|
try {
|
|
345
382
|
await c.put(jobId, updated, { expectedVersion: entry.version });
|
|
346
383
|
log.info(`[cancelJob] Job ${jobId} cancelled`);
|
|
347
384
|
return true;
|
|
348
385
|
} catch {
|
|
386
|
+
// OCC conflict — retry
|
|
349
387
|
await occRetryDelay(attempt);
|
|
350
388
|
}
|
|
351
389
|
}
|
|
352
390
|
return false;
|
|
353
391
|
}
|
|
354
392
|
|
|
355
|
-
/**
|
|
393
|
+
/**
|
|
394
|
+
* Find the next pending job for a given lane, ordered by priority then
|
|
395
|
+
* creation order (id is a zero-padded autoincrement → lexical = FIFO).
|
|
396
|
+
*/
|
|
356
397
|
export async function findPendingJobsByLane(lane: string, limit = 10): Promise<JobDoc[]> {
|
|
357
|
-
const c = await col<JobDoc>(
|
|
398
|
+
const c = await col<JobDoc>("jobQueue");
|
|
358
399
|
const entries = await c.findBy("lane", lane);
|
|
359
400
|
return entries
|
|
360
401
|
.filter((e) => e.doc.status === "pending" && e.doc.not_before <= Date.now())
|
|
@@ -366,8 +407,12 @@ export async function findPendingJobsByLane(lane: string, limit = 10): Promise<J
|
|
|
366
407
|
.map((e) => e.doc);
|
|
367
408
|
}
|
|
368
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Scan for jobs whose lease has expired (running + lease_expires_at < now)
|
|
412
|
+
* across the entire collection.
|
|
413
|
+
*/
|
|
369
414
|
export async function findExpiredLeases(): Promise<JobDoc[]> {
|
|
370
|
-
const c = await col<JobDoc>(
|
|
415
|
+
const c = await col<JobDoc>("jobQueue");
|
|
371
416
|
const entries = await c.findBy("status", "running");
|
|
372
417
|
const now = Date.now();
|
|
373
418
|
return entries
|
|
@@ -375,25 +420,17 @@ export async function findExpiredLeases(): Promise<JobDoc[]> {
|
|
|
375
420
|
.map((e) => e.doc);
|
|
376
421
|
}
|
|
377
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Find all pending jobs across any lane.
|
|
425
|
+
*/
|
|
378
426
|
export async function findAllPendingJobs(): Promise<JobDoc[]> {
|
|
379
|
-
const c = await col<JobDoc>(
|
|
427
|
+
const c = await col<JobDoc>("jobQueue");
|
|
380
428
|
const entries = await c.findBy("status", "pending");
|
|
381
429
|
return entries.filter((e) => e.doc.not_before <= Date.now()).map((e) => e.doc);
|
|
382
430
|
}
|
|
383
431
|
|
|
384
432
|
export async function getJob(jobId: string): Promise<JobDoc | null> {
|
|
385
|
-
const c = await col<JobDoc>(
|
|
433
|
+
const c = await col<JobDoc>("jobQueue");
|
|
386
434
|
const entry = await c.get(jobId);
|
|
387
435
|
return entry ? entry.doc : null;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export async function ensureJobStoreIndexes(): Promise<void> {
|
|
391
|
-
const c = await col<JobDoc>(COLLECTION);
|
|
392
|
-
await c.createIndex("status");
|
|
393
|
-
await c.createIndex("lane");
|
|
394
|
-
await c.createIndex("type");
|
|
395
|
-
await c.createIndex("run_id");
|
|
396
|
-
await c.createIndex("idempotency_key");
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
export { COLLECTION as JOB_QUEUE_COLLECTION };
|
|
436
|
+
}
|