nexus-ai-pro 2.1.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 (194) hide show
  1. package/API_STABILITY.md +38 -0
  2. package/CHANGELOG.md +145 -1
  3. package/README.md +13 -8
  4. package/dist/cache/memory-cache.d.ts +8 -0
  5. package/dist/cache/memory-cache.js +9 -0
  6. package/dist/cache/semantic-cache.d.ts +6 -3
  7. package/dist/cache/semantic-cache.js +5 -2
  8. package/dist/cli/args.js +3 -0
  9. package/dist/cli/db.d.ts +16 -4
  10. package/dist/cli/db.js +169 -22
  11. package/dist/cli/doctor.d.ts +17 -0
  12. package/dist/cli/doctor.js +53 -0
  13. package/dist/cli.js +12 -1
  14. package/dist/core/lifecycle.js +11 -6
  15. package/dist/core/nexus.d.ts +6 -0
  16. package/dist/core/nexus.js +103 -22
  17. package/dist/doctor/index.d.ts +90 -0
  18. package/dist/doctor/index.js +237 -0
  19. package/dist/graph/functional.d.ts +80 -7
  20. package/dist/graph/functional.js +138 -35
  21. package/dist/graph/graph.d.ts +15 -5
  22. package/dist/graph/graph.js +72 -20
  23. package/dist/images/evals.js +4 -2
  24. package/dist/operations/adapters.d.ts +52 -4
  25. package/dist/operations/adapters.js +194 -7
  26. package/dist/operations/runner.js +1 -0
  27. package/dist/operations/stats.js +8 -1
  28. package/dist/operations/store.d.ts +5 -1
  29. package/dist/operations/store.js +7 -0
  30. package/dist/ops/circuit-breaker.d.ts +52 -0
  31. package/dist/ops/circuit-breaker.js +4 -0
  32. package/dist/ops/circuit-store.d.ts +13 -2
  33. package/dist/ops/circuit-store.js +68 -0
  34. package/dist/ops/circuit-sync.d.ts +17 -0
  35. package/dist/ops/circuit-sync.js +86 -0
  36. package/dist/ops/rate-limiter.d.ts +2 -0
  37. package/dist/ops/rate-limiter.js +2 -0
  38. package/dist/postgres/circuits.d.ts +21 -4
  39. package/dist/postgres/circuits.js +65 -4
  40. package/dist/postgres/client.d.ts +0 -2
  41. package/dist/postgres/client.js +0 -4
  42. package/dist/postgres/evaluate.d.ts +16 -4
  43. package/dist/postgres/evaluate.js +17 -5
  44. package/dist/postgres/index.d.ts +32 -11
  45. package/dist/postgres/index.js +65 -31
  46. package/dist/postgres/migrations.d.ts +60 -0
  47. package/dist/postgres/migrations.js +121 -0
  48. package/dist/postgres/operations.d.ts +14 -3
  49. package/dist/postgres/operations.js +45 -15
  50. package/dist/postgres/prompts.d.ts +11 -2
  51. package/dist/postgres/prompts.js +14 -3
  52. package/dist/postgres/rollups.d.ts +35 -0
  53. package/dist/postgres/rollups.js +129 -0
  54. package/dist/postgres/store.d.ts +11 -2
  55. package/dist/postgres/store.js +14 -3
  56. package/dist/postgres/traces.d.ts +13 -3
  57. package/dist/postgres/traces.js +25 -3
  58. package/dist/postgres/vectors.d.ts +11 -2
  59. package/dist/postgres/vectors.js +14 -3
  60. package/dist/router/auto-router.d.ts +34 -7
  61. package/dist/router/auto-router.js +201 -68
  62. package/dist/server/assistant.d.ts +4 -0
  63. package/dist/server/assistant.js +8 -0
  64. package/dist/server/errors.d.ts +12 -0
  65. package/dist/server/errors.js +7 -0
  66. package/dist/server/index.d.ts +1 -1
  67. package/dist/server/index.js +1 -1
  68. package/dist/server/runs.d.ts +24 -1
  69. package/dist/server/runs.js +90 -14
  70. package/dist/sqlite/index.d.ts +16 -3
  71. package/dist/sqlite/index.js +16 -3
  72. package/dist/sqlite/migrations.d.ts +26 -0
  73. package/dist/sqlite/migrations.js +63 -0
  74. package/dist/sqlite/operations.d.ts +14 -3
  75. package/dist/sqlite/operations.js +42 -15
  76. package/dist/sqlite/store.d.ts +9 -2
  77. package/dist/sqlite/store.js +13 -3
  78. package/dist/sqlite/vectors.d.ts +11 -2
  79. package/dist/sqlite/vectors.js +13 -2
  80. package/dist/store/tenant.d.ts +12 -0
  81. package/dist/store/tenant.js +29 -0
  82. package/dist/tenancy/index.d.ts +113 -0
  83. package/dist/tenancy/index.js +233 -0
  84. package/dist/tracing/instrument.js +2 -0
  85. package/dist/types/config.d.ts +24 -2
  86. package/dist/types/graph.d.ts +11 -1
  87. package/dist/types/messages.d.ts +5 -0
  88. package/dist/types/operations.d.ts +6 -0
  89. package/dist/types/response.d.ts +20 -0
  90. package/dist/types/server.d.ts +10 -1
  91. package/dist/utils/schema-migrations.d.ts +97 -0
  92. package/dist/utils/schema-migrations.js +91 -0
  93. package/dist/utils/signals.d.ts +17 -0
  94. package/dist/utils/signals.js +26 -0
  95. package/dist/utils/tenant.d.ts +14 -0
  96. package/dist/utils/tenant.js +16 -0
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist-cjs/cache/memory-cache.d.ts +8 -0
  100. package/dist-cjs/cache/memory-cache.js +9 -0
  101. package/dist-cjs/cache/semantic-cache.d.ts +6 -3
  102. package/dist-cjs/cache/semantic-cache.js +5 -2
  103. package/dist-cjs/cli/args.js +3 -0
  104. package/dist-cjs/cli/db.d.ts +16 -4
  105. package/dist-cjs/cli/db.js +200 -20
  106. package/dist-cjs/cli/doctor.d.ts +17 -0
  107. package/dist-cjs/cli/doctor.js +56 -0
  108. package/dist-cjs/core/lifecycle.js +11 -6
  109. package/dist-cjs/core/nexus.d.ts +6 -0
  110. package/dist-cjs/core/nexus.js +103 -22
  111. package/dist-cjs/doctor/index.d.ts +90 -0
  112. package/dist-cjs/doctor/index.js +276 -0
  113. package/dist-cjs/graph/functional.d.ts +80 -7
  114. package/dist-cjs/graph/functional.js +137 -34
  115. package/dist-cjs/graph/graph.d.ts +15 -5
  116. package/dist-cjs/graph/graph.js +72 -20
  117. package/dist-cjs/images/evals.js +4 -2
  118. package/dist-cjs/operations/adapters.d.ts +52 -4
  119. package/dist-cjs/operations/adapters.js +193 -6
  120. package/dist-cjs/operations/runner.js +1 -0
  121. package/dist-cjs/operations/stats.js +8 -1
  122. package/dist-cjs/operations/store.d.ts +5 -1
  123. package/dist-cjs/operations/store.js +7 -0
  124. package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
  125. package/dist-cjs/ops/circuit-breaker.js +4 -0
  126. package/dist-cjs/ops/circuit-store.d.ts +13 -2
  127. package/dist-cjs/ops/circuit-store.js +68 -0
  128. package/dist-cjs/ops/circuit-sync.d.ts +17 -0
  129. package/dist-cjs/ops/circuit-sync.js +87 -0
  130. package/dist-cjs/ops/rate-limiter.d.ts +2 -0
  131. package/dist-cjs/ops/rate-limiter.js +2 -0
  132. package/dist-cjs/postgres/circuits.d.ts +21 -4
  133. package/dist-cjs/postgres/circuits.js +98 -3
  134. package/dist-cjs/postgres/client.d.ts +0 -2
  135. package/dist-cjs/postgres/client.js +0 -5
  136. package/dist-cjs/postgres/evaluate.d.ts +16 -4
  137. package/dist-cjs/postgres/evaluate.js +50 -4
  138. package/dist-cjs/postgres/index.d.ts +32 -11
  139. package/dist-cjs/postgres/index.js +69 -20
  140. package/dist-cjs/postgres/migrations.d.ts +60 -0
  141. package/dist-cjs/postgres/migrations.js +129 -0
  142. package/dist-cjs/postgres/operations.d.ts +14 -3
  143. package/dist-cjs/postgres/operations.js +78 -14
  144. package/dist-cjs/postgres/prompts.d.ts +11 -2
  145. package/dist-cjs/postgres/prompts.js +47 -2
  146. package/dist-cjs/postgres/rollups.d.ts +35 -0
  147. package/dist-cjs/postgres/rollups.js +168 -0
  148. package/dist-cjs/postgres/store.d.ts +11 -2
  149. package/dist-cjs/postgres/store.js +47 -2
  150. package/dist-cjs/postgres/traces.d.ts +13 -3
  151. package/dist-cjs/postgres/traces.js +58 -2
  152. package/dist-cjs/postgres/vectors.d.ts +11 -2
  153. package/dist-cjs/postgres/vectors.js +47 -2
  154. package/dist-cjs/router/auto-router.d.ts +34 -7
  155. package/dist-cjs/router/auto-router.js +201 -68
  156. package/dist-cjs/server/assistant.d.ts +4 -0
  157. package/dist-cjs/server/assistant.js +8 -0
  158. package/dist-cjs/server/errors.d.ts +12 -0
  159. package/dist-cjs/server/errors.js +9 -1
  160. package/dist-cjs/server/index.d.ts +1 -1
  161. package/dist-cjs/server/index.js +2 -1
  162. package/dist-cjs/server/runs.d.ts +24 -1
  163. package/dist-cjs/server/runs.js +89 -13
  164. package/dist-cjs/sqlite/index.d.ts +16 -3
  165. package/dist-cjs/sqlite/index.js +31 -10
  166. package/dist-cjs/sqlite/migrations.d.ts +26 -0
  167. package/dist-cjs/sqlite/migrations.js +71 -0
  168. package/dist-cjs/sqlite/operations.d.ts +14 -3
  169. package/dist-cjs/sqlite/operations.js +76 -15
  170. package/dist-cjs/sqlite/store.d.ts +9 -2
  171. package/dist-cjs/sqlite/store.js +47 -3
  172. package/dist-cjs/sqlite/vectors.d.ts +11 -2
  173. package/dist-cjs/sqlite/vectors.js +47 -2
  174. package/dist-cjs/store/tenant.d.ts +12 -0
  175. package/dist-cjs/store/tenant.js +33 -0
  176. package/dist-cjs/tenancy/index.d.ts +113 -0
  177. package/dist-cjs/tenancy/index.js +250 -0
  178. package/dist-cjs/tracing/instrument.js +2 -0
  179. package/dist-cjs/types/config.d.ts +24 -2
  180. package/dist-cjs/types/graph.d.ts +11 -1
  181. package/dist-cjs/types/messages.d.ts +5 -0
  182. package/dist-cjs/types/operations.d.ts +6 -0
  183. package/dist-cjs/types/response.d.ts +20 -0
  184. package/dist-cjs/types/server.d.ts +10 -1
  185. package/dist-cjs/utils/schema-migrations.d.ts +97 -0
  186. package/dist-cjs/utils/schema-migrations.js +102 -0
  187. package/dist-cjs/utils/signals.d.ts +17 -0
  188. package/dist-cjs/utils/signals.js +29 -0
  189. package/dist-cjs/utils/tenant.d.ts +14 -0
  190. package/dist-cjs/utils/tenant.js +22 -0
  191. package/dist-cjs/version.d.ts +1 -1
  192. package/dist-cjs/version.js +1 -1
  193. package/llms.txt +4 -3
  194. package/package.json +71 -2
@@ -12,12 +12,14 @@ export interface GraphLike {
12
12
  threadId?: string;
13
13
  signal?: AbortSignal;
14
14
  metadata?: Record<string, unknown>;
15
+ tenantId?: string;
15
16
  control?: DrainSwitch;
16
17
  }): AsyncIterable<unknown>;
17
18
  /** Answers an interrupt and continues the thread. */
18
19
  resume(threadId: string, value: unknown, options: {
19
20
  signal?: AbortSignal;
20
21
  metadata?: Record<string, unknown>;
22
+ tenantId?: string;
21
23
  control?: DrainSwitch;
22
24
  }): AsyncIterable<unknown>;
23
25
  /** The thread's checkpoint, at its latest step or at one given. */
@@ -31,6 +33,7 @@ export interface GraphLike {
31
33
  continue?(threadId: string, options: {
32
34
  signal?: AbortSignal;
33
35
  metadata?: Record<string, unknown>;
36
+ tenantId?: string;
34
37
  control?: DrainSwitch;
35
38
  }): AsyncIterable<unknown>;
36
39
  /** Writes values into a thread's state, which is how a rollback is applied. */
@@ -47,6 +50,7 @@ export interface GraphLike {
47
50
  /** The options of a graph's `events()` the adapter passes. */
48
51
  type GraphEventOptionsLike = {
49
52
  threadId?: string;
53
+ tenantId?: string;
50
54
  signal?: AbortSignal;
51
55
  metadata?: Record<string, unknown>;
52
56
  control?: DrainSwitch;
@@ -4,6 +4,7 @@ export function graphAssistant(graph, options = {}) {
4
4
  ...(threadId ? { threadId } : {}),
5
5
  signal: context.signal,
6
6
  metadata: { ...options.metadata, runId: context.runId },
7
+ ...tenantOf(context),
7
8
  ...(context.control ? { control: context.control } : {}),
8
9
  include: [...(projections ?? []), 'values'],
9
10
  });
@@ -17,6 +18,7 @@ export function graphAssistant(graph, options = {}) {
17
18
  threadId: context.threadId,
18
19
  signal: context.signal,
19
20
  metadata: { ...options.metadata, runId: context.runId },
21
+ ...tenantOf(context),
20
22
  ...(context.control ? { control: context.control } : {}),
21
23
  });
22
24
  },
@@ -27,6 +29,7 @@ export function graphAssistant(graph, options = {}) {
27
29
  return graph.resume(threadId, value, {
28
30
  signal: context.signal,
29
31
  metadata: { ...options.metadata, runId: context.runId },
32
+ ...tenantOf(context),
30
33
  ...(context.control ? { control: context.control } : {}),
31
34
  });
32
35
  },
@@ -50,6 +53,7 @@ export function graphAssistant(graph, options = {}) {
50
53
  return graph.continue(threadId, {
51
54
  signal: context.signal,
52
55
  metadata: { ...options.metadata, runId: context.runId },
56
+ ...tenantOf(context),
53
57
  ...(context.control ? { control: context.control } : {}),
54
58
  });
55
59
  },
@@ -76,6 +80,10 @@ export function functionAssistant(run, options = {}) {
76
80
  },
77
81
  };
78
82
  }
83
+ function tenantOf(context) {
84
+ const tenantId = context.principal?.tenantId;
85
+ return tenantId ? { tenantId } : {};
86
+ }
79
87
  async function* withResult(events) {
80
88
  for await (const event of events)
81
89
  yield event;
@@ -32,6 +32,18 @@ export declare class ForbiddenError extends ServerError {
32
32
  /** The scope the route required. */
33
33
  scope: string);
34
34
  }
35
+ /**
36
+ * Thrown by an assistant to hand its run to another worker while this replica drains: the server puts
37
+ * the run back on the queue as its next attempt instead of failing it. A function assistant checks
38
+ * `context.control?.draining` between its own steps, saves where it got with `context.saveProgress()`,
39
+ * and throws this; the next worker reads `context.progress`. Thrown when the replica is not draining,
40
+ * it fails the run like any error.
41
+ */
42
+ export declare class RunHandOffError extends Error {
43
+ /** Always `RUN_HANDED_OFF`. */
44
+ readonly code = "RUN_HANDED_OFF";
45
+ constructor(message?: string);
46
+ }
35
47
  /** Raised when a thread is already running something and the busy policy is `reject`. */
36
48
  export declare class ThreadBusyError extends ServerError {
37
49
  /** The thread. */
@@ -35,6 +35,13 @@ export class ForbiddenError extends ServerError {
35
35
  this.name = 'ForbiddenError';
36
36
  }
37
37
  }
38
+ export class RunHandOffError extends Error {
39
+ code = 'RUN_HANDED_OFF';
40
+ constructor(message = 'Handed to another worker while this one drains') {
41
+ super(message);
42
+ this.name = 'RunHandOffError';
43
+ }
44
+ }
38
45
  export class ThreadBusyError extends ServerError {
39
46
  threadId;
40
47
  runId;
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export { functionAssistant, type GraphAssistantOptions, graphAssistant, type GraphLike, } from './assistant.js';
11
11
  export { CronScheduler, type CronSchedulerOptions, type DueCronJob, parseCron } from './cron.js';
12
- export { AssistantCapabilityError, BadRequestError, ForbiddenError, NotFoundError, ServerError, type TenantLimit, TenantLimitError, ThreadBusyError, UnauthorizedError, } from './errors.js';
12
+ export { AssistantCapabilityError, BadRequestError, ForbiddenError, NotFoundError, RunHandOffError, ServerError, type TenantLimit, TenantLimitError, ThreadBusyError, UnauthorizedError, } from './errors.js';
13
13
  export { MemoryRunEventLog, type MemoryRunEventLogOptions, type RedisEventLogLikeClient, RedisRunEventLog, type RedisRunEventLogOptions, } from './events.js';
14
14
  export { type NodeListenerOptions, type NodeRequestLike, type NodeResponseLike, toNodeListener, } from './node.js';
15
15
  export { type DrainResult, RunManager, type RunManagerOptions, type RunQueueOptions, type StartRunOptions, } from './runs.js';
@@ -1,6 +1,6 @@
1
1
  export { functionAssistant, graphAssistant, } from './assistant.js';
2
2
  export { CronScheduler, parseCron } from './cron.js';
3
- export { AssistantCapabilityError, BadRequestError, ForbiddenError, NotFoundError, ServerError, TenantLimitError, ThreadBusyError, UnauthorizedError, } from './errors.js';
3
+ export { AssistantCapabilityError, BadRequestError, ForbiddenError, NotFoundError, RunHandOffError, ServerError, TenantLimitError, ThreadBusyError, UnauthorizedError, } from './errors.js';
4
4
  export { MemoryRunEventLog, RedisRunEventLog, } from './events.js';
5
5
  export { toNodeListener, } from './node.js';
6
6
  export { RunManager, } from './runs.js';
@@ -159,6 +159,8 @@ export declare class RunManager {
159
159
  * it with a `TenantLimitError`.
160
160
  */
161
161
  start(options: StartRunOptions): Promise<RunRecord>;
162
+ /** The rest of `start()`, once the thread, if any, is this run's. */
163
+ private startClaimed;
162
164
  /** Cancels a run, including one another replica is executing. */
163
165
  cancel(runId: string, principal?: Principal, reason?: string): Promise<RunRecord>;
164
166
  /**
@@ -210,8 +212,29 @@ export declare class RunManager {
210
212
  private freeSlot;
211
213
  private schedule;
212
214
  private tick;
213
- /** Applies the busy policy, returning the thread once it is free to run something new. */
215
+ /**
216
+ * Applies the busy policy, returning the thread once this run holds it. The thread is claimed
217
+ * atomically, so of several replicas starting runs on one thread at once, exactly one proceeds and
218
+ * the rest meet the policy; a thread still busy after the policy has run its course is refused.
219
+ */
214
220
  private settleBusyThread;
221
+ /**
222
+ * Claims a thread for a run. The claim is a record in the operation store under a unique
223
+ * idempotency key, moved from run to run by compare-and-set, so two replicas cannot both take a free
224
+ * thread: creating it races on the key, and taking it over races on the sequence. Resolves
225
+ * undefined once this run holds the thread, or the id of the unfinished run that does.
226
+ *
227
+ * A store without `findByIdempotencyKey` falls back to the thread record's `activeRunId`, which is
228
+ * what every release before 2.2 used, and which two replicas racing can both see empty.
229
+ */
230
+ private claimThread;
231
+ /**
232
+ * Whether a thread's holder no longer needs it: its run finished, or it was never recorded and its
233
+ * claim is old enough that the replica starting it must have died.
234
+ */
235
+ private runIsFree;
236
+ /** Frees a thread this run holds. Losing the compare-and-set means another run took it over, which is fine. */
237
+ private releaseClaim;
215
238
  /** Waits for a run to finish, for the `enqueue` policy. */
216
239
  private waitForRun;
217
240
  private releaseThread;
@@ -1,4 +1,4 @@
1
- import { OperationReleasedError } from '../operations/errors.js';
1
+ import { OperationDuplicateError, OperationReleasedError } from '../operations/errors.js';
2
2
  import { OperationRunner } from '../operations/runner.js';
3
3
  import { operationStats } from '../operations/stats.js';
4
4
  import { MemoryOperationStore } from '../operations/store.js';
@@ -9,6 +9,8 @@ import { MemoryServerStore, RUNS_NAMESPACE } from './state.js';
9
9
  const THREADS = ['nexus', 'server', 'threads'];
10
10
  const RUNS = RUNS_NAMESPACE;
11
11
  const KIND_PREFIX = 'assistant:';
12
+ const THREAD_CLAIM = 'nexus-thread:';
13
+ const UNRECORDED_CLAIM_MS = 60_000;
12
14
  const DEFAULT_CONCURRENCY = 10;
13
15
  const DEFAULT_POLL_MS = 1_000;
14
16
  const DEFAULT_RECOVER_MS = 30_000;
@@ -120,12 +122,22 @@ export class RunManager {
120
122
  }
121
123
  async start(options) {
122
124
  const base = this.assistant(options.assistant);
125
+ const runId = `run-${randomId()}`;
123
126
  let thread;
124
127
  if (options.threadId) {
125
128
  thread = await this.thread(options.threadId, options.principal);
126
- thread = await this.settleBusyThread(thread, options);
129
+ thread = await this.settleBusyThread(thread, options, runId);
127
130
  }
128
- const runId = `run-${randomId()}`;
131
+ try {
132
+ return await this.startClaimed(runId, base, thread, options);
133
+ }
134
+ catch (error) {
135
+ if (thread)
136
+ await this.releaseClaim(thread.id, runId).catch((cause) => this.report(cause, { runId }));
137
+ throw error;
138
+ }
139
+ }
140
+ async startClaimed(runId, base, thread, options) {
129
141
  let assistant = base;
130
142
  let revision;
131
143
  if (base.route) {
@@ -350,6 +362,8 @@ export class RunManager {
350
362
  acceptedId = (await this.runner.enqueue(submit)).id;
351
363
  }
352
364
  if (acceptedId !== runId) {
365
+ if (thread)
366
+ await this.releaseClaim(thread.id, runId);
353
367
  await this.state.delete(RUNS, runId);
354
368
  await this.releaseTenant(runId, options.principal?.tenantId);
355
369
  return (await this.state.get(RUNS, acceptedId)) ?? run;
@@ -393,6 +407,8 @@ export class RunManager {
393
407
  metadata: plan.metadata,
394
408
  attempt: context.attempt,
395
409
  ...(plan.revision ? { revision: plan.revision.id } : {}),
410
+ saveProgress: (details) => context.heartbeat(details),
411
+ ...(context.previousHeartbeat === undefined ? {} : { progress: context.previousHeartbeat }),
396
412
  recordCost: async (usd) => {
397
413
  if (!Number.isFinite(usd) || usd <= 0)
398
414
  return;
@@ -534,21 +550,21 @@ export class RunManager {
534
550
  const pollMs = this.options.queue?.pollMs ?? DEFAULT_POLL_MS;
535
551
  this.schedule(claimed > 0 && this.freeSlots() > 0 ? 0 : Math.min(pollMs, this.options.recoverEveryMs ?? pollMs));
536
552
  }
537
- async settleBusyThread(thread, options) {
553
+ async settleBusyThread(thread, options, runId) {
538
554
  const policy = options.onBusy ?? this.options.onBusy ?? 'reject';
539
555
  let current = thread;
540
- for (let attempt = 0; attempt < 2; attempt += 1) {
541
- const activeId = current.activeRunId;
556
+ const deadline = this.now().getTime() + (this.options.queueTimeoutMs ?? 30_000);
557
+ for (let attempt = 0; attempt < 4 || (policy === 'enqueue' && this.now().getTime() < deadline); attempt += 1) {
558
+ const activeId = await this.claimThread(current, runId);
542
559
  if (!activeId)
543
560
  return current;
544
561
  const active = await this.state.get(RUNS, activeId);
545
- if (!active || isFinished(active.status)) {
546
- current = { ...current, activeRunId: undefined };
547
- await this.state.put(THREADS, current.id, current);
548
- return current;
549
- }
550
562
  if (policy === 'reject')
551
563
  throw new ThreadBusyError(current.id, activeId);
564
+ if (!active) {
565
+ await delay(50);
566
+ continue;
567
+ }
552
568
  if (policy === 'enqueue') {
553
569
  await this.waitForRun(activeId);
554
570
  current = await this.thread(current.id, options.principal);
@@ -565,9 +581,67 @@ export class RunManager {
565
581
  }
566
582
  current = { ...current, activeRunId: undefined };
567
583
  await this.state.put(THREADS, current.id, current);
568
- return current;
569
584
  }
570
- return current;
585
+ throw new ThreadBusyError(current.id, current.activeRunId ?? 'another run');
586
+ }
587
+ async claimThread(thread, runId) {
588
+ if (!this.store.findByIdempotencyKey) {
589
+ const activeId = thread.activeRunId;
590
+ return activeId && !(await this.runIsFree(activeId, undefined)) ? activeId : undefined;
591
+ }
592
+ const key = `${THREAD_CLAIM}${thread.id}`;
593
+ for (let attempt = 0; attempt < 8; attempt += 1) {
594
+ const claim = await this.store.findByIdempotencyKey(key);
595
+ const at = this.now().toISOString();
596
+ if (!claim) {
597
+ try {
598
+ await this.store.create({
599
+ id: `${key}:${runId}`,
600
+ status: 'succeeded',
601
+ attempt: 1,
602
+ maxAttempts: 1,
603
+ sequence: 0,
604
+ createdAt: at,
605
+ updatedAt: at,
606
+ kind: 'server.thread',
607
+ idempotencyKey: key,
608
+ result: { runId },
609
+ });
610
+ return undefined;
611
+ }
612
+ catch (error) {
613
+ if (error instanceof OperationDuplicateError)
614
+ continue;
615
+ throw error;
616
+ }
617
+ }
618
+ const holder = claim.result?.runId;
619
+ if (holder === runId)
620
+ return undefined;
621
+ if (holder && !(await this.runIsFree(holder, claim.updatedAt)))
622
+ return holder;
623
+ const taken = await this.store.update({ ...claim, result: { runId }, sequence: claim.sequence + 1, updatedAt: at }, claim.sequence);
624
+ if (taken)
625
+ return undefined;
626
+ }
627
+ const holder = (await this.store.findByIdempotencyKey(key))?.result?.runId;
628
+ return holder ?? 'another run';
629
+ }
630
+ async runIsFree(runId, claimedAt) {
631
+ const run = await this.state.get(RUNS, runId);
632
+ if (run)
633
+ return isFinished(run.status);
634
+ if (claimedAt === undefined)
635
+ return true;
636
+ return this.now().getTime() - Date.parse(claimedAt) > UNRECORDED_CLAIM_MS;
637
+ }
638
+ async releaseClaim(threadId, runId) {
639
+ if (!this.store.findByIdempotencyKey)
640
+ return;
641
+ const claim = await this.store.findByIdempotencyKey(`${THREAD_CLAIM}${threadId}`);
642
+ if (!claim || claim.result?.runId !== runId)
643
+ return;
644
+ await this.store.update({ ...claim, result: {}, sequence: claim.sequence + 1, updatedAt: this.now().toISOString() }, claim.sequence);
571
645
  }
572
646
  async waitForRun(runId) {
573
647
  const deadline = this.now().getTime() + (this.options.queueTimeoutMs ?? 30_000);
@@ -584,6 +658,7 @@ export class RunManager {
584
658
  async releaseThread(threadId, runId) {
585
659
  if (!threadId)
586
660
  return;
661
+ await this.releaseClaim(threadId, runId).catch((error) => this.report(error, { runId }));
587
662
  const thread = await this.state.get(THREADS, threadId);
588
663
  if (!thread || thread.activeRunId !== runId)
589
664
  return;
@@ -678,5 +753,6 @@ function delay(ms) {
678
753
  });
679
754
  }
680
755
  function isDrained(error) {
681
- return typeof error === 'object' && error !== null && error.code === 'GRAPH_DRAINED';
756
+ const code = typeof error === 'object' && error !== null ? error.code : undefined;
757
+ return code === 'GRAPH_DRAINED' || code === 'RUN_HANDED_OFF';
682
758
  }
@@ -1,4 +1,17 @@
1
+ import type { SchemaMigration } from '../utils/schema-migrations.js';
2
+ export { type AppliedMigration, type MigrationResult, type MigrationStatus, migrationChecksum, type SchemaMigration, SchemaMigrationError, } from '../utils/schema-migrations.js';
1
3
  export { fromLibsql, fromSqliteDatabase, type LibsqlLikeClient, type SqliteDatabaseLike, type SqliteLikeClient, type SqliteValue, } from './client.js';
2
- export { SqliteOperationStore, type SqliteOperationStoreOptions, sqliteOperationStoreMigration } from './operations.js';
3
- export { SqliteStore, type SqliteStoreOptions, sqliteStoreMigration } from './store.js';
4
- export { SqliteVectorStore, type SqliteVectorStoreOptions, sqliteVectorStoreMigration } from './vectors.js';
4
+ export { applySqliteMigrations, type SqliteMigrateOptions, sqliteMigrationStatus } from './migrations.js';
5
+ export { SqliteOperationStore, type SqliteOperationStoreOptions, sqliteOperationStoreMigration, sqliteOperationStoreMigrations, } from './operations.js';
6
+ export { SqliteStore, type SqliteStoreOptions, sqliteStoreMigration, sqliteStoreMigrations } from './store.js';
7
+ export { SqliteVectorStore, type SqliteVectorStoreOptions, sqliteVectorStoreMigration, sqliteVectorStoreMigrations, } from './vectors.js';
8
+ /** The SQLite adapters a migration can include. */
9
+ export type SqliteAdapter = 'operations' | 'store' | 'vectors';
10
+ /**
11
+ * The versioned migrations of the chosen adapters on their default tables, for
12
+ * `applySqliteMigrations()`, `sqliteMigrationStatus()`, and `nexus db migrate`. Defaults to every
13
+ * adapter. A store on a custom table migrates through its own `migrate()`.
14
+ */
15
+ export declare function sqliteMigrations(options?: {
16
+ adapters?: readonly SqliteAdapter[];
17
+ }): SchemaMigration[];
@@ -1,4 +1,17 @@
1
+ import { sqliteOperationStoreMigrations } from './operations.js';
2
+ import { sqliteStoreMigrations } from './store.js';
3
+ import { sqliteVectorStoreMigrations } from './vectors.js';
4
+ export { migrationChecksum, SchemaMigrationError, } from '../utils/schema-migrations.js';
1
5
  export { fromLibsql, fromSqliteDatabase, } from './client.js';
2
- export { SqliteOperationStore, sqliteOperationStoreMigration } from './operations.js';
3
- export { SqliteStore, sqliteStoreMigration } from './store.js';
4
- export { SqliteVectorStore, sqliteVectorStoreMigration } from './vectors.js';
6
+ export { applySqliteMigrations, sqliteMigrationStatus } from './migrations.js';
7
+ export { SqliteOperationStore, sqliteOperationStoreMigration, sqliteOperationStoreMigrations, } from './operations.js';
8
+ export { SqliteStore, sqliteStoreMigration, sqliteStoreMigrations } from './store.js';
9
+ export { SqliteVectorStore, sqliteVectorStoreMigration, sqliteVectorStoreMigrations, } from './vectors.js';
10
+ export function sqliteMigrations(options = {}) {
11
+ const adapters = new Set(options.adapters ?? ['operations', 'store', 'vectors']);
12
+ return [
13
+ ...(adapters.has('operations') ? sqliteOperationStoreMigrations() : []),
14
+ ...(adapters.has('store') ? sqliteStoreMigrations() : []),
15
+ ...(adapters.has('vectors') ? sqliteVectorStoreMigrations() : []),
16
+ ];
17
+ }
@@ -0,0 +1,26 @@
1
+ import { type MigrationResult, type MigrationStatus, type SchemaMigration } from '../utils/schema-migrations.js';
2
+ import { type SqliteDatabaseLike, type SqliteLikeClient } from './client.js';
3
+ export { type AppliedMigration, type MigrationResult, type MigrationStatus, migrationChecksum, type SchemaMigration, SchemaMigrationError, } from '../utils/schema-migrations.js';
4
+ /** Options for applying or inspecting SQLite migrations. */
5
+ export interface SqliteMigrateOptions {
6
+ /** Plans without writing anything, not even the table that records migrations. */
7
+ dryRun?: boolean;
8
+ /** The table that records applied migrations. Defaults to `nexus_schema_migrations`. */
9
+ table?: string;
10
+ }
11
+ /**
12
+ * Where a SQLite database stands against a list of migrations. Reads only: on a database that has
13
+ * never been migrated, everything is pending.
14
+ */
15
+ export declare function sqliteMigrationStatus(database: SqliteLikeClient | SqliteDatabaseLike, migrations: readonly SchemaMigration[], options?: Pick<SqliteMigrateOptions, 'table'>): Promise<MigrationStatus>;
16
+ /**
17
+ * Applies the pending migrations in order, recording each with a checksum.
18
+ *
19
+ * Each migration runs inside `BEGIN IMMEDIATE`, which takes SQLite's write lock: its statements and
20
+ * the row recording it land together or not at all, and a second process migrating the same file
21
+ * waits for the first instead of interleaving with it. A migration whose statements changed after it
22
+ * ran is refused before anything is written.
23
+ */
24
+ export declare function applySqliteMigrations(database: SqliteLikeClient | SqliteDatabaseLike, migrations: readonly SchemaMigration[], options?: SqliteMigrateOptions): Promise<MigrationResult>;
25
+ /** The statement that creates the migrations table, which `migrate()` runs first. */
26
+ export declare function sqliteMigrationsTableStatement(table?: string): string;
@@ -0,0 +1,63 @@
1
+ import { assertUnchanged, MIGRATIONS_TABLE, migrationChecksum, planMigrations, sqlLiteral, } from '../utils/schema-migrations.js';
2
+ import { quoteSqliteTable, toSqliteClient } from './client.js';
3
+ export { migrationChecksum, SchemaMigrationError, } from '../utils/schema-migrations.js';
4
+ export async function sqliteMigrationStatus(database, migrations, options = {}) {
5
+ return planMigrations(migrations, await readApplied(toSqliteClient(database), options.table ?? MIGRATIONS_TABLE));
6
+ }
7
+ export async function applySqliteMigrations(database, migrations, options = {}) {
8
+ const client = toSqliteClient(database);
9
+ const tableName = options.table ?? MIGRATIONS_TABLE;
10
+ if (options.dryRun) {
11
+ const status = await sqliteMigrationStatus(client, migrations, options);
12
+ assertUnchanged(status);
13
+ return { applied: status.pending, statements: status.pending.flatMap((m) => [...m.statements]), dryRun: true };
14
+ }
15
+ planMigrations(migrations, []);
16
+ await client.exec(sqliteMigrationsTableStatement(tableName));
17
+ const status = planMigrations(migrations, await readApplied(client, tableName));
18
+ assertUnchanged(status);
19
+ const table = quoteSqliteTable(tableName);
20
+ const statements = [];
21
+ for (const migration of status.pending) {
22
+ const record = `INSERT OR IGNORE INTO ${table} (component, version, name, checksum, applied_at) VALUES (${sqlLiteral(migration.component)}, ${migration.version}, ${sqlLiteral(migration.name)}, ${sqlLiteral(migrationChecksum(migration))}, ${sqlLiteral(new Date().toISOString())})`;
23
+ const script = ['BEGIN IMMEDIATE', ...migration.statements, record, 'COMMIT'].join(';\n');
24
+ try {
25
+ await client.exec(`${script};`);
26
+ }
27
+ catch (error) {
28
+ try {
29
+ await client.exec('ROLLBACK');
30
+ }
31
+ catch {
32
+ }
33
+ throw error;
34
+ }
35
+ statements.push(...migration.statements);
36
+ }
37
+ return { applied: status.pending, statements, dryRun: false };
38
+ }
39
+ export function sqliteMigrationsTableStatement(table = MIGRATIONS_TABLE) {
40
+ return `CREATE TABLE IF NOT EXISTS ${quoteSqliteTable(table)} (
41
+ component TEXT NOT NULL,
42
+ version INTEGER NOT NULL,
43
+ name TEXT NOT NULL,
44
+ checksum TEXT NOT NULL,
45
+ applied_at TEXT NOT NULL,
46
+ PRIMARY KEY (component, version)
47
+ )`;
48
+ }
49
+ async function readApplied(client, table) {
50
+ const exists = (await client.all(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`, [
51
+ table,
52
+ ]));
53
+ if (exists.length === 0)
54
+ return [];
55
+ const rows = (await client.all(`SELECT component, version, name, checksum, applied_at FROM ${quoteSqliteTable(table)} ORDER BY applied_at, component, version`, []));
56
+ return rows.map((row) => ({
57
+ component: String(row.component),
58
+ version: Number(row.version),
59
+ name: String(row.name),
60
+ checksum: String(row.checksum),
61
+ appliedAt: String(row.applied_at),
62
+ }));
63
+ }
@@ -1,12 +1,20 @@
1
1
  import { type OperationRecord, type OperationStore, type OperationStoreFilter, type OperationStoreStats } from '../types/operations.js';
2
+ import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
2
3
  import { type SqliteDatabaseLike, type SqliteLikeClient } from './client.js';
4
+ import type { SqliteMigrateOptions } from './migrations.js';
3
5
  /** Options for the SQLite operation store. */
4
6
  export interface SqliteOperationStoreOptions {
5
7
  /** Table name. Defaults to `nexus_operations`. */
6
8
  table?: string;
7
9
  }
8
- /** The schema, as statements. Applied by `migrate()`, or by the application's own tooling. */
10
+ /** The schema, as statements: every migration's, in order. For the application's own tooling. */
9
11
  export declare function sqliteOperationStoreMigration(options?: SqliteOperationStoreOptions): string[];
12
+ /**
13
+ * The versioned schema, which `migrate()` applies. Version 1 is the 2.0 table; version 2 indexes
14
+ * queued work by age, so a claim reads the oldest few rows instead of sorting the queue; version 3
15
+ * indexes queued work by tenant.
16
+ */
17
+ export declare function sqliteOperationStoreMigrations(options?: SqliteOperationStoreOptions): SchemaMigration[];
10
18
  /**
11
19
  * Operation records in SQLite: durable operations, and — through `OperationStoreCheckpointer` — graph
12
20
  * and workflow checkpoints, on one machine with no server to run.
@@ -20,8 +28,11 @@ export declare class SqliteOperationStore<TResult = unknown> implements Operatio
20
28
  private readonly client;
21
29
  private readonly table;
22
30
  constructor(database: SqliteLikeClient | SqliteDatabaseLike, options?: SqliteOperationStoreOptions);
23
- /** Creates the table and indexes if they do not exist. Never runs implicitly. */
24
- migrate(): Promise<void>;
31
+ /**
32
+ * Applies this store's pending migrations, recorded in `nexus_schema_migrations`, each inside
33
+ * `BEGIN IMMEDIATE`. Never runs implicitly. Safe on a file an older release created.
34
+ */
35
+ migrate(options?: SqliteMigrateOptions): Promise<MigrationResult>;
25
36
  /**
26
37
  * Inserts a new record. Throws `OperationDuplicateError` when its idempotency key is already
27
38
  * claimed. Refuses records carrying raw bytes.
@@ -5,8 +5,31 @@ import { isSqliteUniqueViolation, quoteSqliteTable, toSqliteClient, } from './cl
5
5
  const DEFAULT_TABLE = 'nexus_operations';
6
6
  const TERMINAL = TERMINAL_OPERATION_STATUSES.map((status) => `'${status}'`).join(', ');
7
7
  export function sqliteOperationStoreMigration(options = {}) {
8
+ return sqliteOperationStoreMigrations(options).flatMap((migration) => [...migration.statements]);
9
+ }
10
+ export function sqliteOperationStoreMigrations(options = {}) {
8
11
  const name = options.table ?? DEFAULT_TABLE;
9
12
  const table = quoteSqliteTable(name);
13
+ const component = `operations:${name}`;
14
+ return [
15
+ { component, version: 1, name: 'create the operations table', statements: createStatements(name, table) },
16
+ {
17
+ component,
18
+ version: 2,
19
+ name: 'index queued work by age',
20
+ statements: [`CREATE INDEX IF NOT EXISTS "${name}_queued" ON ${table} (status, created_at, id)`],
21
+ },
22
+ {
23
+ component,
24
+ version: 3,
25
+ name: 'index queued work by tenant',
26
+ statements: [
27
+ `CREATE INDEX IF NOT EXISTS "${name}_tenant_queued" ON ${table} (status, json_extract(doc, '$.tenantId'), created_at, id)`,
28
+ ],
29
+ },
30
+ ];
31
+ }
32
+ function createStatements(name, table) {
10
33
  return [
11
34
  `CREATE TABLE IF NOT EXISTS ${table} (
12
35
  id TEXT PRIMARY KEY,
@@ -32,9 +55,9 @@ export class SqliteOperationStore {
32
55
  this.client = toSqliteClient(database);
33
56
  this.table = quoteSqliteTable(options.table ?? DEFAULT_TABLE);
34
57
  }
35
- async migrate() {
36
- for (const statement of sqliteOperationStoreMigration(this.options))
37
- await this.client.exec(statement);
58
+ async migrate(options = {}) {
59
+ const { applySqliteMigrations } = await import('./migrations.js');
60
+ return applySqliteMigrations(this.client, sqliteOperationStoreMigrations(this.options), options);
38
61
  }
39
62
  async create(record) {
40
63
  assertSerializableRecord(record);
@@ -99,29 +122,21 @@ export class SqliteOperationStore {
99
122
  }
100
123
  async listQueued(limit, filter) {
101
124
  const params = [new Date().toISOString()];
102
- let kind = '';
103
- if (filter?.kindPrefix) {
104
- params.push(filter.kindPrefix, filter.kindPrefix);
105
- kind = ' AND substr(kind, 1, length(?)) = ?';
106
- }
125
+ const narrowed = filterSql(filter, params);
107
126
  params.push(limit);
108
127
  return this.many(`SELECT doc FROM ${this.table}
109
- WHERE status = 'queued' AND (lease_expires_at IS NULL OR lease_expires_at <= ?)${kind}
128
+ WHERE status = 'queued' AND (lease_expires_at IS NULL OR lease_expires_at <= ?)${narrowed}
110
129
  ORDER BY created_at, id
111
130
  LIMIT ?`, params);
112
131
  }
113
132
  async stats(now, filter) {
114
133
  const params = [now];
115
- let kind = '';
116
- if (filter?.kindPrefix) {
117
- params.push(filter.kindPrefix, filter.kindPrefix);
118
- kind = ' AND substr(kind, 1, length(?)) = ?';
119
- }
134
+ const narrowed = filterSql(filter, params);
120
135
  const rows = (await this.client.all(`SELECT status, count(*) AS count,
121
136
  min(CASE WHEN status = 'queued' THEN created_at END) AS oldest,
122
137
  sum(CASE WHEN status = 'running' AND (lease_expires_at IS NULL OR lease_expires_at <= ?) THEN 1 ELSE 0 END) AS lapsed
123
138
  FROM ${this.table}
124
- WHERE status NOT IN (${TERMINAL})${kind}
139
+ WHERE status NOT IN (${TERMINAL})${narrowed}
125
140
  GROUP BY status`, params));
126
141
  const byStatus = {};
127
142
  let oldestQueuedAt;
@@ -146,3 +161,15 @@ export class SqliteOperationStore {
146
161
  return rows.map((row) => JSON.parse(row.doc));
147
162
  }
148
163
  }
164
+ function filterSql(filter, params) {
165
+ let sql = '';
166
+ if (filter?.kindPrefix) {
167
+ params.push(filter.kindPrefix, filter.kindPrefix);
168
+ sql += ' AND substr(kind, 1, length(?)) = ?';
169
+ }
170
+ if (filter?.tenantId !== undefined) {
171
+ params.push(filter.tenantId);
172
+ sql += " AND json_extract(doc, '$.tenantId') = ?";
173
+ }
174
+ return sql;
175
+ }
@@ -1,5 +1,7 @@
1
1
  import type { Store, StoreIndexOptions, StoreItem, StoreNamespace, StorePutOptions, StoreSearchOptions } from '../types/store.js';
2
+ import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
2
3
  import { type SqliteDatabaseLike, type SqliteLikeClient } from './client.js';
4
+ import type { SqliteMigrateOptions } from './migrations.js';
3
5
  /** Options for the SQLite store. */
4
6
  export interface SqliteStoreOptions {
5
7
  /** Table name. Defaults to `nexus_store`. */
@@ -11,6 +13,8 @@ export interface SqliteStoreOptions {
11
13
  }
12
14
  /** The schema, as statements. */
13
15
  export declare function sqliteStoreMigration(options?: Pick<SqliteStoreOptions, 'table'>): string[];
16
+ /** The versioned schema, which `migrate()` applies. Version 1 is the 2.0 schema. */
17
+ export declare function sqliteStoreMigrations(options?: Pick<SqliteStoreOptions, 'table'>): SchemaMigration[];
14
18
  /**
15
19
  * Long-term memory in SQLite, for one machine that needs memory to survive a restart.
16
20
  *
@@ -25,8 +29,11 @@ export declare class SqliteStore implements Store {
25
29
  private readonly table;
26
30
  private readonly now;
27
31
  constructor(database: SqliteLikeClient | SqliteDatabaseLike, options?: SqliteStoreOptions);
28
- /** Creates the table and indexes if they do not exist. Never runs implicitly. */
29
- migrate(): Promise<void>;
32
+ /**
33
+ * Applies this store's pending migrations, recorded in `nexus_schema_migrations`, each inside
34
+ * `BEGIN IMMEDIATE`. Never runs implicitly. Safe on a file an older release created.
35
+ */
36
+ migrate(options?: SqliteMigrateOptions): Promise<MigrationResult>;
30
37
  /** Stores an item, keeping its original `createdAt` when it replaces one. */
31
38
  put<V>(namespace: StoreNamespace, key: string, value: V, options?: StorePutOptions): Promise<void>;
32
39
  /** Reads an item, or `undefined` when it does not exist or has expired. */