@lota-sdk/core 0.4.8 → 0.4.9

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 (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/thread-defaults.ts +33 -21
  9. package/src/create-runtime.ts +725 -387
  10. package/src/db/base.service.ts +52 -28
  11. package/src/db/cursor-pagination.ts +71 -30
  12. package/src/db/memory-store.helpers.ts +4 -7
  13. package/src/db/memory-store.ts +856 -598
  14. package/src/db/memory.ts +398 -275
  15. package/src/db/record-id.ts +32 -10
  16. package/src/db/schema-fingerprint.ts +30 -12
  17. package/src/db/service-normalization.ts +255 -0
  18. package/src/db/service.ts +726 -761
  19. package/src/db/startup.ts +140 -66
  20. package/src/db/transaction-conflict.ts +15 -0
  21. package/src/effect/awaitable-effect.ts +87 -0
  22. package/src/effect/errors.ts +121 -0
  23. package/src/effect/helpers.ts +98 -0
  24. package/src/effect/index.ts +22 -0
  25. package/src/effect/layers.ts +228 -0
  26. package/src/effect/runtime-ref.ts +25 -0
  27. package/src/effect/runtime.ts +31 -0
  28. package/src/effect/services.ts +57 -0
  29. package/src/effect/zod.ts +43 -0
  30. package/src/embeddings/provider.ts +122 -76
  31. package/src/index.ts +46 -1
  32. package/src/openrouter/direct-provider.ts +11 -35
  33. package/src/queues/autonomous-job.queue.ts +130 -74
  34. package/src/queues/context-compaction.queue.ts +60 -15
  35. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  36. package/src/queues/document-processor.queue.ts +52 -77
  37. package/src/queues/memory-consolidation.queue.ts +47 -32
  38. package/src/queues/organization-learning.queue.ts +13 -4
  39. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  40. package/src/queues/plan-scheduler.queue.ts +107 -31
  41. package/src/queues/post-chat-memory.queue.ts +66 -24
  42. package/src/queues/queue-factory.ts +142 -52
  43. package/src/queues/standalone-worker.ts +39 -0
  44. package/src/queues/title-generation.queue.ts +54 -9
  45. package/src/redis/connection.ts +84 -32
  46. package/src/redis/index.ts +6 -8
  47. package/src/redis/org-memory-lock.ts +60 -27
  48. package/src/redis/redis-lease-lock.ts +200 -121
  49. package/src/redis/runtime-connection.ts +10 -0
  50. package/src/redis/stream-context.ts +84 -46
  51. package/src/runtime/agent-identity-overrides.ts +2 -2
  52. package/src/runtime/agent-runtime-policy.ts +4 -1
  53. package/src/runtime/agent-stream-helpers.ts +20 -9
  54. package/src/runtime/chat-run-orchestration.ts +102 -19
  55. package/src/runtime/chat-run-registry.ts +36 -2
  56. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  57. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  58. package/src/runtime/execution-plan-visibility.ts +2 -2
  59. package/src/runtime/execution-plan.ts +42 -15
  60. package/src/runtime/graph-designer.ts +11 -7
  61. package/src/runtime/helper-model.ts +135 -48
  62. package/src/runtime/index.ts +7 -7
  63. package/src/runtime/indexed-repositories-policy.ts +3 -3
  64. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  65. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  66. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  67. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  68. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  69. package/src/runtime/plugin-resolution.ts +144 -24
  70. package/src/runtime/plugin-types.ts +9 -1
  71. package/src/runtime/post-turn-side-effects.ts +197 -130
  72. package/src/runtime/retrieval-adapters.ts +38 -4
  73. package/src/runtime/runtime-config.ts +150 -61
  74. package/src/runtime/runtime-extensions.ts +21 -34
  75. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  76. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  77. package/src/runtime/social-chat/social-chat.ts +594 -0
  78. package/src/runtime/specialist-runner.ts +36 -10
  79. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  80. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  81. package/src/runtime/thread-chat-helpers.ts +2 -2
  82. package/src/runtime/thread-plan-turn.ts +2 -1
  83. package/src/runtime/thread-turn-context.ts +172 -94
  84. package/src/runtime/turn-lifecycle.ts +93 -27
  85. package/src/services/agent-activity.service.ts +287 -203
  86. package/src/services/agent-executor.service.ts +329 -217
  87. package/src/services/artifact.service.ts +225 -148
  88. package/src/services/attachment.service.ts +137 -115
  89. package/src/services/autonomous-job.service.ts +888 -491
  90. package/src/services/chat-run-registry.service.ts +11 -1
  91. package/src/services/context-compaction.service.ts +136 -86
  92. package/src/services/document-chunk.service.ts +162 -90
  93. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  94. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  95. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  96. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  97. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  98. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  99. package/src/services/feedback-loop.service.ts +132 -76
  100. package/src/services/global-orchestrator.service.ts +80 -170
  101. package/src/services/graph-full-routing.ts +182 -0
  102. package/src/services/index.ts +18 -21
  103. package/src/services/institutional-memory.service.ts +220 -123
  104. package/src/services/learned-skill.service.ts +364 -259
  105. package/src/services/memory/memory-conversation.ts +95 -0
  106. package/src/services/memory/memory-org-memory.ts +39 -0
  107. package/src/services/memory/memory-preseeded.ts +80 -0
  108. package/src/services/memory/memory-rerank.ts +297 -0
  109. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  110. package/src/services/memory/memory.service.ts +692 -0
  111. package/src/services/memory/rerank.service.ts +209 -0
  112. package/src/services/monitoring-window.service.ts +92 -70
  113. package/src/services/mutating-approval.service.ts +62 -53
  114. package/src/services/node-workspace.service.ts +141 -98
  115. package/src/services/notification.service.ts +17 -16
  116. package/src/services/organization-member.service.ts +120 -66
  117. package/src/services/organization.service.ts +144 -51
  118. package/src/services/ownership-dispatcher.service.ts +415 -264
  119. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  120. package/src/services/plan/plan-agent-query.service.ts +322 -0
  121. package/src/services/plan/plan-approval.service.ts +102 -0
  122. package/src/services/plan/plan-artifact.service.ts +60 -0
  123. package/src/services/plan/plan-builder.service.ts +76 -0
  124. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  125. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  126. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  127. package/src/services/plan/plan-coordination.service.ts +181 -0
  128. package/src/services/plan/plan-cycle.service.ts +398 -0
  129. package/src/services/plan/plan-deadline.service.ts +547 -0
  130. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  131. package/src/services/plan/plan-executor-context.ts +35 -0
  132. package/src/services/plan/plan-executor-graph.ts +475 -0
  133. package/src/services/plan/plan-executor-helpers.ts +322 -0
  134. package/src/services/plan/plan-executor-persistence.ts +209 -0
  135. package/src/services/plan/plan-executor.service.ts +1654 -0
  136. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  137. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  138. package/src/services/plan/plan-run-serialization.ts +15 -0
  139. package/src/services/plan/plan-run.service.ts +644 -0
  140. package/src/services/plan/plan-scheduler.service.ts +385 -0
  141. package/src/services/plan/plan-template.service.ts +224 -0
  142. package/src/services/plan/plan-transaction-events.ts +33 -0
  143. package/src/services/plan/plan-validator.service.ts +907 -0
  144. package/src/services/plan/plan-workspace.service.ts +125 -0
  145. package/src/services/plugin-executor.service.ts +97 -68
  146. package/src/services/quality-metrics.service.ts +112 -94
  147. package/src/services/queue-job.service.ts +296 -230
  148. package/src/services/recent-activity-title.service.ts +65 -36
  149. package/src/services/recent-activity.service.ts +274 -259
  150. package/src/services/skill-resolver.service.ts +38 -12
  151. package/src/services/social-chat-history.service.ts +176 -125
  152. package/src/services/system-executor.service.ts +91 -61
  153. package/src/services/thread/thread-active-run.ts +203 -0
  154. package/src/services/thread/thread-bootstrap.ts +369 -0
  155. package/src/services/thread/thread-listing.ts +198 -0
  156. package/src/services/thread/thread-memory-block.ts +117 -0
  157. package/src/services/thread/thread-message.service.ts +363 -0
  158. package/src/services/thread/thread-record-store.ts +155 -0
  159. package/src/services/thread/thread-title.service.ts +74 -0
  160. package/src/services/thread/thread-turn-execution.ts +280 -0
  161. package/src/services/thread/thread-turn-message-context.ts +73 -0
  162. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  163. package/src/services/thread/thread-turn-streaming.ts +402 -0
  164. package/src/services/thread/thread-turn-tracing.ts +35 -0
  165. package/src/services/thread/thread-turn.ts +343 -0
  166. package/src/services/thread/thread.service.ts +335 -0
  167. package/src/services/user.service.ts +82 -32
  168. package/src/services/write-intent-validator.service.ts +63 -51
  169. package/src/storage/attachment-parser.ts +69 -27
  170. package/src/storage/attachment-storage.service.ts +331 -275
  171. package/src/storage/generated-document-storage.service.ts +66 -34
  172. package/src/system-agents/agent-result.ts +3 -1
  173. package/src/system-agents/context-compaction.agent.ts +2 -2
  174. package/src/system-agents/delegated-agent-factory.ts +159 -90
  175. package/src/system-agents/memory-reranker.agent.ts +2 -2
  176. package/src/system-agents/memory.agent.ts +2 -2
  177. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  178. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  179. package/src/system-agents/skill-extractor.agent.ts +2 -2
  180. package/src/system-agents/skill-manager.agent.ts +2 -2
  181. package/src/system-agents/thread-router.agent.ts +157 -113
  182. package/src/system-agents/title-generator.agent.ts +2 -2
  183. package/src/tools/execution-plan.tool.ts +220 -161
  184. package/src/tools/fetch-webpage.tool.ts +21 -17
  185. package/src/tools/firecrawl-client.ts +16 -6
  186. package/src/tools/index.ts +1 -0
  187. package/src/tools/memory-block.tool.ts +14 -6
  188. package/src/tools/plan-approval.tool.ts +49 -47
  189. package/src/tools/read-file-parts.tool.ts +44 -33
  190. package/src/tools/remember-memory.tool.ts +65 -45
  191. package/src/tools/search-web.tool.ts +26 -22
  192. package/src/tools/search.tool.ts +41 -29
  193. package/src/tools/team-think.tool.ts +124 -83
  194. package/src/tools/user-questions.tool.ts +4 -3
  195. package/src/tools/web-tool-shared.ts +6 -0
  196. package/src/utils/async.ts +17 -23
  197. package/src/utils/crypto.ts +21 -0
  198. package/src/utils/date-time.ts +40 -1
  199. package/src/utils/errors.ts +95 -16
  200. package/src/utils/hono-error-handler.ts +24 -39
  201. package/src/utils/index.ts +2 -1
  202. package/src/utils/null-proto-record.ts +41 -0
  203. package/src/utils/sse-keepalive.ts +124 -21
  204. package/src/workers/bootstrap.ts +186 -51
  205. package/src/workers/memory-consolidation.worker.ts +325 -237
  206. package/src/workers/organization-learning.worker.ts +50 -16
  207. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  208. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  209. package/src/workers/skill-extraction.runner.ts +176 -93
  210. package/src/workers/utils/file-section-chunker.ts +8 -10
  211. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  212. package/src/workers/utils/repomix-file-sections.ts +2 -2
  213. package/src/workers/utils/thread-message-query.ts +97 -38
  214. package/src/workers/worker-utils.ts +56 -31
  215. package/src/config/debug-logger.ts +0 -47
  216. package/src/redis/connection-accessor.ts +0 -26
  217. package/src/runtime/context-compaction-runtime.ts +0 -87
  218. package/src/runtime/social-chat-agent-runner.ts +0 -118
  219. package/src/runtime/social-chat.ts +0 -516
  220. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  221. package/src/services/adaptive-playbook.service.ts +0 -152
  222. package/src/services/artifact-provenance.service.ts +0 -172
  223. package/src/services/chat-attachments.service.ts +0 -17
  224. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  225. package/src/services/execution-plan.service.ts +0 -1118
  226. package/src/services/memory.service.ts +0 -914
  227. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  228. package/src/services/plan-agent-query.service.ts +0 -267
  229. package/src/services/plan-approval.service.ts +0 -83
  230. package/src/services/plan-artifact.service.ts +0 -50
  231. package/src/services/plan-builder.service.ts +0 -67
  232. package/src/services/plan-checkpoint.service.ts +0 -81
  233. package/src/services/plan-completion-side-effects.ts +0 -80
  234. package/src/services/plan-coordination.service.ts +0 -157
  235. package/src/services/plan-cycle.service.ts +0 -284
  236. package/src/services/plan-deadline.service.ts +0 -430
  237. package/src/services/plan-event-delivery.service.ts +0 -166
  238. package/src/services/plan-executor.service.ts +0 -1950
  239. package/src/services/plan-run.service.ts +0 -515
  240. package/src/services/plan-scheduler.service.ts +0 -240
  241. package/src/services/plan-template.service.ts +0 -177
  242. package/src/services/plan-validator.service.ts +0 -818
  243. package/src/services/plan-workspace.service.ts +0 -83
  244. package/src/services/rerank.service.ts +0 -156
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -1,31 +1,65 @@
1
1
  import type { SandboxedJob } from 'bullmq'
2
+ import { Effect } from 'effect'
3
+ import type { Context } from 'effect'
2
4
 
3
5
  import { serverLogger } from '../config/logger'
6
+ import { effectTryPromise } from '../effect/helpers'
7
+ import { DatabaseServiceTag, RuntimeAdaptersServiceTag, RuntimeConfigServiceTag } from '../effect/services'
4
8
  import type { OrganizationLearningJob } from '../queues/organization-learning.queue'
9
+ import { LearnedSkillServiceTag } from '../services/learned-skill.service'
10
+ import { MemoryServiceTag } from '../services/memory/memory.service'
11
+ import { SocialChatHistoryServiceTag } from '../services/social-chat-history.service'
5
12
  import { initializeSandboxedWorkerRuntime } from './bootstrap'
6
13
  import { runRegularChatMemoryDigest } from './regular-chat-memory-digest.runner'
14
+ import type { RegularChatDigestServices } from './regular-chat-memory-digest.runner'
7
15
  import { runSkillExtraction } from './skill-extraction.runner'
16
+ import type { SkillExtractionServices } from './skill-extraction.runner'
8
17
  import { toSandboxedWorkerError } from './utils/sandbox-error'
9
18
  import { createTracedWorkerProcessor } from './worker-utils'
10
19
 
11
- await initializeSandboxedWorkerRuntime()
20
+ const runtime = await initializeSandboxedWorkerRuntime()
21
+ const resolve = async <I, T>(tag: Context.Key<I, T>): Promise<T> => await runtime.runPromise(Effect.service(tag))
22
+ const regularChatDigestServices: RegularChatDigestServices = {
23
+ databaseService: await resolve(DatabaseServiceTag),
24
+ memoryService: await resolve(MemoryServiceTag),
25
+ socialChatHistoryService: await resolve(SocialChatHistoryServiceTag),
26
+ runtimeAdapters: await resolve(RuntimeAdaptersServiceTag),
27
+ }
28
+ const workerRuntimeConfig = await resolve(RuntimeConfigServiceTag)
29
+ const skillExtractionServices: SkillExtractionServices = {
30
+ databaseService: await resolve(DatabaseServiceTag),
31
+ learnedSkillService: await resolve(LearnedSkillServiceTag),
32
+ socialChatHistoryService: await resolve(SocialChatHistoryServiceTag),
33
+ runtimeAdapters: await resolve(RuntimeAdaptersServiceTag),
34
+ embeddingModel: workerRuntimeConfig.aiGateway.embeddingModel,
35
+ openRouterApiKey: workerRuntimeConfig.aiGateway.openRouterApiKey,
36
+ }
12
37
 
13
38
  // One sandboxed worker handles both organization-learning branches so the
14
39
  // queue can dispatch digest and skill jobs without separate worker images.
15
- const handler = async (job: SandboxedJob<OrganizationLearningJob>) => {
16
- try {
17
- if (job.data.kind === 'regular-chat-memory-digest') {
18
- await runRegularChatMemoryDigest(job.data)
19
- return
20
- }
21
- await runSkillExtraction(job.data)
22
- } catch (error) {
23
- const message =
24
- job.data.kind === 'regular-chat-memory-digest' ? 'Regular chat memory digest failed' : 'Skill extraction failed'
25
- const serialized = toSandboxedWorkerError(error, message)
26
- serverLogger.error`${serialized.message}`
27
- throw serialized
28
- }
29
- }
40
+ const handler = (job: SandboxedJob<OrganizationLearningJob>) =>
41
+ Effect.runPromise(
42
+ Effect.gen(function* () {
43
+ if (job.data.kind === 'regular-chat-memory-digest') {
44
+ const digestJob = job.data
45
+ return yield* effectTryPromise(() => runRegularChatMemoryDigest(digestJob, regularChatDigestServices))
46
+ }
47
+
48
+ const skillJob = job.data
49
+ return yield* effectTryPromise(() => runSkillExtraction(skillJob, skillExtractionServices))
50
+ }).pipe(
51
+ Effect.catch((error) =>
52
+ Effect.gen(function* () {
53
+ const message =
54
+ job.data.kind === 'regular-chat-memory-digest'
55
+ ? 'Regular chat memory digest failed'
56
+ : 'Skill extraction failed'
57
+ const serialized = toSandboxedWorkerError(error, message)
58
+ serverLogger.error`${serialized.message}`
59
+ return yield* Effect.fail(serialized)
60
+ }),
61
+ ),
62
+ ),
63
+ )
30
64
 
31
65
  export default createTracedWorkerProcessor('organization-learning', handler)
@@ -1,40 +1,40 @@
1
1
  import { isAgentName } from '../config/agent-defaults'
2
- import { compactWhitespace } from '../utils/string'
2
+ import { compactWhitespace, readRecord, readString } from '../utils/string'
3
+ import type { DigestMessage } from './utils/thread-message-query'
3
4
 
4
- interface DigestMessageForTranscript {
5
- source: 'thread' | 'social'
6
- sourceId: string
7
- role: 'system' | 'user' | 'assistant'
8
- parts: Array<Record<string, unknown>>
9
- metadata?: Record<string, unknown>
10
- }
5
+ type DigestMessageForTranscript = Pick<DigestMessage, 'source' | 'sourceId' | 'role' | 'parts' | 'metadata'>
6
+ type DigestMessagePart = DigestMessageForTranscript['parts'][number]
11
7
 
12
- function normalizeFilePartMetadata(part: Record<string, unknown>): string | null {
8
+ function normalizeFilePartMetadata(part: DigestMessagePart): string | null {
13
9
  if (part.type !== 'file') return null
10
+ const partRecord = readRecord(part)
11
+ if (!partRecord) return null
14
12
 
15
- const filename = typeof part.filename === 'string' && part.filename.trim() ? part.filename.trim() : 'attachment'
16
- const mediaType = typeof part.mediaType === 'string' && part.mediaType.trim() ? part.mediaType.trim() : 'unknown'
17
- const storageKey = typeof part.storageKey === 'string' && part.storageKey.trim() ? part.storageKey.trim() : 'unknown'
13
+ const filename = readString(partRecord.filename) ?? 'attachment'
14
+ const mediaType = readString(partRecord.mediaType) ?? 'unknown'
15
+ const providerMetadata = readRecord(partRecord.providerMetadata)
16
+ const lotaMetadata = readRecord(providerMetadata?.lota)
17
+ const storageKey = readString(partRecord.storageKey) ?? readString(lotaMetadata?.attachmentStorageKey) ?? 'unknown'
18
18
  const sizeBytes =
19
- typeof part.sizeBytes === 'number' && Number.isFinite(part.sizeBytes) && part.sizeBytes >= 0
20
- ? Math.trunc(part.sizeBytes)
19
+ typeof partRecord.sizeBytes === 'number' && Number.isFinite(partRecord.sizeBytes) && partRecord.sizeBytes >= 0
20
+ ? Math.trunc(partRecord.sizeBytes)
21
21
  : null
22
22
 
23
23
  const sizeSegment = sizeBytes === null ? '' : `, sizeBytes=${sizeBytes}`
24
24
  return `${filename} (${mediaType}${sizeSegment}, storageKey=${storageKey})`
25
25
  }
26
26
 
27
- function extractAssistantLabel(message: DigestMessageForTranscript): string {
27
+ function extractAssistantLabel(
28
+ message: DigestMessageForTranscript,
29
+ isKnownAgentName: (value: string) => boolean = isAgentName,
30
+ ): string {
28
31
  const metadataAgentId =
29
- message.metadata && typeof message.metadata.agentId === 'string'
30
- ? message.metadata.agentId.trim().toLowerCase()
31
- : ''
32
- if (metadataAgentId && isAgentName(metadataAgentId)) {
32
+ typeof message.metadata?.agentId === 'string' ? message.metadata.agentId.trim().toLowerCase() : ''
33
+ if (metadataAgentId && isKnownAgentName(metadataAgentId)) {
33
34
  return metadataAgentId
34
35
  }
35
36
 
36
- const metadataAgentName =
37
- message.metadata && typeof message.metadata.agentName === 'string' ? message.metadata.agentName.trim() : ''
37
+ const metadataAgentName = typeof message.metadata?.agentName === 'string' ? message.metadata.agentName.trim() : ''
38
38
  if (metadataAgentName) {
39
39
  return metadataAgentName
40
40
  }
@@ -42,12 +42,13 @@ function extractAssistantLabel(message: DigestMessageForTranscript): string {
42
42
  return 'assistant'
43
43
  }
44
44
 
45
- export function buildDigestTranscript(params: { messages: DigestMessageForTranscript[] }): {
46
- transcript: string
47
- involvedAgentNames: string[]
48
- } {
45
+ export function buildDigestTranscript(params: {
46
+ messages: DigestMessageForTranscript[]
47
+ isKnownAgentName?: (value: string) => boolean
48
+ }): { transcript: string; involvedAgentNames: string[] } {
49
49
  const lines: string[] = []
50
50
  const involvedAgentNames = new Set<string>()
51
+ const isKnownAgentName = params.isKnownAgentName ?? isAgentName
51
52
 
52
53
  for (const message of params.messages) {
53
54
  if (message.role !== 'user' && message.role !== 'assistant') continue
@@ -70,8 +71,8 @@ export function buildDigestTranscript(params: { messages: DigestMessageForTransc
70
71
  continue
71
72
  }
72
73
 
73
- const assistantLabel = extractAssistantLabel(message)
74
- if (isAgentName(assistantLabel)) {
74
+ const assistantLabel = extractAssistantLabel(message, isKnownAgentName)
75
+ if (isKnownAgentName(assistantLabel)) {
75
76
  involvedAgentNames.add(assistantLabel)
76
77
  }
77
78
 
@@ -1,21 +1,25 @@
1
+ import { Cause, Effect } from 'effect'
2
+ import type { Context } from 'effect'
1
3
  import { BoundQuery } from 'surrealdb'
2
4
  import { z } from 'zod'
3
5
 
4
6
  import { serverLogger } from '../config/logger'
5
7
  import { ensureRecordId, recordIdToString } from '../db/record-id'
6
8
  import type { RecordIdRef } from '../db/record-id'
7
- import { databaseService } from '../db/service'
9
+ import type { SurrealDBService } from '../db/service'
8
10
  import { TABLES } from '../db/tables'
11
+ import { effectTryPromise } from '../effect/helpers'
9
12
  import {
10
13
  clearRegularChatMemoryDigestDeduplicationKey,
11
14
  enqueueRegularChatMemoryDigest,
12
15
  } from '../queues/organization-learning.queue'
13
16
  import type { RegularChatMemoryDigestJob } from '../queues/organization-learning.queue'
14
17
  import { createHelperModelRuntime } from '../runtime/helper-model'
15
- import { getRuntimeAdapters, withConfiguredWorkspaceMemoryLock } from '../runtime/runtime-extensions'
16
- import { memoryService } from '../services/memory.service'
17
- import { socialChatHistoryService } from '../services/social-chat-history.service'
18
+ import type { LotaRuntimeAdapters, LotaRuntimeBackgroundCursor } from '../runtime/runtime-extensions'
19
+ import type { MemoryServiceTag } from '../services/memory/memory.service'
20
+ import type { SocialChatHistoryServiceTag } from '../services/social-chat-history.service'
18
21
  import { createRegularChatMemoryDigestAgent } from '../system-agents/regular-chat-memory-digest.agent'
22
+ import { nowIsoDateTimeString } from '../utils/date-time'
19
23
  import { compactWhitespace } from '../utils/string'
20
24
  import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
21
25
  import {
@@ -24,7 +28,7 @@ import {
24
28
  listThreadIdsForOrg,
25
29
  normalizeBlock,
26
30
  } from './utils/thread-message-query'
27
- import type { DigestCursor, DigestMessage } from './utils/thread-message-query'
31
+ import type { DigestMessage } from './utils/thread-message-query'
28
32
 
29
33
  // Onboarding extracts memory immediately inside the turn flow. This delayed
30
34
  // runner handles the regular-chat path after onboarding so longer transcripts
@@ -33,6 +37,12 @@ const StructuredProfilePatchSchema = z.record(z.string(), z.unknown()).default({
33
37
 
34
38
  const REGULAR_CHAT_MEMORY_DIGEST_TIMEOUT_MS = 10 * 60 * 1000
35
39
  const WorkspaceMemoryRowSchema = z.object({ content: z.string() })
40
+ export interface RegularChatDigestServices {
41
+ databaseService: SurrealDBService
42
+ memoryService: Context.Service.Shape<typeof MemoryServiceTag>
43
+ socialChatHistoryService: Context.Service.Shape<typeof SocialChatHistoryServiceTag>
44
+ runtimeAdapters: LotaRuntimeAdapters
45
+ }
36
46
 
37
47
  const ExtractedFactSchema = z.object({
38
48
  content: z.string().trim().min(1),
@@ -96,109 +106,122 @@ function buildPrompt(params: {
96
106
  ].join('\n')
97
107
  }
98
108
 
99
- function getLastCursor(messages: DigestMessage[]): DigestCursor | null {
109
+ function getLastCursor(messages: DigestMessage[]): LotaRuntimeBackgroundCursor | null {
100
110
  return messages.length > 0 ? messages[messages.length - 1].cursor : null
101
111
  }
102
112
 
103
- async function hasNewEligibleThreadMessages(params: {
113
+ function hasNewEligibleThreadMessages(params: {
114
+ db: SurrealDBService
104
115
  threadIds: RecordIdRef[]
105
- cursor: DigestCursor | null
116
+ cursor: LotaRuntimeBackgroundCursor | null
106
117
  onboardingCutoff: Date | null
107
118
  }): Promise<boolean> {
108
- if (params.threadIds.length === 0) return false
109
-
110
- let query: BoundQuery | null = null
111
- if (params.cursor) {
112
- const cursorRowId = ensureRecordId(params.cursor.id, TABLES.THREAD_MESSAGE)
113
- query = new BoundQuery(
114
- `SELECT id, createdAt FROM ${TABLES.THREAD_MESSAGE}
115
- WHERE threadId IN $threadIds
116
- AND (
117
- createdAt > $cursorCreatedAt
118
- OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
119
- )
120
- ORDER BY createdAt ASC, id ASC
121
- LIMIT 1`,
122
- { threadIds: params.threadIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
123
- )
124
- } else if (params.onboardingCutoff) {
125
- query = new BoundQuery(
126
- `SELECT id, createdAt FROM ${TABLES.THREAD_MESSAGE}
127
- WHERE threadId IN $threadIds
128
- AND createdAt > $onboardingCutoff
129
- ORDER BY createdAt ASC, id ASC
130
- LIMIT 1`,
131
- { threadIds: params.threadIds, onboardingCutoff: params.onboardingCutoff },
132
- )
133
- }
119
+ return Effect.runPromise(
120
+ Effect.gen(function* () {
121
+ if (params.threadIds.length === 0) return false
122
+
123
+ let query: BoundQuery | null = null
124
+ if (params.cursor) {
125
+ const cursorRowId = ensureRecordId(params.cursor.id, TABLES.THREAD_MESSAGE)
126
+ query = new BoundQuery(
127
+ `SELECT id, createdAt FROM ${TABLES.THREAD_MESSAGE}
128
+ WHERE threadId IN $threadIds
129
+ AND (
130
+ createdAt > $cursorCreatedAt
131
+ OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
132
+ )
133
+ ORDER BY createdAt ASC, id ASC
134
+ LIMIT 1`,
135
+ { threadIds: params.threadIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
136
+ )
137
+ } else if (params.onboardingCutoff) {
138
+ query = new BoundQuery(
139
+ `SELECT id, createdAt FROM ${TABLES.THREAD_MESSAGE}
140
+ WHERE threadId IN $threadIds
141
+ AND createdAt > $onboardingCutoff
142
+ ORDER BY createdAt ASC, id ASC
143
+ LIMIT 1`,
144
+ { threadIds: params.threadIds, onboardingCutoff: params.onboardingCutoff },
145
+ )
146
+ }
134
147
 
135
- if (!query) return false
136
- const rows = await databaseService.query<unknown>(query)
137
- return rows.length > 0
148
+ if (!query) return false
149
+ const rows = yield* effectTryPromise(() => params.db.query<unknown>(query))
150
+ return rows.length > 0
151
+ }),
152
+ )
138
153
  }
139
154
 
140
- async function loadExistingOrganizationMemories(orgId: string): Promise<Array<{ content: string }>> {
141
- return databaseService.queryMany(
142
- new BoundQuery(
143
- `SELECT content, createdAt, id FROM ${TABLES.MEMORY}
144
- WHERE metadata.orgId = $orgId
145
- AND archivedAt IS NONE
146
- AND (validUntil IS NONE OR validUntil > time::now())
147
- ORDER BY createdAt DESC, id DESC
148
- LIMIT 250`,
149
- { orgId },
155
+ function loadExistingOrganizationMemories(db: SurrealDBService, orgId: string): Promise<Array<{ content: string }>> {
156
+ return Effect.runPromise(
157
+ effectTryPromise(() =>
158
+ db.queryMany(
159
+ new BoundQuery(
160
+ `SELECT content, createdAt, id FROM ${TABLES.MEMORY}
161
+ WHERE metadata.orgId = $orgId
162
+ AND archivedAt IS NONE
163
+ AND (validUntil IS NONE OR validUntil > time::now())
164
+ ORDER BY createdAt DESC, id DESC
165
+ LIMIT 250`,
166
+ { orgId },
167
+ ),
168
+ WorkspaceMemoryRowSchema,
169
+ ),
150
170
  ),
151
- WorkspaceMemoryRowSchema,
152
171
  )
153
172
  }
154
173
 
155
- export async function runRegularChatMemoryDigest(
156
- data: RegularChatMemoryDigestJob,
157
- ): Promise<RegularChatDigestRunResult> {
158
- const orgRef = ensureRecordId(data.orgId, TABLES.ORGANIZATION)
159
- const orgId = recordIdToString(orgRef, TABLES.ORGANIZATION)
160
- const workspaceProvider = getRuntimeAdapters().workspaceProvider
161
- if (!workspaceProvider) {
162
- serverLogger.info`Skipping regular chat memory digest for ${orgId}: workspaceProvider is not configured`
163
- return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
164
- }
165
-
166
- return withConfiguredWorkspaceMemoryLock(orgId, async () => {
167
- if (
168
- !workspaceProvider.getBackgroundCursor ||
169
- !workspaceProvider.setBackgroundCursor ||
170
- !workspaceProvider.applyProfileProjection
171
- ) {
174
+ function runRegularChatMemoryDigestEffect(
175
+ services: RegularChatDigestServices,
176
+ orgRef: RecordIdRef,
177
+ orgId: string,
178
+ workspaceProvider: NonNullable<LotaRuntimeAdapters['workspaceProvider']>,
179
+ ) {
180
+ return Effect.gen(function* () {
181
+ const getBackgroundCursor = workspaceProvider.getBackgroundCursor?.bind(workspaceProvider)
182
+ const setBackgroundCursor = workspaceProvider.setBackgroundCursor?.bind(workspaceProvider)
183
+ const applyProfileProjection = workspaceProvider.applyProfileProjection?.bind(workspaceProvider)
184
+ if (!getBackgroundCursor || !setBackgroundCursor || !applyProfileProjection) {
172
185
  serverLogger.info`Skipping regular chat memory digest for ${orgId}: workspaceProvider background/profile methods are incomplete`
173
186
  return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
174
187
  }
175
188
 
176
- const workspace = await workspaceProvider.getWorkspace(orgRef)
177
- const lifecycleState = await workspaceProvider.getLifecycleState?.(workspace)
189
+ const workspace = yield* effectTryPromise(() => workspaceProvider.getWorkspace(orgRef))
190
+ const lifecycleState = workspaceProvider.getLifecycleState
191
+ ? yield* effectTryPromise(() => Promise.resolve(workspaceProvider.getLifecycleState?.(workspace)))
192
+ : undefined
178
193
  if (lifecycleState?.bootstrapActive ?? false) {
179
194
  serverLogger.info`Skipping regular chat memory digest for ${orgId}: onboarding is not completed`
180
195
  return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
181
196
  }
182
- const projectionState = await workspaceProvider.readProfileProjectionState?.(workspace)
197
+ const projectionState = workspaceProvider.readProfileProjectionState
198
+ ? yield* effectTryPromise(() => Promise.resolve(workspaceProvider.readProfileProjectionState?.(workspace)))
199
+ : undefined
183
200
 
184
- const existingThreadCursor = await workspaceProvider.getBackgroundCursor('regular-chat-digest', orgRef)
201
+ const existingThreadCursor = yield* effectTryPromise(() => getBackgroundCursor('regular-chat-digest', orgRef))
185
202
  const threadOnboardingCutoff = resolveWorkspaceBootstrapCutoff({
186
203
  hasExistingCursor: existingThreadCursor !== null,
187
204
  bootstrapCompletedAt: lifecycleState?.bootstrapCompletedAt,
188
205
  })
189
206
 
190
- const threadIds = await listThreadIdsForOrg(orgRef)
191
- const threadMessages = await listEligibleThreadMessages({
192
- threadIds,
193
- cursor: existingThreadCursor,
194
- onboardingCutoff: threadOnboardingCutoff,
195
- })
196
- const existingSocialCursor = await socialChatHistoryService.getBackgroundCursor('regular-chat-digest', orgId)
207
+ const threadIds = yield* effectTryPromise(() => listThreadIdsForOrg(services.databaseService, orgRef))
208
+ const threadMessages = yield* effectTryPromise(() =>
209
+ listEligibleThreadMessages({
210
+ db: services.databaseService,
211
+ threadIds,
212
+ cursor: existingThreadCursor,
213
+ onboardingCutoff: threadOnboardingCutoff,
214
+ }),
215
+ )
216
+ const existingSocialCursor = yield* services.socialChatHistoryService.getBackgroundCursor(
217
+ 'regular-chat-digest',
218
+ orgId,
219
+ )
197
220
  const socialOnboardingCutoff = resolveWorkspaceBootstrapCutoff({
198
221
  hasExistingCursor: existingSocialCursor !== null,
199
222
  bootstrapCompletedAt: lifecycleState?.bootstrapCompletedAt,
200
223
  })
201
- const socialMessages = await socialChatHistoryService.listWorkspaceMessages({
224
+ const socialMessages = yield* services.socialChatHistoryService.listWorkspaceMessages({
202
225
  workspaceId: orgId,
203
226
  cursor: existingSocialCursor,
204
227
  onboardingCutoff: socialOnboardingCutoff,
@@ -211,38 +234,42 @@ export async function runRegularChatMemoryDigest(
211
234
 
212
235
  const combinedMessages = [...threadMessages, ...socialMessages].sort(compareDigestMessageOrder)
213
236
  const { transcript, involvedAgentNames } = buildDigestTranscript({ messages: combinedMessages })
214
- const existingMemories = await loadExistingOrganizationMemories(orgId)
215
-
216
- const synthesis = await helperModelRuntime.generateHelperStructured({
217
- tag: 'regular-chat-memory-digest',
218
- createAgent: createRegularChatMemoryDigestAgent,
219
- timeoutMs: REGULAR_CHAT_MEMORY_DIGEST_TIMEOUT_MS,
220
- messages: [
221
- {
222
- role: 'user',
223
- content: buildPrompt({
224
- workspaceName: projectionState?.workspaceName || 'Workspace',
225
- currentSummaryBlock: projectionState?.summaryBlock ?? '',
226
- currentStructuredProfile: JSON.stringify(projectionState?.structuredProfile ?? {}, null, 2),
227
- existingMemories: buildMemoryContext(existingMemories),
228
- transcript,
229
- }),
230
- },
231
- ],
232
- schema: RegularChatMemoryDigestOutputSchema,
233
- })
237
+ const existingMemories = yield* effectTryPromise(() =>
238
+ loadExistingOrganizationMemories(services.databaseService, orgId),
239
+ )
240
+
241
+ const synthesis = yield* effectTryPromise(() =>
242
+ helperModelRuntime.generateHelperStructured({
243
+ tag: 'regular-chat-memory-digest',
244
+ createAgent: createRegularChatMemoryDigestAgent,
245
+ timeoutMs: REGULAR_CHAT_MEMORY_DIGEST_TIMEOUT_MS,
246
+ messages: [
247
+ {
248
+ role: 'user',
249
+ content: buildPrompt({
250
+ workspaceName: projectionState?.workspaceName || 'Workspace',
251
+ currentSummaryBlock: projectionState?.summaryBlock ?? '',
252
+ currentStructuredProfile: JSON.stringify(projectionState?.structuredProfile ?? {}, null, 2),
253
+ existingMemories: buildMemoryContext(existingMemories),
254
+ transcript,
255
+ }),
256
+ },
257
+ ],
258
+ schema: RegularChatMemoryDigestOutputSchema,
259
+ }),
260
+ )
234
261
 
235
262
  const summaryBlock = normalizeBlock(synthesis.summaryBlock)
236
263
  if (!summaryBlock) {
237
- throw new Error('Regular chat memory digest returned an empty summaryBlock')
264
+ return yield* new Cause.UnknownError(new Error('Regular chat memory digest returned an empty summaryBlock'))
238
265
  }
239
266
 
240
267
  const processedThreadCursor = getLastCursor(threadMessages)
241
268
  const processedSocialCursor = getLastCursor(socialMessages)
242
- const digestRunAt = new Date().toISOString()
269
+ const digestRunAt = nowIsoDateTimeString()
243
270
 
244
271
  if (synthesis.facts.length > 0) {
245
- await memoryService.addExtractedFactsToScopes({
272
+ yield* services.memoryService.addExtractedFactsToScopes({
246
273
  orgId,
247
274
  facts: synthesis.facts,
248
275
  source: 'regular_chat_digest',
@@ -266,25 +293,27 @@ export async function runRegularChatMemoryDigest(
266
293
  })
267
294
  }
268
295
 
269
- await workspaceProvider.applyProfileProjection(orgRef, {
270
- summaryBlock,
271
- structuredPatch: synthesis.structuredProfilePatch,
272
- })
296
+ yield* effectTryPromise(() =>
297
+ applyProfileProjection(orgRef, { summaryBlock, structuredPatch: synthesis.structuredProfilePatch }),
298
+ )
273
299
  if (processedThreadCursor) {
274
- await workspaceProvider.setBackgroundCursor('regular-chat-digest', orgRef, processedThreadCursor)
300
+ yield* effectTryPromise(() => setBackgroundCursor('regular-chat-digest', orgRef, processedThreadCursor))
275
301
  }
276
302
  if (processedSocialCursor) {
277
- await socialChatHistoryService.setBackgroundCursor('regular-chat-digest', orgId, processedSocialCursor)
303
+ yield* services.socialChatHistoryService.setBackgroundCursor('regular-chat-digest', orgId, processedSocialCursor)
278
304
  }
279
305
 
280
306
  const threadBoundaryCursor = processedThreadCursor ?? existingThreadCursor
281
- const hasMoreThreadMessages = await hasNewEligibleThreadMessages({
282
- threadIds,
283
- cursor: threadBoundaryCursor,
284
- onboardingCutoff: threadBoundaryCursor ? null : threadOnboardingCutoff,
285
- })
307
+ const hasMoreThreadMessages = yield* effectTryPromise(() =>
308
+ hasNewEligibleThreadMessages({
309
+ db: services.databaseService,
310
+ threadIds,
311
+ cursor: threadBoundaryCursor,
312
+ onboardingCutoff: threadBoundaryCursor ? null : threadOnboardingCutoff,
313
+ }),
314
+ )
286
315
  const socialBoundaryCursor = processedSocialCursor ?? existingSocialCursor
287
- const hasMoreSocialMessages = await socialChatHistoryService.hasWorkspaceMessages({
316
+ const hasMoreSocialMessages = yield* services.socialChatHistoryService.hasWorkspaceMessages({
288
317
  workspaceId: orgId,
289
318
  cursor: socialBoundaryCursor,
290
319
  onboardingCutoff: socialBoundaryCursor ? null : socialOnboardingCutoff,
@@ -292,8 +321,8 @@ export async function runRegularChatMemoryDigest(
292
321
 
293
322
  const followUpScheduled = hasMoreThreadMessages || hasMoreSocialMessages
294
323
  if (followUpScheduled) {
295
- await clearRegularChatMemoryDigestDeduplicationKey(orgId)
296
- await enqueueRegularChatMemoryDigest({ orgId })
324
+ yield* effectTryPromise(() => clearRegularChatMemoryDigestDeduplicationKey(orgId))
325
+ yield* effectTryPromise(() => enqueueRegularChatMemoryDigest({ orgId }))
297
326
  }
298
327
 
299
328
  serverLogger.info`Regular chat memory digest completed for ${orgId}: threadMessages=${threadMessages.length}, socialMessages=${socialMessages.length}, facts=${synthesis.facts.length}, followUpScheduled=${followUpScheduled}`
@@ -306,3 +335,35 @@ export async function runRegularChatMemoryDigest(
306
335
  }
307
336
  })
308
337
  }
338
+
339
+ export function runRegularChatMemoryDigest(
340
+ data: RegularChatMemoryDigestJob,
341
+ services: RegularChatDigestServices,
342
+ ): Promise<RegularChatDigestRunResult> {
343
+ const { databaseService, memoryService, socialChatHistoryService, runtimeAdapters } = services
344
+ const orgRef = ensureRecordId(data.orgId, TABLES.ORGANIZATION)
345
+ const orgId = recordIdToString(orgRef, TABLES.ORGANIZATION)
346
+ const workspaceProvider = runtimeAdapters.workspaceProvider
347
+ if (!workspaceProvider) {
348
+ serverLogger.info`Skipping regular chat memory digest for ${orgId}: workspaceProvider is not configured`
349
+ return Promise.resolve({
350
+ skipped: true,
351
+ processedThreadMessages: 0,
352
+ processedSocialMessages: 0,
353
+ followUpScheduled: false,
354
+ })
355
+ }
356
+
357
+ const withMemoryLock = runtimeAdapters.withWorkspaceMemoryLock
358
+ const runDigest = () =>
359
+ Effect.runPromise(
360
+ runRegularChatMemoryDigestEffect(
361
+ { databaseService, memoryService, socialChatHistoryService, runtimeAdapters },
362
+ orgRef,
363
+ orgId,
364
+ workspaceProvider,
365
+ ),
366
+ )
367
+
368
+ return withMemoryLock ? withMemoryLock(orgId, runDigest) : runDigest()
369
+ }