@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
@@ -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
+ )