@lota-sdk/core 0.1.15 → 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 (138) 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 +8 -7
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/index.ts +0 -2
  11. package/src/bifrost/bifrost.ts +2 -7
  12. package/src/config/agent-defaults.ts +31 -21
  13. package/src/config/agent-types.ts +11 -0
  14. package/src/config/constants.ts +2 -14
  15. package/src/config/debug-logger.ts +5 -1
  16. package/src/config/index.ts +3 -0
  17. package/src/config/model-constants.ts +16 -34
  18. package/src/config/search.ts +1 -15
  19. package/src/create-runtime.ts +244 -178
  20. package/src/db/cursor-pagination.ts +3 -6
  21. package/src/db/index.ts +2 -0
  22. package/src/db/memory-store.rows.ts +7 -7
  23. package/src/db/memory-store.ts +14 -18
  24. package/src/db/memory.ts +13 -13
  25. package/src/db/service.ts +153 -79
  26. package/src/db/startup.ts +6 -10
  27. package/src/db/surreal-mutation.ts +43 -0
  28. package/src/db/tables.ts +7 -0
  29. package/src/db/workstream-message-row.ts +15 -0
  30. package/src/embeddings/provider.ts +1 -1
  31. package/src/queues/context-compaction.queue.ts +15 -46
  32. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  33. package/src/queues/index.ts +3 -0
  34. package/src/queues/memory-consolidation.queue.ts +16 -51
  35. package/src/queues/plan-scheduler.queue.ts +97 -0
  36. package/src/queues/post-chat-memory.queue.ts +15 -56
  37. package/src/queues/queue-factory.ts +100 -0
  38. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  39. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  40. package/src/queues/skill-extraction.queue.ts +15 -47
  41. package/src/queues/workstream-title-generation.queue.ts +15 -47
  42. package/src/redis/connection.ts +6 -0
  43. package/src/redis/index.ts +1 -1
  44. package/src/redis/stream-context.ts +11 -0
  45. package/src/runtime/agent-runtime-policy.ts +106 -21
  46. package/src/runtime/approval-continuation.ts +12 -6
  47. package/src/runtime/context-compaction-runtime.ts +1 -1
  48. package/src/runtime/context-compaction.ts +22 -60
  49. package/src/runtime/execution-plan.ts +22 -18
  50. package/src/runtime/graph-designer.ts +15 -0
  51. package/src/runtime/helper-model.ts +9 -197
  52. package/src/runtime/index.ts +2 -0
  53. package/src/runtime/llm-content.ts +1 -1
  54. package/src/runtime/memory-block.ts +9 -11
  55. package/src/runtime/memory-pipeline.ts +6 -9
  56. package/src/runtime/plugin-resolution.ts +35 -0
  57. package/src/runtime/plugin-types.ts +72 -0
  58. package/src/runtime/retrieval-adapters.ts +1 -1
  59. package/src/runtime/runtime-config.ts +25 -12
  60. package/src/runtime/runtime-extensions.ts +2 -2
  61. package/src/runtime/runtime-worker-registry.ts +6 -0
  62. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  63. package/src/runtime/team-consultation-prompts.ts +11 -2
  64. package/src/runtime/title-helpers.ts +2 -4
  65. package/src/runtime/workstream-chat-helpers.ts +1 -1
  66. package/src/services/adaptive-playbook.service.ts +152 -0
  67. package/src/services/agent-executor.service.ts +293 -0
  68. package/src/services/artifact-provenance.service.ts +172 -0
  69. package/src/services/attachment.service.ts +6 -11
  70. package/src/services/context-compaction.service.ts +72 -55
  71. package/src/services/context-enrichment.service.ts +33 -0
  72. package/src/services/coordination-registry.service.ts +117 -0
  73. package/src/services/document-chunk.service.ts +1 -1
  74. package/src/services/domain-agent-executor.service.ts +71 -0
  75. package/src/services/execution-plan.service.ts +269 -50
  76. package/src/services/feedback-loop.service.ts +96 -0
  77. package/src/services/global-orchestrator.service.ts +148 -0
  78. package/src/services/index.ts +26 -0
  79. package/src/services/institutional-memory.service.ts +145 -0
  80. package/src/services/learned-skill.service.ts +24 -5
  81. package/src/services/memory-assessment.service.ts +3 -2
  82. package/src/services/memory-utils.ts +3 -8
  83. package/src/services/memory.service.ts +42 -59
  84. package/src/services/monitoring-window.service.ts +86 -0
  85. package/src/services/mutating-approval.service.ts +1 -1
  86. package/src/services/node-workspace.service.ts +155 -0
  87. package/src/services/notification.service.ts +39 -0
  88. package/src/services/organization-member.service.ts +11 -4
  89. package/src/services/organization.service.ts +5 -5
  90. package/src/services/ownership-dispatcher.service.ts +403 -0
  91. package/src/services/plan-approval.service.ts +1 -1
  92. package/src/services/plan-builder.service.ts +1 -0
  93. package/src/services/plan-checkpoint.service.ts +30 -2
  94. package/src/services/plan-compiler.service.ts +5 -0
  95. package/src/services/plan-coordination.service.ts +152 -0
  96. package/src/services/plan-cycle.service.ts +284 -0
  97. package/src/services/plan-deadline.service.ts +287 -0
  98. package/src/services/plan-executor.service.ts +384 -40
  99. package/src/services/plan-run.service.ts +41 -7
  100. package/src/services/plan-scheduler.service.ts +240 -0
  101. package/src/services/plan-template.service.ts +117 -0
  102. package/src/services/plan-validator.service.ts +84 -2
  103. package/src/services/plan-workspace.service.ts +83 -0
  104. package/src/services/playbook-registry.service.ts +67 -0
  105. package/src/services/plugin-executor.service.ts +103 -0
  106. package/src/services/quality-metrics.service.ts +132 -0
  107. package/src/services/recent-activity.service.ts +27 -31
  108. package/src/services/skill-resolver.service.ts +19 -0
  109. package/src/services/system-executor.service.ts +105 -0
  110. package/src/services/workstream-message.service.ts +12 -34
  111. package/src/services/workstream-plan-registry.service.ts +22 -0
  112. package/src/services/workstream-title.service.ts +3 -1
  113. package/src/services/workstream-turn-preparation.service.ts +34 -66
  114. package/src/services/workstream.service.ts +33 -55
  115. package/src/services/workstream.types.ts +9 -9
  116. package/src/services/write-intent-validator.service.ts +81 -0
  117. package/src/storage/attachment-parser.ts +1 -1
  118. package/src/storage/attachment-utils.ts +1 -1
  119. package/src/storage/generated-document-storage.service.ts +3 -2
  120. package/src/system-agents/delegated-agent-factory.ts +2 -0
  121. package/src/tools/execution-plan.tool.ts +17 -23
  122. package/src/tools/index.ts +0 -1
  123. package/src/tools/team-think.tool.ts +6 -4
  124. package/src/utils/async.ts +2 -1
  125. package/src/utils/date-time.ts +4 -32
  126. package/src/utils/env.ts +8 -0
  127. package/src/utils/errors.ts +42 -10
  128. package/src/utils/index.ts +9 -0
  129. package/src/utils/string.ts +114 -1
  130. package/src/workers/index.ts +1 -0
  131. package/src/workers/regular-chat-memory-digest.runner.ts +2 -2
  132. package/src/workers/skill-extraction.runner.ts +1 -1
  133. package/src/workers/utils/file-section-chunker.ts +2 -1
  134. package/src/workers/utils/repomix-file-sections.ts +2 -2
  135. package/src/workers/utils/sandbox-error.ts +11 -2
  136. package/src/workers/utils/workstream-message-query.ts +11 -20
  137. package/src/workers/worker-utils.ts +2 -2
  138. package/src/tools/log-hello-world.tool.ts +0 -17
package/src/db/memory.ts CHANGED
@@ -12,7 +12,7 @@ import { getFactRetrievalMessages } from '../runtime/memory-prompts-fact'
12
12
  import { parseMessages } from '../runtime/memory-prompts-parse'
13
13
  import { getClassifyMemoryDeltaPrompt } from '../runtime/memory-prompts-update'
14
14
  import { getRuntimeConfig } from '../runtime/runtime-config'
15
- import { compactWhitespace } from '../utils/string'
15
+ import { compactWhitespace, truncateText } from '../utils/string'
16
16
  import type { SurrealMemoryStore } from './memory-store'
17
17
  import { getDefaultMemoryStore } from './memory-store'
18
18
  import { hashContent, isUniqueIndexConflict } from './memory-store.helpers'
@@ -82,7 +82,7 @@ export class Memory {
82
82
  durability?: Durability
83
83
  },
84
84
  ): Promise<string> {
85
- return await this.store.insert(
85
+ return this.store.insert(
86
86
  content,
87
87
  options.scopeId,
88
88
  options.memoryType,
@@ -137,7 +137,7 @@ export class Memory {
137
137
  return results
138
138
  }
139
139
 
140
- return await this.store.enrichWithNeighbors(results)
140
+ return this.store.enrichWithNeighbors(results)
141
141
  }
142
142
 
143
143
  async listTopMemories(options: {
@@ -147,11 +147,11 @@ export class Memory {
147
147
  durability?: Durability
148
148
  minImportance?: number
149
149
  }): Promise<MemoryRecord[]> {
150
- return await this.store.listTopMemories(options)
150
+ return this.store.listTopMemories(options)
151
151
  }
152
152
 
153
153
  async list(options: MemoryListOptions): Promise<MemoryRecord[]> {
154
- return await this.store.list(options)
154
+ return this.store.list(options)
155
155
  }
156
156
 
157
157
  async updateMemory(id: string, newContent: string): Promise<void> {
@@ -163,7 +163,7 @@ export class Memory {
163
163
  }
164
164
 
165
165
  async getStaleMemories(scopeId: string, limit?: number): Promise<MemorySearchResult[]> {
166
- return await this.store.getStaleMemories(scopeId, limit)
166
+ return this.store.getStaleMemories(scopeId, limit)
167
167
  }
168
168
 
169
169
  async add(
@@ -244,12 +244,12 @@ export class Memory {
244
244
  const factMaps = buildMemoryFactMaps(facts)
245
245
 
246
246
  const existingMemories = await this.store.list({ scopeId: options.scopeId, memoryType: options.memoryType })
247
- const oldMemoryFormat = existingMemories.map((m) => ({ id: m.id, text: m.content }))
247
+ const memoryDeltaInput = existingMemories.map((m) => ({ id: m.id, text: m.content }))
248
248
 
249
249
  const factContents = facts.map((f) => f.content)
250
- const updates = await this.determineUpdates(oldMemoryFormat, factContents)
250
+ const updates = await this.determineUpdates(memoryDeltaInput, factContents)
251
251
 
252
- return { options, updates, factMaps, existingMemories: oldMemoryFormat }
252
+ return { options, updates, factMaps, existingMemories: memoryDeltaInput }
253
253
  }
254
254
 
255
255
  private async extractFacts(
@@ -317,8 +317,8 @@ export class Memory {
317
317
  private normalizeMemoryDeltaText(value: string, maxChars?: number): string {
318
318
  const normalized = compactWhitespace(value)
319
319
  if (!normalized) return ''
320
- if (typeof maxChars !== 'number' || normalized.length <= maxChars) return normalized
321
- return `${normalized.slice(0, maxChars - 3)}...`
320
+ if (typeof maxChars !== 'number') return normalized
321
+ return truncateText(normalized, maxChars)
322
322
  }
323
323
 
324
324
  private tokenizeMemoryDeltaText(value: string): string[] {
@@ -442,7 +442,7 @@ export class Memory {
442
442
  for (const action of plan.actions) {
443
443
  switch (action.type) {
444
444
  case 'add': {
445
- const truncatedContent = action.text.length > 50 ? `${action.text.slice(0, 50)}...` : action.text
445
+ const truncatedContent = truncateText(action.text, 50)
446
446
  const metadata = { ...options.metadata, memoryCategory: action.category }
447
447
  const hash = hashContent(action.text, options.scopeId, options.memoryType)
448
448
  let newId: string
@@ -483,7 +483,7 @@ export class Memory {
483
483
  metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
484
484
  })
485
485
  idMap.set(action.refId, action.refId)
486
- aiLogger.debug`Updated memory ${action.refId}: ${action.text.slice(0, 50)}...`
486
+ aiLogger.debug`Updated memory ${action.refId}: ${truncateText(action.text, 50)}`
487
487
  break
488
488
  }
489
489
 
package/src/db/service.ts CHANGED
@@ -12,6 +12,7 @@ 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'
15
16
  import { withTimeout } from '../utils/async'
16
17
  import { isRecord } from '../utils/string'
17
18
  import type { RecordIdInput } from './record-id'
@@ -185,7 +186,8 @@ export class SurrealDBService {
185
186
 
186
187
  try {
187
188
  await this.client.close()
188
- } catch {
189
+ } catch (error) {
190
+ serverLogger.warn`Failed to close database client: ${error}`
189
191
  } finally {
190
192
  this.client = null
191
193
  }
@@ -327,14 +329,39 @@ export class SurrealDBService {
327
329
  }
328
330
  }
329
331
 
330
- private normalizeQueryRows<T>(statement: unknown, schema?: z.ZodType<T>): T[] {
332
+ private normalizeQueryRows(statement: unknown, schema?: z.ZodTypeAny): unknown[] {
331
333
  if (Array.isArray(statement)) {
332
- return schema ? statement.map((row) => schema.parse(row)) : (statement as T[])
334
+ return schema ? statement.map((row) => this.parseSchema(schema, row)) : (statement as unknown[])
333
335
  }
334
336
  if (statement === null || statement === undefined) {
335
337
  return []
336
338
  }
337
- 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))
338
365
  }
339
366
 
340
367
  private buildFilterExpression(filter: Record<string, unknown>): ExprLike | undefined {
@@ -351,6 +378,33 @@ export class SurrealDBService {
351
378
  return and(...expressions)
352
379
  }
353
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
+
354
408
  private normalizeBoundQuery(query: BoundQuery | BoundQueryLike): BoundQuery {
355
409
  if (!(query instanceof BoundQuery) && !isBoundQueryLike(query)) {
356
410
  throw new SurrealDBError('Invalid query object: expected a BoundQuery-like value')
@@ -385,7 +439,7 @@ export class SurrealDBService {
385
439
  return value
386
440
  }
387
441
 
388
- if ('tb' in value && 'id' in value) {
442
+ if ('tb' in value && 'id' in value && Object.keys(value).length === 2) {
389
443
  return ensureRecordId(value as RecordIdInput)
390
444
  }
391
445
 
@@ -412,25 +466,19 @@ export class SurrealDBService {
412
466
  return this.normalizeRuntimeValue(bindings) as Record<string, unknown>
413
467
  }
414
468
 
469
+ private normalizeMutationFieldValue(value: unknown): unknown {
470
+ if (value === null || value === undefined) {
471
+ return undefined
472
+ }
473
+
474
+ return this.normalizeRuntimeValue(value)
475
+ }
476
+
415
477
  // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
416
478
  private normalizeMutationData(data: Record<string, unknown>): Record<string, unknown> {
417
- const normalized = this.normalizeRuntimeValue(data) as Record<string, unknown>
418
- return this.stripNullValues(normalized)
419
- }
420
-
421
- /**
422
- * SurrealDB v3 `option<T>` fields expect `NONE` (key omitted), not `null`.
423
- * Strip top-level `null` values so they are omitted from the serialized payload,
424
- * which the driver interprets as NONE.
425
- */
426
- private stripNullValues(data: Record<string, unknown>): Record<string, unknown> {
427
- const result: Record<string, unknown> = {}
428
- for (const [key, value] of Object.entries(data)) {
429
- if (value !== null) {
430
- result[key] = value
431
- }
432
- }
433
- return result
479
+ return Object.fromEntries(
480
+ Object.entries(data).map(([key, value]) => [key, this.normalizeMutationFieldValue(value)]),
481
+ ) as Record<string, unknown>
434
482
  }
435
483
 
436
484
  private normalizeTableValue(value: unknown): Table {
@@ -461,7 +509,7 @@ export class SurrealDBService {
461
509
 
462
510
  if (value && typeof value === 'object') {
463
511
  const record = value as { tb?: unknown; id?: unknown }
464
- if (typeof record.tb === 'string' && record.id !== undefined) {
512
+ if (typeof record.tb === 'string' && record.id !== undefined && Object.keys(value).length === 2) {
465
513
  return true
466
514
  }
467
515
 
@@ -487,20 +535,38 @@ export class SurrealDBService {
487
535
  content: (data) => this.wrapMutationBuilder(builder.content(this.normalizeMutationData(data))),
488
536
  replace: (data) => this.wrapMutationBuilder(builder.replace(this.normalizeMutationData(data))),
489
537
  merge: (data) => this.wrapMutationBuilder(builder.merge(this.normalizeMutationData(data))),
490
- output: (mode) => builder.output(mode),
538
+ output: async (mode) => this.normalizeParseValue(await builder.output(mode)),
491
539
  }
492
540
  }
493
541
 
494
542
  private wrapCreateBuilder(builder: CreateMutationBuilder): CreateMutationBuilder {
495
543
  return {
496
544
  content: (data) => this.wrapCreateBuilder(builder.content(this.normalizeMutationData(data))),
497
- output: (mode) => builder.output(mode),
545
+ output: async (mode) => this.normalizeParseValue(await builder.output(mode)),
498
546
  }
499
547
  }
500
548
 
501
549
  private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
502
550
  return {
503
- 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
+ },
504
570
  create: (target: unknown) => {
505
571
  const normalizedTarget = this.normalizeCreateTarget(target)
506
572
  const builder = normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
@@ -510,15 +576,18 @@ export class SurrealDBService {
510
576
  update: (target: unknown) =>
511
577
  // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal MutationBuilder interface
512
578
  this.wrapMutationBuilder(tx.update(ensureRecordId(target as RecordIdInput)) as unknown as MutationBuilder),
513
- delete: (target: unknown) => tx.delete(ensureRecordId(target as RecordIdInput)),
514
- relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
515
- tx.relate(
516
- ensureRecordId(from as RecordIdInput),
517
- this.normalizeTableValue(edgeTable),
518
- ensureRecordId(to as RecordIdInput),
519
- data
520
- ? (this.normalizeMutationData(data as Record<string, unknown>) as Values<Record<string, unknown>>)
521
- : 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
+ ),
522
591
  ),
523
592
  commit: () => tx.commit(),
524
593
  cancel: () => tx.cancel(),
@@ -534,7 +603,7 @@ export class SurrealDBService {
534
603
  return statements.at(0) ?? []
535
604
  }
536
605
 
537
- async queryAll<T>(query: BoundQuery | BoundQueryLike, schema?: z.ZodType<T>): Promise<T[][]> {
606
+ async queryAll<T>(query: BoundQuery | BoundQueryLike, schema?: z.ZodTypeAny): Promise<T[][]> {
538
607
  const client = await this.ensureConnected()
539
608
  const boundQuery = this.normalizeBoundQuery(query)
540
609
  const queryText = this.resolveQueryText(boundQuery)
@@ -547,22 +616,22 @@ export class SurrealDBService {
547
616
  const failure = response.error
548
617
  throw new SurrealDBError(`Statement ${index + 1}: ${failure.message}`, queryText, { cause: failure })
549
618
  }
550
- return this.normalizeQueryRows<T>(response.result, schema)
619
+ return this.normalizeQueryRows(response.result, schema) as T[]
551
620
  })
552
621
  } catch (error) {
553
622
  return this.toSurrealError(error, queryText)
554
623
  }
555
624
  }
556
625
 
557
- 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> {
558
627
  const results = await this.query<unknown>(query)
559
628
  const first = results.at(0)
560
- return first ? schema.parse(first) : null
629
+ return first ? this.parseSchema(schema, first) : null
561
630
  }
562
631
 
563
- 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>[]> {
564
633
  const results = await this.query<unknown>(query)
565
- return results.map((row) => schema.parse(row))
634
+ return results.map((row) => this.parseSchema(schema, row))
566
635
  }
567
636
 
568
637
  private async runSqlFile(file: Bun.BunFile): Promise<void> {
@@ -574,13 +643,13 @@ export class SurrealDBService {
574
643
  await this.queryAll<unknown>(new BoundQuery(sql))
575
644
  }
576
645
 
577
- async applySchemaAndMigrations(schemaFiles: readonly Bun.BunFile[]): Promise<void> {
646
+ async applySchema(schemaFiles: readonly Bun.BunFile[]): Promise<void> {
578
647
  for (const schemaFile of schemaFiles) {
579
648
  await this.runSqlFile(schemaFile)
580
649
  }
581
650
  }
582
651
 
583
- async findOne<T extends z.ZodType>(
652
+ async findOne<T extends z.ZodTypeAny>(
584
653
  table: DatabaseTable,
585
654
  filter: Record<string, unknown>,
586
655
  schema: T,
@@ -596,13 +665,13 @@ export class SurrealDBService {
596
665
 
597
666
  const rows = await query.limit(1)
598
667
  const first = rows.at(0)
599
- return first ? schema.parse(first) : null
668
+ return first ? this.parseSchema(schema, first) : null
600
669
  } catch (error) {
601
670
  return this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
602
671
  }
603
672
  }
604
673
 
605
- async findMany<T extends z.ZodType>(
674
+ async findMany<T extends z.ZodTypeAny>(
606
675
  table: DatabaseTable,
607
676
  filter: Record<string, unknown>,
608
677
  schema: T,
@@ -614,8 +683,15 @@ export class SurrealDBService {
614
683
  const selection = this.buildFilterExpression(filter)
615
684
  const orderBy = options?.orderBy
616
685
 
617
- if (options && orderBy) {
618
- 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
619
695
  const vars: Record<string, unknown> = this.normalizeMutationData(filter)
620
696
  let sql = `SELECT * FROM ${table}`
621
697
  if (filterKeys.length > 0) {
@@ -632,7 +708,7 @@ export class SurrealDBService {
632
708
  vars.offsetParam = offset
633
709
  }
634
710
  const rows = await this.query<unknown>(new BoundQuery(sql, vars))
635
- return rows.map((row) => schema.parse(row))
711
+ return rows.map((row) => this.parseSchema(schema, row))
636
712
  }
637
713
 
638
714
  try {
@@ -648,13 +724,13 @@ export class SurrealDBService {
648
724
  }
649
725
 
650
726
  const rows = await query
651
- return rows.map((row) => schema.parse(row))
727
+ return rows.map((row) => this.parseSchema(schema, row))
652
728
  } catch (error) {
653
729
  return this.toSurrealError(error, `SELECT * FROM ${table}`)
654
730
  }
655
731
  }
656
732
 
657
- async create<T extends z.ZodType>(
733
+ async create<T extends z.ZodTypeAny>(
658
734
  table: DatabaseTable,
659
735
  data: Record<string, unknown>,
660
736
  schema: T,
@@ -677,13 +753,13 @@ export class SurrealDBService {
677
753
  throw new SurrealDBError(`Failed to create record in ${table}`)
678
754
  }
679
755
 
680
- return schema.parse(first)
756
+ return this.parseSchema(schema, first)
681
757
  } catch (error) {
682
758
  return this.toSurrealError(error, `CREATE ${table}`)
683
759
  }
684
760
  }
685
761
 
686
- async createWithId<T extends z.ZodType>(
762
+ async createWithId<T extends z.ZodTypeAny>(
687
763
  table: DatabaseTable,
688
764
  id: unknown,
689
765
  data: Record<string, unknown>,
@@ -699,13 +775,13 @@ export class SurrealDBService {
699
775
 
700
776
  try {
701
777
  const created = await client.create<unknown>(recordId).content(this.normalizeMutationData(data)).output('after')
702
- return schema.parse(created)
778
+ return this.parseSchema(schema, created)
703
779
  } catch (error) {
704
780
  return this.toSurrealError(error, `CREATE ${recordId.toString()}`)
705
781
  }
706
782
  }
707
783
 
708
- async update<T extends z.ZodType>(
784
+ async update<T extends z.ZodTypeAny>(
709
785
  table: DatabaseTable,
710
786
  id: unknown,
711
787
  data: Record<string, unknown>,
@@ -726,13 +802,13 @@ export class SurrealDBService {
726
802
  const builder = client.update<unknown>(recordId)
727
803
  const configured = configureMutation(builder, mutation, this.normalizeMutationData(data))
728
804
  const updated = await configured.output('after')
729
- return updated ? schema.parse(updated) : null
805
+ return updated ? this.parseSchema(schema, updated) : null
730
806
  } catch (error) {
731
807
  return this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
732
808
  }
733
809
  }
734
810
 
735
- async upsert<T extends z.ZodType>(
811
+ async upsert<T extends z.ZodTypeAny>(
736
812
  table: DatabaseTable,
737
813
  id: unknown,
738
814
  data: Record<string, unknown>,
@@ -755,7 +831,7 @@ export class SurrealDBService {
755
831
  if (!upserted) {
756
832
  throw new SurrealDBError(`Failed to upsert record in ${table}`)
757
833
  }
758
- return schema.parse(upserted)
834
+ return this.parseSchema(schema, upserted)
759
835
  } catch (error) {
760
836
  return this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
761
837
  }
@@ -763,45 +839,42 @@ export class SurrealDBService {
763
839
 
764
840
  async deleteById(table: DatabaseTable, id: unknown): Promise<boolean> {
765
841
  const recordId = this.normalizeRecordId(id, table)
766
- const client = await this.ensureConnected()
767
842
 
768
843
  try {
769
- const matched = await client.select<{ id: unknown }>(new Table(table)).where(eq('id', recordId)).limit(1)
770
- 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')) {
771
848
  return false
772
849
  }
773
- await client.delete<unknown>(recordId).output('before')
774
- return true
775
- } catch (error) {
776
850
  return this.toSurrealError(error, `DELETE ${recordId.toString()}`)
777
851
  }
778
852
  }
779
853
 
780
854
  async deleteWhere(table: DatabaseTable, filter: Record<string, unknown>): Promise<number> {
855
+ this.assertValidIdentifier(table, 'table name')
781
856
  const filterKeys = Object.keys(filter)
782
857
  if (filterKeys.length === 0) {
783
858
  throw new SurrealDBError(`Refusing to delete all records in ${table} without a filter`)
784
859
  }
785
-
786
- const selection = this.buildFilterExpression(filter)
787
- if (!selection) {
788
- throw new SurrealDBError(`Invalid delete filter for table ${table}`)
789
- }
790
-
791
- const client = await this.ensureConnected()
860
+ const { clause, bindings } = this.buildBoundFilterClauses(filter)
792
861
 
793
862
  try {
794
- const matched = await client.select<{ id: unknown }>(new Table(table)).where(selection)
795
- if (matched.length === 0) {
796
- return 0
797
- }
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
+ }>
798
867
 
799
- for (const row of matched) {
800
- const id = this.normalizeRecordId(row.id, table)
801
- await client.delete(id)
802
- }
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
+ }
803
875
 
804
- return matched.length
876
+ return matched.length
877
+ })
805
878
  } catch (error) {
806
879
  return this.toSurrealError(error, `DELETE ${table} WHERE ...`)
807
880
  }
@@ -939,6 +1012,7 @@ export const databaseService = new Proxy({} as SurrealDBService, {
939
1012
  const resolved = resolveConfiguredDatabaseService()
940
1013
  const value: unknown = Reflect.get(resolved, property)
941
1014
  if (typeof value === 'function') {
1015
+ // SurrealDB SDK type gap — Reflect.get returns unknown, but we know the resolved target shape
942
1016
  return bindTargetMethod(
943
1017
  resolved as unknown as Record<PropertyKey, unknown>,
944
1018
  value as (...args: unknown[]) => unknown,
package/src/db/startup.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { recordIdSchema } from '@lota-sdk/shared'
1
2
  import { BoundQuery, RecordId } from 'surrealdb'
2
3
  import { z } from 'zod'
3
4
 
@@ -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
  }
@@ -0,0 +1,43 @@
1
+ export type SurrealSetClause = { bindingEntries: Record<string, unknown>; statement: string }
2
+
3
+ export function buildRequiredSurrealSetClause(field: string, bindingName: string, value: unknown): SurrealSetClause {
4
+ return { statement: `${field} = $${bindingName}`, bindingEntries: { [bindingName]: value } }
5
+ }
6
+
7
+ export function buildOptionalSurrealSetClause(field: string, value: unknown): SurrealSetClause
8
+ export function buildOptionalSurrealSetClause(field: string, bindingName: string, value: unknown): SurrealSetClause
9
+ export function buildOptionalSurrealSetClause(
10
+ field: string,
11
+ bindingNameOrValue: unknown,
12
+ value?: unknown,
13
+ ): SurrealSetClause {
14
+ const hasExplicitBinding = arguments.length === 3
15
+ const resolvedValue = hasExplicitBinding ? value : bindingNameOrValue
16
+ const resolvedBindingName = hasExplicitBinding
17
+ ? (bindingNameOrValue as string)
18
+ : `resource_${field.replace(/[^a-zA-Z0-9]+/g, '_')}`
19
+
20
+ if (resolvedValue === null || resolvedValue === undefined) {
21
+ return { statement: `${field} = NONE`, bindingEntries: {} }
22
+ }
23
+
24
+ return buildRequiredSurrealSetClause(field, resolvedBindingName, resolvedValue)
25
+ }
26
+
27
+ export function combineSurrealSetClauses(...clauses: SurrealSetClause[]): {
28
+ bindings: Record<string, unknown>
29
+ statement: string
30
+ } {
31
+ const bindings: Record<string, unknown> = {}
32
+ for (const clause of clauses) {
33
+ for (const [key, value] of Object.entries(clause.bindingEntries)) {
34
+ bindings[key] = value
35
+ }
36
+ }
37
+
38
+ return { bindings, statement: clauses.map((clause) => clause.statement).join(', ') }
39
+ }
40
+
41
+ export function compactDefinedRecord<T extends Record<string, unknown>>(value: T): Partial<T> {
42
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>
43
+ }
package/src/db/tables.ts CHANGED
@@ -17,11 +17,18 @@ export const TABLES = {
17
17
  PLAN_CHECKPOINT: 'planCheckpoint',
18
18
  PLAN_VALIDATION_ISSUE: 'planValidationIssue',
19
19
  PLAN_EVENT: 'planEvent',
20
+ PLAN_SCHEDULE: 'planSchedule',
21
+ PLAN_TEMPLATE: 'planTemplate',
22
+ PLAN_CYCLE: 'planCycle',
20
23
  ORGANIZATION: 'organization',
21
24
  ORGANIZATION_MEMBER: 'organizationMember',
22
25
  USER: 'user',
23
26
  RECENT_ACTIVITY_EVENT: 'recentActivityEvent',
24
27
  RECENT_ACTIVITY: 'recentActivity',
28
+ PLAYBOOK: 'playbook',
29
+ PLAYBOOK_VERSION: 'playbookVersion',
30
+ INSTITUTIONAL_MEMORY: 'institutionalMemory',
31
+ QUALITY_METRIC: 'qualityMetric',
25
32
  } as const
26
33
 
27
34
  export type DatabaseTable = (typeof TABLES)[keyof typeof TABLES] | (string & {})
@@ -0,0 +1,15 @@
1
+ import { recordIdSchema } from '@lota-sdk/shared'
2
+ import { z } from 'zod'
3
+
4
+ export const WorkstreamMessageRowSchema = z.object({
5
+ id: recordIdSchema,
6
+ workstreamId: recordIdSchema,
7
+ messageId: z.string(),
8
+ role: z.enum(['system', 'user', 'assistant']),
9
+ parts: z.array(z.record(z.string(), z.unknown())).optional(),
10
+ metadata: z.record(z.string(), z.unknown()).nullish(),
11
+ createdAt: z.coerce.date(),
12
+ updatedAt: z.coerce.date().optional(),
13
+ })
14
+
15
+ export type WorkstreamMessageRow = z.infer<typeof WorkstreamMessageRowSchema>