nexus-ai-pro 2.0.0 → 2.2.0

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 (282) hide show
  1. package/API_STABILITY.md +65 -0
  2. package/CHANGELOG.md +267 -1
  3. package/MIGRATING.md +1 -1
  4. package/README.md +17 -8
  5. package/dist/agent/create-agent.d.ts +9 -1
  6. package/dist/agent/create-agent.js +40 -1
  7. package/dist/cache/memory-cache.d.ts +8 -0
  8. package/dist/cache/memory-cache.js +9 -0
  9. package/dist/cache/semantic-cache.d.ts +6 -3
  10. package/dist/cache/semantic-cache.js +5 -2
  11. package/dist/cli/args.js +3 -0
  12. package/dist/cli/db.d.ts +16 -4
  13. package/dist/cli/db.js +169 -22
  14. package/dist/cli/doctor.d.ts +17 -0
  15. package/dist/cli/doctor.js +53 -0
  16. package/dist/cli/graph.d.ts +9 -0
  17. package/dist/cli/graph.js +47 -0
  18. package/dist/cli.js +21 -2
  19. package/dist/core/lifecycle.js +11 -6
  20. package/dist/core/nexus.d.ts +12 -2
  21. package/dist/core/nexus.js +106 -23
  22. package/dist/doctor/index.d.ts +90 -0
  23. package/dist/doctor/index.js +237 -0
  24. package/dist/evals/judge.d.ts +11 -0
  25. package/dist/evals/judge.js +33 -0
  26. package/dist/evaluate/index.d.ts +1 -0
  27. package/dist/evaluate/index.js +1 -0
  28. package/dist/evaluate/provenance.d.ts +22 -0
  29. package/dist/evaluate/provenance.js +10 -0
  30. package/dist/evaluate/run.js +20 -4
  31. package/dist/graph/durability.d.ts +27 -0
  32. package/dist/graph/durability.js +66 -0
  33. package/dist/graph/errors.d.ts +30 -4
  34. package/dist/graph/errors.js +18 -2
  35. package/dist/graph/event-stream.d.ts +75 -0
  36. package/dist/graph/event-stream.js +289 -0
  37. package/dist/graph/functional.d.ts +80 -7
  38. package/dist/graph/functional.js +138 -35
  39. package/dist/graph/graph.d.ts +56 -8
  40. package/dist/graph/graph.js +331 -49
  41. package/dist/graph/index.d.ts +4 -2
  42. package/dist/graph/index.js +3 -1
  43. package/dist/graph/lint.d.ts +57 -0
  44. package/dist/graph/lint.js +141 -0
  45. package/dist/graph/run-control.d.ts +28 -0
  46. package/dist/graph/run-control.js +26 -0
  47. package/dist/images/evals.js +4 -2
  48. package/dist/operations/adapters.d.ts +52 -4
  49. package/dist/operations/adapters.js +194 -7
  50. package/dist/operations/runner.js +14 -15
  51. package/dist/operations/stats.js +8 -1
  52. package/dist/operations/store.d.ts +5 -1
  53. package/dist/operations/store.js +7 -0
  54. package/dist/ops/circuit-breaker.d.ts +52 -0
  55. package/dist/ops/circuit-breaker.js +4 -0
  56. package/dist/ops/circuit-store.d.ts +13 -2
  57. package/dist/ops/circuit-store.js +68 -0
  58. package/dist/ops/circuit-sync.d.ts +17 -0
  59. package/dist/ops/circuit-sync.js +86 -0
  60. package/dist/ops/health.d.ts +24 -0
  61. package/dist/ops/health.js +38 -10
  62. package/dist/ops/index.d.ts +1 -1
  63. package/dist/ops/index.js +1 -1
  64. package/dist/ops/rate-limit-adapters.d.ts +23 -0
  65. package/dist/ops/rate-limit-adapters.js +34 -0
  66. package/dist/ops/rate-limiter.d.ts +14 -2
  67. package/dist/ops/rate-limiter.js +31 -0
  68. package/dist/postgres/circuits.d.ts +21 -4
  69. package/dist/postgres/circuits.js +65 -4
  70. package/dist/postgres/client.d.ts +0 -2
  71. package/dist/postgres/client.js +0 -4
  72. package/dist/postgres/evaluate.d.ts +16 -4
  73. package/dist/postgres/evaluate.js +17 -5
  74. package/dist/postgres/index.d.ts +32 -11
  75. package/dist/postgres/index.js +65 -31
  76. package/dist/postgres/migrations.d.ts +60 -0
  77. package/dist/postgres/migrations.js +121 -0
  78. package/dist/postgres/operations.d.ts +14 -3
  79. package/dist/postgres/operations.js +45 -15
  80. package/dist/postgres/prompts.d.ts +11 -2
  81. package/dist/postgres/prompts.js +14 -3
  82. package/dist/postgres/rollups.d.ts +35 -0
  83. package/dist/postgres/rollups.js +129 -0
  84. package/dist/postgres/store.d.ts +11 -2
  85. package/dist/postgres/store.js +14 -3
  86. package/dist/postgres/traces.d.ts +13 -3
  87. package/dist/postgres/traces.js +25 -3
  88. package/dist/postgres/vectors.d.ts +11 -2
  89. package/dist/postgres/vectors.js +14 -3
  90. package/dist/router/auto-router.d.ts +34 -7
  91. package/dist/router/auto-router.js +201 -68
  92. package/dist/server/assistant.d.ts +44 -0
  93. package/dist/server/assistant.js +39 -0
  94. package/dist/server/errors.d.ts +12 -0
  95. package/dist/server/errors.js +7 -0
  96. package/dist/server/index.d.ts +1 -1
  97. package/dist/server/index.js +1 -1
  98. package/dist/server/runs.d.ts +28 -1
  99. package/dist/server/runs.js +110 -14
  100. package/dist/sqlite/index.d.ts +16 -3
  101. package/dist/sqlite/index.js +16 -3
  102. package/dist/sqlite/migrations.d.ts +26 -0
  103. package/dist/sqlite/migrations.js +63 -0
  104. package/dist/sqlite/operations.d.ts +14 -3
  105. package/dist/sqlite/operations.js +42 -15
  106. package/dist/sqlite/store.d.ts +9 -2
  107. package/dist/sqlite/store.js +13 -3
  108. package/dist/sqlite/vectors.d.ts +11 -2
  109. package/dist/sqlite/vectors.js +13 -2
  110. package/dist/store/tenant.d.ts +12 -0
  111. package/dist/store/tenant.js +29 -0
  112. package/dist/tenancy/index.d.ts +113 -0
  113. package/dist/tenancy/index.js +233 -0
  114. package/dist/testing/record.d.ts +42 -0
  115. package/dist/testing/record.js +57 -5
  116. package/dist/tracing/index.d.ts +3 -3
  117. package/dist/tracing/index.js +2 -2
  118. package/dist/tracing/instrument.js +30 -0
  119. package/dist/tracing/otlp.d.ts +91 -0
  120. package/dist/tracing/otlp.js +124 -0
  121. package/dist/tracing/rollups.d.ts +100 -0
  122. package/dist/tracing/rollups.js +139 -0
  123. package/dist/tracing/stores.d.ts +17 -0
  124. package/dist/tracing/stores.js +23 -0
  125. package/dist/tracing/tracer.d.ts +50 -4
  126. package/dist/tracing/tracer.js +85 -6
  127. package/dist/types/config.d.ts +33 -2
  128. package/dist/types/evaluate.d.ts +37 -0
  129. package/dist/types/graph.d.ts +354 -9
  130. package/dist/types/graph.js +3 -1
  131. package/dist/types/messages.d.ts +5 -0
  132. package/dist/types/operations.d.ts +22 -2
  133. package/dist/types/response.d.ts +20 -0
  134. package/dist/types/server.d.ts +18 -0
  135. package/dist/types/tracing.d.ts +11 -0
  136. package/dist/utils/schema-migrations.d.ts +97 -0
  137. package/dist/utils/schema-migrations.js +91 -0
  138. package/dist/utils/signals.d.ts +17 -0
  139. package/dist/utils/signals.js +26 -0
  140. package/dist/utils/tenant.d.ts +14 -0
  141. package/dist/utils/tenant.js +16 -0
  142. package/dist/version.d.ts +5 -0
  143. package/dist/version.js +1 -0
  144. package/dist-cjs/agent/create-agent.d.ts +9 -1
  145. package/dist-cjs/agent/create-agent.js +40 -1
  146. package/dist-cjs/cache/memory-cache.d.ts +8 -0
  147. package/dist-cjs/cache/memory-cache.js +9 -0
  148. package/dist-cjs/cache/semantic-cache.d.ts +6 -3
  149. package/dist-cjs/cache/semantic-cache.js +5 -2
  150. package/dist-cjs/cli/args.js +3 -0
  151. package/dist-cjs/cli/db.d.ts +16 -4
  152. package/dist-cjs/cli/db.js +200 -20
  153. package/dist-cjs/cli/doctor.d.ts +17 -0
  154. package/dist-cjs/cli/doctor.js +56 -0
  155. package/dist-cjs/cli/graph.d.ts +9 -0
  156. package/dist-cjs/cli/graph.js +50 -0
  157. package/dist-cjs/core/lifecycle.js +11 -6
  158. package/dist-cjs/core/nexus.d.ts +12 -2
  159. package/dist-cjs/core/nexus.js +106 -23
  160. package/dist-cjs/doctor/index.d.ts +90 -0
  161. package/dist-cjs/doctor/index.js +276 -0
  162. package/dist-cjs/evals/judge.d.ts +11 -0
  163. package/dist-cjs/evals/judge.js +33 -0
  164. package/dist-cjs/evaluate/index.d.ts +1 -0
  165. package/dist-cjs/evaluate/index.js +3 -1
  166. package/dist-cjs/evaluate/provenance.d.ts +22 -0
  167. package/dist-cjs/evaluate/provenance.js +14 -0
  168. package/dist-cjs/evaluate/run.js +20 -4
  169. package/dist-cjs/graph/durability.d.ts +27 -0
  170. package/dist-cjs/graph/durability.js +70 -0
  171. package/dist-cjs/graph/errors.d.ts +30 -4
  172. package/dist-cjs/graph/errors.js +20 -3
  173. package/dist-cjs/graph/event-stream.d.ts +75 -0
  174. package/dist-cjs/graph/event-stream.js +295 -0
  175. package/dist-cjs/graph/functional.d.ts +80 -7
  176. package/dist-cjs/graph/functional.js +137 -34
  177. package/dist-cjs/graph/graph.d.ts +56 -8
  178. package/dist-cjs/graph/graph.js +330 -48
  179. package/dist-cjs/graph/index.d.ts +4 -2
  180. package/dist-cjs/graph/index.js +7 -1
  181. package/dist-cjs/graph/lint.d.ts +57 -0
  182. package/dist-cjs/graph/lint.js +144 -0
  183. package/dist-cjs/graph/run-control.d.ts +28 -0
  184. package/dist-cjs/graph/run-control.js +30 -0
  185. package/dist-cjs/images/evals.js +4 -2
  186. package/dist-cjs/operations/adapters.d.ts +52 -4
  187. package/dist-cjs/operations/adapters.js +193 -6
  188. package/dist-cjs/operations/runner.js +14 -15
  189. package/dist-cjs/operations/stats.js +8 -1
  190. package/dist-cjs/operations/store.d.ts +5 -1
  191. package/dist-cjs/operations/store.js +7 -0
  192. package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
  193. package/dist-cjs/ops/circuit-breaker.js +4 -0
  194. package/dist-cjs/ops/circuit-store.d.ts +13 -2
  195. package/dist-cjs/ops/circuit-store.js +68 -0
  196. package/dist-cjs/ops/circuit-sync.d.ts +17 -0
  197. package/dist-cjs/ops/circuit-sync.js +87 -0
  198. package/dist-cjs/ops/health.d.ts +24 -0
  199. package/dist-cjs/ops/health.js +38 -10
  200. package/dist-cjs/ops/index.d.ts +1 -1
  201. package/dist-cjs/ops/rate-limit-adapters.d.ts +23 -0
  202. package/dist-cjs/ops/rate-limit-adapters.js +35 -1
  203. package/dist-cjs/ops/rate-limiter.d.ts +14 -2
  204. package/dist-cjs/ops/rate-limiter.js +32 -0
  205. package/dist-cjs/postgres/circuits.d.ts +21 -4
  206. package/dist-cjs/postgres/circuits.js +98 -3
  207. package/dist-cjs/postgres/client.d.ts +0 -2
  208. package/dist-cjs/postgres/client.js +0 -5
  209. package/dist-cjs/postgres/evaluate.d.ts +16 -4
  210. package/dist-cjs/postgres/evaluate.js +50 -4
  211. package/dist-cjs/postgres/index.d.ts +32 -11
  212. package/dist-cjs/postgres/index.js +69 -20
  213. package/dist-cjs/postgres/migrations.d.ts +60 -0
  214. package/dist-cjs/postgres/migrations.js +129 -0
  215. package/dist-cjs/postgres/operations.d.ts +14 -3
  216. package/dist-cjs/postgres/operations.js +78 -14
  217. package/dist-cjs/postgres/prompts.d.ts +11 -2
  218. package/dist-cjs/postgres/prompts.js +47 -2
  219. package/dist-cjs/postgres/rollups.d.ts +35 -0
  220. package/dist-cjs/postgres/rollups.js +168 -0
  221. package/dist-cjs/postgres/store.d.ts +11 -2
  222. package/dist-cjs/postgres/store.js +47 -2
  223. package/dist-cjs/postgres/traces.d.ts +13 -3
  224. package/dist-cjs/postgres/traces.js +58 -2
  225. package/dist-cjs/postgres/vectors.d.ts +11 -2
  226. package/dist-cjs/postgres/vectors.js +47 -2
  227. package/dist-cjs/router/auto-router.d.ts +34 -7
  228. package/dist-cjs/router/auto-router.js +201 -68
  229. package/dist-cjs/server/assistant.d.ts +44 -0
  230. package/dist-cjs/server/assistant.js +39 -0
  231. package/dist-cjs/server/errors.d.ts +12 -0
  232. package/dist-cjs/server/errors.js +9 -1
  233. package/dist-cjs/server/index.d.ts +1 -1
  234. package/dist-cjs/server/index.js +2 -1
  235. package/dist-cjs/server/runs.d.ts +28 -1
  236. package/dist-cjs/server/runs.js +109 -13
  237. package/dist-cjs/sqlite/index.d.ts +16 -3
  238. package/dist-cjs/sqlite/index.js +31 -10
  239. package/dist-cjs/sqlite/migrations.d.ts +26 -0
  240. package/dist-cjs/sqlite/migrations.js +71 -0
  241. package/dist-cjs/sqlite/operations.d.ts +14 -3
  242. package/dist-cjs/sqlite/operations.js +76 -15
  243. package/dist-cjs/sqlite/store.d.ts +9 -2
  244. package/dist-cjs/sqlite/store.js +47 -3
  245. package/dist-cjs/sqlite/vectors.d.ts +11 -2
  246. package/dist-cjs/sqlite/vectors.js +47 -2
  247. package/dist-cjs/store/tenant.d.ts +12 -0
  248. package/dist-cjs/store/tenant.js +33 -0
  249. package/dist-cjs/tenancy/index.d.ts +113 -0
  250. package/dist-cjs/tenancy/index.js +250 -0
  251. package/dist-cjs/testing/record.d.ts +42 -0
  252. package/dist-cjs/testing/record.js +57 -5
  253. package/dist-cjs/tracing/index.d.ts +3 -3
  254. package/dist-cjs/tracing/index.js +4 -1
  255. package/dist-cjs/tracing/instrument.js +30 -0
  256. package/dist-cjs/tracing/otlp.d.ts +91 -0
  257. package/dist-cjs/tracing/otlp.js +129 -0
  258. package/dist-cjs/tracing/rollups.d.ts +100 -0
  259. package/dist-cjs/tracing/rollups.js +147 -0
  260. package/dist-cjs/tracing/stores.d.ts +17 -0
  261. package/dist-cjs/tracing/stores.js +24 -0
  262. package/dist-cjs/tracing/tracer.d.ts +50 -4
  263. package/dist-cjs/tracing/tracer.js +87 -6
  264. package/dist-cjs/types/config.d.ts +33 -2
  265. package/dist-cjs/types/evaluate.d.ts +37 -0
  266. package/dist-cjs/types/graph.d.ts +354 -9
  267. package/dist-cjs/types/graph.js +3 -1
  268. package/dist-cjs/types/messages.d.ts +5 -0
  269. package/dist-cjs/types/operations.d.ts +22 -2
  270. package/dist-cjs/types/response.d.ts +20 -0
  271. package/dist-cjs/types/server.d.ts +18 -0
  272. package/dist-cjs/types/tracing.d.ts +11 -0
  273. package/dist-cjs/utils/schema-migrations.d.ts +97 -0
  274. package/dist-cjs/utils/schema-migrations.js +102 -0
  275. package/dist-cjs/utils/signals.d.ts +17 -0
  276. package/dist-cjs/utils/signals.js +29 -0
  277. package/dist-cjs/utils/tenant.d.ts +14 -0
  278. package/dist-cjs/utils/tenant.js +22 -0
  279. package/dist-cjs/version.d.ts +5 -0
  280. package/dist-cjs/version.js +4 -0
  281. package/llms.txt +4 -3
  282. package/package.json +110 -2
@@ -2,5 +2,7 @@ export { CompiledGraph, StateGraph, createGraph } from './graph.js';
2
2
  export { migrateCheckpoint, toCheckpoint, } from './checkpoint-migration.js';
3
3
  export { appendList, appendSet, counter, lastValue, mergeObject, reducerChannel, } from './channels.js';
4
4
  export { MemoryGraphCheckpointer, OperationStoreCheckpointer, } from './checkpointer.js';
5
- export { GraphError, GraphInterrupt, GraphNodeError, GraphNodeTimeoutError, GraphNotInterruptedError, GraphStepLimitError, GraphThreadNotFoundError, GraphValidationError, interruptKey, } from './errors.js';
5
+ export { GraphDrainedError, GraphError, GraphInterrupt, GraphNodeError, GraphNodeTimeoutError, GraphNotInterruptedError, GraphStepLimitError, GraphThreadNotFoundError, GraphValidationError, interruptKey, } from './errors.js';
6
+ export { RunControl } from './run-control.js';
7
+ export { GraphEventStream, GraphStreamOverflowError } from './event-stream.js';
6
8
  export { Command, END, Send, START } from '../types/graph.js';
@@ -0,0 +1,57 @@
1
+ import type { GraphDescription } from '../types/graph.js';
2
+ /** How much a lint finding matters: `error` fails `nexus graph lint`, the others only report. */
3
+ export type GraphLintSeverity = 'error' | 'warning' | 'info';
4
+ /** The rules `lintGraph()` checks. */
5
+ export type GraphLintCode = 'UNBOUNDED_CYCLE' | 'RETRY_WITHOUT_IDEMPOTENCY' | 'RETRY_WITHOUT_TIMEOUT' | 'DEFERRED_DURABILITY_WITH_SIDE_EFFECTS' | 'MEMORY_CHECKPOINTER' | 'NO_CHECKPOINTER' | 'UNBOUNDED_CONCURRENCY' | 'DYNAMIC_ROUTE';
6
+ /** One design problem `lintGraph()` found. */
7
+ export interface GraphLintFinding {
8
+ /** Which rule found it. */
9
+ code: GraphLintCode;
10
+ /** How much it matters. */
11
+ severity: GraphLintSeverity;
12
+ /** The node it is about, as a path through subgraphs such as `review/approve`. Absent for the graph itself. */
13
+ node?: string;
14
+ /** What is wrong. */
15
+ message: string;
16
+ /** What to do about it. */
17
+ fix: string;
18
+ }
19
+ /** Options for `lintGraph()`. */
20
+ export interface GraphLintOptions {
21
+ /**
22
+ * The graph runs where a restart, a second replica, or a hand-off is normal: under the agent
23
+ * server, a job queue, or more than one process. An in-process checkpointer is then an error, not
24
+ * a note.
25
+ */
26
+ deployed?: boolean;
27
+ /** Rules to skip, by code. */
28
+ ignore?: GraphLintCode[];
29
+ }
30
+ /**
31
+ * Finds designs that work in a demo and fail in production, by reading a compiled graph's shape.
32
+ *
33
+ * It needs no run and calls no node: it reads `describe()`, so it also works on a description saved
34
+ * as JSON. The checks:
35
+ *
36
+ * | Code | Severity | Finds |
37
+ * | --- | --- | --- |
38
+ * | `UNBOUNDED_CYCLE` | error | A cycle of static edges, which only the step limit can stop |
39
+ * | `RETRY_WITHOUT_IDEMPOTENCY` | warning | A retried node not declared `idempotent`, whose side effects can repeat |
40
+ * | `RETRY_WITHOUT_TIMEOUT` | warning | A retried node with no timeout, whose hung attempt is never retried |
41
+ * | `DEFERRED_DURABILITY_WITH_SIDE_EFFECTS` | warning | `async` or `exit` durability with nodes not declared `idempotent` |
42
+ * | `MEMORY_CHECKPOINTER` | warning, or error when `deployed` | The in-process default, which a restart loses |
43
+ * | `NO_CHECKPOINTER` | info | No checkpoints, so no interrupt, resume, or recovery |
44
+ * | `UNBOUNDED_CONCURRENCY` | warning | A fan-out with no limit on tasks at once |
45
+ * | `DYNAMIC_ROUTE` | info | A router with no mapping or `ends`, so routes cannot be checked |
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { lintGraph } from 'nexus-ai-pro/graph/lint';
50
+ *
51
+ * const findings = lintGraph(graph, { deployed: true });
52
+ * for (const finding of findings) console.log(finding.severity, finding.code, finding.message);
53
+ * ```
54
+ */
55
+ export declare function lintGraph(graph: {
56
+ describe(): GraphDescription;
57
+ } | GraphDescription, options?: GraphLintOptions): GraphLintFinding[];
@@ -0,0 +1,141 @@
1
+ export function lintGraph(graph, options = {}) {
2
+ const description = 'describe' in graph && typeof graph.describe === 'function' ? graph.describe() : graph;
3
+ const findings = [];
4
+ lintDescription(description, '', options, findings, true);
5
+ const ignored = new Set(options.ignore ?? []);
6
+ return findings.filter((finding) => !ignored.has(finding.code));
7
+ }
8
+ function lintDescription(description, prefix, options, findings, top) {
9
+ const at = (node) => `${prefix}${node}`;
10
+ for (const cycle of staticCycles(description)) {
11
+ findings.push({
12
+ code: 'UNBOUNDED_CYCLE',
13
+ severity: 'error',
14
+ node: at(cycle[0]),
15
+ message: `${cycle.map((node) => `"${at(node)}"`).join(' → ')} loop through static edges only, so the run can only stop at the step limit.`,
16
+ fix: 'Route out of the loop with a conditional edge, a Command with declared ends, or an edge to END.',
17
+ });
18
+ }
19
+ const sideEffecting = [];
20
+ for (const node of description.nodes) {
21
+ if (!node.idempotent)
22
+ sideEffecting.push(node.id);
23
+ if (node.retry && !node.idempotent) {
24
+ findings.push({
25
+ code: 'RETRY_WITHOUT_IDEMPOTENCY',
26
+ severity: 'warning',
27
+ node: at(node.id),
28
+ message: `"${at(node.id)}" is retried up to ${node.maxAttempts ?? 'several'} times but is not declared idempotent, so a side effect can happen more than once.`,
29
+ fix: 'Give its side effects an idempotency key and declare `idempotent: true`, or stop retrying it.',
30
+ });
31
+ }
32
+ const timed = node.timeoutMs !== undefined || node.timeout?.runMs !== undefined || node.timeout?.idleMs !== undefined;
33
+ if (node.retry && !timed) {
34
+ findings.push({
35
+ code: 'RETRY_WITHOUT_TIMEOUT',
36
+ severity: 'warning',
37
+ node: at(node.id),
38
+ message: `"${at(node.id)}" is retried but has no timeout, so an attempt that hangs is never retried.`,
39
+ fix: 'Set `timeout: { runMs }`, or `idleMs` with `context.heartbeat()` for long work.',
40
+ });
41
+ }
42
+ if (node.subgraph)
43
+ lintDescription(node.subgraph, `${at(node.id)}/`, options, findings, false);
44
+ }
45
+ if (!top)
46
+ return;
47
+ if ((description.durability === 'async' || description.durability === 'exit') && sideEffecting.length > 0) {
48
+ findings.push({
49
+ code: 'DEFERRED_DURABILITY_WITH_SIDE_EFFECTS',
50
+ severity: 'warning',
51
+ message: `Durability is "${description.durability}", so a crash can repeat supersteps, and ${sideEffecting.map((node) => `"${node}"`).join(', ')} ${sideEffecting.length === 1 ? 'is' : 'are'} not declared idempotent.`,
52
+ fix: 'Use `sync` durability for graphs with side effects, or make those nodes idempotent and declare it.',
53
+ });
54
+ }
55
+ if (description.checkpointer === 'memory') {
56
+ findings.push({
57
+ code: 'MEMORY_CHECKPOINTER',
58
+ severity: options.deployed ? 'error' : 'warning',
59
+ message: 'Checkpoints go to the in-process default, so a restart loses every thread, and another process cannot resume one.',
60
+ fix: 'Pass a persistent checkpointer, such as an OperationStoreCheckpointer over Postgres, SQLite, or Redis.',
61
+ });
62
+ }
63
+ if (description.checkpointer === 'none') {
64
+ findings.push({
65
+ code: 'NO_CHECKPOINTER',
66
+ severity: 'info',
67
+ message: 'The graph writes no checkpoints, so it cannot interrupt, resume, drain, or recover after a crash.',
68
+ fix: 'Pass a checkpointer if any of those is needed.',
69
+ });
70
+ }
71
+ if (description.maxConcurrency !== undefined && !Number.isFinite(description.maxConcurrency)) {
72
+ findings.push({
73
+ code: 'UNBOUNDED_CONCURRENCY',
74
+ severity: 'warning',
75
+ message: 'Tasks in a superstep run with no limit, so a large Send fan-out starts all of them at once.',
76
+ fix: 'Set `maxConcurrency` to what the services behind the nodes can take.',
77
+ });
78
+ }
79
+ for (const node of description.dynamic) {
80
+ findings.push({
81
+ code: 'DYNAMIC_ROUTE',
82
+ severity: 'info',
83
+ node: at(node),
84
+ message: `The router after "${at(node)}" has no mapping and the node declares no ends, so its routes cannot be checked or drawn.`,
85
+ fix: "Give the conditional edge a mapping, or declare the node's `ends`.",
86
+ });
87
+ }
88
+ }
89
+ function staticCycles(description) {
90
+ const deciding = new Set(description.dynamic);
91
+ for (const node of description.nodes)
92
+ if (node.ends?.length)
93
+ deciding.add(node.id);
94
+ for (const edge of description.edges)
95
+ if (edge.conditional)
96
+ deciding.add(edge.from);
97
+ const next = new Map();
98
+ for (const edge of description.edges) {
99
+ if (edge.conditional || deciding.has(edge.from))
100
+ continue;
101
+ next.set(edge.from, [...(next.get(edge.from) ?? []), edge.to]);
102
+ }
103
+ const cycles = [];
104
+ const index = new Map();
105
+ const low = new Map();
106
+ const stack = [];
107
+ const onStack = new Set();
108
+ let counter = 0;
109
+ const visit = (node) => {
110
+ index.set(node, counter);
111
+ low.set(node, counter);
112
+ counter += 1;
113
+ stack.push(node);
114
+ onStack.add(node);
115
+ for (const to of next.get(node) ?? []) {
116
+ if (!index.has(to)) {
117
+ visit(to);
118
+ low.set(node, Math.min(low.get(node), low.get(to)));
119
+ }
120
+ else if (onStack.has(to)) {
121
+ low.set(node, Math.min(low.get(node), index.get(to)));
122
+ }
123
+ }
124
+ if (low.get(node) !== index.get(node))
125
+ return;
126
+ const component = [];
127
+ let member;
128
+ do {
129
+ member = stack.pop();
130
+ onStack.delete(member);
131
+ component.push(member);
132
+ } while (member !== node);
133
+ const selfLoop = component.length === 1 && (next.get(node) ?? []).includes(node);
134
+ if (component.length > 1 || selfLoop)
135
+ cycles.push(component.reverse());
136
+ };
137
+ for (const node of description.nodes)
138
+ if (!index.has(node.id))
139
+ visit(node.id);
140
+ return cycles;
141
+ }
@@ -0,0 +1,28 @@
1
+ import type { RunControlLike } from '../types/graph.js';
2
+ /**
3
+ * Stops graph runs cleanly: each finishes its current superstep, writes its checkpoint, and ends with
4
+ * `GraphDrainedError`, so another worker can `continue()` it.
5
+ *
6
+ * An abort signal stops a run in the middle of a superstep, cancelling the nodes in flight. A drain
7
+ * waits for them, which is what a rolling deploy, a spot eviction, or a scale-down wants: nothing
8
+ * half done, nothing lost. One control can drain any number of runs at once.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const control = new RunControl();
13
+ * process.on('SIGTERM', () => control.drain('sigterm'));
14
+ * await graph.invoke(input, { threadId, control });
15
+ * ```
16
+ */
17
+ export declare class RunControl implements RunControlLike {
18
+ private requested;
19
+ private readonly listeners;
20
+ /** True once `drain()` was called. */
21
+ get draining(): boolean;
22
+ /** Why the drain was requested. */
23
+ get reason(): string | undefined;
24
+ /** Asks every run using this control to stop after its current superstep. A second call is ignored. */
25
+ drain(reason?: string): void;
26
+ /** Calls `listener` when a drain is requested. Returns a function that removes it. */
27
+ onDrain(listener: (reason?: string) => void): () => void;
28
+ }
@@ -0,0 +1,26 @@
1
+ export class RunControl {
2
+ requested;
3
+ listeners = new Set();
4
+ get draining() {
5
+ return this.requested !== undefined;
6
+ }
7
+ get reason() {
8
+ return this.requested?.reason;
9
+ }
10
+ drain(reason) {
11
+ if (this.requested)
12
+ return;
13
+ this.requested = { reason };
14
+ for (const listener of this.listeners) {
15
+ try {
16
+ listener(reason);
17
+ }
18
+ catch {
19
+ }
20
+ }
21
+ }
22
+ onDrain(listener) {
23
+ this.listeners.add(listener);
24
+ return () => this.listeners.delete(listener);
25
+ }
26
+ }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { linkSignals } from '../utils/signals.js';
2
3
  import { ImageValidationError } from './errors.js';
3
4
  export class MemoryReviewQueue {
4
5
  items = [];
@@ -32,7 +33,8 @@ export class MediaEvalRunner {
32
33
  const runsOf = new Map(examples.map((example, index) => [example.id, runCounts[index]]));
33
34
  const outcomes = new Map();
34
35
  const stop = new AbortController();
35
- const signal = runOptions.signal ? AbortSignal.any([stop.signal, runOptions.signal]) : stop.signal;
36
+ const linked = linkSignals([stop.signal, runOptions.signal]);
37
+ const signal = linked.signal;
36
38
  const { evaluate } = await import('../evaluate/run.js');
37
39
  const experiment = await evaluate(async (evalCase, { example, run }) => {
38
40
  try {
@@ -57,7 +59,7 @@ export class MediaEvalRunner {
57
59
  signal,
58
60
  ...(runOptions.store ? { store: runOptions.store } : {}),
59
61
  ...(runOptions.metadata ? { metadata: runOptions.metadata } : {}),
60
- });
62
+ }).finally(linked.dispose);
61
63
  const caseReports = [];
62
64
  const allScores = {};
63
65
  const safety = { truePositives: 0, falsePositives: 0, trueNegatives: 0, falseNegatives: 0 };
@@ -3,7 +3,8 @@ import type { OperationDispatcher, OperationRecord, OperationStore, OperationSto
3
3
  * The Redis commands the operation store needs.
4
4
  *
5
5
  * Structural rather than tied to one client, so `ioredis`, `node-redis`, or a cluster proxy all
6
- * satisfy it without this package depending on any of them.
6
+ * satisfy it without this package depending on any of them. The sorted-set commands are needed only
7
+ * with `index: true`; `ioredis` has them all.
7
8
  */
8
9
  export interface RedisOperationLikeClient {
9
10
  /** Reads a hash field. */
@@ -16,6 +17,14 @@ export interface RedisOperationLikeClient {
16
17
  hvals(key: string): Promise<string[]> | string[];
17
18
  /** Optional CAS primitive. When absent the store falls back to a read-compare-write. */
18
19
  eval?(script: string, numKeys: number, ...args: string[]): Promise<unknown> | unknown;
20
+ /** Reads several hash fields in one round trip. Used by the index when present. */
21
+ hmget?(key: string, ...fields: string[]): Promise<Array<string | null>> | Array<string | null>;
22
+ /** Adds a sorted-set member. Needed with `index: true`. */
23
+ zadd?(key: string, score: number, member: string): Promise<unknown> | unknown;
24
+ /** Removes a sorted-set member. Needed with `index: true`. */
25
+ zrem?(key: string, member: string): Promise<unknown> | unknown;
26
+ /** Members by score, as `ZRANGEBYSCORE key min max LIMIT offset count`. Needed with `index: true`. */
27
+ zrangebyscore?(key: string, min: number | string, max: number | string, limit: 'LIMIT', offset: number, count: number): Promise<string[]> | string[];
19
28
  }
20
29
  /** Options for the Redis operation store. */
21
30
  export interface RedisOperationStoreOptions {
@@ -23,6 +32,19 @@ export interface RedisOperationStoreOptions {
23
32
  prefix?: string;
24
33
  /** Disables the Lua compare-and-set even when the client exposes `eval`. */
25
34
  useEval?: boolean;
35
+ /**
36
+ * Keeps queued work in a sorted set by age, and leased work in one by lease expiry, so
37
+ * `listQueued()` and `claimExpired()` read the oldest few records instead of every one: a claim
38
+ * costs the same with 100,000 operations queued as with 1,000. The records stay the source of
39
+ * truth; an index entry only says where to look, and one that no longer matches its record is
40
+ * skipped and removed.
41
+ *
42
+ * Off by default, because a 2.1 worker writes records without index entries. Turn it on once every
43
+ * worker sharing the prefix runs 2.2, then call `reindex()` once to index what is already there.
44
+ * Needs `zadd`, `zrem`, and `zrangebyscore`; with `eval`, a record and its index entries change in
45
+ * one step.
46
+ */
47
+ index?: boolean;
26
48
  }
27
49
  /**
28
50
  * Redis-backed operation storage, so a submitted operation survives a restart.
@@ -36,8 +58,13 @@ export declare class RedisOperationStore<TResult = unknown> implements Operation
36
58
  private readonly client;
37
59
  private readonly prefix;
38
60
  private readonly useEval;
61
+ private readonly indexed;
39
62
  constructor(client: RedisOperationLikeClient, options?: RedisOperationStoreOptions);
40
- /** Stores a new record and indexes its idempotency key. Refuses records carrying raw bytes. */
63
+ /**
64
+ * Stores a new record and indexes its idempotency key. Throws `OperationDuplicateError` when another
65
+ * record holds the key, checked in the same Lua call as the write when the client has `eval`.
66
+ * Refuses records carrying raw bytes.
67
+ */
41
68
  create(record: OperationRecord<TResult>): Promise<void>;
42
69
  /** Reads a record. */
43
70
  read(id: string): Promise<OperationRecord<TResult> | undefined>;
@@ -50,19 +77,40 @@ export declare class RedisOperationStore<TResult = unknown> implements Operation
50
77
  delete(id: string): Promise<boolean>;
51
78
  /**
52
79
  * Records whose lease has expired, or running records without one, up to `limit`, for another
53
- * worker to take over.
80
+ * worker to take over. With the index, reads only records whose lease has lapsed.
54
81
  */
55
82
  claimExpired(now: string, limit: number): Promise<Array<OperationRecord<TResult>>>;
56
83
  /** Finds the record that claimed an idempotency key. */
57
84
  findByIdempotencyKey(key: string): Promise<OperationRecord<TResult> | undefined>;
58
85
  /** Every record. */
59
86
  list(): Promise<Array<OperationRecord<TResult>>>;
60
- /** Queued records no worker holds, oldest first. Reads every record, as `list()` does. */
87
+ /**
88
+ * Queued records no worker holds, oldest first. With the index, reads them oldest first from the
89
+ * sorted set and stops at `limit`; without it, reads every record, as `list()` does.
90
+ */
61
91
  listQueued(limit: number, filter?: OperationStoreFilter): Promise<Array<OperationRecord<TResult>>>;
62
92
  /** Counts unfinished records by status. Reads every record, as `list()` does. */
63
93
  stats(now: string, filter?: OperationStoreFilter): Promise<OperationStoreStats>;
94
+ /**
95
+ * Indexes every record already stored, for turning `index` on over existing data. Safe to run
96
+ * while workers are busy, and to run again: it only adds entries, then removes those that no
97
+ * longer match their record, checking each record as it does. Resolves to how many records it read.
98
+ */
99
+ reindex(): Promise<number>;
100
+ /**
101
+ * Reads one index in score order up to `max`, a page at a time, keeping records that pass `keep`
102
+ * until `limit`. Entries whose record is gone or no longer `belongs` are removed afterwards.
103
+ */
104
+ private scanIndex;
105
+ private readMany;
106
+ /** Removes index entries that no longer describe their record. Only with `eval`, where the check is atomic. */
107
+ private prune;
108
+ /** Writes a record's index entries without `eval`, after the record itself. */
109
+ private place;
64
110
  private recordsKey;
65
111
  private idempotencyKey;
112
+ private queuedKey;
113
+ private leasesKey;
66
114
  }
67
115
  /** The part of a BullMQ `Queue` the dispatcher needs. */
68
116
  export interface BullMQLikeOperationQueue {
@@ -1,4 +1,5 @@
1
- import { isTerminalOperationStatus } from '../types/operations.js';
1
+ import { isTerminalOperationStatus, TERMINAL_OPERATION_STATUSES } from '../types/operations.js';
2
+ import { OperationDuplicateError } from './errors.js';
2
3
  import { assertSerializableRecord } from './serialization.js';
3
4
  import { countRecords, isUnheldQueued, matchesFilter } from './stats.js';
4
5
  const CAS_SCRIPT = `
@@ -9,21 +10,102 @@ if tonumber(decoded.sequence) ~= tonumber(ARGV[2]) then return 0 end
9
10
  redis.call('HSET', KEYS[1], ARGV[1], ARGV[3])
10
11
  return 1
11
12
  `;
13
+ const PLACE_LUA = `
14
+ local function place(key, score, id)
15
+ if score ~= '' then redis.call('ZADD', key, score, id) else redis.call('ZREM', key, id) end
16
+ end
17
+ `;
18
+ const UNIQUE_KEY_LUA = `
19
+ if ARGV[3] ~= '' then
20
+ local owner = redis.call('HGET', KEYS[2], ARGV[3])
21
+ if owner and owner ~= ARGV[1] and redis.call('HEXISTS', KEYS[1], owner) == 1 then return 0 end
22
+ end
23
+ `;
24
+ const CREATE_SCRIPT = `${UNIQUE_KEY_LUA}
25
+ redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
26
+ if ARGV[3] ~= '' then redis.call('HSET', KEYS[2], ARGV[3], ARGV[1]) end
27
+ return 1
28
+ `;
29
+ const INDEXED_CREATE_SCRIPT = `${PLACE_LUA}${UNIQUE_KEY_LUA}
30
+ redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
31
+ if ARGV[3] ~= '' then redis.call('HSET', KEYS[2], ARGV[3], ARGV[1]) end
32
+ place(KEYS[3], ARGV[4], ARGV[1])
33
+ place(KEYS[4], ARGV[5], ARGV[1])
34
+ return 1
35
+ `;
36
+ const INDEXED_CAS_SCRIPT = `${PLACE_LUA}
37
+ local current = redis.call('HGET', KEYS[1], ARGV[1])
38
+ if current == false then return 0 end
39
+ local decoded = cjson.decode(current)
40
+ if tonumber(decoded.sequence) ~= tonumber(ARGV[2]) then return 0 end
41
+ redis.call('HSET', KEYS[1], ARGV[1], ARGV[3])
42
+ place(KEYS[2], ARGV[4], ARGV[1])
43
+ place(KEYS[3], ARGV[5], ARGV[1])
44
+ return 1
45
+ `;
46
+ const PRUNE_SCRIPT = `
47
+ local terminal = { ${TERMINAL_OPERATION_STATUSES.map((status) => `${status} = true`).join(', ')} }
48
+ local removed = 0
49
+ for i = 2, #ARGV do
50
+ local raw = redis.call('HGET', KEYS[1], ARGV[i])
51
+ local keep = false
52
+ if raw then
53
+ local record = cjson.decode(raw)
54
+ if ARGV[1] == 'queued' then
55
+ keep = record.status == 'queued'
56
+ else
57
+ keep = (not terminal[record.status]) and (record.lease ~= nil or record.status == 'running')
58
+ end
59
+ end
60
+ if not keep then
61
+ redis.call('ZREM', KEYS[2], ARGV[i])
62
+ removed = removed + 1
63
+ end
64
+ end
65
+ return removed
66
+ `;
12
67
  export class RedisOperationStore {
13
68
  client;
14
69
  prefix;
15
70
  useEval;
71
+ indexed;
16
72
  constructor(client, options = {}) {
17
73
  this.client = client;
18
74
  this.prefix = options.prefix ?? 'nexus-ai-pro:operations:';
19
75
  this.useEval = options.useEval !== false && typeof client.eval === 'function';
76
+ this.indexed = options.index === true;
77
+ if (this.indexed && !(client.zadd && client.zrem && client.zrangebyscore)) {
78
+ throw new TypeError('A Redis operation store with index: true needs zadd, zrem, and zrangebyscore');
79
+ }
20
80
  }
21
81
  async create(record) {
22
82
  assertSerializableRecord(record);
23
- await this.client.hset(this.recordsKey(), record.id, JSON.stringify(record));
83
+ const encoded = JSON.stringify(record);
84
+ const duplicate = () => new OperationDuplicateError(record.idempotencyKey);
85
+ if (this.useEval && this.client.eval && !this.indexed) {
86
+ const created = await this.client.eval(CREATE_SCRIPT, 2, this.recordsKey(), this.idempotencyKey(), record.id, encoded, record.idempotencyKey ?? '');
87
+ if (Number(created) !== 1)
88
+ throw duplicate();
89
+ return;
90
+ }
91
+ if (this.indexed && this.useEval && this.client.eval) {
92
+ const { queued, lease } = indexScores(record);
93
+ const created = await this.client.eval(INDEXED_CREATE_SCRIPT, 4, this.recordsKey(), this.idempotencyKey(), this.queuedKey(), this.leasesKey(), record.id, encoded, record.idempotencyKey ?? '', queued, lease);
94
+ if (Number(created) !== 1)
95
+ throw duplicate();
96
+ return;
97
+ }
98
+ if (record.idempotencyKey) {
99
+ const owner = await this.client.hget(this.idempotencyKey(), record.idempotencyKey);
100
+ if (owner && owner !== record.id && (await this.client.hget(this.recordsKey(), owner)))
101
+ throw duplicate();
102
+ }
103
+ await this.client.hset(this.recordsKey(), record.id, encoded);
24
104
  if (record.idempotencyKey) {
25
105
  await this.client.hset(this.idempotencyKey(), record.idempotencyKey, record.id);
26
106
  }
107
+ if (this.indexed)
108
+ await this.place(record);
27
109
  }
28
110
  async read(id) {
29
111
  const raw = await this.client.hget(this.recordsKey(), id);
@@ -33,6 +115,11 @@ export class RedisOperationStore {
33
115
  assertSerializableRecord(record);
34
116
  const encoded = JSON.stringify(record);
35
117
  if (this.useEval && this.client.eval) {
118
+ if (this.indexed) {
119
+ const { queued, lease } = indexScores(record);
120
+ const result = await this.client.eval(INDEXED_CAS_SCRIPT, 3, this.recordsKey(), this.queuedKey(), this.leasesKey(), record.id, String(expectedSequence), encoded, queued, lease);
121
+ return Number(result) === 1;
122
+ }
36
123
  const result = await this.client.eval(CAS_SCRIPT, 1, this.recordsKey(), record.id, String(expectedSequence), encoded);
37
124
  return Number(result) === 1;
38
125
  }
@@ -40,6 +127,8 @@ export class RedisOperationStore {
40
127
  if (!current || current.sequence !== expectedSequence)
41
128
  return false;
42
129
  await this.client.hset(this.recordsKey(), record.id, encoded);
130
+ if (this.indexed)
131
+ await this.place(record);
43
132
  return true;
44
133
  }
45
134
  async delete(id) {
@@ -47,19 +136,22 @@ export class RedisOperationStore {
47
136
  if (record?.idempotencyKey)
48
137
  await this.client.hdel(this.idempotencyKey(), record.idempotencyKey);
49
138
  await this.client.hdel(this.recordsKey(), id);
139
+ if (this.indexed) {
140
+ await this.client.zrem?.(this.queuedKey(), id);
141
+ await this.client.zrem?.(this.leasesKey(), id);
142
+ }
50
143
  return record !== undefined;
51
144
  }
52
145
  async claimExpired(now, limit) {
146
+ if (this.indexed) {
147
+ return this.scanIndex('leases', Date.parse(now), limit, (record) => isExpired(record, now), isLeased);
148
+ }
53
149
  const records = await this.list();
54
150
  const expired = [];
55
151
  for (const record of records) {
56
152
  if (expired.length >= limit)
57
153
  break;
58
- if (isTerminalOperationStatus(record.status))
59
- continue;
60
- if (record.lease && record.lease.expiresAt > now)
61
- continue;
62
- if (record.status === 'running' || record.lease)
154
+ if (isExpired(record, now))
63
155
  expired.push(record);
64
156
  }
65
157
  return expired;
@@ -74,6 +166,9 @@ export class RedisOperationStore {
74
166
  }
75
167
  async listQueued(limit, filter) {
76
168
  const now = new Date().toISOString();
169
+ if (this.indexed) {
170
+ return this.scanIndex('queued', Number.POSITIVE_INFINITY, limit, (record) => isUnheldQueued(record, now) && matchesFilter(record, filter), (record) => record.status === 'queued');
171
+ }
77
172
  return (await this.list())
78
173
  .filter((record) => isUnheldQueued(record, now) && matchesFilter(record, filter))
79
174
  .sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0))
@@ -82,12 +177,104 @@ export class RedisOperationStore {
82
177
  async stats(now, filter) {
83
178
  return countRecords(await this.list(), now, filter);
84
179
  }
180
+ async reindex() {
181
+ if (!this.indexed)
182
+ throw new TypeError('reindex() needs a store created with index: true');
183
+ const records = await this.list();
184
+ for (const record of records) {
185
+ const { queued, lease } = indexScores(record);
186
+ if (queued !== '')
187
+ await this.client.zadd?.(this.queuedKey(), Number(queued), record.id);
188
+ if (lease !== '')
189
+ await this.client.zadd?.(this.leasesKey(), Number(lease), record.id);
190
+ }
191
+ await this.prune('queued', records.filter((record) => record.status !== 'queued').map((record) => record.id));
192
+ await this.prune('leases', records.filter((record) => !isLeased(record)).map((record) => record.id));
193
+ return records.length;
194
+ }
195
+ async scanIndex(index, max, limit, keep, belongs) {
196
+ const found = [];
197
+ if (limit <= 0)
198
+ return found;
199
+ const stale = [];
200
+ const key = index === 'queued' ? this.queuedKey() : this.leasesKey();
201
+ const upper = max === Number.POSITIVE_INFINITY ? '+inf' : max;
202
+ const page = Math.max(16, limit * 4);
203
+ for (let offset = 0; found.length < limit; offset += page) {
204
+ const ids = (await this.client.zrangebyscore?.(key, '-inf', upper, 'LIMIT', offset, page)) ?? [];
205
+ const records = await this.readMany(ids);
206
+ for (const [position, id] of ids.entries()) {
207
+ const record = records[position];
208
+ if (!record || !belongs(record)) {
209
+ stale.push(id);
210
+ continue;
211
+ }
212
+ if (keep(record))
213
+ found.push(record);
214
+ if (found.length >= limit)
215
+ break;
216
+ }
217
+ if (ids.length < page)
218
+ break;
219
+ }
220
+ await this.prune(index, stale);
221
+ return found;
222
+ }
223
+ async readMany(ids) {
224
+ if (ids.length === 0)
225
+ return [];
226
+ const raws = this.client.hmget
227
+ ? await this.client.hmget(this.recordsKey(), ...ids)
228
+ : await Promise.all(ids.map((id) => this.client.hget(this.recordsKey(), id)));
229
+ return raws.map((raw) => (raw ? JSON.parse(raw) : undefined));
230
+ }
231
+ async prune(index, ids) {
232
+ if (ids.length === 0 || !this.useEval || !this.client.eval)
233
+ return;
234
+ const key = index === 'queued' ? this.queuedKey() : this.leasesKey();
235
+ for (let start = 0; start < ids.length; start += 500) {
236
+ await this.client.eval(PRUNE_SCRIPT, 2, this.recordsKey(), key, index, ...ids.slice(start, start + 500));
237
+ }
238
+ }
239
+ async place(record) {
240
+ const { queued, lease } = indexScores(record);
241
+ if (queued !== '')
242
+ await this.client.zadd?.(this.queuedKey(), Number(queued), record.id);
243
+ else
244
+ await this.client.zrem?.(this.queuedKey(), record.id);
245
+ if (lease !== '')
246
+ await this.client.zadd?.(this.leasesKey(), Number(lease), record.id);
247
+ else
248
+ await this.client.zrem?.(this.leasesKey(), record.id);
249
+ }
85
250
  recordsKey() {
86
251
  return `${this.prefix}records`;
87
252
  }
88
253
  idempotencyKey() {
89
254
  return `${this.prefix}idempotency`;
90
255
  }
256
+ queuedKey() {
257
+ return `${this.prefix}queued`;
258
+ }
259
+ leasesKey() {
260
+ return `${this.prefix}leases`;
261
+ }
262
+ }
263
+ function isExpired(record, now) {
264
+ if (isTerminalOperationStatus(record.status))
265
+ return false;
266
+ if (record.lease && record.lease.expiresAt > now)
267
+ return false;
268
+ return record.status === 'running' || record.lease !== undefined;
269
+ }
270
+ function isLeased(record) {
271
+ return !isTerminalOperationStatus(record.status) && (record.lease !== undefined || record.status === 'running');
272
+ }
273
+ function indexScores(record) {
274
+ return {
275
+ queued: record.status === 'queued' ? String(Date.parse(record.createdAt)) : '',
276
+ lease: isLeased(record) ? String(record.lease ? Date.parse(record.lease.expiresAt) : 0) : '',
277
+ };
91
278
  }
92
279
  export class BullMQOperationDispatcher {
93
280
  queue;