@lota-sdk/core 0.1.13 → 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 +45 -51
  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.13",
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.13",
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)