@lota-sdk/core 0.4.7 → 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/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  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
+ }