@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,12 +1,18 @@
1
+ import type { Context, Cause } from 'effect'
2
+ import { Effect } from 'effect'
3
+
1
4
  import { serverLogger } from '../config/logger'
2
5
  import { ensureRecordId, recordIdToString } from '../db/record-id'
6
+ import type { RecordIdRef } from '../db/record-id'
7
+ import type { SurrealDBService } from '../db/service'
3
8
  import { TABLES } from '../db/tables'
4
- import { getDefaultEmbeddings } from '../embeddings/provider'
9
+ import { effectTryPromise } from '../effect/helpers'
10
+ import { ProviderEmbeddings } from '../embeddings/provider'
5
11
  import type { SkillExtractionJob } from '../queues/organization-learning.queue'
6
12
  import { createHelperModelRuntime } from '../runtime/helper-model'
7
- import { getRuntimeAdapters, withConfiguredWorkspaceMemoryLock } from '../runtime/runtime-extensions'
8
- import { learnedSkillService } from '../services/learned-skill.service'
9
- import { socialChatHistoryService } from '../services/social-chat-history.service'
13
+ import type { LotaRuntimeAdapters } from '../runtime/runtime-extensions'
14
+ import type { LearnedSkillServiceTag } from '../services/learned-skill.service'
15
+ import type { SocialChatHistoryServiceTag } from '../services/social-chat-history.service'
10
16
  import { createSkillExtractorAgent, SkillExtractionOutputSchema } from '../system-agents/skill-extractor.agent'
11
17
  import type { SkillCandidate } from '../system-agents/skill-extractor.agent'
12
18
  import { createSkillManagerAgent, SkillManagerOutputSchema } from '../system-agents/skill-manager.agent'
@@ -20,16 +26,36 @@ import {
20
26
  const SKILL_EXTRACTION_TIMEOUT_MS = 10 * 60 * 1000
21
27
  const MIN_MESSAGE_THRESHOLD = 10
22
28
 
29
+ export interface SkillExtractionServices {
30
+ databaseService: SurrealDBService
31
+ learnedSkillService: Context.Service.Shape<typeof LearnedSkillServiceTag>
32
+ socialChatHistoryService: Context.Service.Shape<typeof SocialChatHistoryServiceTag>
33
+ runtimeAdapters: LotaRuntimeAdapters
34
+ embeddingModel: string
35
+ openRouterApiKey?: string
36
+ }
37
+
23
38
  interface SkillExtractionRunResult {
24
39
  skipped: boolean
25
40
  processedMessages: number
26
41
  extractedSkills: number
27
42
  }
28
43
 
29
- const embeddings = getDefaultEmbeddings()
30
-
31
44
  const helperModelRuntime = createHelperModelRuntime()
32
45
 
46
+ type CursorAwareWorkspaceProvider = NonNullable<LotaRuntimeAdapters['workspaceProvider']> & {
47
+ getBackgroundCursor: NonNullable<NonNullable<LotaRuntimeAdapters['workspaceProvider']>['getBackgroundCursor']>
48
+ setBackgroundCursor: NonNullable<NonNullable<LotaRuntimeAdapters['workspaceProvider']>['setBackgroundCursor']>
49
+ }
50
+
51
+ function hasCursorAwareWorkspaceProvider(
52
+ workspaceProvider: LotaRuntimeAdapters['workspaceProvider'],
53
+ ): workspaceProvider is CursorAwareWorkspaceProvider {
54
+ const getBackgroundCursor = workspaceProvider?.getBackgroundCursor?.bind(workspaceProvider)
55
+ const setBackgroundCursor = workspaceProvider?.setBackgroundCursor?.bind(workspaceProvider)
56
+ return Boolean(getBackgroundCursor && setBackgroundCursor)
57
+ }
58
+
33
59
  function buildExtractionPrompt(params: { workspaceName: string; transcript: string; existingSkills: string }): string {
34
60
  return [
35
61
  `Workspace name: ${params.workspaceName}`,
@@ -71,45 +97,46 @@ function buildManagerPrompt(params: {
71
97
  return parts.join('\n')
72
98
  }
73
99
 
74
- export async function runSkillExtraction(data: SkillExtractionJob): Promise<SkillExtractionRunResult> {
75
- const orgRef = ensureRecordId(data.orgId, TABLES.ORGANIZATION)
76
- const orgId = recordIdToString(orgRef, TABLES.ORGANIZATION)
77
- const workspaceProvider = getRuntimeAdapters().workspaceProvider
78
- const cursorAwareWorkspaceProvider =
79
- workspaceProvider?.getBackgroundCursor && workspaceProvider.setBackgroundCursor
80
- ? (workspaceProvider as typeof workspaceProvider & {
81
- getBackgroundCursor: NonNullable<typeof workspaceProvider.getBackgroundCursor>
82
- setBackgroundCursor: NonNullable<typeof workspaceProvider.setBackgroundCursor>
83
- })
100
+ function runSkillExtractionEffect(
101
+ services: SkillExtractionServices,
102
+ orgRef: RecordIdRef,
103
+ orgId: string,
104
+ cursorAwareWorkspaceProvider: CursorAwareWorkspaceProvider,
105
+ embeddings: ProviderEmbeddings,
106
+ ) {
107
+ return Effect.gen(function* () {
108
+ const getBackgroundCursor = cursorAwareWorkspaceProvider.getBackgroundCursor.bind(cursorAwareWorkspaceProvider)
109
+ const setBackgroundCursor = cursorAwareWorkspaceProvider.setBackgroundCursor.bind(cursorAwareWorkspaceProvider)
110
+ const workspace = yield* effectTryPromise(() => cursorAwareWorkspaceProvider.getWorkspace(orgRef))
111
+ const lifecycleState = cursorAwareWorkspaceProvider.getLifecycleState
112
+ ? yield* effectTryPromise(() => Promise.resolve(cursorAwareWorkspaceProvider.getLifecycleState?.(workspace)))
84
113
  : undefined
85
- if (!cursorAwareWorkspaceProvider) {
86
- serverLogger.info`Skipping skill extraction for ${orgId}: workspaceProvider background cursor methods are not configured`
87
- return { skipped: true, processedMessages: 0, extractedSkills: 0 }
88
- }
89
-
90
- return withConfiguredWorkspaceMemoryLock(orgId, async () => {
91
- const workspace = await cursorAwareWorkspaceProvider.getWorkspace(orgRef)
92
- const lifecycleState = await cursorAwareWorkspaceProvider.getLifecycleState?.(workspace)
93
114
  if (lifecycleState?.bootstrapActive ?? false) {
94
115
  serverLogger.info`Skipping skill extraction for ${orgId}: onboarding is not completed`
95
116
  return { skipped: true, processedMessages: 0, extractedSkills: 0 }
96
117
  }
97
- const projectionState = await cursorAwareWorkspaceProvider.readProfileProjectionState?.(workspace)
118
+ const projectionState = cursorAwareWorkspaceProvider.readProfileProjectionState
119
+ ? yield* effectTryPromise(() =>
120
+ Promise.resolve(cursorAwareWorkspaceProvider.readProfileProjectionState?.(workspace)),
121
+ )
122
+ : undefined
98
123
 
99
- const existingCursor = await cursorAwareWorkspaceProvider.getBackgroundCursor('skill-extraction', orgRef)
124
+ const existingCursor = yield* effectTryPromise(() => getBackgroundCursor('skill-extraction', orgRef))
100
125
  const onboardingCutoff = resolveWorkspaceBootstrapCutoff({
101
126
  hasExistingCursor: existingCursor !== null,
102
127
  bootstrapCompletedAt: lifecycleState?.bootstrapCompletedAt,
103
128
  })
104
- const existingSocialCursor = await socialChatHistoryService.getBackgroundCursor('skill-extraction', orgId)
129
+ const existingSocialCursor = yield* services.socialChatHistoryService.getBackgroundCursor('skill-extraction', orgId)
105
130
  const socialOnboardingCutoff = resolveWorkspaceBootstrapCutoff({
106
131
  hasExistingCursor: existingSocialCursor !== null,
107
132
  bootstrapCompletedAt: lifecycleState?.bootstrapCompletedAt,
108
133
  })
109
134
 
110
- const threadIds = await listThreadIdsForOrg(orgRef)
111
- const threadMessages = await listEligibleThreadMessages({ threadIds, cursor: existingCursor, onboardingCutoff })
112
- const socialMessages = await socialChatHistoryService.listWorkspaceMessages({
135
+ const threadIds = yield* effectTryPromise(() => listThreadIdsForOrg(services.databaseService, orgRef))
136
+ const threadMessages = yield* effectTryPromise(() =>
137
+ listEligibleThreadMessages({ db: services.databaseService, threadIds, cursor: existingCursor, onboardingCutoff }),
138
+ )
139
+ const socialMessages = yield* services.socialChatHistoryService.listWorkspaceMessages({
113
140
  workspaceId: orgId,
114
141
  cursor: existingSocialCursor,
115
142
  onboardingCutoff: socialOnboardingCutoff,
@@ -124,79 +151,82 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
124
151
  const sortedMessages = [...messages].sort(compareDigestMessageOrder)
125
152
  const { transcript } = buildDigestTranscript({ messages: sortedMessages })
126
153
 
127
- const existingSkills = await learnedSkillService.listForOrg(orgId)
154
+ const existingSkills = yield* services.learnedSkillService.listForOrg(orgId)
128
155
  const existingSkillsSummary =
129
156
  existingSkills.length > 0
130
157
  ? existingSkills.map((skill, i) => `${i + 1}. ${skill.name}: ${skill.description}`).join('\n')
131
158
  : 'None'
132
159
 
133
- const extraction = await helperModelRuntime.generateHelperStructured({
134
- tag: 'skill-extraction',
135
- createAgent: createSkillExtractorAgent,
136
- timeoutMs: SKILL_EXTRACTION_TIMEOUT_MS,
137
- messages: [
138
- {
139
- role: 'user',
140
- content: buildExtractionPrompt({
141
- workspaceName: projectionState?.workspaceName || 'Workspace',
142
- transcript,
143
- existingSkills: existingSkillsSummary,
144
- }),
145
- },
146
- ],
147
- schema: SkillExtractionOutputSchema,
148
- })
160
+ const extraction = yield* effectTryPromise(() =>
161
+ helperModelRuntime.generateHelperStructured({
162
+ tag: 'skill-extraction',
163
+ createAgent: createSkillExtractorAgent,
164
+ timeoutMs: SKILL_EXTRACTION_TIMEOUT_MS,
165
+ messages: [
166
+ {
167
+ role: 'user',
168
+ content: buildExtractionPrompt({
169
+ workspaceName: projectionState?.workspaceName || 'Workspace',
170
+ transcript,
171
+ existingSkills: existingSkillsSummary,
172
+ }),
173
+ },
174
+ ],
175
+ schema: SkillExtractionOutputSchema,
176
+ }),
177
+ )
149
178
 
150
179
  const skillCandidates = extraction.candidates.filter((c) => c.classification === 'skill')
151
- let extractedSkills = 0
152
180
 
153
- for (const candidate of skillCandidates) {
154
- try {
155
- const hash = learnedSkillService.generateHash(candidate.description, candidate.instructions)
156
- const existingByHash = await learnedSkillService.findByHash(orgId, hash)
157
- if (existingByHash) {
181
+ const processCandidate = (candidate: SkillCandidate): Effect.Effect<number, Cause.UnknownError, never> => {
182
+ const processCandidateEffect = Effect.gen(function* () {
183
+ const hash = services.learnedSkillService.generateHash(candidate.description, candidate.instructions)
184
+ const existingByHash = yield* services.learnedSkillService.findByHash(orgId, hash)
185
+ if (existingByHash !== null) {
158
186
  serverLogger.info`Skipping duplicate skill candidate ${candidate.name} (hash match)`
159
- continue
187
+ return 0
160
188
  }
161
189
 
162
- const mostSimilar = await learnedSkillService.findMostSimilar(orgId, candidate.description)
163
-
164
- const managerResult = await helperModelRuntime.generateHelperStructured({
165
- tag: 'skill-manager',
166
- createAgent: createSkillManagerAgent,
167
- timeoutMs: SKILL_EXTRACTION_TIMEOUT_MS,
168
- messages: [
169
- {
170
- role: 'user',
171
- content: buildManagerPrompt({
172
- candidate,
173
- existingSkill: mostSimilar
174
- ? {
175
- name: mostSimilar.name,
176
- description: mostSimilar.description,
177
- instructions: mostSimilar.instructions,
178
- version: mostSimilar.version,
179
- }
180
- : null,
181
- }),
182
- },
183
- ],
184
- schema: SkillManagerOutputSchema,
185
- })
190
+ const mostSimilar = yield* services.learnedSkillService.findMostSimilar(orgId, candidate.description)
191
+
192
+ const managerResult = yield* effectTryPromise(() =>
193
+ helperModelRuntime.generateHelperStructured({
194
+ tag: 'skill-manager',
195
+ createAgent: createSkillManagerAgent,
196
+ timeoutMs: SKILL_EXTRACTION_TIMEOUT_MS,
197
+ messages: [
198
+ {
199
+ role: 'user',
200
+ content: buildManagerPrompt({
201
+ candidate,
202
+ existingSkill: mostSimilar
203
+ ? {
204
+ name: mostSimilar.name,
205
+ description: mostSimilar.description,
206
+ instructions: mostSimilar.instructions,
207
+ version: mostSimilar.version,
208
+ }
209
+ : null,
210
+ }),
211
+ },
212
+ ],
213
+ schema: SkillManagerOutputSchema,
214
+ }),
215
+ )
186
216
 
187
217
  if (managerResult.decision === 'discard') {
188
218
  serverLogger.info`Discarding skill candidate ${candidate.name}: ${managerResult.reason}`
189
- continue
219
+ return 0
190
220
  }
191
221
 
192
- const embedding = await embeddings.embedQuery(candidate.description)
222
+ const embedding = yield* effectTryPromise(() => embeddings.embedQuery(candidate.description))
193
223
  if (embedding.length === 0) {
194
224
  serverLogger.warn`Skipping skill candidate ${candidate.name}: empty embedding`
195
- continue
225
+ return 0
196
226
  }
197
227
 
198
228
  if (managerResult.decision === 'add') {
199
- await learnedSkillService.create({
229
+ yield* services.learnedSkillService.create({
200
230
  name: candidate.name,
201
231
  description: candidate.description,
202
232
  instructions: candidate.instructions,
@@ -210,14 +240,16 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
210
240
  embedding,
211
241
  hash,
212
242
  })
213
- extractedSkills++
214
243
  serverLogger.info`Added new learned skill: ${candidate.name}`
215
- } else if (mostSimilar && managerResult.mergedSkill) {
244
+ return 1
245
+ }
246
+
247
+ if (mostSimilar && managerResult.mergedSkill) {
216
248
  const merged = managerResult.mergedSkill
217
- const mergedHash = learnedSkillService.generateHash(merged.description, merged.instructions)
218
- const mergedEmbedding = await embeddings.embedQuery(merged.description)
249
+ const mergedHash = services.learnedSkillService.generateHash(merged.description, merged.instructions)
250
+ const mergedEmbedding = yield* effectTryPromise(() => embeddings.embedQuery(merged.description))
219
251
 
220
- await learnedSkillService.update(mostSimilar.id, {
252
+ yield* services.learnedSkillService.update(mostSimilar.id, {
221
253
  name: merged.name,
222
254
  description: merged.description,
223
255
  instructions: merged.instructions,
@@ -229,21 +261,34 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
229
261
  embedding: mergedEmbedding,
230
262
  hash: mergedHash,
231
263
  })
232
- extractedSkills++
233
264
  serverLogger.info`Merged skill candidate into ${mostSimilar.name} (v${mostSimilar.version + 1})`
265
+ return 1
234
266
  }
235
- } catch (candidateError) {
236
- serverLogger.warn`Failed to process skill candidate ${candidate.name}: ${candidateError}`
237
- }
267
+
268
+ return 0
269
+ })
270
+
271
+ return Effect.matchCauseEffect(processCandidateEffect, {
272
+ onFailure: (cause) =>
273
+ Effect.sync(() => {
274
+ serverLogger.warn`Failed to process skill candidate ${candidate.name}: ${cause}`
275
+ return 0
276
+ }),
277
+ onSuccess: (value) => Effect.succeed(value),
278
+ })
238
279
  }
280
+ const extractedSkills = (yield* Effect.forEach(skillCandidates, processCandidate, { concurrency: 2 })).reduce(
281
+ (sum, n) => sum + n,
282
+ 0,
283
+ )
239
284
 
240
285
  const lastThreadMessage = threadMessages.at(-1)
241
286
  const lastSocialMessage = socialMessages.at(-1)
242
287
  if (lastThreadMessage) {
243
- await cursorAwareWorkspaceProvider.setBackgroundCursor('skill-extraction', orgRef, lastThreadMessage.cursor)
288
+ yield* effectTryPromise(() => setBackgroundCursor('skill-extraction', orgRef, lastThreadMessage.cursor))
244
289
  }
245
290
  if (lastSocialMessage) {
246
- await socialChatHistoryService.setBackgroundCursor('skill-extraction', orgId, lastSocialMessage.cursor)
291
+ yield* services.socialChatHistoryService.setBackgroundCursor('skill-extraction', orgId, lastSocialMessage.cursor)
247
292
  }
248
293
 
249
294
  serverLogger.info`Skill extraction completed for ${orgId}: messages=${messages.length}, socialMessages=${socialMessages.length}, extracted=${extractedSkills}`
@@ -251,3 +296,41 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
251
296
  return { skipped: false, processedMessages: messages.length, extractedSkills }
252
297
  })
253
298
  }
299
+
300
+ export function runSkillExtraction(
301
+ data: SkillExtractionJob,
302
+ services: SkillExtractionServices,
303
+ ): Promise<SkillExtractionRunResult> {
304
+ const { databaseService, learnedSkillService, socialChatHistoryService, runtimeAdapters } = services
305
+ const orgRef = ensureRecordId(data.orgId, TABLES.ORGANIZATION)
306
+ const orgId = recordIdToString(orgRef, TABLES.ORGANIZATION)
307
+ const workspaceProvider = runtimeAdapters.workspaceProvider
308
+ if (!hasCursorAwareWorkspaceProvider(workspaceProvider)) {
309
+ serverLogger.info`Skipping skill extraction for ${orgId}: workspaceProvider background cursor methods are not configured`
310
+ return Promise.resolve({ skipped: true, processedMessages: 0, extractedSkills: 0 })
311
+ }
312
+
313
+ const embeddings = new ProviderEmbeddings({
314
+ modelId: services.embeddingModel,
315
+ openRouterApiKey: services.openRouterApiKey,
316
+ })
317
+ const withMemoryLock = runtimeAdapters.withWorkspaceMemoryLock
318
+ const runExtraction = () =>
319
+ Effect.runPromise(
320
+ runSkillExtractionEffect(
321
+ {
322
+ databaseService,
323
+ learnedSkillService,
324
+ socialChatHistoryService,
325
+ runtimeAdapters,
326
+ embeddingModel: services.embeddingModel,
327
+ },
328
+ orgRef,
329
+ orgId,
330
+ workspaceProvider,
331
+ embeddings,
332
+ ),
333
+ )
334
+
335
+ return withMemoryLock ? withMemoryLock(orgId, runExtraction) : runExtraction()
336
+ }
@@ -50,7 +50,7 @@ function normalizeMinChunkChars(value: number | undefined, maxChars: number): nu
50
50
  return Math.min(normalized, Math.floor(maxChars * 0.6))
51
51
  }
52
52
 
53
- async function splitTextByCharBudget(text: string, maxChars: number): Promise<string[]> {
53
+ function splitTextByCharBudget(text: string, maxChars: number): string[] {
54
54
  const source = text.trim()
55
55
  if (!source) return []
56
56
  if (source.length <= maxChars) return [source]
@@ -157,16 +157,16 @@ function mergeTinyTailParts(parts: string[], options: { minChunkChars: number; m
157
157
  return merged
158
158
  }
159
159
 
160
- async function splitOversizedSection(
160
+ function splitOversizedSection(
161
161
  section: FileSection,
162
162
  options: { maxChars: number; minChunkChars: number; preserveCodeFenceIntegrity: boolean },
163
- ): Promise<FileSection[]> {
163
+ ): FileSection[] {
164
164
  if (section.content.length <= options.maxChars) {
165
165
  return [section]
166
166
  }
167
167
 
168
168
  if (section.kind !== 'file' || !section.filePath) {
169
- const chunks = await splitTextByCharBudget(section.content, options.maxChars)
169
+ const chunks = splitTextByCharBudget(section.content, options.maxChars)
170
170
  return chunks.map((content) => ({ kind: section.kind, content }))
171
171
  }
172
172
 
@@ -223,10 +223,10 @@ function mergeTinyChunks(
223
223
  return merged
224
224
  }
225
225
 
226
- export async function chunkFileSections(
226
+ export function chunkFileSections(
227
227
  fileSections: readonly FileSection[],
228
228
  options: FileSectionChunkOptions = {},
229
- ): Promise<FileSectionChunk[]> {
229
+ ): FileSectionChunk[] {
230
230
  const maxChars = normalizeMaxChars(options.maxChars)
231
231
  const minChunkChars = normalizeMinChunkChars(options.minChunkChars, maxChars)
232
232
  const preserveCodeFenceIntegrity = options.preserveCodeFenceIntegrity ?? true
@@ -235,10 +235,8 @@ export async function chunkFileSections(
235
235
  .map((section) => ({ kind: section.kind, content: section.content.trim(), filePath: section.filePath }))
236
236
  .filter((section) => section.content.length > 0)
237
237
 
238
- const splitSections = await Promise.all(
239
- rawSections.map(
240
- async (section) => await splitOversizedSection(section, { maxChars, minChunkChars, preserveCodeFenceIntegrity }),
241
- ),
238
+ const splitSections = rawSections.map((section) =>
239
+ splitOversizedSection(section, { maxChars, minChunkChars, preserveCodeFenceIntegrity }),
242
240
  )
243
241
  const sections = splitSections.flat()
244
242