@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.
Files changed (278) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +78 -23
  3. package/bun.lock +55 -29
  4. package/bunfig.toml +4 -2
  5. package/docs/API-AGENTS.md +78 -27
  6. package/docs/API-CONTEXT-COMPILER.md +31 -34
  7. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  8. package/docs/HIVE-HARNESS.md +1 -1
  9. package/docs/INDEX.md +4 -4
  10. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  11. package/package.json +17 -12
  12. package/packages/cli/package.json +2 -2
  13. package/packages/cli/src/commands/create-app.test.ts +36 -7
  14. package/packages/cli/src/commands/init.ts +3 -3
  15. package/packages/cli/src/commands/run.ts +1 -1
  16. package/packages/cli/src/commands/test.ts +37 -25
  17. package/packages/cli/src/commands/trace.ts +30 -28
  18. package/packages/cli/templates/hive-app/.env.example +10 -2
  19. package/packages/cli/templates/hive-app/README.md +103 -0
  20. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  21. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  22. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  23. package/packages/core/package.json +13 -12
  24. package/packages/core/src/agent/acceptance-checks.ts +172 -0
  25. package/packages/core/src/agent/agent-catalog.ts +348 -0
  26. package/packages/core/src/agent/agent-loop.ts +1373 -0
  27. package/packages/core/src/agent/capability-search.ts +186 -0
  28. package/packages/core/src/agent/catalog-selector.ts +103 -0
  29. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  30. package/packages/core/src/agent/context-compiler.ts +689 -0
  31. package/packages/core/src/agent/conversation-store.ts +381 -0
  32. package/packages/core/src/agent/curator.ts +276 -0
  33. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  34. package/packages/core/src/agent/goal-runner.ts +323 -0
  35. package/packages/core/src/agent/index.ts +17 -12
  36. package/packages/core/src/agent/llm-client.ts +266 -0
  37. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  38. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  39. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  40. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  41. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  42. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  43. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  44. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  45. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  47. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  48. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  49. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  50. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  51. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  52. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  54. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  55. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  56. package/packages/core/src/agent/playbook-selector.ts +119 -0
  57. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  58. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  59. package/packages/core/src/agent/providers/index.ts +35 -16
  60. package/packages/core/src/agent/reflector.ts +320 -0
  61. package/packages/core/src/agent/routing-intent.ts +22 -0
  62. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  63. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  64. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  65. package/packages/core/src/agent/skill-selector.ts +374 -0
  66. package/packages/core/src/agent/stuck-loop.ts +209 -0
  67. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  68. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  69. package/packages/core/src/api/createAgent.test.ts +139 -27
  70. package/packages/core/src/api/createAgent.ts +232 -44
  71. package/packages/core/src/artifacts/store.ts +162 -0
  72. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  73. package/packages/core/src/canvas/canvas.test.ts +8 -4
  74. package/packages/core/src/canvas/emitter.ts +131 -80
  75. package/packages/core/src/canvas/index.ts +1 -3
  76. package/packages/core/src/channels/base.ts +9 -1
  77. package/packages/core/src/channels/discord.ts +5 -4
  78. package/packages/core/src/channels/manager.ts +122 -30
  79. package/packages/core/src/channels/slack.ts +5 -4
  80. package/packages/core/src/channels/telegram.ts +36 -6
  81. package/packages/core/src/channels/webchat.ts +11 -10
  82. package/packages/core/src/channels/whatsapp.ts +23 -7
  83. package/packages/core/src/config/index.ts +13 -2
  84. package/packages/core/src/config/loader.ts +76 -29
  85. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  86. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  87. package/packages/core/src/events/agent-bus.ts +44 -68
  88. package/packages/core/src/events/channel-narration.ts +150 -0
  89. package/packages/core/src/events/narration.ts +82 -0
  90. package/packages/core/src/events/tool-narration.ts +62 -0
  91. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  92. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  93. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  94. package/packages/core/src/gateway/lane-queue.ts +173 -0
  95. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  96. package/packages/core/src/gateway/server.ts +1 -1
  97. package/packages/core/src/harness/index.ts +46 -27
  98. package/packages/core/src/index.ts +33 -27
  99. package/packages/core/src/mcp/hot-reload.ts +32 -23
  100. package/packages/core/src/mcp/index.ts +6 -3
  101. package/packages/core/src/mcp/singleton.ts +1 -4
  102. package/packages/core/src/mcp/tool-sync.ts +138 -0
  103. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  104. package/packages/core/src/memory/Scratchpad.ts +27 -34
  105. package/packages/core/src/multimodal/vision-service.ts +44 -38
  106. package/packages/core/src/resilience/retry.ts +95 -0
  107. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  108. package/packages/core/src/scheduler/index.ts +9 -7
  109. package/packages/core/src/scheduler/integration.ts +46 -26
  110. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  111. package/packages/core/src/scheduler/types.ts +7 -2
  112. package/packages/core/src/security/Pairing.ts +1 -1
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  115. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  116. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  117. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  118. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  119. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  120. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  121. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  122. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  123. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  124. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  125. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  128. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  129. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  130. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  131. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  132. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  133. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  134. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  135. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  136. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  137. package/packages/core/src/skills/skills.test.ts +52 -11
  138. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  139. package/packages/core/src/storage/bootstrap.ts +151 -0
  140. package/packages/core/src/storage/causal-events.ts +84 -0
  141. package/packages/core/src/storage/collections.ts +680 -0
  142. package/packages/core/src/storage/crypto.ts +205 -74
  143. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  144. package/packages/core/src/storage/hivedb.ts +61 -0
  145. package/packages/core/src/storage/index.ts +111 -18
  146. package/packages/core/src/storage/model-id.ts +53 -0
  147. package/packages/core/src/storage/onboarding.ts +540 -972
  148. package/packages/core/src/storage/reconcile.ts +238 -0
  149. package/packages/core/src/storage/seed.ts +572 -406
  150. package/packages/core/src/storage/usage.ts +285 -225
  151. package/packages/core/src/storage/user-email.ts +11 -0
  152. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  153. package/packages/core/src/swarm/EventBridge.ts +1 -1
  154. package/packages/core/src/swarm/index.ts +12 -9
  155. package/packages/core/src/tool-runtime/index.ts +146 -23
  156. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  157. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  158. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  159. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  160. package/packages/core/src/tools/agents/index.ts +784 -292
  161. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  162. package/packages/core/src/tools/api/api-request.ts +174 -0
  163. package/packages/core/src/tools/api/index.ts +16 -0
  164. package/packages/core/src/tools/cli/index.ts +4 -0
  165. package/packages/core/src/tools/core/index.ts +281 -112
  166. package/packages/core/src/tools/cron/index.ts +121 -124
  167. package/packages/core/src/tools/index.ts +63 -78
  168. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  169. package/packages/core/src/tools/types.ts +3 -1
  170. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  171. package/packages/core/src/tools/web/browser-backend.ts +129 -0
  172. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  173. package/packages/core/src/tools/web/browser-service.ts +80 -35
  174. package/packages/core/src/tools/web/browser-type.ts +3 -8
  175. package/packages/core/src/tools/web/index.ts +4 -4
  176. package/packages/core/src/tools/web/webview-backend.ts +412 -0
  177. package/packages/core/src/voice/index.ts +89 -63
  178. package/packages/core/src/workers/agent.worker.ts +2 -2
  179. package/packages/core/src/workers/workers.test.ts +3 -10
  180. package/scripts/bump-version.ts +248 -0
  181. package/scripts/generate-skill-bundle.ts +108 -0
  182. package/test/acceptance-checks.test.ts +403 -0
  183. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  184. package/test/browser-backend.test.ts +308 -0
  185. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  186. package/test/causal-events.test.ts +117 -0
  187. package/test/compaction.test.ts +105 -0
  188. package/test/context-compiler.test.ts +269 -0
  189. package/test/curator.test.ts +130 -0
  190. package/test/durable-queue.test.ts +114 -0
  191. package/test/harness-barrel.test.ts +64 -0
  192. package/test/hive-helpers.test.ts +130 -0
  193. package/test/hivedb-search.test.ts +189 -0
  194. package/test/internal-turns.test.ts +166 -0
  195. package/test/job-idempotency.test.ts +68 -0
  196. package/test/job-retry-backoff.test.ts +184 -0
  197. package/test/job-store.test.ts +381 -0
  198. package/test/llm-retry.test.ts +97 -0
  199. package/test/memory-perf.test.ts +774 -0
  200. package/test/minimal-loadout.test.ts +78 -0
  201. package/test/model-catalog.test.ts +105 -0
  202. package/test/preload.ts +12 -0
  203. package/test/reflector.test.ts +320 -0
  204. package/test/retention-cap.test.ts +91 -0
  205. package/test/retired-capabilities-pruned.test.ts +192 -0
  206. package/test/run-store.test.ts +355 -0
  207. package/test/scratchpad.test.ts +74 -0
  208. package/test/secrets-durability.test.ts +119 -0
  209. package/test/seed-model-reseed.test.ts +155 -0
  210. package/test/setup-agent-seed.test.ts +264 -0
  211. package/test/tool-inventory.test.ts +65 -0
  212. package/test/tool-runtime.test.ts +258 -0
  213. package/test/tool-selector-runtime-tools.test.ts +117 -0
  214. package/test/toon.test.ts +429 -0
  215. package/tsconfig.json +2 -0
  216. package/packages/core/src/ace/Curator.ts +0 -158
  217. package/packages/core/src/ace/Reflector.ts +0 -200
  218. package/packages/core/src/ace/index.ts +0 -4
  219. package/packages/core/src/agent/AgentRunner.ts +0 -711
  220. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  221. package/packages/core/src/agent/ContextGuard.ts +0 -91
  222. package/packages/core/src/agent/ConversationStore.ts +0 -254
  223. package/packages/core/src/agent/Hooks.ts +0 -166
  224. package/packages/core/src/agent/StuckLoop.ts +0 -133
  225. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  226. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  227. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  228. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  229. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  230. package/packages/core/src/agent/selectors/index.ts +0 -6
  231. package/packages/core/src/auth/auth.ts +0 -121
  232. package/packages/core/src/auth/index.ts +0 -1
  233. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  234. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  235. package/packages/core/src/harness/collections.ts +0 -98
  236. package/packages/core/src/harness/goal-verifier.ts +0 -141
  237. package/packages/core/src/harness/harness.test.ts +0 -236
  238. package/packages/core/src/harness/reconcile.ts +0 -149
  239. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  240. package/packages/core/src/multimodal/VisionService.ts +0 -293
  241. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  242. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  243. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  244. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  245. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  246. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  247. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  248. package/packages/core/src/scheduler/dag/index.ts +0 -26
  249. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  250. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  251. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  252. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  253. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  254. package/packages/core/src/storage/hiveSeed.ts +0 -308
  255. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  256. package/packages/core/src/storage/schema.ts +0 -689
  257. package/packages/core/src/storage/storage.test.ts +0 -37
  258. package/packages/core/src/swarm/AgentBus.ts +0 -460
  259. package/packages/core/src/swarm/EventBus.ts +0 -169
  260. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  261. package/packages/core/src/tools/bridge-events.ts +0 -26
  262. package/packages/core/src/tools/canvas/index.ts +0 -375
  263. package/packages/core/src/tools/codebridge/index.ts +0 -342
  264. package/packages/core/src/tools/meeting/index.ts +0 -353
  265. package/packages/core/src/tools/projects/index.ts +0 -37
  266. package/packages/core/src/tools/projects/project-create.ts +0 -94
  267. package/packages/core/src/tools/projects/project-done.ts +0 -66
  268. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  269. package/packages/core/src/tools/projects/project-list.ts +0 -96
  270. package/packages/core/src/tools/projects/project-update.ts +0 -72
  271. package/packages/core/src/tools/projects/task-create.ts +0 -68
  272. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  273. package/packages/core/src/tools/projects/task-update.ts +0 -93
  274. package/packages/core/src/tools/voice/index.ts +0 -104
  275. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  276. package/packages/core/src/tools/web/api-request.ts +0 -239
  277. package/test/setup-db.ts +0 -216
  278. /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 harness_jobQueue
3
- * HiveDB collection. Ported from `hive`'s gateway/durable-queue.ts,
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
- * - Logical failures ({ok:false, retryable:true}) retry with backoff+jitter
15
- * via `failJobOrRetry`, except for `nonRetryableTypes` (default: chat_turn)
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 "./collections";
34
- import { getBootId } from "./boot-id";
34
+ import type { JobDoc } from "../storage/collections";
35
+ import { getBootId } from "../storage/boot-id";
35
36
 
36
- const log = logger.child("harness:durable-queue");
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
- const DEFAULT_NON_RETRYABLE_TYPES = ["chat_turn"];
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<string, JobExecutor>();
72
+ const executors = new Map<JobType, JobExecutor>();
70
73
 
71
- export function registerExecutor(type: string, executor: JobExecutor): void {
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 DurableLaneQueueOptions {
77
- maxGlobalConcurrency?: number;
78
- taskTimeoutMs?: number;
79
- jobRetryPolicy?: JobRetryPolicy;
80
- /** Job types that should never auto-retry a logical failure (e.g. user-facing turns). Default: ["chat_turn"]. */
81
- nonRetryableTypes?: string[];
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: DurableLaneQueueOptions = {}) {
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. Callers should use `enqueue`
105
- * instead of any in-memory queue for work that must survive crashes.
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: string;
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
- /** Cancel a job by id (pending or running). A running job also gets its AbortSignal fired. */
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
- /** Cancel all pending and running jobs in a lane. */
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 maintenance tick (expired-lease reclaim + due-pending
162
- * redispatch) and dispatch any jobs left pending by a previous boot.
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/user-facing types bypass the global cap so a busy batch
221
- // of background jobs can't starve them matches `hive`'s chat_turn
222
- // convention: nonRetryableTypes doubles as the "must stay responsive" set.
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
- if (!this.nonRetryableTypes.has(job.type) && result.retryable !== false) {
267
- await failJobOrRetry(job.id, error, this.bootId, this.jobRetryPolicy);
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 (!this.nonRetryableTypes.has(job.type)) {
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?: DurableLaneQueueOptions): DurableLaneQueue {
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 harness_jobQueue
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 "./db-helpers";
10
- import type { JobDoc } from "./collections";
11
- import { getBootId } from "./boot-id";
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("harness:job-store");
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
- let leaseDurationMs = DEFAULT_LEASE_DURATION_MS;
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>(COLLECTION);
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: string;
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(COLLECTION);
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>(COLLECTION);
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>(COLLECTION);
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 + leaseDurationMs,
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>(COLLECTION);
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 = { ...doc, lease_expires_at: Date.now() + leaseDurationMs };
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>(COLLECTION);
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>(COLLECTION);
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>(COLLECTION);
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 — used at boot, where every "running"
291
- * row belongs to a dead process in a single-process HiveDB deployment.
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>(COLLECTION);
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 = { ...doc, status: "pending", boot_id: null, lease_expires_at: null };
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>(COLLECTION);
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 = { ...doc, status: "cancelled", finished_at: Date.now(), boot_id: null, lease_expires_at: null };
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
- /** Find the next pending job for a given lane, ordered by priority then creation order. */
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>(COLLECTION);
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>(COLLECTION);
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>(COLLECTION);
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>(COLLECTION);
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
+ }