@lota-sdk/core 0.4.8 → 0.4.10

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 (272) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +96 -22
  3. package/src/ai-gateway/ai-gateway.ts +766 -223
  4. package/src/config/agent-defaults.ts +189 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/constants.ts +8 -2
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +299 -19
  10. package/src/config/thread-defaults.ts +40 -20
  11. package/src/create-runtime.ts +200 -449
  12. package/src/db/base.service.ts +52 -28
  13. package/src/db/cursor-pagination.ts +71 -30
  14. package/src/db/memory-query-builder.ts +2 -1
  15. package/src/db/memory-store.helpers.ts +4 -7
  16. package/src/db/memory-store.ts +868 -601
  17. package/src/db/memory.ts +396 -280
  18. package/src/db/record-id.ts +32 -10
  19. package/src/db/schema-fingerprint.ts +30 -12
  20. package/src/db/service-normalization.ts +288 -0
  21. package/src/db/service.ts +912 -779
  22. package/src/db/startup.ts +153 -68
  23. package/src/db/transaction-conflict.ts +15 -0
  24. package/src/effect/awaitable-effect.ts +96 -0
  25. package/src/effect/errors.ts +121 -0
  26. package/src/effect/helpers.ts +123 -0
  27. package/src/effect/index.ts +24 -0
  28. package/src/effect/layers.ts +238 -0
  29. package/src/effect/runtime-ref.ts +25 -0
  30. package/src/effect/runtime.ts +46 -0
  31. package/src/effect/services.ts +61 -0
  32. package/src/effect/zod.ts +43 -0
  33. package/src/embeddings/provider.ts +128 -83
  34. package/src/index.ts +48 -1
  35. package/src/openrouter/direct-provider.ts +11 -35
  36. package/src/queues/autonomous-job.queue.ts +117 -73
  37. package/src/queues/context-compaction.queue.ts +50 -17
  38. package/src/queues/delayed-node-promotion.queue.ts +46 -17
  39. package/src/queues/document-processor.queue.ts +52 -77
  40. package/src/queues/memory-consolidation.queue.ts +47 -32
  41. package/src/queues/organization-learning.queue.ts +26 -4
  42. package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
  43. package/src/queues/plan-scheduler.queue.ts +97 -33
  44. package/src/queues/post-chat-memory.queue.ts +56 -26
  45. package/src/queues/queue-factory.ts +227 -59
  46. package/src/queues/standalone-worker.ts +39 -0
  47. package/src/queues/title-generation.queue.ts +45 -11
  48. package/src/redis/connection.ts +182 -113
  49. package/src/redis/index.ts +6 -8
  50. package/src/redis/org-memory-lock.ts +60 -27
  51. package/src/redis/redis-lease-lock.ts +200 -121
  52. package/src/redis/runtime-connection.ts +20 -0
  53. package/src/redis/stream-context.ts +92 -46
  54. package/src/runtime/agent-identity-overrides.ts +2 -2
  55. package/src/runtime/agent-runtime-policy.ts +5 -2
  56. package/src/runtime/agent-stream-helpers.ts +24 -9
  57. package/src/runtime/chat-run-orchestration.ts +102 -19
  58. package/src/runtime/chat-run-registry.ts +36 -2
  59. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  60. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
  61. package/src/runtime/domain-layer.ts +192 -0
  62. package/src/runtime/execution-plan-visibility.ts +2 -2
  63. package/src/runtime/execution-plan.ts +42 -15
  64. package/src/runtime/graph-designer.ts +16 -4
  65. package/src/runtime/helper-model.ts +139 -48
  66. package/src/runtime/index.ts +7 -8
  67. package/src/runtime/indexed-repositories-policy.ts +3 -3
  68. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
  69. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  70. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
  71. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  72. package/src/runtime/memory/memory-scope.ts +53 -0
  73. package/src/runtime/plugin-resolution.ts +124 -25
  74. package/src/runtime/plugin-types.ts +9 -1
  75. package/src/runtime/post-turn-side-effects.ts +177 -130
  76. package/src/runtime/retrieval-adapters.ts +40 -6
  77. package/src/runtime/runtime-accessors.ts +92 -0
  78. package/src/runtime/runtime-config.ts +150 -61
  79. package/src/runtime/runtime-extensions.ts +23 -25
  80. package/src/runtime/runtime-lifecycle.ts +124 -0
  81. package/src/runtime/runtime-services.ts +386 -0
  82. package/src/runtime/runtime-token.ts +47 -0
  83. package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
  84. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
  85. package/src/runtime/social-chat/social-chat.ts +630 -0
  86. package/src/runtime/specialist-runner.ts +36 -10
  87. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
  88. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  89. package/src/runtime/thread-chat-helpers.ts +2 -2
  90. package/src/runtime/thread-plan-turn.ts +2 -1
  91. package/src/runtime/thread-turn-context.ts +183 -111
  92. package/src/runtime/turn-lifecycle.ts +93 -27
  93. package/src/services/agent-activity.service.ts +287 -203
  94. package/src/services/agent-executor.service.ts +253 -149
  95. package/src/services/artifact.service.ts +231 -149
  96. package/src/services/attachment.service.ts +171 -115
  97. package/src/services/autonomous-job.service.ts +890 -491
  98. package/src/services/background-work.service.ts +54 -0
  99. package/src/services/chat-run-registry.service.ts +13 -1
  100. package/src/services/context-compaction.service.ts +136 -86
  101. package/src/services/document-chunk.service.ts +151 -88
  102. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  103. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  104. package/src/services/execution-plan/execution-plan-graph.ts +278 -0
  105. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  106. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  107. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  108. package/src/services/feedback-loop.service.ts +132 -76
  109. package/src/services/global-orchestrator.service.ts +101 -168
  110. package/src/services/graph-full-routing.ts +193 -0
  111. package/src/services/index.ts +19 -21
  112. package/src/services/institutional-memory.service.ts +213 -125
  113. package/src/services/learned-skill.service.ts +368 -260
  114. package/src/services/memory/memory-conversation.ts +95 -0
  115. package/src/services/memory/memory-errors.ts +27 -0
  116. package/src/services/memory/memory-org-memory.ts +50 -0
  117. package/src/services/memory/memory-preseeded.ts +86 -0
  118. package/src/services/memory/memory-rerank.ts +297 -0
  119. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
  120. package/src/services/memory/memory.service.ts +674 -0
  121. package/src/services/memory/rerank.service.ts +201 -0
  122. package/src/services/monitoring-window.service.ts +92 -70
  123. package/src/services/mutating-approval.service.ts +62 -53
  124. package/src/services/node-workspace.service.ts +141 -98
  125. package/src/services/notification.service.ts +29 -16
  126. package/src/services/organization-member.service.ts +120 -66
  127. package/src/services/organization.service.ts +153 -77
  128. package/src/services/ownership-dispatcher.service.ts +456 -263
  129. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  130. package/src/services/plan/plan-agent-query.service.ts +322 -0
  131. package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
  132. package/src/services/plan/plan-artifact.service.ts +60 -0
  133. package/src/services/plan/plan-builder.service.ts +76 -0
  134. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  135. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  136. package/src/services/plan/plan-completion-side-effects.ts +169 -0
  137. package/src/services/plan/plan-coordination.service.ts +181 -0
  138. package/src/services/plan/plan-cycle.service.ts +405 -0
  139. package/src/services/plan/plan-deadline.service.ts +533 -0
  140. package/src/services/plan/plan-event-delivery.service.ts +266 -0
  141. package/src/services/plan/plan-executor-context.ts +35 -0
  142. package/src/services/plan/plan-executor-graph.ts +522 -0
  143. package/src/services/plan/plan-executor-helpers.ts +307 -0
  144. package/src/services/plan/plan-executor-persistence.ts +209 -0
  145. package/src/services/plan/plan-executor.service.ts +1737 -0
  146. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  147. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  148. package/src/services/plan/plan-run-serialization.ts +15 -0
  149. package/src/services/plan/plan-run.service.ts +637 -0
  150. package/src/services/plan/plan-scheduler.service.ts +379 -0
  151. package/src/services/plan/plan-template.service.ts +224 -0
  152. package/src/services/plan/plan-transaction-events.ts +36 -0
  153. package/src/services/plan/plan-validator.service.ts +907 -0
  154. package/src/services/plan/plan-workspace.service.ts +131 -0
  155. package/src/services/plugin-executor.service.ts +102 -68
  156. package/src/services/quality-metrics.service.ts +112 -94
  157. package/src/services/queue-job.service.ts +288 -231
  158. package/src/services/recent-activity-title.service.ts +73 -36
  159. package/src/services/recent-activity.service.ts +274 -259
  160. package/src/services/skill-resolver.service.ts +38 -12
  161. package/src/services/social-chat-history.service.ts +190 -122
  162. package/src/services/system-executor.service.ts +96 -61
  163. package/src/services/thread/thread-active-run.ts +203 -0
  164. package/src/services/thread/thread-bootstrap.ts +385 -0
  165. package/src/services/thread/thread-listing.ts +199 -0
  166. package/src/services/thread/thread-memory-block.ts +130 -0
  167. package/src/services/thread/thread-message.service.ts +379 -0
  168. package/src/services/thread/thread-record-store.ts +155 -0
  169. package/src/services/thread/thread-title.service.ts +74 -0
  170. package/src/services/thread/thread-turn-execution.ts +280 -0
  171. package/src/services/thread/thread-turn-message-context.ts +73 -0
  172. package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
  173. package/src/services/thread/thread-turn-streaming.ts +403 -0
  174. package/src/services/thread/thread-turn-tracing.ts +35 -0
  175. package/src/services/thread/thread-turn.ts +376 -0
  176. package/src/services/thread/thread.service.ts +344 -0
  177. package/src/services/user.service.ts +82 -32
  178. package/src/services/write-intent-validator.service.ts +63 -51
  179. package/src/storage/attachment-parser.ts +69 -27
  180. package/src/storage/attachment-storage.service.ts +334 -275
  181. package/src/storage/generated-document-storage.service.ts +66 -34
  182. package/src/system-agents/agent-result.ts +3 -1
  183. package/src/system-agents/context-compaction.agent.ts +3 -3
  184. package/src/system-agents/delegated-agent-factory.ts +159 -90
  185. package/src/system-agents/helper-agent-options.ts +1 -1
  186. package/src/system-agents/memory-reranker.agent.ts +3 -3
  187. package/src/system-agents/memory.agent.ts +3 -3
  188. package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
  189. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
  190. package/src/system-agents/skill-extractor.agent.ts +3 -3
  191. package/src/system-agents/skill-manager.agent.ts +3 -3
  192. package/src/system-agents/thread-router.agent.ts +157 -113
  193. package/src/system-agents/title-generator.agent.ts +3 -3
  194. package/src/tools/execution-plan.tool.ts +241 -171
  195. package/src/tools/fetch-webpage.tool.ts +29 -18
  196. package/src/tools/firecrawl-client.ts +26 -6
  197. package/src/tools/index.ts +1 -0
  198. package/src/tools/memory-block.tool.ts +14 -6
  199. package/src/tools/plan-approval.tool.ts +57 -47
  200. package/src/tools/read-file-parts.tool.ts +44 -33
  201. package/src/tools/remember-memory.tool.ts +65 -45
  202. package/src/tools/search-web.tool.ts +33 -22
  203. package/src/tools/search.tool.ts +41 -29
  204. package/src/tools/team-think.tool.ts +125 -84
  205. package/src/tools/user-questions.tool.ts +4 -3
  206. package/src/tools/web-tool-shared.ts +6 -0
  207. package/src/utils/async.ts +25 -22
  208. package/src/utils/crypto.ts +21 -0
  209. package/src/utils/date-time.ts +40 -1
  210. package/src/utils/errors.ts +111 -20
  211. package/src/utils/hono-error-handler.ts +24 -39
  212. package/src/utils/index.ts +2 -1
  213. package/src/utils/null-proto-record.ts +41 -0
  214. package/src/utils/sse-keepalive.ts +124 -21
  215. package/src/workers/bootstrap.ts +164 -52
  216. package/src/workers/memory-consolidation.worker.ts +325 -237
  217. package/src/workers/organization-learning.worker.ts +50 -16
  218. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  219. package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
  220. package/src/workers/skill-extraction.runner.ts +176 -93
  221. package/src/workers/utils/file-section-chunker.ts +8 -10
  222. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  223. package/src/workers/utils/repomix-file-sections.ts +2 -2
  224. package/src/workers/utils/thread-message-query.ts +97 -38
  225. package/src/workers/worker-utils.ts +74 -31
  226. package/src/config/debug-logger.ts +0 -47
  227. package/src/config/search.ts +0 -3
  228. package/src/redis/connection-accessor.ts +0 -26
  229. package/src/runtime/agent-types.ts +0 -1
  230. package/src/runtime/context-compaction-runtime.ts +0 -87
  231. package/src/runtime/memory-scope.ts +0 -43
  232. package/src/runtime/social-chat-agent-runner.ts +0 -118
  233. package/src/runtime/social-chat.ts +0 -516
  234. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  235. package/src/services/adaptive-playbook.service.ts +0 -152
  236. package/src/services/artifact-provenance.service.ts +0 -172
  237. package/src/services/chat-attachments.service.ts +0 -17
  238. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  239. package/src/services/execution-plan.service.ts +0 -1118
  240. package/src/services/memory.service.ts +0 -914
  241. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  242. package/src/services/plan-agent-query.service.ts +0 -267
  243. package/src/services/plan-artifact.service.ts +0 -50
  244. package/src/services/plan-builder.service.ts +0 -67
  245. package/src/services/plan-checkpoint.service.ts +0 -81
  246. package/src/services/plan-completion-side-effects.ts +0 -80
  247. package/src/services/plan-coordination.service.ts +0 -157
  248. package/src/services/plan-cycle.service.ts +0 -284
  249. package/src/services/plan-deadline.service.ts +0 -430
  250. package/src/services/plan-event-delivery.service.ts +0 -166
  251. package/src/services/plan-executor.service.ts +0 -1950
  252. package/src/services/plan-run.service.ts +0 -515
  253. package/src/services/plan-scheduler.service.ts +0 -240
  254. package/src/services/plan-template.service.ts +0 -177
  255. package/src/services/plan-validator.service.ts +0 -818
  256. package/src/services/plan-workspace.service.ts +0 -83
  257. package/src/services/rerank.service.ts +0 -156
  258. package/src/services/thread-message.service.ts +0 -275
  259. package/src/services/thread-plan-registry.service.ts +0 -22
  260. package/src/services/thread-title.service.ts +0 -39
  261. package/src/services/thread-turn-preparation.service.ts +0 -1147
  262. package/src/services/thread-turn.ts +0 -172
  263. package/src/services/thread.service.ts +0 -869
  264. package/src/utils/env.ts +0 -8
  265. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  266. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  267. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  268. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  269. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  270. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  271. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  272. /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, zodParse } 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 yield* zodParse(ArtifactRecordSchema.array(), 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,216 @@ 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
- const created = ArtifactRecordSchema.parse(createdRecord)
250
+ const created = yield* zodParse(ArtifactRecordSchema, 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 yield* zodParse(GetArtifactResultSchema, {
396
+ artifact,
397
+ content,
398
+ versions: versions.map(toVersionSummary),
399
+ backlinks,
400
+ })
401
+ })
402
+
403
+ const listBacklinksEffect = (artifact: Pick<ArtifactRecord, 'id' | 'organizationId'>) =>
404
+ db
405
+ .queryMany(
406
+ new BoundQuery(
407
+ `SELECT * FROM ${TABLES.ARTIFACT} WHERE organizationId = $organizationId AND references[*].targetId CONTAINS $targetId`,
408
+ {
409
+ organizationId: ensureRecordId(artifact.organizationId, TABLES.ORGANIZATION),
410
+ targetId: recordIdToString(artifact.id, TABLES.ARTIFACT),
411
+ },
412
+ ),
413
+ ArtifactRecordSchema,
414
+ )
415
+ .pipe(
416
+ Effect.mapError(
417
+ (cause) =>
418
+ new DatabaseError({
419
+ message: `Failed to load backlinks for ${recordIdToString(artifact.id, TABLES.ARTIFACT)}.`,
420
+ cause,
421
+ }),
422
+ ),
423
+ )
424
+
425
+ return {
426
+ publishArtifactInTransaction: (
427
+ params: PublishArtifactArgs,
428
+ tx: DatabaseTransaction,
429
+ options?: PublishArtifactTransactionOptions,
430
+ ) => publishArtifactInTransactionEffect(params, tx, options),
431
+ publishArtifact: (params: PublishArtifactArgs) => publishArtifactEffect(params),
432
+ getArtifactRecord: (artifactId: RecordIdInput) => getArtifactRecordEffect(artifactId),
433
+ listArtifacts: (params: {
434
+ organizationId: RecordIdInput
435
+ canonicalKey?: string
436
+ status?: ArtifactStatus
437
+ includeNonActive?: boolean
438
+ sourceThreadId?: RecordIdInput
439
+ limit?: number
440
+ }) => listArtifactsEffect(params),
441
+ getArtifact: (artifactId: RecordIdInput) => getArtifactEffect(artifactId),
442
+ listBacklinks: (artifact: Pick<ArtifactRecord, 'id' | 'organizationId'>) => listBacklinksEffect(artifact),
372
443
  }
373
444
  }
374
445
 
375
- export const artifactService = new ArtifactService()
446
+ export class ArtifactServiceTag extends Context.Service<ArtifactServiceTag, ReturnType<typeof makeArtifactService>>()(
447
+ '@lota-sdk/core/ArtifactService',
448
+ ) {}
449
+
450
+ export const ArtifactServiceLive = Layer.effect(
451
+ ArtifactServiceTag,
452
+ Effect.gen(function* () {
453
+ const db = yield* DatabaseServiceTag
454
+ const storage = yield* GeneratedDocumentStorageServiceTag
455
+ return makeArtifactService({ db, storage })
456
+ }),
457
+ )