@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
@@ -4,17 +4,21 @@ import type {
4
4
  ArtifactReference,
5
5
  ArtifactStatus,
6
6
  ArtifactVersionSummary,
7
- GetArtifactResult,
8
7
  PublishArtifactArgs,
9
8
  } from '@lota-sdk/shared'
9
+ import { Cause, Context, Effect, Layer } from 'effect'
10
10
  import { BoundQuery } from 'surrealdb'
11
11
 
12
12
  import type { RecordIdInput } from '../db/record-id'
13
13
  import { ensureRecordId, recordIdToString } from '../db/record-id'
14
- import { databaseService } from '../db/service'
15
- import type { DatabaseTransaction } from '../db/service'
14
+ import type { SurrealDBService, DatabaseTransaction } from '../db/service'
16
15
  import { TABLES } from '../db/tables'
17
- import { generatedDocumentStorageService } from '../storage/generated-document-storage.service'
16
+ import { ConfigurationError, DatabaseError, NotFoundError } from '../effect/errors'
17
+ import { DatabaseServiceTag } from '../effect/services'
18
+ import { toValidationError } from '../effect/zod'
19
+ import type { makeGeneratedDocumentStorageService } from '../storage/generated-document-storage.service'
20
+ import { GeneratedDocumentStorageServiceTag } from '../storage/generated-document-storage.service'
21
+ import { sha256Hex } from '../utils/crypto'
18
22
  import { toIsoDateTimeString } from '../utils/date-time'
19
23
 
20
24
  const ARTIFACT_PUBLISH_MAX_ATTEMPTS = 5
@@ -48,14 +52,6 @@ function describePublishInputShape(value: unknown): string {
48
52
  return typeof value
49
53
  }
50
54
 
51
- function formatPublishError(error: unknown): string {
52
- if (error instanceof Error && error.message.trim()) {
53
- return error.message
54
- }
55
-
56
- return typeof error === 'string' ? error : JSON.stringify(error)
57
- }
58
-
59
55
  function slugify(value: string): string {
60
56
  const normalized = value
61
57
  .trim()
@@ -70,7 +66,7 @@ function encodeStorageSegment(value: string): string {
70
66
  }
71
67
 
72
68
  function hashCanonicalSegment(value: string): string {
73
- return new Bun.CryptoHasher('sha256').update(value.trim()).digest('hex')
69
+ return sha256Hex(value.trim())
74
70
  }
75
71
 
76
72
  function buildCanonicalKey(
@@ -160,61 +156,69 @@ function isRetryablePublishError(error: unknown): boolean {
160
156
 
161
157
  interface PublishArtifactTransactionOptions {
162
158
  onStorageWrite?: (storageKey: string) => void
163
- onStorageCleanup?: (storageKey: string) => void
164
159
  }
165
160
 
166
- class ArtifactService {
167
- private async listArtifactsForCanonicalKeyInTransaction(params: {
161
+ interface ArtifactServiceDeps {
162
+ db: SurrealDBService
163
+ storage: ReturnType<typeof makeGeneratedDocumentStorageService>
164
+ }
165
+
166
+ export function makeArtifactService(deps: ArtifactServiceDeps) {
167
+ const { db, storage } = deps
168
+
169
+ const listArtifactsForCanonicalKeyInTransactionEffect = (params: {
168
170
  tx: DatabaseTransaction
169
171
  organizationId: string
170
172
  canonicalKey: string
171
- }): Promise<ArtifactRecord[]> {
172
- const records = await params.tx.query({
173
- query: `SELECT * FROM ${TABLES.ARTIFACT} WHERE organizationId = $organizationId AND canonicalKey = $canonicalKey ORDER BY version DESC`,
174
- bindings: {
175
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
176
- canonicalKey: params.canonicalKey,
177
- },
173
+ }) =>
174
+ Effect.gen(function* () {
175
+ const records = yield* params.tx.query({
176
+ query: `SELECT * FROM ${TABLES.ARTIFACT} WHERE organizationId = $organizationId AND canonicalKey = $canonicalKey ORDER BY version DESC`,
177
+ bindings: {
178
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
179
+ canonicalKey: params.canonicalKey,
180
+ },
181
+ })
182
+
183
+ return ArtifactRecordSchema.array().parse(records)
178
184
  })
179
185
 
180
- return ArtifactRecordSchema.array().parse(records)
181
- }
182
-
183
- async publishArtifactInTransaction(
186
+ const publishArtifactInTransactionEffect = (
184
187
  args: PublishArtifactArgs,
185
188
  tx: DatabaseTransaction,
186
189
  options: PublishArtifactTransactionOptions = {},
187
- ): Promise<ArtifactRecord> {
188
- let params: PublishArtifactArgs
189
- try {
190
- params = PublishArtifactArgsSchema.parse(args)
191
- } catch (error) {
192
- throw new Error(
193
- `artifact service transaction input parse failed (${describePublishInputShape(args)}): ${formatPublishError(error)}`,
194
- )
195
- }
196
- const organizationRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
197
- const canonicalKey = buildCanonicalKey(params)
198
- const existing = await this.listArtifactsForCanonicalKeyInTransaction({
199
- tx,
200
- organizationId: params.organizationId,
201
- canonicalKey,
202
- })
203
- const latestVersion = existing.reduce((max, record) => Math.max(max, record.version), 0)
204
- const version = latestVersion + 1
205
- const publishAttemptId = Bun.randomUUIDv7()
206
- const references = extractInternalReferences(params.content)
207
- const stored = await generatedDocumentStorageService.writeTextArtifact({
208
- organizationId: params.organizationId,
209
- namespace: 'artifacts',
210
- relativePath: buildArtifactRelativePath({ canonicalKey, version, title: params.title, publishAttemptId }),
211
- content: params.content,
212
- mediaType: 'text/markdown',
213
- })
214
- options.onStorageWrite?.(stored.storageKey)
190
+ ) =>
191
+ Effect.gen(function* () {
192
+ const parsedArgs = PublishArtifactArgsSchema.safeParse(args)
193
+ if (!parsedArgs.success) {
194
+ return yield* toValidationError(
195
+ parsedArgs.error,
196
+ `artifact service transaction input parse failed (${describePublishInputShape(args)})`,
197
+ )
198
+ }
215
199
 
216
- try {
217
- const createdResult = await tx
200
+ const params = parsedArgs.data
201
+ const organizationRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
202
+ const canonicalKey = buildCanonicalKey(params)
203
+ const existing = yield* listArtifactsForCanonicalKeyInTransactionEffect({
204
+ tx,
205
+ organizationId: params.organizationId,
206
+ canonicalKey,
207
+ })
208
+ const latestVersion = existing.reduce((max, record) => Math.max(max, record.version), 0)
209
+ const version = latestVersion + 1
210
+ const publishAttemptId = Bun.randomUUIDv7()
211
+ const references = extractInternalReferences(params.content)
212
+ const stored = yield* storage.writeTextArtifact({
213
+ organizationId: params.organizationId,
214
+ namespace: 'artifacts',
215
+ relativePath: buildArtifactRelativePath({ canonicalKey, version, title: params.title, publishAttemptId }),
216
+ content: params.content,
217
+ mediaType: 'text/markdown',
218
+ })
219
+ options.onStorageWrite?.(stored.storageKey)
220
+
221
+ const createdResult = yield* tx
218
222
  .create(TABLES.ARTIFACT)
219
223
  .content({
220
224
  organizationId: organizationRef,
@@ -238,138 +242,211 @@ class ArtifactService {
238
242
  .output('after')
239
243
  const createdRecord: unknown = Array.isArray(createdResult) ? createdResult.at(0) : createdResult
240
244
  if (!createdRecord) {
241
- throw new Error(`Artifact create returned no record for canonical key ${canonicalKey}.`)
245
+ return yield* new ConfigurationError({
246
+ message: `Artifact create returned no record for canonical key ${canonicalKey}.`,
247
+ })
242
248
  }
243
249
 
244
250
  const created = ArtifactRecordSchema.parse(createdRecord)
245
251
 
246
252
  const previousActive = existing.find((record) => record.status === 'active')
247
253
  if (previousActive) {
248
- await tx
254
+ yield* tx
249
255
  .update(ensureRecordId(previousActive.id, TABLES.ARTIFACT))
250
256
  .merge({ status: 'superseded', supersededBy: ensureRecordId(created.id, TABLES.ARTIFACT) })
251
257
  .output('after')
252
258
  }
253
259
 
254
260
  return created
255
- } catch (error) {
256
- await generatedDocumentStorageService.deleteTextArtifact(stored.storageKey).catch(() => undefined)
257
- options.onStorageCleanup?.(stored.storageKey)
258
- throw error
259
- }
260
- }
261
+ })
261
262
 
262
- async publishArtifact(args: PublishArtifactArgs): Promise<ArtifactRecord> {
263
- let params: PublishArtifactArgs
264
- try {
265
- params = PublishArtifactArgsSchema.parse(args)
266
- } catch (error) {
267
- throw new Error(
268
- `artifact service input parse failed (${describePublishInputShape(args)}): ${formatPublishError(error)}`,
269
- )
270
- }
263
+ const publishArtifactEffect = (args: PublishArtifactArgs) =>
264
+ Effect.gen(function* () {
265
+ const parsedArgs = PublishArtifactArgsSchema.safeParse(args)
266
+ if (!parsedArgs.success) {
267
+ return yield* toValidationError(
268
+ parsedArgs.error,
269
+ `artifact service input parse failed (${describePublishInputShape(args)})`,
270
+ )
271
+ }
271
272
 
272
- let lastError: unknown = null
273
- for (let attempt = 1; attempt <= ARTIFACT_PUBLISH_MAX_ATTEMPTS; attempt += 1) {
274
- const publishAttemptState: { pendingStorageKey?: string } = {}
275
- try {
276
- return await databaseService.withTransaction(
277
- async (tx) =>
278
- await this.publishArtifactInTransaction(params, tx, {
273
+ const params = parsedArgs.data
274
+ let lastError: unknown = null
275
+ for (let attempt = 1; attempt <= ARTIFACT_PUBLISH_MAX_ATTEMPTS; attempt += 1) {
276
+ const publishAttemptState: { pendingStorageKey?: string } = {}
277
+ const attemptExit = yield* Effect.exit(
278
+ db.withTransaction((tx) =>
279
+ publishArtifactInTransactionEffect(params, tx, {
279
280
  onStorageWrite: (storageKey) => {
280
281
  publishAttemptState.pendingStorageKey = storageKey
281
282
  },
282
- onStorageCleanup: (storageKey) => {
283
- if (publishAttemptState.pendingStorageKey === storageKey) {
284
- publishAttemptState.pendingStorageKey = undefined
285
- }
286
- },
287
283
  }),
284
+ ),
288
285
  )
289
- } catch (error) {
286
+ if (attemptExit._tag === 'Success') {
287
+ return attemptExit.value
288
+ }
289
+
290
+ const error = Cause.squash(attemptExit.cause)
290
291
  const storageKeyToCleanup = publishAttemptState.pendingStorageKey
291
292
  publishAttemptState.pendingStorageKey = undefined
292
293
  if (typeof storageKeyToCleanup === 'string') {
293
- await generatedDocumentStorageService.deleteTextArtifact(storageKeyToCleanup).catch(() => undefined)
294
+ yield* Effect.ignore(storage.deleteTextArtifact(storageKeyToCleanup))
294
295
  }
295
296
  lastError = error
296
297
  if (!isRetryablePublishError(error) || attempt === ARTIFACT_PUBLISH_MAX_ATTEMPTS) {
297
- throw error
298
+ if (error instanceof Error) {
299
+ return yield* Effect.fail(error)
300
+ }
301
+
302
+ return yield* new ConfigurationError({ message: 'Artifact publish failed.' })
298
303
  }
299
304
  }
300
- }
301
305
 
302
- throw lastError instanceof Error ? lastError : new Error('Artifact publish failed.')
303
- }
306
+ if (lastError instanceof Error) {
307
+ return yield* Effect.fail(lastError)
308
+ }
304
309
 
305
- async getArtifactRecord(artifactId: RecordIdInput): Promise<ArtifactRecord | null> {
306
- return await databaseService.findOne(
307
- TABLES.ARTIFACT,
308
- { id: ensureRecordId(artifactId, TABLES.ARTIFACT) },
309
- ArtifactRecordSchema,
310
- )
311
- }
310
+ return yield* new ConfigurationError({ message: 'Artifact publish failed.' })
311
+ })
312
+
313
+ const getArtifactRecordEffect = (artifactId: RecordIdInput) =>
314
+ db
315
+ .findOne(TABLES.ARTIFACT, { id: ensureRecordId(artifactId, TABLES.ARTIFACT) }, ArtifactRecordSchema)
316
+ .pipe(
317
+ Effect.mapError(
318
+ (cause) =>
319
+ new DatabaseError({
320
+ message: `Failed to load artifact ${recordIdToString(artifactId, TABLES.ARTIFACT)}.`,
321
+ cause,
322
+ }),
323
+ ),
324
+ )
312
325
 
313
- async listArtifacts(params: {
326
+ const listArtifactsEffect = (params: {
314
327
  organizationId: RecordIdInput
315
328
  canonicalKey?: string
316
329
  status?: ArtifactStatus
317
330
  includeNonActive?: boolean
318
331
  sourceThreadId?: RecordIdInput
319
332
  limit?: number
320
- }): Promise<ArtifactRecord[]> {
321
- const records = await databaseService.findMany(
322
- TABLES.ARTIFACT,
323
- {
324
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
325
- ...(params.canonicalKey ? { canonicalKey: params.canonicalKey } : {}),
326
- ...(params.sourceThreadId ? { sourceThreadId: ensureRecordId(params.sourceThreadId, TABLES.THREAD) } : {}),
327
- },
328
- ArtifactRecordSchema,
329
- { orderBy: 'createdAt', orderDir: 'DESC', ...(params.limit ? { limit: params.limit } : {}) },
330
- )
331
-
332
- if (params.status) {
333
- return records.filter((record) => record.status === params.status)
334
- }
333
+ }) =>
334
+ Effect.gen(function* () {
335
+ const records = yield* db
336
+ .findMany(
337
+ TABLES.ARTIFACT,
338
+ {
339
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
340
+ ...(params.canonicalKey ? { canonicalKey: params.canonicalKey } : {}),
341
+ ...(params.sourceThreadId ? { sourceThreadId: ensureRecordId(params.sourceThreadId, TABLES.THREAD) } : {}),
342
+ },
343
+ ArtifactRecordSchema,
344
+ { orderBy: 'createdAt', orderDir: 'DESC', ...(params.limit ? { limit: params.limit } : {}) },
345
+ )
346
+ .pipe(
347
+ Effect.mapError(
348
+ (cause) =>
349
+ new DatabaseError({
350
+ message: `Failed to list artifacts for organization ${recordIdToString(params.organizationId, TABLES.ORGANIZATION)}.`,
351
+ cause,
352
+ }),
353
+ ),
354
+ )
335
355
 
336
- if (params.includeNonActive) {
337
- return records
338
- }
356
+ if (params.status) {
357
+ return records.filter((record) => record.status === params.status)
358
+ }
339
359
 
340
- return records.filter((record) => record.status === 'active')
341
- }
360
+ if (params.includeNonActive) {
361
+ return records
362
+ }
342
363
 
343
- async getArtifact(artifactId: RecordIdInput): Promise<GetArtifactResult> {
344
- const artifact = await this.getArtifactRecord(artifactId)
345
- if (!artifact) {
346
- throw new Error(`Artifact not found: ${recordIdToString(artifactId, TABLES.ARTIFACT)}`)
347
- }
364
+ return records.filter((record) => record.status === 'active')
365
+ })
348
366
 
349
- const [content, versions, backlinks] = await Promise.all([
350
- generatedDocumentStorageService.readTextArtifact(artifact.storageKey),
351
- this.listArtifacts({
352
- organizationId: artifact.organizationId,
353
- canonicalKey: artifact.canonicalKey,
354
- includeNonActive: true,
355
- }),
356
- this.listBacklinks(artifact),
357
- ])
358
-
359
- return GetArtifactResultSchema.parse({ artifact, content, versions: versions.map(toVersionSummary), backlinks })
360
- }
367
+ const getArtifactEffect = (artifactId: RecordIdInput) =>
368
+ Effect.gen(function* () {
369
+ const artifact = yield* getArtifactRecordEffect(artifactId)
370
+ if (!artifact) {
371
+ return yield* new NotFoundError({
372
+ resource: TABLES.ARTIFACT,
373
+ id: recordIdToString(artifactId, TABLES.ARTIFACT),
374
+ message: `Artifact not found: ${recordIdToString(artifactId, TABLES.ARTIFACT)}`,
375
+ })
376
+ }
361
377
 
362
- async listBacklinks(artifact: Pick<ArtifactRecord, 'id' | 'organizationId'>): Promise<ArtifactRecord[]> {
363
- const artifactId = recordIdToString(artifact.id, TABLES.ARTIFACT)
364
- const records = await databaseService.queryMany(
365
- new BoundQuery(
366
- `SELECT * FROM ${TABLES.ARTIFACT} WHERE organizationId = $organizationId AND references[*].targetId CONTAINS $targetId`,
367
- { organizationId: ensureRecordId(artifact.organizationId, TABLES.ORGANIZATION), targetId: artifactId },
368
- ),
369
- ArtifactRecordSchema,
370
- )
371
- return records
378
+ const [content, versions, backlinks] = yield* Effect.all([
379
+ storage
380
+ .readTextArtifact(artifact.storageKey)
381
+ .pipe(
382
+ Effect.mapError(
383
+ (cause) =>
384
+ new DatabaseError({ message: `Failed to read artifact content for ${artifact.canonicalKey}.`, cause }),
385
+ ),
386
+ ),
387
+ listArtifactsEffect({
388
+ organizationId: artifact.organizationId,
389
+ canonicalKey: artifact.canonicalKey,
390
+ includeNonActive: true,
391
+ }),
392
+ listBacklinksEffect(artifact),
393
+ ])
394
+
395
+ return GetArtifactResultSchema.parse({ artifact, content, versions: versions.map(toVersionSummary), backlinks })
396
+ })
397
+
398
+ const listBacklinksEffect = (artifact: Pick<ArtifactRecord, 'id' | 'organizationId'>) =>
399
+ db
400
+ .queryMany(
401
+ new BoundQuery(
402
+ `SELECT * FROM ${TABLES.ARTIFACT} WHERE organizationId = $organizationId AND references[*].targetId CONTAINS $targetId`,
403
+ {
404
+ organizationId: ensureRecordId(artifact.organizationId, TABLES.ORGANIZATION),
405
+ targetId: recordIdToString(artifact.id, TABLES.ARTIFACT),
406
+ },
407
+ ),
408
+ ArtifactRecordSchema,
409
+ )
410
+ .pipe(
411
+ Effect.mapError(
412
+ (cause) =>
413
+ new DatabaseError({
414
+ message: `Failed to load backlinks for ${recordIdToString(artifact.id, TABLES.ARTIFACT)}.`,
415
+ cause,
416
+ }),
417
+ ),
418
+ )
419
+
420
+ return {
421
+ publishArtifactInTransaction: (
422
+ params: PublishArtifactArgs,
423
+ tx: DatabaseTransaction,
424
+ options?: PublishArtifactTransactionOptions,
425
+ ) => publishArtifactInTransactionEffect(params, tx, options),
426
+ publishArtifact: (params: PublishArtifactArgs) => publishArtifactEffect(params),
427
+ getArtifactRecord: (artifactId: RecordIdInput) => getArtifactRecordEffect(artifactId),
428
+ listArtifacts: (params: {
429
+ organizationId: RecordIdInput
430
+ canonicalKey?: string
431
+ status?: ArtifactStatus
432
+ includeNonActive?: boolean
433
+ sourceThreadId?: RecordIdInput
434
+ limit?: number
435
+ }) => listArtifactsEffect(params),
436
+ getArtifact: (artifactId: RecordIdInput) => getArtifactEffect(artifactId),
437
+ listBacklinks: (artifact: Pick<ArtifactRecord, 'id' | 'organizationId'>) => listBacklinksEffect(artifact),
372
438
  }
373
439
  }
374
440
 
375
- export const artifactService = new ArtifactService()
441
+ export class ArtifactServiceTag extends Context.Service<ArtifactServiceTag, ReturnType<typeof makeArtifactService>>()(
442
+ 'ArtifactService',
443
+ ) {}
444
+
445
+ export const ArtifactServiceLive = Layer.effect(
446
+ ArtifactServiceTag,
447
+ Effect.gen(function* () {
448
+ const db = yield* DatabaseServiceTag
449
+ const storage = yield* GeneratedDocumentStorageServiceTag
450
+ return makeArtifactService({ db, storage })
451
+ }),
452
+ )