@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
@@ -15,9 +15,11 @@ import { TABLES } from './db/tables'
15
15
  import type { RedisConnectionManager } from './redis/connection'
16
16
  import { createRedisConnectionManager } from './redis/connection'
17
17
  import { setRedisConnectionManager } from './redis/index'
18
+ import { closeSharedSubscriber } from './redis/stream-context'
18
19
  import type { isApprovalContinuationRequest } from './runtime/approval-continuation'
19
20
  import { routeWorkstreamChatMessages } from './runtime/chat-request-routing'
20
- import type { LotaPlugin } from './runtime/plugin-types'
21
+ import { configureGraphDesigner } from './runtime/graph-designer'
22
+ import type { LotaPlugin, SystemNodeExecutor } from './runtime/plugin-types'
21
23
  import { configureRuntimeConfig, LOTA_RUNTIME_ENV_KEYS, parseLotaRuntimeConfig } from './runtime/runtime-config'
22
24
  import type { LotaRuntimeConfig, ResolvedLotaRuntimeConfig } from './runtime/runtime-config'
23
25
  import { configureRuntimeExtensions } from './runtime/runtime-extensions'
@@ -25,22 +27,27 @@ import type { LotaRuntimeWorkers } from './runtime/runtime-worker-registry'
25
27
  import { buildRuntimeWorkerRegistry } from './runtime/runtime-worker-registry'
26
28
  import type { attachmentService } from './services/attachment.service'
27
29
  import { attachmentService as attachmentServiceSingleton } from './services/attachment.service'
30
+ import { coordinationRegistryService as coordinationRegistryServiceSingleton } from './services/coordination-registry.service'
28
31
  import type { documentChunkService } from './services/document-chunk.service'
29
32
  import { documentChunkService as documentChunkServiceSingleton } from './services/document-chunk.service'
33
+ import { domainAgentExecutorService } from './services/domain-agent-executor.service'
30
34
  import type { executionPlanService } from './services/execution-plan.service'
31
35
  import { executionPlanService as executionPlanServiceSingleton } from './services/execution-plan.service'
32
36
  import type { memoryService } from './services/memory.service'
33
37
  import { memoryService as memoryServiceSingleton } from './services/memory.service'
34
38
  import type { verifyMutatingApproval } from './services/mutating-approval.service'
35
39
  import { verifyMutatingApproval as verifyMutatingApprovalSingleton } from './services/mutating-approval.service'
40
+ import { configureNotificationService } from './services/notification.service'
36
41
  import type { organizationMemberService } from './services/organization-member.service'
37
42
  import { organizationMemberService as organizationMemberServiceSingleton } from './services/organization-member.service'
38
43
  import type { organizationService } from './services/organization.service'
39
44
  import { organizationService as organizationServiceSingleton } from './services/organization.service'
45
+ import { playbookRegistryService } from './services/playbook-registry.service'
40
46
  import type { recentActivityTitleService } from './services/recent-activity-title.service'
41
47
  import { recentActivityTitleService as recentActivityTitleServiceSingleton } from './services/recent-activity-title.service'
42
48
  import type { recentActivityService } from './services/recent-activity.service'
43
49
  import { recentActivityService as recentActivityServiceSingleton } from './services/recent-activity.service'
50
+ import { getBuiltInSystemExecutors } from './services/system-executor.service'
44
51
  import type { userService } from './services/user.service'
45
52
  import { userService as userServiceSingleton } from './services/user.service'
46
53
  import type { workstreamMessageService } from './services/workstream-message.service'
@@ -75,6 +82,24 @@ type UnarchiveSdkWorkstream = (
75
82
  status?: 'regular',
76
83
  ) => ReturnType<typeof workstreamServiceSingleton.updateStatus>
77
84
 
85
+ let activeRuntimeToken: symbol | null = null
86
+
87
+ function claimRuntimeToken(): symbol {
88
+ if (activeRuntimeToken) {
89
+ throw new Error('createLotaRuntime() is process-scoped. Disconnect the active runtime before creating another one.')
90
+ }
91
+
92
+ const token = Symbol('lota-runtime')
93
+ activeRuntimeToken = token
94
+ return token
95
+ }
96
+
97
+ function releaseRuntimeToken(token: symbol) {
98
+ if (activeRuntimeToken === token) {
99
+ activeRuntimeToken = null
100
+ }
101
+ }
102
+
78
103
  export interface LotaRuntime {
79
104
  services: {
80
105
  database: SurrealDBService
@@ -99,6 +124,7 @@ export interface LotaRuntime {
99
124
  createWorkstreamTurnStream: typeof createWorkstreamTurnStream
100
125
  isApprovalContinuationRequest: typeof isApprovalContinuationRequest
101
126
  runWorkstreamTurnInBackground: typeof runWorkstreamTurnInBackground
127
+ syncPlaybookTemplates: typeof playbookRegistryService.syncPlaybookTemplates
102
128
  }
103
129
  lota: {
104
130
  organizations: {
@@ -162,201 +188,241 @@ export interface LotaRuntime {
162
188
  contributions: { envKeys: readonly string[]; schemaFiles: Array<string | URL> }
163
189
  config: ResolvedLotaRuntimeConfig
164
190
  plugins: Record<string, LotaPlugin>
191
+ systemExecutors: Record<string, SystemNodeExecutor>
165
192
  connectPluginDatabases(): Promise<void>
166
193
  connect(): Promise<void>
167
194
  disconnect(): Promise<void>
168
195
  }
169
196
 
170
197
  export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<LotaRuntime> {
171
- const resolvedConfig = parseLotaRuntimeConfig(config)
172
- configureRuntimeConfig(resolvedConfig)
198
+ const runtimeToken = claimRuntimeToken()
173
199
 
174
- await configureLotaLogger(resolvedConfig.logging.level)
200
+ try {
201
+ const resolvedConfig = parseLotaRuntimeConfig(config)
202
+ const systemExecutors = { ...getBuiltInSystemExecutors(), ...resolvedConfig.systemExecutors }
203
+ const runtimeConfig = { ...resolvedConfig, systemExecutors } satisfies ResolvedLotaRuntimeConfig
204
+ configureRuntimeConfig(runtimeConfig)
175
205
 
176
- const db = new SurrealDBServiceClass({
177
- url: resolvedConfig.database.url,
178
- namespace: resolvedConfig.database.namespace,
179
- database: LOTA_SDK_DATABASE_NAME,
180
- username: resolvedConfig.database.username,
181
- password: resolvedConfig.database.password,
182
- })
183
- setDatabaseService(db)
206
+ await configureLotaLogger(runtimeConfig.logging.level)
184
207
 
185
- const redisManager = createRedisConnectionManager({ url: resolvedConfig.redis.url })
186
- setRedisConnectionManager(redisManager)
187
- configureEmbeddingCache(redisManager.getConnection(), resolvedConfig.memory.embeddingCacheTtlSeconds)
188
- configureBackgroundProcessing(resolvedConfig.backgroundProcessing)
208
+ const db = new SurrealDBServiceClass({
209
+ url: runtimeConfig.database.url,
210
+ namespace: runtimeConfig.database.namespace,
211
+ database: LOTA_SDK_DATABASE_NAME,
212
+ username: runtimeConfig.database.username,
213
+ password: runtimeConfig.database.password,
214
+ })
215
+ setDatabaseService(db)
189
216
 
190
- configureAgents({
191
- roster: resolvedConfig.agents.roster,
192
- leadAgentId: resolvedConfig.agents.leadAgentId,
193
- displayNames: resolvedConfig.agents.displayNames,
194
- shortDisplayNames: resolvedConfig.agents.shortDisplayNames,
195
- teamConsultParticipants: resolvedConfig.agents.teamConsultParticipants,
196
- getCoreWorkstreamProfile: resolvedConfig.agents.getCoreWorkstreamProfile,
197
- })
198
- configureAgentFactory({
199
- createAgent: resolvedConfig.agents.createAgent,
200
- buildAgentTools: resolvedConfig.agents.buildAgentTools,
201
- getAgentRuntimeConfig: resolvedConfig.agents.getAgentRuntimeConfig,
202
- pluginRuntime: resolvedConfig.pluginRuntime,
203
- })
204
- configureWorkstreams({ agentRoster: resolvedConfig.agents.roster, config: resolvedConfig.workstreams })
205
- configureRuntimeExtensions({
206
- adapters: resolvedConfig.runtimeAdapters,
207
- turnHooks: resolvedConfig.turnHooks,
208
- toolProviders: (resolvedConfig.toolProviders ?? {}) as never,
209
- extraWorkers: resolvedConfig.extraWorkers,
210
- })
217
+ const redisManager = createRedisConnectionManager({ url: runtimeConfig.redis.url })
218
+ setRedisConnectionManager(redisManager)
219
+ configureEmbeddingCache(redisManager.getConnection(), runtimeConfig.memory.embeddingCacheTtlSeconds)
220
+ configureBackgroundProcessing(runtimeConfig.backgroundProcessing)
211
221
 
212
- const pluginRuntime = resolvedConfig.pluginRuntime ?? {}
213
- const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
214
- const schemaFiles = [...getBuiltInSchemaFiles(), ...(resolvedConfig.extraSchemaFiles ?? [])]
215
- const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
216
- const contributionEnvKeys = [...LOTA_RUNTIME_ENV_KEYS, ...pluginContributions.flatMap((plugin) => plugin.envKeys)]
217
- const connectPluginDatabases = createPluginDatabaseConnector(pluginRuntime)
218
- const workers = buildRuntimeWorkerRegistry(resolvedConfig.extraWorkers)
222
+ configureAgents({
223
+ roster: runtimeConfig.agents.roster,
224
+ leadAgentId: runtimeConfig.agents.leadAgentId,
225
+ displayNames: runtimeConfig.agents.displayNames,
226
+ shortDisplayNames: runtimeConfig.agents.shortDisplayNames,
227
+ teamConsultParticipants: runtimeConfig.agents.teamConsultParticipants,
228
+ getCoreWorkstreamProfile: runtimeConfig.agents.getCoreWorkstreamProfile,
229
+ })
230
+ configureAgentFactory({
231
+ createAgent: runtimeConfig.agents.createAgent,
232
+ buildAgentTools: runtimeConfig.agents.buildAgentTools,
233
+ getAgentRuntimeConfig: runtimeConfig.agents.getAgentRuntimeConfig,
234
+ pluginRuntime: runtimeConfig.pluginRuntime,
235
+ })
236
+ configureWorkstreams({ agentRoster: runtimeConfig.agents.roster, config: runtimeConfig.workstreams })
237
+ configureNotificationService(runtimeConfig.notificationService ?? null)
238
+ configureRuntimeExtensions({
239
+ adapters: runtimeConfig.runtimeAdapters,
240
+ turnHooks: runtimeConfig.turnHooks,
241
+ toolProviders: (runtimeConfig.toolProviders ?? {}) as never,
242
+ extraWorkers: runtimeConfig.extraWorkers,
243
+ })
219
244
 
220
- const lota = {
221
- organizations: {
222
- create: organizationServiceSingleton.createOrganization.bind(organizationServiceSingleton),
223
- upsert: organizationServiceSingleton.upsertOrganization.bind(organizationServiceSingleton),
224
- get: organizationServiceSingleton.getOrganization.bind(organizationServiceSingleton),
225
- list: organizationServiceSingleton.listOrganizations.bind(organizationServiceSingleton),
226
- update: organizationServiceSingleton.updateOrganization.bind(organizationServiceSingleton),
227
- delete: organizationServiceSingleton.deleteOrganization.bind(organizationServiceSingleton),
228
- },
229
- users: {
230
- upsert: userServiceSingleton.upsertUser.bind(userServiceSingleton),
231
- get: userServiceSingleton.getUser.bind(userServiceSingleton),
232
- list: userServiceSingleton.listUsers.bind(userServiceSingleton),
233
- update: userServiceSingleton.updateUser.bind(userServiceSingleton),
234
- delete: userServiceSingleton.deleteUser.bind(userServiceSingleton),
235
- },
236
- memberships: {
237
- add: organizationMemberServiceSingleton.addMembership.bind(organizationMemberServiceSingleton),
238
- listForOrganization: organizationMemberServiceSingleton.listMembershipsForOrganization.bind(
239
- organizationMemberServiceSingleton,
240
- ),
241
- listForUser: organizationMemberServiceSingleton.listMembershipsForUser.bind(organizationMemberServiceSingleton),
242
- remove: organizationMemberServiceSingleton.removeMembership.bind(organizationMemberServiceSingleton),
243
- isMember: organizationMemberServiceSingleton.isMember.bind(organizationMemberServiceSingleton),
244
- },
245
- workstreams: {
246
- create: workstreamServiceSingleton.createWorkstream.bind(workstreamServiceSingleton),
247
- list: workstreamServiceSingleton.listWorkstreams.bind(workstreamServiceSingleton),
248
- get: workstreamServiceSingleton.getWorkstream.bind(workstreamServiceSingleton),
249
- update: workstreamServiceSingleton.updateTitle.bind(workstreamServiceSingleton),
250
- archive: async (workstreamId, status = 'archived') =>
251
- await workstreamServiceSingleton.updateStatus(workstreamId, status),
252
- unarchive: async (workstreamId, status = 'regular') =>
253
- await workstreamServiceSingleton.updateStatus(workstreamId, status),
254
- delete: workstreamServiceSingleton.deleteWorkstream.bind(workstreamServiceSingleton),
255
- stop: workstreamServiceSingleton.stopActiveRun.bind(workstreamServiceSingleton),
256
- listMessages: workstreamMessageServiceSingleton.listMessageHistoryPage.bind(workstreamMessageServiceSingleton),
257
- getMessage: async ({ workstreamId, messageId }) => {
258
- const messages = await workstreamMessageServiceSingleton.listMessages(
259
- ensureRecordId(workstreamId, TABLES.WORKSTREAM),
260
- )
261
- const message = messages.find((candidate) => candidate.id === messageId)
262
- if (!message) {
263
- throw new Error(`Workstream message not found: ${messageId}`)
264
- }
265
- return message
266
- },
267
- sendMessage: async ({ workstreamId, organizationId, userId, userName, messages }) => {
268
- const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
269
- const workstream = await workstreamServiceSingleton.getWorkstream(workstreamRef)
270
- const routed = routeWorkstreamChatMessages(messages)
271
- if (routed.kind !== 'turn') {
272
- throw new Error(routed.kind === 'invalid' ? routed.message : 'Expected a user turn payload.')
273
- }
245
+ const pluginRuntime = runtimeConfig.pluginRuntime ?? {}
246
+ domainAgentExecutorService.configure(pluginRuntime)
247
+ if (runtimeConfig.graphDesigner) {
248
+ configureGraphDesigner(runtimeConfig.graphDesigner)
249
+ }
250
+
251
+ for (const [pluginRef, plugin] of Object.entries(pluginRuntime)) {
252
+ const signals = plugin.contributions.signals
253
+ if (signals && signals.length > 0) {
254
+ coordinationRegistryServiceSingleton.register(pluginRef, [...signals])
255
+ }
256
+ }
257
+ coordinationRegistryServiceSingleton.validate()
258
+ // Collect playbook contributions early to fail fast on misconfiguration
259
+ playbookRegistryService.collectPlaybooks()
260
+
261
+ const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
262
+ const schemaFiles = [...getBuiltInSchemaFiles(), ...(runtimeConfig.extraSchemaFiles ?? [])]
263
+ const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
264
+ const contributionEnvKeys = [...LOTA_RUNTIME_ENV_KEYS, ...pluginContributions.flatMap((plugin) => plugin.envKeys)]
265
+ const connectPluginDatabases = createPluginDatabaseConnector(pluginRuntime)
266
+ const workers = buildRuntimeWorkerRegistry(runtimeConfig.extraWorkers)
274
267
 
275
- return await createWorkstreamTurnStreamSingleton({
276
- workstream,
277
- workstreamRef,
278
- orgRef: ensureRecordId(organizationId, TABLES.ORGANIZATION),
279
- userRef: ensureRecordId(userId, TABLES.USER),
280
- userName,
281
- inputMessage: routed.inputMessage,
282
- })
268
+ const lota = {
269
+ organizations: {
270
+ create: organizationServiceSingleton.createOrganization.bind(organizationServiceSingleton),
271
+ upsert: organizationServiceSingleton.upsertOrganization.bind(organizationServiceSingleton),
272
+ get: organizationServiceSingleton.getOrganization.bind(organizationServiceSingleton),
273
+ list: organizationServiceSingleton.listOrganizations.bind(organizationServiceSingleton),
274
+ update: organizationServiceSingleton.updateOrganization.bind(organizationServiceSingleton),
275
+ delete: organizationServiceSingleton.deleteOrganization.bind(organizationServiceSingleton),
276
+ },
277
+ users: {
278
+ upsert: userServiceSingleton.upsertUser.bind(userServiceSingleton),
279
+ get: userServiceSingleton.getUser.bind(userServiceSingleton),
280
+ list: userServiceSingleton.listUsers.bind(userServiceSingleton),
281
+ update: userServiceSingleton.updateUser.bind(userServiceSingleton),
282
+ delete: userServiceSingleton.deleteUser.bind(userServiceSingleton),
283
+ },
284
+ memberships: {
285
+ add: organizationMemberServiceSingleton.addMembership.bind(organizationMemberServiceSingleton),
286
+ listForOrganization: organizationMemberServiceSingleton.listMembershipsForOrganization.bind(
287
+ organizationMemberServiceSingleton,
288
+ ),
289
+ listForUser: organizationMemberServiceSingleton.listMembershipsForUser.bind(organizationMemberServiceSingleton),
290
+ remove: organizationMemberServiceSingleton.removeMembership.bind(organizationMemberServiceSingleton),
291
+ isMember: organizationMemberServiceSingleton.isMember.bind(organizationMemberServiceSingleton),
283
292
  },
284
- continueApproval: async ({ workstreamId, organizationId, userId, userName, messages }) => {
285
- const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
286
- const workstream = await workstreamServiceSingleton.getWorkstream(workstreamRef)
287
- const routed = routeWorkstreamChatMessages(messages)
288
- if (routed.kind !== 'approval-continuation') {
289
- throw new Error(
290
- routed.kind === 'invalid' ? routed.message : 'Expected approval continuation messages payload.',
293
+ workstreams: {
294
+ create: workstreamServiceSingleton.createWorkstream.bind(workstreamServiceSingleton),
295
+ list: workstreamServiceSingleton.listWorkstreams.bind(workstreamServiceSingleton),
296
+ get: workstreamServiceSingleton.getWorkstream.bind(workstreamServiceSingleton),
297
+ update: workstreamServiceSingleton.updateTitle.bind(workstreamServiceSingleton),
298
+ archive: async (workstreamId, status = 'archived') =>
299
+ await workstreamServiceSingleton.updateStatus(workstreamId, status),
300
+ unarchive: async (workstreamId, status = 'regular') =>
301
+ await workstreamServiceSingleton.updateStatus(workstreamId, status),
302
+ delete: workstreamServiceSingleton.deleteWorkstream.bind(workstreamServiceSingleton),
303
+ stop: workstreamServiceSingleton.stopActiveRun.bind(workstreamServiceSingleton),
304
+ listMessages: workstreamMessageServiceSingleton.listMessageHistoryPage.bind(workstreamMessageServiceSingleton),
305
+ getMessage: async ({ workstreamId, messageId }) => {
306
+ const messages = await workstreamMessageServiceSingleton.listMessages(
307
+ ensureRecordId(workstreamId, TABLES.WORKSTREAM),
291
308
  )
292
- }
309
+ const message = messages.find((candidate) => candidate.id === messageId)
310
+ if (!message) {
311
+ throw new Error(`Workstream message not found: ${messageId}`)
312
+ }
313
+ return message
314
+ },
315
+ sendMessage: async ({ workstreamId, organizationId, userId, userName, messages }) => {
316
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
317
+ const workstream = await workstreamServiceSingleton.getWorkstream(workstreamRef)
318
+ const routed = routeWorkstreamChatMessages(messages)
319
+ if (routed.kind !== 'turn') {
320
+ throw new Error(routed.kind === 'invalid' ? routed.message : 'Expected a user turn payload.')
321
+ }
322
+
323
+ return createWorkstreamTurnStreamSingleton({
324
+ workstream,
325
+ workstreamRef,
326
+ orgRef: ensureRecordId(organizationId, TABLES.ORGANIZATION),
327
+ userRef: ensureRecordId(userId, TABLES.USER),
328
+ userName,
329
+ inputMessage: routed.inputMessage,
330
+ })
331
+ },
332
+ continueApproval: async ({ workstreamId, organizationId, userId, userName, messages }) => {
333
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
334
+ const workstream = await workstreamServiceSingleton.getWorkstream(workstreamRef)
335
+ const routed = routeWorkstreamChatMessages(messages)
336
+ if (routed.kind !== 'approval-continuation') {
337
+ throw new Error(
338
+ routed.kind === 'invalid' ? routed.message : 'Expected approval continuation messages payload.',
339
+ )
340
+ }
341
+
342
+ return createWorkstreamApprovalContinuationStreamSingleton({
343
+ workstream,
344
+ workstreamRef,
345
+ orgRef: ensureRecordId(organizationId, TABLES.ORGANIZATION),
346
+ userRef: ensureRecordId(userId, TABLES.USER),
347
+ userName,
348
+ approvalMessages: routed.approvalMessages,
349
+ })
350
+ },
351
+ uploadAttachment: attachmentServiceSingleton.uploadWorkstreamAttachment.bind(attachmentServiceSingleton),
352
+ },
353
+ } satisfies LotaRuntime['lota']
293
354
 
294
- return await createWorkstreamApprovalContinuationStreamSingleton({
295
- workstream,
296
- workstreamRef,
297
- orgRef: ensureRecordId(organizationId, TABLES.ORGANIZATION),
298
- userRef: ensureRecordId(userId, TABLES.USER),
299
- userName,
300
- approvalMessages: routed.approvalMessages,
301
- })
355
+ let disconnected = false
356
+
357
+ return {
358
+ services: {
359
+ database: db,
360
+ redis: redisManager,
361
+ closeRedisConnection: async () => await redisManager.closeConnection(),
362
+ attachmentService: attachmentServiceSingleton,
363
+ documentChunkService: documentChunkServiceSingleton,
364
+ generatedDocumentStorageService: generatedDocumentStorageServiceSingleton,
365
+ memoryService: memoryServiceSingleton,
366
+ verifyMutatingApproval: verifyMutatingApprovalSingleton,
367
+ organizationService: organizationServiceSingleton,
368
+ organizationMemberService: organizationMemberServiceSingleton,
369
+ userService: userServiceSingleton,
370
+ recentActivityService: recentActivityServiceSingleton,
371
+ recentActivityTitleService: recentActivityTitleServiceSingleton,
372
+ executionPlanService: executionPlanServiceSingleton,
373
+ workstreamMessageService: workstreamMessageServiceSingleton,
374
+ workstreamService: workstreamServiceSingleton,
375
+ workstreamTitleService: workstreamTitleServiceSingleton,
376
+ createWorkstreamApprovalContinuationStream: createWorkstreamApprovalContinuationStreamSingleton,
377
+ createWorkstreamNativeToolApprovalStream: createWorkstreamNativeToolApprovalStreamSingleton,
378
+ createWorkstreamTurnStream: createWorkstreamTurnStreamSingleton,
379
+ isApprovalContinuationRequest: isApprovalContinuationRequestSingleton,
380
+ runWorkstreamTurnInBackground: runWorkstreamTurnInBackgroundSingleton,
381
+ syncPlaybookTemplates: playbookRegistryService.syncPlaybookTemplates.bind(playbookRegistryService),
382
+ },
383
+ lota,
384
+ redis: {
385
+ manager: redisManager,
386
+ getConnection: () => redisManager.getConnection(),
387
+ getConnectionForBullMQ: () => redisManager.getConnectionForBullMQ(),
388
+ closeConnection: async () => await redisManager.closeConnection(),
389
+ },
390
+ workers,
391
+ schemaFiles,
392
+ contributions: { envKeys: [...new Set(contributionEnvKeys)], schemaFiles: hostContributionSchemaFiles },
393
+ config: runtimeConfig,
394
+ plugins: pluginRuntime,
395
+ systemExecutors,
396
+ async connectPluginDatabases() {
397
+ await connectPluginDatabases()
398
+ },
399
+ async connect() {
400
+ await db.connect()
401
+ const bunFiles = schemaFiles.map((schemaFile) =>
402
+ schemaFile instanceof URL ? Bun.file(schemaFile.pathname) : Bun.file(schemaFile),
403
+ )
404
+ await db.applySchema(bunFiles)
405
+ const schemaFingerprint = await computeSchemaFingerprint(schemaFiles)
406
+ await publishDatabaseBootstrap({ databaseService: db, schemaFingerprint })
302
407
  },
303
- uploadAttachment: attachmentServiceSingleton.uploadWorkstreamAttachment.bind(attachmentServiceSingleton),
304
- },
305
- } satisfies LotaRuntime['lota']
408
+ async disconnect() {
409
+ if (disconnected) {
410
+ return
411
+ }
412
+ disconnected = true
306
413
 
307
- return {
308
- services: {
309
- database: db,
310
- redis: redisManager,
311
- closeRedisConnection: async () => await redisManager.closeConnection(),
312
- attachmentService: attachmentServiceSingleton,
313
- documentChunkService: documentChunkServiceSingleton,
314
- generatedDocumentStorageService: generatedDocumentStorageServiceSingleton,
315
- memoryService: memoryServiceSingleton,
316
- verifyMutatingApproval: verifyMutatingApprovalSingleton,
317
- organizationService: organizationServiceSingleton,
318
- organizationMemberService: organizationMemberServiceSingleton,
319
- userService: userServiceSingleton,
320
- recentActivityService: recentActivityServiceSingleton,
321
- recentActivityTitleService: recentActivityTitleServiceSingleton,
322
- executionPlanService: executionPlanServiceSingleton,
323
- workstreamMessageService: workstreamMessageServiceSingleton,
324
- workstreamService: workstreamServiceSingleton,
325
- workstreamTitleService: workstreamTitleServiceSingleton,
326
- createWorkstreamApprovalContinuationStream: createWorkstreamApprovalContinuationStreamSingleton,
327
- createWorkstreamNativeToolApprovalStream: createWorkstreamNativeToolApprovalStreamSingleton,
328
- createWorkstreamTurnStream: createWorkstreamTurnStreamSingleton,
329
- isApprovalContinuationRequest: isApprovalContinuationRequestSingleton,
330
- runWorkstreamTurnInBackground: runWorkstreamTurnInBackgroundSingleton,
331
- },
332
- lota,
333
- redis: {
334
- manager: redisManager,
335
- getConnection: () => redisManager.getConnection(),
336
- getConnectionForBullMQ: () => redisManager.getConnectionForBullMQ(),
337
- closeConnection: async () => await redisManager.closeConnection(),
338
- },
339
- workers,
340
- schemaFiles,
341
- contributions: { envKeys: [...new Set(contributionEnvKeys)], schemaFiles: hostContributionSchemaFiles },
342
- config: resolvedConfig,
343
- plugins: pluginRuntime,
344
- async connectPluginDatabases() {
345
- await connectPluginDatabases()
346
- },
347
- async connect() {
348
- await db.connect()
349
- const bunFiles = schemaFiles.map((schemaFile) =>
350
- schemaFile instanceof URL ? Bun.file(schemaFile.pathname) : Bun.file(schemaFile),
351
- )
352
- await db.applySchemaAndMigrations(bunFiles)
353
- const schemaFingerprint = await computeSchemaFingerprint(schemaFiles)
354
- await publishDatabaseBootstrap({ databaseService: db, schemaFingerprint })
355
- },
356
- async disconnect() {
357
- await db.disconnect()
358
- await redisManager.closeConnection()
359
- },
414
+ try {
415
+ await closeSharedSubscriber()
416
+ await db.disconnect()
417
+ await redisManager.closeConnection()
418
+ } finally {
419
+ releaseRuntimeToken(runtimeToken)
420
+ }
421
+ },
422
+ }
423
+ } catch (error) {
424
+ releaseRuntimeToken(runtimeToken)
425
+ throw error
360
426
  }
361
427
  }
362
428
 
@@ -1,4 +1,3 @@
1
- import { toTimestamp } from '@lota-sdk/shared'
2
1
  import type { ChatMessage } from '@lota-sdk/shared'
3
2
  import type { BoundQuery, RecordId } from 'surrealdb'
4
3
  import { z } from 'zod'
@@ -7,7 +6,7 @@ import type { RecordIdRef } from './record-id'
7
6
  import { databaseService } from './service'
8
7
  import type { DatabaseTable } from './tables'
9
8
 
10
- export const CursorRowSchema = z.object({ createdAt: z.union([z.date(), z.string(), z.number()]) })
9
+ export const CursorRowSchema = z.object({ createdAt: z.coerce.date() })
11
10
 
12
11
  export interface MessageHistoryPage {
13
12
  messages: ChatMessage[]
@@ -64,10 +63,8 @@ async function listRowsBefore(
64
63
  throw new Error(`Cursor message not found in ${config.table}: ${params.beforeMessageId}`)
65
64
  }
66
65
 
67
- const cursorCreatedAt = new Date(toTimestamp(cursorRow.createdAt) ?? Date.now())
66
+ const cursorCreatedAt = cursorRow.createdAt
68
67
  const cursorId = config.toRowId(params.parentId, params.beforeMessageId)
69
68
 
70
- return await databaseService.query<unknown>(
71
- config.queryBefore(params.parentId, cursorCreatedAt, cursorId, params.take),
72
- )
69
+ return databaseService.query<unknown>(config.queryBefore(params.parentId, cursorCreatedAt, cursorId, params.take))
73
70
  }
package/src/db/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './base.service'
1
2
  export * from './cursor-pagination'
2
3
  export * from './memory'
3
4
  export * from './memory-store'
@@ -7,4 +8,5 @@ export * from './record-id'
7
8
  export * from './sdk-database'
8
9
  export * from './service'
9
10
  export * from './startup'
11
+ export * from './surreal-mutation'
10
12
  export * from './tables'
@@ -13,17 +13,17 @@ export interface SurrealMemoryRow {
13
13
  importance: number
14
14
  accessCount: number
15
15
  needsReview: boolean
16
- lastAccessedAt?: Date | string
17
- createdAt: Date | string
18
- updatedAt?: Date | string
19
- validFrom: Date | string
20
- validUntil?: Date | string
21
- archivedAt?: Date | string
16
+ lastAccessedAt?: Date
17
+ createdAt: Date
18
+ updatedAt?: Date
19
+ validFrom: Date
20
+ validUntil?: Date
21
+ archivedAt?: Date
22
22
  }
23
23
 
24
24
  export interface BasicSearchRow {
25
25
  id: RecordIdInput
26
26
  content: string
27
27
  metadata: Record<string, unknown>
28
- createdAt?: Date | string
28
+ createdAt?: Date
29
29
  }
@@ -3,6 +3,8 @@ import { BoundQuery, eq, inside } from 'surrealdb'
3
3
  import { aiLogger } from '../config/logger'
4
4
  import { DEFAULT_MEMORY_SEARCH_LIMIT } from '../config/search'
5
5
  import { getDefaultEmbeddings } from '../embeddings/provider'
6
+ import { withTimeout } from '../utils/async'
7
+ import { clampImportance, truncateText } from '../utils/string'
6
8
  import { memoryQueryBuilder } from './memory-query-builder'
7
9
  import type { RelationCounts } from './memory-store.helpers'
8
10
  import { hashContent, mapRowToMemoryRecord, processGraphAwareRows } from './memory-store.helpers'
@@ -92,7 +94,7 @@ export class SurrealMemoryStore {
92
94
  LIMIT $limit
93
95
  `
94
96
 
95
- return await databaseService.query<BasicSearchRow>(
97
+ return databaseService.query<BasicSearchRow>(
96
98
  new BoundQuery(sql, { scopeId: options.scopeId, memoryType: options.memoryType, limit: options.limit }),
97
99
  )
98
100
  }
@@ -184,7 +186,7 @@ export class SurrealMemoryStore {
184
186
  const normalized = content.trim()
185
187
  if (!normalized) return []
186
188
 
187
- return await this.embeddings.embedQuery(normalized)
189
+ return this.embeddings.embedQuery(normalized)
188
190
  }
189
191
 
190
192
  async warmEmbedding(content: string): Promise<void> {
@@ -355,11 +357,11 @@ export class SurrealMemoryStore {
355
357
  const hash = hashContent(content, scopeId, memoryType)
356
358
  const embedding = await this.generateEmbedding(content)
357
359
 
358
- importance = Math.max(0, Math.min(1, importance))
360
+ importance = clampImportance(importance)
359
361
 
360
362
  const nearDup = await this.findNearDuplicate(embedding, scopeId, content)
361
363
  if (nearDup) {
362
- const mergedImportance = Math.max(0, Math.min(1, Math.max(nearDup.importance, importance)))
364
+ const mergedImportance = clampImportance(Math.max(nearDup.importance, importance))
363
365
  const keepNew = content.length >= nearDup.content.length
364
366
  const winnerContent = keepNew ? content : nearDup.content
365
367
  await this.update(nearDup.id, winnerContent, { importance: mergedImportance })
@@ -580,12 +582,11 @@ export class SurrealMemoryStore {
580
582
 
581
583
  let results: LinearRow[]
582
584
  try {
583
- results = await Promise.race([
585
+ results = await withTimeout(
584
586
  this.queryFinalStatement<LinearRow>(sql, bindVars),
585
- new Promise<never>((_, reject) =>
586
- setTimeout(() => reject(new Error('Hybrid search timeout')), SurrealMemoryStore.HYBRID_SEARCH_TIMEOUT_MS),
587
- ),
588
- ])
587
+ SurrealMemoryStore.HYBRID_SEARCH_TIMEOUT_MS,
588
+ 'Hybrid search',
589
+ )
589
590
  } catch {
590
591
  const elapsed = performance.now() - searchStart
591
592
  aiLogger.warn`Hybrid search timed out after ${elapsed.toFixed(0)}ms (scopeId: ${options.scopeId}). Falling back to vector-only.`
@@ -669,18 +670,13 @@ export class SurrealMemoryStore {
669
670
 
670
671
  const importance =
671
672
  typeof options?.importance === 'number'
672
- ? Math.max(existing.importance, Math.max(0, Math.min(1, options.importance)))
673
+ ? Math.max(existing.importance, clampImportance(options.importance))
673
674
  : undefined
674
675
 
675
676
  const durability = options?.durability
676
677
  const metadata = options?.metadata ? { ...existing.metadata, ...options.metadata } : undefined
677
678
 
678
- const updatePayload: Record<string, unknown> = {
679
- content: newContent,
680
- embedding: newEmbedding,
681
- hash: newHash,
682
- updatedAt: new Date(),
683
- }
679
+ const updatePayload: Record<string, unknown> = { content: newContent, embedding: newEmbedding, hash: newHash }
684
680
  if (importance !== undefined) {
685
681
  updatePayload.importance = importance
686
682
  }
@@ -760,7 +756,7 @@ export class SurrealMemoryStore {
760
756
  }
761
757
 
762
758
  async addRelation(fromId: string, toId: string, relationType: RelationType, confidence: number = 1.0): Promise<void> {
763
- confidence = Math.max(0, Math.min(1, confidence))
759
+ confidence = clampImportance(confidence)
764
760
  const fromRef = ensureRecordId(fromId, TABLES.MEMORY)
765
761
  const toRef = ensureRecordId(toId, TABLES.MEMORY)
766
762
  await databaseService.relate(fromRef, MEMORY_RELATION_TABLE, toRef, { relationType, confidence })
@@ -953,7 +949,7 @@ export class SurrealMemoryStore {
953
949
  if (!neighbor.content || seen.has(neighborId)) continue
954
950
  seen.add(neighborId)
955
951
  const label = neighbor.relationType ? `[${neighbor.relationType}]` : ''
956
- const truncated = neighbor.content.length > 200 ? `${neighbor.content.slice(0, 197)}...` : neighbor.content
952
+ const truncated = truncateText(neighbor.content, 200)
957
953
  contexts.push(`${label} ${truncated}`.trim())
958
954
  }
959
955
  if (contexts.length > 0) {