@lota-sdk/core 0.1.14 → 0.1.15

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 (95) hide show
  1. package/package.json +5 -5
  2. package/src/ai/embedding-cache.ts +7 -6
  3. package/src/ai/index.ts +1 -0
  4. package/src/bifrost/bifrost.ts +12 -7
  5. package/src/config/agent-defaults.ts +1 -1
  6. package/src/config/logger.ts +7 -9
  7. package/src/{runtime.ts → create-runtime.ts} +6 -6
  8. package/src/db/cursor-pagination.ts +1 -1
  9. package/src/db/memory-store.ts +10 -6
  10. package/src/db/memory.ts +6 -4
  11. package/src/db/schema-fingerprint.ts +1 -0
  12. package/src/db/service.ts +40 -43
  13. package/src/db/startup.ts +3 -3
  14. package/src/index.ts +1 -1
  15. package/src/queues/context-compaction.queue.ts +4 -8
  16. package/src/queues/document-processor.queue.ts +7 -7
  17. package/src/queues/memory-consolidation.queue.ts +7 -8
  18. package/src/queues/post-chat-memory.queue.ts +2 -6
  19. package/src/queues/recent-activity-title-refinement.queue.ts +2 -6
  20. package/src/queues/regular-chat-memory-digest.queue.ts +4 -7
  21. package/src/queues/skill-extraction.queue.ts +4 -7
  22. package/src/queues/workstream-title-generation.queue.ts +2 -6
  23. package/src/redis/connection.ts +6 -3
  24. package/src/redis/index.ts +1 -0
  25. package/src/redis/org-memory-lock.ts +1 -1
  26. package/src/redis/redis-lease-lock.ts +41 -8
  27. package/src/runtime/agent-stream-helpers.ts +2 -1
  28. package/src/runtime/context-compaction-constants.ts +1 -1
  29. package/src/runtime/context-compaction-runtime.ts +6 -4
  30. package/src/runtime/context-compaction.ts +19 -38
  31. package/src/runtime/execution-plan.ts +2 -2
  32. package/src/runtime/helper-model.ts +3 -1
  33. package/src/runtime/index.ts +12 -1
  34. package/src/runtime/memory-block.ts +3 -2
  35. package/src/runtime/memory-pipeline.ts +24 -5
  36. package/src/runtime/plugin-types.ts +1 -1
  37. package/src/runtime/runtime-extensions.ts +89 -13
  38. package/src/runtime/title-helpers.ts +11 -2
  39. package/src/runtime/workstream-chat-helpers.ts +5 -6
  40. package/src/runtime/workstream-routing-policy.ts +0 -30
  41. package/src/runtime/workstream-state.ts +17 -7
  42. package/src/services/attachment.service.ts +1 -1
  43. package/src/services/context-compaction.service.ts +3 -3
  44. package/src/services/document-chunk.service.ts +37 -32
  45. package/src/services/execution-plan.service.ts +2 -0
  46. package/src/services/learned-skill.service.ts +6 -10
  47. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -8
  48. package/src/services/memory.service.ts +21 -18
  49. package/src/services/organization-member.service.ts +1 -1
  50. package/src/services/plan-artifact.service.ts +1 -0
  51. package/src/services/plan-executor.service.ts +2 -18
  52. package/src/services/plan-helpers.ts +15 -0
  53. package/src/services/plan-validator.service.ts +3 -18
  54. package/src/services/recent-activity-title.service.ts +3 -10
  55. package/src/services/recent-activity.service.ts +6 -12
  56. package/src/services/workstream-message.service.ts +26 -16
  57. package/src/services/workstream-title.service.ts +1 -9
  58. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +401 -314
  59. package/src/services/workstream-turn.ts +2 -2
  60. package/src/services/workstream.service.ts +22 -10
  61. package/src/services/workstream.types.ts +7 -16
  62. package/src/storage/attachment-storage.service.ts +4 -4
  63. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +1 -4
  64. package/src/storage/index.ts +2 -2
  65. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  66. package/src/system-agents/delegated-agent-factory.ts +3 -2
  67. package/src/system-agents/index.ts +8 -0
  68. package/src/system-agents/memory-reranker.agent.ts +1 -1
  69. package/src/system-agents/memory.agent.ts +1 -1
  70. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  71. package/src/tools/execution-plan.tool.ts +6 -2
  72. package/src/tools/fetch-webpage.tool.ts +20 -18
  73. package/src/tools/index.ts +2 -2
  74. package/src/tools/read-file-parts.tool.ts +1 -1
  75. package/src/tools/search-web.tool.ts +18 -15
  76. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  77. package/src/tools/team-think.tool.ts +9 -5
  78. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  79. package/src/utils/async.ts +1 -1
  80. package/src/utils/errors.ts +15 -0
  81. package/src/utils/hono-error-handler.ts +1 -2
  82. package/src/utils/index.ts +10 -2
  83. package/src/utils/string.ts +14 -0
  84. package/src/workers/bootstrap.ts +2 -2
  85. package/src/workers/memory-consolidation.worker.ts +12 -12
  86. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  87. package/src/workers/regular-chat-memory-digest.runner.ts +9 -103
  88. package/src/workers/skill-extraction.runner.ts +7 -101
  89. package/src/workers/utils/file-section-chunker.ts +5 -3
  90. package/src/workers/utils/workstream-message-query.ts +106 -0
  91. package/src/workers/worker-utils.ts +4 -0
  92. package/src/runtime/retrieval-pipeline.ts +0 -3
  93. package/src/utils/error.ts +0 -10
  94. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  95. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -28,19 +28,19 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@ai-sdk/devtools": "^0.0.15",
31
- "@ai-sdk/openai": "^3.0.41",
31
+ "@ai-sdk/openai": "^3.0.47",
32
32
  "@logtape/logtape": "^2.0.4",
33
- "@lota-sdk/shared": "0.1.14",
33
+ "@lota-sdk/shared": "0.1.15",
34
34
  "@mendable/firecrawl-js": "^4.16.0",
35
35
  "@surrealdb/node": "^3.0.3",
36
- "ai": "^6.0.116",
36
+ "ai": "^6.0.134",
37
37
  "bullmq": "^5.71.0",
38
38
  "hono": "^4.12.8",
39
39
  "ioredis": "5.9.3",
40
40
  "mammoth": "^1.12.0",
41
41
  "pdf-parse": "^2.4.5",
42
42
  "resumable-stream": "^2.2.12",
43
- "surrealdb": "^2.0.2",
43
+ "surrealdb": "^2.0.3",
44
44
  "zod": "^4.3.6"
45
45
  }
46
46
  }
@@ -4,17 +4,18 @@ import type IORedis from 'ioredis'
4
4
 
5
5
  import { aiLogger } from '../config/logger'
6
6
 
7
- const DEFAULT_TTL_SECONDS = 3600
7
+ export const DEFAULT_EMBEDDING_CACHE_TTL_SECONDS = 3600
8
+ const EMBEDDING_CACHE_KEY_PREFIX = 'emb'
8
9
 
9
10
  export class EmbeddingCache {
10
11
  constructor(
11
12
  private redis: IORedis,
12
- private ttlSeconds: number = DEFAULT_TTL_SECONDS,
13
+ private ttlSeconds: number = DEFAULT_EMBEDDING_CACHE_TTL_SECONDS,
13
14
  ) {}
14
15
 
15
16
  private buildKey(model: string, text: string): string {
16
17
  const hash = createHash('sha256').update(text).digest('hex')
17
- return `emb:${model}:${hash}`
18
+ return `${EMBEDDING_CACHE_KEY_PREFIX}:${model}:${hash}`
18
19
  }
19
20
 
20
21
  async get(model: string, text: string): Promise<number[] | null> {
@@ -23,7 +24,7 @@ export class EmbeddingCache {
23
24
  if (!cached) return null
24
25
  return JSON.parse(cached.toString()) as number[]
25
26
  } catch (error) {
26
- aiLogger.debug`Embedding cache get failed: ${error}`
27
+ aiLogger.warn`Embedding cache get failed: ${error}`
27
28
  return null
28
29
  }
29
30
  }
@@ -32,7 +33,7 @@ export class EmbeddingCache {
32
33
  try {
33
34
  await this.redis.set(this.buildKey(model, text), JSON.stringify(embedding), 'EX', this.ttlSeconds)
34
35
  } catch (error) {
35
- aiLogger.debug`Embedding cache set failed: ${error}`
36
+ aiLogger.warn`Embedding cache set failed: ${error}`
36
37
  }
37
38
  }
38
39
  }
@@ -40,7 +41,7 @@ export class EmbeddingCache {
40
41
  let embeddingCacheInstance: EmbeddingCache | null = null
41
42
 
42
43
  export function configureEmbeddingCache(redis: IORedis, ttlSeconds?: number): void {
43
- embeddingCacheInstance = new EmbeddingCache(redis, ttlSeconds ?? DEFAULT_TTL_SECONDS)
44
+ embeddingCacheInstance = new EmbeddingCache(redis, ttlSeconds ?? DEFAULT_EMBEDDING_CACHE_TTL_SECONDS)
44
45
  }
45
46
 
46
47
  export function getEmbeddingCache(): EmbeddingCache | null {
package/src/ai/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './definitions'
2
2
  export * from './embedding-cache'
3
+ // Re-exported for backwards compatibility — embeddings provider is part of the AI module surface
3
4
  export * from '../embeddings/provider'
@@ -5,6 +5,7 @@ import type { LanguageModelMiddleware } from 'ai'
5
5
 
6
6
  import { isRecord, readString } from '../utils/string'
7
7
 
8
+ type BifrostLanguageModel = Parameters<typeof wrapLanguageModel>[0]['model']
8
9
  type BifrostExtraParams = Record<string, unknown>
9
10
  type BifrostChatResponse = { body?: unknown }
10
11
  type BifrostTransformParamsOptions = Parameters<NonNullable<LanguageModelMiddleware['transformParams']>>[0]
@@ -131,7 +132,7 @@ export function injectBifrostChatReasoningStream(
131
132
  const closeReasoning = () => {
132
133
  if (!reasoningOpen || reasoningClosed) return
133
134
 
134
- controller.enqueue({ type: 'reasoning-end', id: reasoningId } as BifrostStreamPart)
135
+ controller.enqueue({ type: 'reasoning-end', id: reasoningId } satisfies BifrostStreamPart)
135
136
  reasoningClosed = true
136
137
  }
137
138
 
@@ -141,11 +142,15 @@ export function injectBifrostChatReasoningStream(
141
142
 
142
143
  if (reasoningDelta) {
143
144
  if (!reasoningOpen) {
144
- controller.enqueue({ type: 'reasoning-start', id: reasoningId } as BifrostStreamPart)
145
+ controller.enqueue({ type: 'reasoning-start', id: reasoningId } satisfies BifrostStreamPart)
145
146
  reasoningOpen = true
146
147
  }
147
148
 
148
- controller.enqueue({ type: 'reasoning-delta', id: reasoningId, delta: reasoningDelta } as BifrostStreamPart)
149
+ controller.enqueue({
150
+ type: 'reasoning-delta',
151
+ id: reasoningId,
152
+ delta: reasoningDelta,
153
+ } satisfies BifrostStreamPart)
149
154
  }
150
155
  return
151
156
  }
@@ -158,7 +163,7 @@ export function injectBifrostChatReasoningStream(
158
163
  },
159
164
  flush(controller) {
160
165
  if (!reasoningOpen || reasoningClosed) return
161
- controller.enqueue({ type: 'reasoning-end', id: reasoningId } as BifrostStreamPart)
166
+ controller.enqueue({ type: 'reasoning-end', id: reasoningId } satisfies BifrostStreamPart)
162
167
  },
163
168
  }),
164
169
  )
@@ -182,7 +187,7 @@ export function injectBifrostResponsesReasoningStream(
182
187
  id: reasoningDelta.id,
183
188
  delta: reasoningDelta.delta,
184
189
  providerMetadata: { openai: { itemId: reasoningDelta.itemId } },
185
- } as BifrostStreamPart)
190
+ } satisfies BifrostStreamPart)
186
191
  },
187
192
  }),
188
193
  )
@@ -242,12 +247,12 @@ function createBifrostProvider(extraParams?: BifrostExtraParams) {
242
247
  })
243
248
  }
244
249
 
245
- function withBifrostDevTools<TModel>(model: TModel): TModel {
250
+ function withBifrostDevTools<TModel extends BifrostLanguageModel>(model: TModel): TModel {
246
251
  if (process.env.NODE_ENV === 'production') {
247
252
  return model
248
253
  }
249
254
 
250
- return wrapLanguageModel({ model: model as never, middleware: devToolsMiddleware() }) as unknown as TModel
255
+ return wrapLanguageModel({ model, middleware: devToolsMiddleware() }) as TModel
251
256
  }
252
257
 
253
258
  let provider: ReturnType<typeof createOpenAI> | null = null
@@ -51,7 +51,7 @@ export function configureAgents(config: {
51
51
  }
52
52
  }
53
53
 
54
- export function isAgentName(value: unknown): boolean {
54
+ export function isAgentName(value: unknown): value is string {
55
55
  return typeof value === 'string' && new Set(agentRoster).has(value)
56
56
  }
57
57
 
@@ -1,7 +1,9 @@
1
1
  import type { LogLevel } from '@logtape/logtape'
2
2
  import { configure, getAnsiColorFormatter, getConsoleSink, getLogger as getLogTapeLogger } from '@logtape/logtape'
3
3
 
4
- export async function configureLotaLogger(logLevel: LogLevel): Promise<void> {
4
+ const LOG_CATEGORY = 'lota-sdk'
5
+
6
+ export async function configureLotaLogger(logLevel: LogLevel = 'info'): Promise<void> {
5
7
  const formatter = getAnsiColorFormatter({ level: 'FULL' })
6
8
 
7
9
  await configure({
@@ -10,7 +12,7 @@ export async function configureLotaLogger(logLevel: LogLevel): Promise<void> {
10
12
  loggers: [
11
13
  { category: ['logtape', 'meta'], lowestLevel: 'warning', sinks: ['console'] },
12
14
  { category: ['server'], lowestLevel: logLevel, sinks: ['console'] },
13
- { category: ['lota-sdk'], lowestLevel: logLevel, sinks: ['console'] },
15
+ { category: [LOG_CATEGORY], lowestLevel: logLevel, sinks: ['console'] },
14
16
  { category: ['hono'], lowestLevel: logLevel, sinks: ['console'] },
15
17
  ],
16
18
  })
@@ -20,10 +22,6 @@ export function getLogger(category: readonly string[]) {
20
22
  return getLogTapeLogger([...category])
21
23
  }
22
24
 
23
- export async function configureLogger(logLevel?: LogLevel): Promise<void> {
24
- await configureLotaLogger(logLevel ?? 'info')
25
- }
26
-
27
- export const serverLogger = getLogger(['lota-sdk'])
28
- export const chatLogger = getLogger(['lota-sdk', 'chat'])
29
- export const aiLogger = getLogger(['lota-sdk', 'ai'])
25
+ export const serverLogger = getLogger([LOG_CATEGORY])
26
+ export const chatLogger = getLogger([LOG_CATEGORY, 'chat'])
27
+ export const aiLogger = getLogger([LOG_CATEGORY, 'ai'])
@@ -1,7 +1,9 @@
1
+ import type { ChatMessage } from '@lota-sdk/shared'
2
+
1
3
  import { configureEmbeddingCache } from './ai/embedding-cache'
2
4
  import { configureAgentFactory, configureAgents } from './config/agent-defaults'
3
5
  import { configureBackgroundProcessing } from './config/background-processing'
4
- import { configureLogger } from './config/logger'
6
+ import { configureLotaLogger } from './config/logger'
5
7
  import { configureWorkstreams } from './config/workstream-defaults'
6
8
  import { ensureRecordId } from './db/record-id'
7
9
  import { computeSchemaFingerprint } from './db/schema-fingerprint'
@@ -76,7 +78,6 @@ type UnarchiveSdkWorkstream = (
76
78
  export interface LotaRuntime {
77
79
  services: {
78
80
  database: SurrealDBService
79
- databaseService: SurrealDBService
80
81
  redis: RedisConnectionManager
81
82
  closeRedisConnection: () => Promise<void>
82
83
  attachmentService: typeof attachmentService
@@ -132,7 +133,7 @@ export interface LotaRuntime {
132
133
  delete: typeof workstreamServiceSingleton.deleteWorkstream
133
134
  stop: typeof workstreamServiceSingleton.stopActiveRun
134
135
  listMessages: typeof workstreamMessageServiceSingleton.listMessageHistoryPage
135
- getMessage: (params: { workstreamId: string; messageId: string }) => Promise<unknown>
136
+ getMessage: (params: { workstreamId: string; messageId: string }) => Promise<ChatMessage>
136
137
  sendMessage: (params: {
137
138
  workstreamId: string
138
139
  organizationId: string
@@ -170,7 +171,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
170
171
  const resolvedConfig = parseLotaRuntimeConfig(config)
171
172
  configureRuntimeConfig(resolvedConfig)
172
173
 
173
- await configureLogger(resolvedConfig.logging.level)
174
+ await configureLotaLogger(resolvedConfig.logging.level)
174
175
 
175
176
  const db = new SurrealDBServiceClass({
176
177
  url: resolvedConfig.database.url,
@@ -306,7 +307,6 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
306
307
  return {
307
308
  services: {
308
309
  database: db,
309
- databaseService: db,
310
310
  redis: redisManager,
311
311
  closeRedisConnection: async () => await redisManager.closeConnection(),
312
312
  attachmentService: attachmentServiceSingleton,
@@ -375,7 +375,7 @@ function getBuiltInSchemaFiles(): URL[] {
375
375
  function createPluginDatabaseConnector(pluginRuntime: Record<string, LotaPlugin>): () => Promise<void> {
376
376
  return async () => {
377
377
  for (const plugin of Object.values(pluginRuntime)) {
378
- const services = plugin.services as Record<string, unknown>
378
+ const services = plugin.services
379
379
  const connectDatabase = services.connectDatabase
380
380
  if (typeof connectDatabase !== 'function') {
381
381
  continue
@@ -64,7 +64,7 @@ async function listRowsBefore(
64
64
  throw new Error(`Cursor message not found in ${config.table}: ${params.beforeMessageId}`)
65
65
  }
66
66
 
67
- const cursorCreatedAt = new Date(toTimestamp(cursorRow.createdAt))
67
+ const cursorCreatedAt = new Date(toTimestamp(cursorRow.createdAt) ?? Date.now())
68
68
  const cursorId = config.toRowId(params.parentId, params.beforeMessageId)
69
69
 
70
70
  return await databaseService.query<unknown>(
@@ -24,6 +24,10 @@ const MEMORY_TABLE = TABLES.MEMORY
24
24
  const MEMORY_HISTORY_TABLE = TABLES.MEMORY_HISTORY
25
25
  const MEMORY_RELATION_TABLE = TABLES.MEMORY_RELATION
26
26
  const MIN_RELEVANCE_SCORE = 0.25
27
+ const STRONG_GRAPH_BOOSTS = { support: 0.1, contradict: 0.2 } as const
28
+ const WEAK_GRAPH_BOOSTS = { support: 0.05, contradict: 0.1 } as const
29
+ const CANDIDATE_FANOUT_MULTIPLIER = 4
30
+ const CANDIDATE_SLICE_FLOOR = 50
27
31
  const TOUCH_MEMORIES_MAX_ATTEMPTS = 4
28
32
  const TOUCH_MEMORIES_RETRY_BASE_DELAY_MS = 25
29
33
  const TOUCH_MEMORIES_RETRY_JITTER_MS = 20
@@ -153,7 +157,7 @@ export class SurrealMemoryStore {
153
157
  relationCounts,
154
158
  options.limit,
155
159
  (row) => 1 / (1 + row.distance),
156
- { support: 0.1, contradict: 0.2 },
160
+ STRONG_GRAPH_BOOSTS,
157
161
  MIN_RELEVANCE_SCORE,
158
162
  )
159
163
 
@@ -317,7 +321,7 @@ export class SurrealMemoryStore {
317
321
  if (b.textScore !== a.textScore) return b.textScore - a.textScore
318
322
  return a.index - b.index
319
323
  })
320
- .slice(0, Math.max(options.limit * 4, 50))
324
+ .slice(0, Math.max(options.limit * CANDIDATE_FANOUT_MULTIPLIER, CANDIDATE_SLICE_FLOOR))
321
325
 
322
326
  if (options.fastMode) {
323
327
  return this.mapFastRows(scoredRows, options.limit, (row) => row.textScore)
@@ -330,7 +334,7 @@ export class SurrealMemoryStore {
330
334
  recentRelationCounts,
331
335
  options.limit,
332
336
  (row) => row.textScore,
333
- { support: 0.05, contradict: 0.1 },
337
+ WEAK_GRAPH_BOOSTS,
334
338
  MIN_RELEVANCE_SCORE,
335
339
  )
336
340
 
@@ -483,7 +487,7 @@ export class SurrealMemoryStore {
483
487
  relationCounts,
484
488
  limit,
485
489
  (row) => 1 / (1 + row.distance),
486
- { support: 0.1, contradict: 0.2 },
490
+ STRONG_GRAPH_BOOSTS,
487
491
  MIN_RELEVANCE_SCORE,
488
492
  )
489
493
 
@@ -520,7 +524,7 @@ export class SurrealMemoryStore {
520
524
  relationCounts,
521
525
  limit,
522
526
  (row) => row.rrfScore,
523
- { support: 0.05, contradict: 0.1 },
527
+ WEAK_GRAPH_BOOSTS,
524
528
  MIN_RELEVANCE_SCORE,
525
529
  )
526
530
 
@@ -619,7 +623,7 @@ export class SurrealMemoryStore {
619
623
  relationCounts,
620
624
  options.limit,
621
625
  (row) => row.linearScore,
622
- { support: 0.05, contradict: 0.1 },
626
+ WEAK_GRAPH_BOOSTS,
623
627
  MIN_RELEVANCE_SCORE,
624
628
  )
625
629
 
package/src/db/memory.ts CHANGED
@@ -12,6 +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
16
  import type { SurrealMemoryStore } from './memory-store'
16
17
  import { getDefaultMemoryStore } from './memory-store'
17
18
  import { hashContent, isUniqueIndexConflict } from './memory-store.helpers'
@@ -39,6 +40,7 @@ const MEMORY_DELTA_MAX_CANDIDATE_MEMORIES = 80
39
40
  const MEMORY_DELTA_MAX_CANDIDATES_PER_FACT = 10
40
41
  const MEMORY_DELTA_MIN_BASELINE_CANDIDATES = 12
41
42
  const MEMORY_DELTA_MEMORY_TEXT_MAX_CHARS = 400
43
+ const CANDIDATE_FANOUT_MULTIPLIER = 4
42
44
  const helperModelRuntime = createHelperModelRuntime()
43
45
 
44
46
  interface PreparedScopeUpdate {
@@ -277,7 +279,7 @@ export class Memory {
277
279
  typeof extractionOptions?.maxFacts === 'number' ? { maxFacts: extractionOptions.maxFacts } : {},
278
280
  )
279
281
  } catch (error) {
280
- aiLogger.error`Failed to extract facts: ${error}`
282
+ aiLogger.warn`Failed to extract facts: ${error}`
281
283
  return []
282
284
  }
283
285
  }
@@ -313,7 +315,7 @@ export class Memory {
313
315
  }
314
316
 
315
317
  private normalizeMemoryDeltaText(value: string, maxChars?: number): string {
316
- const normalized = value.replace(/\s+/g, ' ').trim()
318
+ const normalized = compactWhitespace(value)
317
319
  if (!normalized) return ''
318
320
  if (typeof maxChars !== 'number' || normalized.length <= maxChars) return normalized
319
321
  return `${normalized.slice(0, maxChars - 3)}...`
@@ -384,7 +386,7 @@ export class Memory {
384
386
  const targetCandidateCount = Math.min(MEMORY_DELTA_MAX_CANDIDATE_MEMORIES, normalizedExisting.length)
385
387
  const baselineCount = Math.min(
386
388
  targetCandidateCount,
387
- Math.max(MEMORY_DELTA_MIN_BASELINE_CANDIDATES, newFacts.length * 4),
389
+ Math.max(MEMORY_DELTA_MIN_BASELINE_CANDIDATES, newFacts.length * CANDIDATE_FANOUT_MULTIPLIER),
388
390
  )
389
391
 
390
392
  const selectedIds = new Set<string>(
@@ -502,7 +504,7 @@ export class Memory {
502
504
  await this.store.addRelation(fromId, toId, relation.relation)
503
505
  aiLogger.debug`Created ${relation.relation} relation: ${fromId} -> ${toId}`
504
506
  } catch (error) {
505
- aiLogger.warn`Failed to create relation: ${error}`
507
+ aiLogger.warn`Failed to create memory relation (non-fatal, graph may be incomplete): ${error}`
506
508
  }
507
509
  }
508
510
  }
@@ -7,6 +7,7 @@ function toSchemaFilePath(value: string | URL): string {
7
7
  export async function computeSchemaFingerprint(schemaFiles: readonly (string | URL)[]): Promise<string> {
8
8
  const hash = createHash('sha256')
9
9
 
10
+ // Sequential reads required: hash must be computed in deterministic file order
10
11
  for (const schemaFile of schemaFiles) {
11
12
  const sortKey = toSchemaFilePath(schemaFile)
12
13
  const file = schemaFile instanceof URL ? Bun.file(schemaFile.pathname) : Bun.file(schemaFile)
package/src/db/service.ts CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  import type { ExprLike, Mutation, SurrealTransaction, Values } from 'surrealdb'
13
13
  import type { z } from 'zod'
14
14
 
15
+ import { withTimeout } from '../utils/async'
16
+ import { isRecord } from '../utils/string'
15
17
  import type { RecordIdInput } from './record-id'
16
18
  import { ensureRecordId, readCustomStringValue } from './record-id'
17
19
  import type { DatabaseTable } from './tables'
@@ -88,12 +90,8 @@ function configureMutation(
88
90
  return builder.merge(data)
89
91
  }
90
92
 
91
- function isRecordValue(value: unknown): value is Record<string, unknown> {
92
- return typeof value === 'object' && value !== null && !Array.isArray(value)
93
- }
94
-
95
93
  function isBoundQueryLike(value: unknown): value is BoundQueryLike {
96
- if (!isRecordValue(value)) {
94
+ if (!isRecord(value)) {
97
95
  return false
98
96
  }
99
97
 
@@ -101,7 +99,7 @@ function isBoundQueryLike(value: unknown): value is BoundQueryLike {
101
99
  return false
102
100
  }
103
101
 
104
- return value.bindings === undefined || isRecordValue(value.bindings)
102
+ return value.bindings === undefined || isRecord(value.bindings)
105
103
  }
106
104
 
107
105
  function toStringLikeValue(value: unknown): string | null {
@@ -122,23 +120,6 @@ const CONNECT_RETRY_BASE_DELAY_MS = 100
122
120
  const CONNECT_RETRY_JITTER_MS = 50
123
121
  const CONNECT_ATTEMPT_TIMEOUT_MS = 5_000
124
122
 
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
123
  export class SurrealDBService {
143
124
  private client: Surreal | null = null
144
125
  private isConnected = false
@@ -252,7 +233,7 @@ export class SurrealDBService {
252
233
  : { username: this.config.username ?? '', password: this.config.password ?? '' },
253
234
  }),
254
235
  CONNECT_ATTEMPT_TIMEOUT_MS,
255
- `Timed out connecting to SurrealDB at ${this.config.url}`,
236
+ `SurrealDB connect (${this.config.url})`,
256
237
  )
257
238
 
258
239
  this.isConnected = true
@@ -278,7 +259,7 @@ export class SurrealDBService {
278
259
  }
279
260
  }
280
261
 
281
- this.toSurrealError(lastError)
262
+ return this.toSurrealError(lastError)
282
263
  })()
283
264
 
284
265
  try {
@@ -327,8 +308,16 @@ export class SurrealDBService {
327
308
 
328
309
  private normalizeRecordId(id: unknown, table: DatabaseTable): ReturnType<typeof ensureRecordId> {
329
310
  try {
330
- return ensureRecordId(id as RecordIdInput, table)
311
+ const recordId = ensureRecordId(id as RecordIdInput, table)
312
+ const resolvedTable = String(recordId.table)
313
+ if (resolvedTable !== table) {
314
+ throw new SurrealDBError(`Record id table mismatch: expected "${table}" but got "${resolvedTable}"`)
315
+ }
316
+ return recordId
331
317
  } catch (error) {
318
+ if (error instanceof SurrealDBError) {
319
+ throw error
320
+ }
332
321
  if (error instanceof Error) {
333
322
  throw new SurrealDBError(`Invalid record id for table "${table}": ${error.message}`, undefined, {
334
323
  cause: error,
@@ -392,7 +381,7 @@ export class SurrealDBService {
392
381
  return value.map((entry) => this.normalizeRuntimeValue(entry))
393
382
  }
394
383
 
395
- if (!isRecordValue(value)) {
384
+ if (!isRecord(value)) {
396
385
  return value
397
386
  }
398
387
 
@@ -413,6 +402,8 @@ export class SurrealDBService {
413
402
  return Object.fromEntries(entries.map(([key, entryValue]) => [key, this.normalizeRuntimeValue(entryValue)]))
414
403
  }
415
404
 
405
+ // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
406
+ // (non-array objects are mapped entry-by-entry and returned as Object.fromEntries)
416
407
  private normalizeBindings(bindings?: Record<string, unknown>): Record<string, unknown> | undefined {
417
408
  if (!bindings) {
418
409
  return undefined
@@ -421,6 +412,7 @@ export class SurrealDBService {
421
412
  return this.normalizeRuntimeValue(bindings) as Record<string, unknown>
422
413
  }
423
414
 
415
+ // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
424
416
  private normalizeMutationData(data: Record<string, unknown>): Record<string, unknown> {
425
417
  const normalized = this.normalizeRuntimeValue(data) as Record<string, unknown>
426
418
  return this.stripNullValues(normalized)
@@ -512,9 +504,11 @@ export class SurrealDBService {
512
504
  create: (target: unknown) => {
513
505
  const normalizedTarget = this.normalizeCreateTarget(target)
514
506
  const builder = normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
507
+ // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal CreateMutationBuilder interface
515
508
  return this.wrapCreateBuilder(builder as unknown as CreateMutationBuilder)
516
509
  },
517
510
  update: (target: unknown) =>
511
+ // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal MutationBuilder interface
518
512
  this.wrapMutationBuilder(tx.update(ensureRecordId(target as RecordIdInput)) as unknown as MutationBuilder),
519
513
  delete: (target: unknown) => tx.delete(ensureRecordId(target as RecordIdInput)),
520
514
  relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
@@ -556,7 +550,7 @@ export class SurrealDBService {
556
550
  return this.normalizeQueryRows<T>(response.result, schema)
557
551
  })
558
552
  } catch (error) {
559
- this.toSurrealError(error, queryText)
553
+ return this.toSurrealError(error, queryText)
560
554
  }
561
555
  }
562
556
 
@@ -604,7 +598,7 @@ export class SurrealDBService {
604
598
  const first = rows.at(0)
605
599
  return first ? schema.parse(first) : null
606
600
  } catch (error) {
607
- this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
601
+ return this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
608
602
  }
609
603
  }
610
604
 
@@ -656,7 +650,7 @@ export class SurrealDBService {
656
650
  const rows = await query
657
651
  return rows.map((row) => schema.parse(row))
658
652
  } catch (error) {
659
- this.toSurrealError(error, `SELECT * FROM ${table}`)
653
+ return this.toSurrealError(error, `SELECT * FROM ${table}`)
660
654
  }
661
655
  }
662
656
 
@@ -685,7 +679,7 @@ export class SurrealDBService {
685
679
 
686
680
  return schema.parse(first)
687
681
  } catch (error) {
688
- this.toSurrealError(error, `CREATE ${table}`)
682
+ return this.toSurrealError(error, `CREATE ${table}`)
689
683
  }
690
684
  }
691
685
 
@@ -707,7 +701,7 @@ export class SurrealDBService {
707
701
  const created = await client.create<unknown>(recordId).content(this.normalizeMutationData(data)).output('after')
708
702
  return schema.parse(created)
709
703
  } catch (error) {
710
- this.toSurrealError(error, `CREATE ${recordId.toString()}`)
704
+ return this.toSurrealError(error, `CREATE ${recordId.toString()}`)
711
705
  }
712
706
  }
713
707
 
@@ -734,7 +728,7 @@ export class SurrealDBService {
734
728
  const updated = await configured.output('after')
735
729
  return updated ? schema.parse(updated) : null
736
730
  } catch (error) {
737
- this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
731
+ return this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
738
732
  }
739
733
  }
740
734
 
@@ -763,7 +757,7 @@ export class SurrealDBService {
763
757
  }
764
758
  return schema.parse(upserted)
765
759
  } catch (error) {
766
- this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
760
+ return this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
767
761
  }
768
762
  }
769
763
 
@@ -779,7 +773,7 @@ export class SurrealDBService {
779
773
  await client.delete<unknown>(recordId).output('before')
780
774
  return true
781
775
  } catch (error) {
782
- this.toSurrealError(error, `DELETE ${recordId.toString()}`)
776
+ return this.toSurrealError(error, `DELETE ${recordId.toString()}`)
783
777
  }
784
778
  }
785
779
 
@@ -809,7 +803,7 @@ export class SurrealDBService {
809
803
 
810
804
  return matched.length
811
805
  } catch (error) {
812
- this.toSurrealError(error, `DELETE ${table} WHERE ...`)
806
+ return this.toSurrealError(error, `DELETE ${table} WHERE ...`)
813
807
  }
814
808
  }
815
809
 
@@ -835,7 +829,7 @@ export class SurrealDBService {
835
829
  }
836
830
  return 1
837
831
  } catch (error) {
838
- this.toSurrealError(error, `UPDATE ${table} WHERE ...`)
832
+ return this.toSurrealError(error, `UPDATE ${table} WHERE ...`)
839
833
  }
840
834
  }
841
835
 
@@ -854,7 +848,7 @@ export class SurrealDBService {
854
848
  .output('after')
855
849
  return inserted as T[]
856
850
  } catch (error) {
857
- this.toSurrealError(error, `INSERT ${table}`)
851
+ return this.toSurrealError(error, `INSERT ${table}`)
858
852
  }
859
853
  }
860
854
 
@@ -884,7 +878,7 @@ export class SurrealDBService {
884
878
  }
885
879
  return related
886
880
  } catch (error) {
887
- this.toSurrealError(error, `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`)
881
+ return this.toSurrealError(error, `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`)
888
882
  }
889
883
  }
890
884
 
@@ -893,7 +887,7 @@ export class SurrealDBService {
893
887
  try {
894
888
  return this.wrapTransaction(await client.beginTransaction())
895
889
  } catch (error) {
896
- this.toSurrealError(error, 'BEGIN TRANSACTION')
890
+ return this.toSurrealError(error, 'BEGIN TRANSACTION')
897
891
  }
898
892
  }
899
893
 
@@ -942,10 +936,13 @@ export const databaseService = new Proxy({} as SurrealDBService, {
942
936
  return databaseServiceOverrides.get(property)
943
937
  }
944
938
 
945
- const target = resolveConfiguredDatabaseService() as unknown as Record<PropertyKey, unknown>
946
- const value = target[property]
939
+ const resolved = resolveConfiguredDatabaseService()
940
+ const value: unknown = Reflect.get(resolved, property)
947
941
  if (typeof value === 'function') {
948
- return bindTargetMethod(target, value as (...args: unknown[]) => unknown)
942
+ return bindTargetMethod(
943
+ resolved as unknown as Record<PropertyKey, unknown>,
944
+ value as (...args: unknown[]) => unknown,
945
+ )
949
946
  }
950
947
 
951
948
  return value
package/src/db/startup.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BoundQuery, RecordId } from 'surrealdb'
2
2
  import { z } from 'zod'
3
3
 
4
- import type { SurrealDBService, SurrealDatabaseLogger } from '../db/service'
5
- import { TABLES } from '../db/tables'
6
- import { getErrorMessage } from '../utils/error'
4
+ import { getErrorMessage } from '../utils/errors'
5
+ import type { SurrealDBService, SurrealDatabaseLogger } from './service'
6
+ import { TABLES } from './tables'
7
7
 
8
8
  const DATABASE_BOOTSTRAP_KEY = 'database-schema-ready'
9
9
  const DEFAULT_RETRY_DELAY_MS = 1_000
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from './runtime'
1
+ export * from './create-runtime'
2
2
  export * from './ai'
3
3
  export * from './bifrost'
4
4
  export * from './config'