@lota-sdk/core 0.1.14 → 0.1.16

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 (174) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
package/src/db/service.ts CHANGED
@@ -12,6 +12,9 @@ import {
12
12
  import type { ExprLike, Mutation, SurrealTransaction, Values } from 'surrealdb'
13
13
  import type { z } from 'zod'
14
14
 
15
+ import { serverLogger } from '../config/logger'
16
+ import { withTimeout } from '../utils/async'
17
+ import { isRecord } from '../utils/string'
15
18
  import type { RecordIdInput } from './record-id'
16
19
  import { ensureRecordId, readCustomStringValue } from './record-id'
17
20
  import type { DatabaseTable } from './tables'
@@ -88,12 +91,8 @@ function configureMutation(
88
91
  return builder.merge(data)
89
92
  }
90
93
 
91
- function isRecordValue(value: unknown): value is Record<string, unknown> {
92
- return typeof value === 'object' && value !== null && !Array.isArray(value)
93
- }
94
-
95
94
  function isBoundQueryLike(value: unknown): value is BoundQueryLike {
96
- if (!isRecordValue(value)) {
95
+ if (!isRecord(value)) {
97
96
  return false
98
97
  }
99
98
 
@@ -101,7 +100,7 @@ function isBoundQueryLike(value: unknown): value is BoundQueryLike {
101
100
  return false
102
101
  }
103
102
 
104
- return value.bindings === undefined || isRecordValue(value.bindings)
103
+ return value.bindings === undefined || isRecord(value.bindings)
105
104
  }
106
105
 
107
106
  function toStringLikeValue(value: unknown): string | null {
@@ -122,23 +121,6 @@ const CONNECT_RETRY_BASE_DELAY_MS = 100
122
121
  const CONNECT_RETRY_JITTER_MS = 50
123
122
  const CONNECT_ATTEMPT_TIMEOUT_MS = 5_000
124
123
 
125
- async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
126
- let timeoutId: ReturnType<typeof setTimeout> | undefined
127
-
128
- try {
129
- return await Promise.race([
130
- promise,
131
- new Promise<T>((_, reject) => {
132
- timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs)
133
- }),
134
- ])
135
- } finally {
136
- if (timeoutId) {
137
- clearTimeout(timeoutId)
138
- }
139
- }
140
- }
141
-
142
124
  export class SurrealDBService {
143
125
  private client: Surreal | null = null
144
126
  private isConnected = false
@@ -204,7 +186,8 @@ export class SurrealDBService {
204
186
 
205
187
  try {
206
188
  await this.client.close()
207
- } catch {
189
+ } catch (error) {
190
+ serverLogger.warn`Failed to close database client: ${error}`
208
191
  } finally {
209
192
  this.client = null
210
193
  }
@@ -252,7 +235,7 @@ export class SurrealDBService {
252
235
  : { username: this.config.username ?? '', password: this.config.password ?? '' },
253
236
  }),
254
237
  CONNECT_ATTEMPT_TIMEOUT_MS,
255
- `Timed out connecting to SurrealDB at ${this.config.url}`,
238
+ `SurrealDB connect (${this.config.url})`,
256
239
  )
257
240
 
258
241
  this.isConnected = true
@@ -278,7 +261,7 @@ export class SurrealDBService {
278
261
  }
279
262
  }
280
263
 
281
- this.toSurrealError(lastError)
264
+ return this.toSurrealError(lastError)
282
265
  })()
283
266
 
284
267
  try {
@@ -327,8 +310,16 @@ export class SurrealDBService {
327
310
 
328
311
  private normalizeRecordId(id: unknown, table: DatabaseTable): ReturnType<typeof ensureRecordId> {
329
312
  try {
330
- return ensureRecordId(id as RecordIdInput, table)
313
+ const recordId = ensureRecordId(id as RecordIdInput, table)
314
+ const resolvedTable = String(recordId.table)
315
+ if (resolvedTable !== table) {
316
+ throw new SurrealDBError(`Record id table mismatch: expected "${table}" but got "${resolvedTable}"`)
317
+ }
318
+ return recordId
331
319
  } catch (error) {
320
+ if (error instanceof SurrealDBError) {
321
+ throw error
322
+ }
332
323
  if (error instanceof Error) {
333
324
  throw new SurrealDBError(`Invalid record id for table "${table}": ${error.message}`, undefined, {
334
325
  cause: error,
@@ -338,14 +329,39 @@ export class SurrealDBService {
338
329
  }
339
330
  }
340
331
 
341
- private normalizeQueryRows<T>(statement: unknown, schema?: z.ZodType<T>): T[] {
332
+ private normalizeQueryRows(statement: unknown, schema?: z.ZodTypeAny): unknown[] {
342
333
  if (Array.isArray(statement)) {
343
- return schema ? statement.map((row) => schema.parse(row)) : (statement as T[])
334
+ return schema ? statement.map((row) => this.parseSchema(schema, row)) : (statement as unknown[])
344
335
  }
345
336
  if (statement === null || statement === undefined) {
346
337
  return []
347
338
  }
348
- return schema ? [schema.parse(statement)] : [statement as T]
339
+ return schema ? [this.parseSchema(schema, statement)] : [statement]
340
+ }
341
+
342
+ private normalizeParseValue(value: unknown): unknown {
343
+ if (
344
+ value instanceof Date ||
345
+ value instanceof RecordId ||
346
+ value instanceof StringRecordId ||
347
+ value instanceof Table
348
+ ) {
349
+ return value
350
+ }
351
+
352
+ if (Array.isArray(value)) {
353
+ return value.map((entry) => this.normalizeParseValue(entry))
354
+ }
355
+
356
+ if (!isRecord(value)) {
357
+ return value
358
+ }
359
+
360
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, this.normalizeParseValue(entry)]))
361
+ }
362
+
363
+ private parseSchema<TSchema extends z.ZodTypeAny>(schema: TSchema, value: unknown): z.infer<TSchema> {
364
+ return schema.parse(this.normalizeParseValue(value))
349
365
  }
350
366
 
351
367
  private buildFilterExpression(filter: Record<string, unknown>): ExprLike | undefined {
@@ -362,6 +378,33 @@ export class SurrealDBService {
362
378
  return and(...expressions)
363
379
  }
364
380
 
381
+ private assertValidIdentifier(name: string, context: string): void {
382
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(name)) {
383
+ throw new SurrealDBError(`Invalid ${context}: "${name}"`)
384
+ }
385
+ }
386
+
387
+ private buildBoundFilterClauses(filter: Record<string, unknown>): {
388
+ clause: string
389
+ bindings: Record<string, unknown>
390
+ } {
391
+ const entries = Object.entries(filter)
392
+ if (entries.length === 0) {
393
+ throw new SurrealDBError('Expected a non-empty filter')
394
+ }
395
+
396
+ const bindings: Record<string, unknown> = {}
397
+ const clauses = entries.map(([field, value], index) => {
398
+ this.assertValidIdentifier(field, 'filter field')
399
+
400
+ const bindingKey = `filter_${index}`
401
+ bindings[bindingKey] = this.normalizeRuntimeValue(value)
402
+ return `${field} = $${bindingKey}`
403
+ })
404
+
405
+ return { clause: clauses.join(' AND '), bindings }
406
+ }
407
+
365
408
  private normalizeBoundQuery(query: BoundQuery | BoundQueryLike): BoundQuery {
366
409
  if (!(query instanceof BoundQuery) && !isBoundQueryLike(query)) {
367
410
  throw new SurrealDBError('Invalid query object: expected a BoundQuery-like value')
@@ -392,11 +435,11 @@ export class SurrealDBService {
392
435
  return value.map((entry) => this.normalizeRuntimeValue(entry))
393
436
  }
394
437
 
395
- if (!isRecordValue(value)) {
438
+ if (!isRecord(value)) {
396
439
  return value
397
440
  }
398
441
 
399
- if ('tb' in value && 'id' in value) {
442
+ if ('tb' in value && 'id' in value && Object.keys(value).length === 2) {
400
443
  return ensureRecordId(value as RecordIdInput)
401
444
  }
402
445
 
@@ -413,6 +456,8 @@ export class SurrealDBService {
413
456
  return Object.fromEntries(entries.map(([key, entryValue]) => [key, this.normalizeRuntimeValue(entryValue)]))
414
457
  }
415
458
 
459
+ // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
460
+ // (non-array objects are mapped entry-by-entry and returned as Object.fromEntries)
416
461
  private normalizeBindings(bindings?: Record<string, unknown>): Record<string, unknown> | undefined {
417
462
  if (!bindings) {
418
463
  return undefined
@@ -421,24 +466,19 @@ export class SurrealDBService {
421
466
  return this.normalizeRuntimeValue(bindings) as Record<string, unknown>
422
467
  }
423
468
 
424
- private normalizeMutationData(data: Record<string, unknown>): Record<string, unknown> {
425
- const normalized = this.normalizeRuntimeValue(data) as Record<string, unknown>
426
- return this.stripNullValues(normalized)
427
- }
428
-
429
- /**
430
- * SurrealDB v3 `option<T>` fields expect `NONE` (key omitted), not `null`.
431
- * Strip top-level `null` values so they are omitted from the serialized payload,
432
- * which the driver interprets as NONE.
433
- */
434
- private stripNullValues(data: Record<string, unknown>): Record<string, unknown> {
435
- const result: Record<string, unknown> = {}
436
- for (const [key, value] of Object.entries(data)) {
437
- if (value !== null) {
438
- result[key] = value
439
- }
469
+ private normalizeMutationFieldValue(value: unknown): unknown {
470
+ if (value === null || value === undefined) {
471
+ return undefined
440
472
  }
441
- return result
473
+
474
+ return this.normalizeRuntimeValue(value)
475
+ }
476
+
477
+ // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
478
+ private normalizeMutationData(data: Record<string, unknown>): Record<string, unknown> {
479
+ return Object.fromEntries(
480
+ Object.entries(data).map(([key, value]) => [key, this.normalizeMutationFieldValue(value)]),
481
+ ) as Record<string, unknown>
442
482
  }
443
483
 
444
484
  private normalizeTableValue(value: unknown): Table {
@@ -469,7 +509,7 @@ export class SurrealDBService {
469
509
 
470
510
  if (value && typeof value === 'object') {
471
511
  const record = value as { tb?: unknown; id?: unknown }
472
- if (typeof record.tb === 'string' && record.id !== undefined) {
512
+ if (typeof record.tb === 'string' && record.id !== undefined && Object.keys(value).length === 2) {
473
513
  return true
474
514
  }
475
515
 
@@ -495,36 +535,59 @@ export class SurrealDBService {
495
535
  content: (data) => this.wrapMutationBuilder(builder.content(this.normalizeMutationData(data))),
496
536
  replace: (data) => this.wrapMutationBuilder(builder.replace(this.normalizeMutationData(data))),
497
537
  merge: (data) => this.wrapMutationBuilder(builder.merge(this.normalizeMutationData(data))),
498
- output: (mode) => builder.output(mode),
538
+ output: async (mode) => this.normalizeParseValue(await builder.output(mode)),
499
539
  }
500
540
  }
501
541
 
502
542
  private wrapCreateBuilder(builder: CreateMutationBuilder): CreateMutationBuilder {
503
543
  return {
504
544
  content: (data) => this.wrapCreateBuilder(builder.content(this.normalizeMutationData(data))),
505
- output: (mode) => builder.output(mode),
545
+ output: async (mode) => this.normalizeParseValue(await builder.output(mode)),
506
546
  }
507
547
  }
508
548
 
509
549
  private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
510
550
  return {
511
- query: (query: unknown) => tx.query(this.normalizeBoundQuery(query as BoundQuery | BoundQueryLike)),
551
+ query: async (query: unknown) => {
552
+ const boundQuery = this.normalizeBoundQuery(query as BoundQuery | BoundQueryLike)
553
+ const queryText = this.resolveQueryText(boundQuery)
554
+
555
+ try {
556
+ const responses = await tx.query(boundQuery).responses()
557
+ const first = responses.at(0)
558
+ if (!first) {
559
+ return []
560
+ }
561
+ if (!first.success) {
562
+ throw new SurrealDBError(first.error.message, queryText, { cause: first.error })
563
+ }
564
+
565
+ return this.normalizeQueryRows(first.result)
566
+ } catch (error) {
567
+ return this.toSurrealError(error, queryText)
568
+ }
569
+ },
512
570
  create: (target: unknown) => {
513
571
  const normalizedTarget = this.normalizeCreateTarget(target)
514
572
  const builder = normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
573
+ // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal CreateMutationBuilder interface
515
574
  return this.wrapCreateBuilder(builder as unknown as CreateMutationBuilder)
516
575
  },
517
576
  update: (target: unknown) =>
577
+ // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal MutationBuilder interface
518
578
  this.wrapMutationBuilder(tx.update(ensureRecordId(target as RecordIdInput)) as unknown as MutationBuilder),
519
- delete: (target: unknown) => tx.delete(ensureRecordId(target as RecordIdInput)),
520
- relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
521
- tx.relate(
522
- ensureRecordId(from as RecordIdInput),
523
- this.normalizeTableValue(edgeTable),
524
- ensureRecordId(to as RecordIdInput),
525
- data
526
- ? (this.normalizeMutationData(data as Record<string, unknown>) as Values<Record<string, unknown>>)
527
- : undefined,
579
+ delete: async (target: unknown) =>
580
+ this.normalizeParseValue(await tx.delete(ensureRecordId(target as RecordIdInput))),
581
+ relate: async (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
582
+ this.normalizeParseValue(
583
+ await tx.relate(
584
+ ensureRecordId(from as RecordIdInput),
585
+ this.normalizeTableValue(edgeTable),
586
+ ensureRecordId(to as RecordIdInput),
587
+ data
588
+ ? (this.normalizeMutationData(data as Record<string, unknown>) as Values<Record<string, unknown>>)
589
+ : undefined,
590
+ ),
528
591
  ),
529
592
  commit: () => tx.commit(),
530
593
  cancel: () => tx.cancel(),
@@ -540,7 +603,7 @@ export class SurrealDBService {
540
603
  return statements.at(0) ?? []
541
604
  }
542
605
 
543
- async queryAll<T>(query: BoundQuery | BoundQueryLike, schema?: z.ZodType<T>): Promise<T[][]> {
606
+ async queryAll<T>(query: BoundQuery | BoundQueryLike, schema?: z.ZodTypeAny): Promise<T[][]> {
544
607
  const client = await this.ensureConnected()
545
608
  const boundQuery = this.normalizeBoundQuery(query)
546
609
  const queryText = this.resolveQueryText(boundQuery)
@@ -553,22 +616,22 @@ export class SurrealDBService {
553
616
  const failure = response.error
554
617
  throw new SurrealDBError(`Statement ${index + 1}: ${failure.message}`, queryText, { cause: failure })
555
618
  }
556
- return this.normalizeQueryRows<T>(response.result, schema)
619
+ return this.normalizeQueryRows(response.result, schema) as T[]
557
620
  })
558
621
  } catch (error) {
559
- this.toSurrealError(error, queryText)
622
+ return this.toSurrealError(error, queryText)
560
623
  }
561
624
  }
562
625
 
563
- async queryOne<T extends z.ZodType>(query: BoundQuery | BoundQueryLike, schema: T): Promise<z.infer<T> | null> {
626
+ async queryOne<T extends z.ZodTypeAny>(query: BoundQuery | BoundQueryLike, schema: T): Promise<z.infer<T> | null> {
564
627
  const results = await this.query<unknown>(query)
565
628
  const first = results.at(0)
566
- return first ? schema.parse(first) : null
629
+ return first ? this.parseSchema(schema, first) : null
567
630
  }
568
631
 
569
- async queryMany<T extends z.ZodType>(query: BoundQuery | BoundQueryLike, schema: T): Promise<z.infer<T>[]> {
632
+ async queryMany<T extends z.ZodTypeAny>(query: BoundQuery | BoundQueryLike, schema: T): Promise<z.infer<T>[]> {
570
633
  const results = await this.query<unknown>(query)
571
- return results.map((row) => schema.parse(row))
634
+ return results.map((row) => this.parseSchema(schema, row))
572
635
  }
573
636
 
574
637
  private async runSqlFile(file: Bun.BunFile): Promise<void> {
@@ -580,13 +643,13 @@ export class SurrealDBService {
580
643
  await this.queryAll<unknown>(new BoundQuery(sql))
581
644
  }
582
645
 
583
- async applySchemaAndMigrations(schemaFiles: readonly Bun.BunFile[]): Promise<void> {
646
+ async applySchema(schemaFiles: readonly Bun.BunFile[]): Promise<void> {
584
647
  for (const schemaFile of schemaFiles) {
585
648
  await this.runSqlFile(schemaFile)
586
649
  }
587
650
  }
588
651
 
589
- async findOne<T extends z.ZodType>(
652
+ async findOne<T extends z.ZodTypeAny>(
590
653
  table: DatabaseTable,
591
654
  filter: Record<string, unknown>,
592
655
  schema: T,
@@ -602,13 +665,13 @@ export class SurrealDBService {
602
665
 
603
666
  const rows = await query.limit(1)
604
667
  const first = rows.at(0)
605
- return first ? schema.parse(first) : null
668
+ return first ? this.parseSchema(schema, first) : null
606
669
  } catch (error) {
607
- this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
670
+ return this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
608
671
  }
609
672
  }
610
673
 
611
- async findMany<T extends z.ZodType>(
674
+ async findMany<T extends z.ZodTypeAny>(
612
675
  table: DatabaseTable,
613
676
  filter: Record<string, unknown>,
614
677
  schema: T,
@@ -620,8 +683,15 @@ export class SurrealDBService {
620
683
  const selection = this.buildFilterExpression(filter)
621
684
  const orderBy = options?.orderBy
622
685
 
623
- if (options && orderBy) {
624
- const { orderDir = 'ASC', limit, offset } = options
686
+ if (orderBy !== undefined) {
687
+ this.assertValidIdentifier(orderBy, 'orderBy field')
688
+ this.assertValidIdentifier(table, 'table name')
689
+ for (const key of filterKeys) {
690
+ this.assertValidIdentifier(key, 'filter field')
691
+ }
692
+ const orderDir = options?.orderDir ?? 'ASC'
693
+ const limit = options?.limit
694
+ const offset = options?.offset
625
695
  const vars: Record<string, unknown> = this.normalizeMutationData(filter)
626
696
  let sql = `SELECT * FROM ${table}`
627
697
  if (filterKeys.length > 0) {
@@ -638,7 +708,7 @@ export class SurrealDBService {
638
708
  vars.offsetParam = offset
639
709
  }
640
710
  const rows = await this.query<unknown>(new BoundQuery(sql, vars))
641
- return rows.map((row) => schema.parse(row))
711
+ return rows.map((row) => this.parseSchema(schema, row))
642
712
  }
643
713
 
644
714
  try {
@@ -654,13 +724,13 @@ export class SurrealDBService {
654
724
  }
655
725
 
656
726
  const rows = await query
657
- return rows.map((row) => schema.parse(row))
727
+ return rows.map((row) => this.parseSchema(schema, row))
658
728
  } catch (error) {
659
- this.toSurrealError(error, `SELECT * FROM ${table}`)
729
+ return this.toSurrealError(error, `SELECT * FROM ${table}`)
660
730
  }
661
731
  }
662
732
 
663
- async create<T extends z.ZodType>(
733
+ async create<T extends z.ZodTypeAny>(
664
734
  table: DatabaseTable,
665
735
  data: Record<string, unknown>,
666
736
  schema: T,
@@ -683,13 +753,13 @@ export class SurrealDBService {
683
753
  throw new SurrealDBError(`Failed to create record in ${table}`)
684
754
  }
685
755
 
686
- return schema.parse(first)
756
+ return this.parseSchema(schema, first)
687
757
  } catch (error) {
688
- this.toSurrealError(error, `CREATE ${table}`)
758
+ return this.toSurrealError(error, `CREATE ${table}`)
689
759
  }
690
760
  }
691
761
 
692
- async createWithId<T extends z.ZodType>(
762
+ async createWithId<T extends z.ZodTypeAny>(
693
763
  table: DatabaseTable,
694
764
  id: unknown,
695
765
  data: Record<string, unknown>,
@@ -705,13 +775,13 @@ export class SurrealDBService {
705
775
 
706
776
  try {
707
777
  const created = await client.create<unknown>(recordId).content(this.normalizeMutationData(data)).output('after')
708
- return schema.parse(created)
778
+ return this.parseSchema(schema, created)
709
779
  } catch (error) {
710
- this.toSurrealError(error, `CREATE ${recordId.toString()}`)
780
+ return this.toSurrealError(error, `CREATE ${recordId.toString()}`)
711
781
  }
712
782
  }
713
783
 
714
- async update<T extends z.ZodType>(
784
+ async update<T extends z.ZodTypeAny>(
715
785
  table: DatabaseTable,
716
786
  id: unknown,
717
787
  data: Record<string, unknown>,
@@ -732,13 +802,13 @@ export class SurrealDBService {
732
802
  const builder = client.update<unknown>(recordId)
733
803
  const configured = configureMutation(builder, mutation, this.normalizeMutationData(data))
734
804
  const updated = await configured.output('after')
735
- return updated ? schema.parse(updated) : null
805
+ return updated ? this.parseSchema(schema, updated) : null
736
806
  } catch (error) {
737
- this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
807
+ return this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
738
808
  }
739
809
  }
740
810
 
741
- async upsert<T extends z.ZodType>(
811
+ async upsert<T extends z.ZodTypeAny>(
742
812
  table: DatabaseTable,
743
813
  id: unknown,
744
814
  data: Record<string, unknown>,
@@ -761,55 +831,52 @@ export class SurrealDBService {
761
831
  if (!upserted) {
762
832
  throw new SurrealDBError(`Failed to upsert record in ${table}`)
763
833
  }
764
- return schema.parse(upserted)
834
+ return this.parseSchema(schema, upserted)
765
835
  } catch (error) {
766
- this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
836
+ return this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
767
837
  }
768
838
  }
769
839
 
770
840
  async deleteById(table: DatabaseTable, id: unknown): Promise<boolean> {
771
841
  const recordId = this.normalizeRecordId(id, table)
772
- const client = await this.ensureConnected()
773
842
 
774
843
  try {
775
- const matched = await client.select<{ id: unknown }>(new Table(table)).where(eq('id', recordId)).limit(1)
776
- if (matched.length === 0) {
844
+ const result = await this.query<unknown>(new BoundQuery(`DELETE $recordId RETURN BEFORE`, { recordId }))
845
+ return result.length > 0
846
+ } catch (error) {
847
+ if (error instanceof Error && error.message.includes('does not exist')) {
777
848
  return false
778
849
  }
779
- await client.delete<unknown>(recordId).output('before')
780
- return true
781
- } catch (error) {
782
- this.toSurrealError(error, `DELETE ${recordId.toString()}`)
850
+ return this.toSurrealError(error, `DELETE ${recordId.toString()}`)
783
851
  }
784
852
  }
785
853
 
786
854
  async deleteWhere(table: DatabaseTable, filter: Record<string, unknown>): Promise<number> {
855
+ this.assertValidIdentifier(table, 'table name')
787
856
  const filterKeys = Object.keys(filter)
788
857
  if (filterKeys.length === 0) {
789
858
  throw new SurrealDBError(`Refusing to delete all records in ${table} without a filter`)
790
859
  }
791
-
792
- const selection = this.buildFilterExpression(filter)
793
- if (!selection) {
794
- throw new SurrealDBError(`Invalid delete filter for table ${table}`)
795
- }
796
-
797
- const client = await this.ensureConnected()
860
+ const { clause, bindings } = this.buildBoundFilterClauses(filter)
798
861
 
799
862
  try {
800
- const matched = await client.select<{ id: unknown }>(new Table(table)).where(selection)
801
- if (matched.length === 0) {
802
- return 0
803
- }
863
+ return await this.withTransaction(async (tx) => {
864
+ const matched = (await tx.query(new BoundQuery(`SELECT id FROM ${table} WHERE ${clause}`, bindings))) as Array<{
865
+ id: unknown
866
+ }>
804
867
 
805
- for (const row of matched) {
806
- const id = this.normalizeRecordId(row.id, table)
807
- await client.delete(id)
808
- }
868
+ if (matched.length === 0) {
869
+ return 0
870
+ }
871
+
872
+ for (const row of matched) {
873
+ await tx.delete(this.normalizeRecordId(row.id, table))
874
+ }
809
875
 
810
- return matched.length
876
+ return matched.length
877
+ })
811
878
  } catch (error) {
812
- this.toSurrealError(error, `DELETE ${table} WHERE ...`)
879
+ return this.toSurrealError(error, `DELETE ${table} WHERE ...`)
813
880
  }
814
881
  }
815
882
 
@@ -835,7 +902,7 @@ export class SurrealDBService {
835
902
  }
836
903
  return 1
837
904
  } catch (error) {
838
- this.toSurrealError(error, `UPDATE ${table} WHERE ...`)
905
+ return this.toSurrealError(error, `UPDATE ${table} WHERE ...`)
839
906
  }
840
907
  }
841
908
 
@@ -854,7 +921,7 @@ export class SurrealDBService {
854
921
  .output('after')
855
922
  return inserted as T[]
856
923
  } catch (error) {
857
- this.toSurrealError(error, `INSERT ${table}`)
924
+ return this.toSurrealError(error, `INSERT ${table}`)
858
925
  }
859
926
  }
860
927
 
@@ -884,7 +951,7 @@ export class SurrealDBService {
884
951
  }
885
952
  return related
886
953
  } catch (error) {
887
- this.toSurrealError(error, `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`)
954
+ return this.toSurrealError(error, `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`)
888
955
  }
889
956
  }
890
957
 
@@ -893,7 +960,7 @@ export class SurrealDBService {
893
960
  try {
894
961
  return this.wrapTransaction(await client.beginTransaction())
895
962
  } catch (error) {
896
- this.toSurrealError(error, 'BEGIN TRANSACTION')
963
+ return this.toSurrealError(error, 'BEGIN TRANSACTION')
897
964
  }
898
965
  }
899
966
 
@@ -942,10 +1009,14 @@ export const databaseService = new Proxy({} as SurrealDBService, {
942
1009
  return databaseServiceOverrides.get(property)
943
1010
  }
944
1011
 
945
- const target = resolveConfiguredDatabaseService() as unknown as Record<PropertyKey, unknown>
946
- const value = target[property]
1012
+ const resolved = resolveConfiguredDatabaseService()
1013
+ const value: unknown = Reflect.get(resolved, property)
947
1014
  if (typeof value === 'function') {
948
- return bindTargetMethod(target, value as (...args: unknown[]) => unknown)
1015
+ // SurrealDB SDK type gap — Reflect.get returns unknown, but we know the resolved target shape
1016
+ return bindTargetMethod(
1017
+ resolved as unknown as Record<PropertyKey, unknown>,
1018
+ value as (...args: unknown[]) => unknown,
1019
+ )
949
1020
  }
950
1021
 
951
1022
  return value
package/src/db/startup.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import { recordIdSchema } from '@lota-sdk/shared'
1
2
  import { BoundQuery, RecordId } from 'surrealdb'
2
3
  import { z } from 'zod'
3
4
 
4
- import type { SurrealDBService, SurrealDatabaseLogger } from '../db/service'
5
- import { TABLES } from '../db/tables'
6
- import { getErrorMessage } from '../utils/error'
5
+ import { getErrorMessage } from '../utils/errors'
6
+ import type { SurrealDBService, SurrealDatabaseLogger } from './service'
7
+ import { TABLES } from './tables'
7
8
 
8
9
  const DATABASE_BOOTSTRAP_KEY = 'database-schema-ready'
9
10
  const DEFAULT_RETRY_DELAY_MS = 1_000
@@ -11,11 +12,11 @@ const DEFAULT_MAX_WAIT_MS = 3 * 60 * 1_000
11
12
  const RETRY_LOG_INTERVAL = 5
12
13
 
13
14
  const RuntimeBootstrapRecordSchema = z.object({
14
- id: z.unknown(),
15
+ id: recordIdSchema,
15
16
  key: z.string(),
16
17
  schemaFingerprint: z.string(),
17
- readyAt: z.union([z.date(), z.string(), z.number()]),
18
- updatedAt: z.union([z.date(), z.string(), z.number()]),
18
+ readyAt: z.coerce.date(),
19
+ updatedAt: z.coerce.date().optional().nullable(),
19
20
  })
20
21
 
21
22
  type StartupLogger = Pick<SurrealDatabaseLogger, 'info' | 'warn' | 'error'>
@@ -62,7 +63,7 @@ export async function connectWithStartupRetry(params: {
62
63
  async function readDatabaseBootstrapRecord(
63
64
  databaseService: SurrealDBService,
64
65
  ): Promise<z.infer<typeof RuntimeBootstrapRecordSchema> | null> {
65
- return await databaseService.queryOne(
66
+ return databaseService.queryOne(
66
67
  new BoundQuery(
67
68
  `SELECT *
68
69
  FROM ${TABLES.RUNTIME_BOOTSTRAP}
@@ -141,12 +142,7 @@ export async function publishDatabaseBootstrap(params: {
141
142
  await params.databaseService.upsert(
142
143
  TABLES.RUNTIME_BOOTSTRAP,
143
144
  new RecordId(TABLES.RUNTIME_BOOTSTRAP, DATABASE_BOOTSTRAP_KEY),
144
- {
145
- key: DATABASE_BOOTSTRAP_KEY,
146
- schemaFingerprint: params.schemaFingerprint,
147
- readyAt: new Date(),
148
- updatedAt: new Date(),
149
- },
145
+ { key: DATABASE_BOOTSTRAP_KEY, schemaFingerprint: params.schemaFingerprint, readyAt: new Date() },
150
146
  RuntimeBootstrapRecordSchema,
151
147
  )
152
148
  }