@lota-sdk/core 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -1,8 +1,12 @@
1
1
  import { inferContentType } from '@lota-sdk/shared'
2
2
  import { S3Client } from 'bun'
3
+ import { Context, Schema, Effect, Layer } from 'effect'
3
4
 
4
5
  import { serverLogger } from '../config/logger'
5
- import { getRuntimeConfig } from '../runtime/runtime-config'
6
+ import { BadRequestError, ForbiddenError } from '../effect/errors'
7
+ import { RuntimeConfigServiceTag } from '../effect/services'
8
+ import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
9
+ import { getErrorMessage } from '../utils/errors'
6
10
  import { readString } from '../utils/string'
7
11
  import {
8
12
  extractAttachmentText,
@@ -10,12 +14,7 @@ import {
10
14
  isPdfAttachmentFile,
11
15
  splitExtractedTextIntoPages,
12
16
  } from './attachment-parser'
13
- import type {
14
- MessagePartLike,
15
- ReadableUploadMetadata,
16
- ReadableUploadPageMode,
17
- ReadableUploadPagePart,
18
- } from './attachment-types'
17
+ import type { MessagePartLike, ReadableUploadMetadata } from './attachment-types'
19
18
  import {
20
19
  buildOrganizationDocumentStorageKey,
21
20
  buildUploadStorageKey,
@@ -37,161 +36,170 @@ export type UploadedThreadAttachment = {
37
36
 
38
37
  type AttachmentContextCandidate = { storageKey: string; name: string; contentType: string; sizeBytes: number | null }
39
38
 
40
- export class AttachmentStorageService {
41
- private readonly client: S3Client
42
-
43
- constructor() {
44
- const config = getRuntimeConfig()
45
- this.client = new S3Client({
46
- accessKeyId: config.s3.accessKeyId,
47
- secretAccessKey: config.s3.secretAccessKey,
48
- bucket: config.s3.bucket,
49
- endpoint: config.s3.endpoint,
50
- region: config.s3.region,
51
- })
52
- }
39
+ class AttachmentStorageError extends Schema.TaggedErrorClass<AttachmentStorageError>()('AttachmentStorageError', {
40
+ operation: Schema.String,
41
+ message: Schema.String,
42
+ cause: Schema.Defect,
43
+ }) {}
53
44
 
54
- getAttachmentUrl(storageKey: string): string {
55
- return this.client.file(storageKey).presign({ expiresIn: getRuntimeConfig().s3.attachmentUrlExpiresIn })
56
- }
45
+ function toAttachmentStorageError(operation: string, cause: unknown): AttachmentStorageError {
46
+ return new AttachmentStorageError({ operation, message: getErrorMessage(cause), cause })
47
+ }
57
48
 
58
- async writeOrganizationDocument({
59
- orgId,
60
- namespace,
61
- relativePath,
62
- content,
63
- contentType,
64
- }: {
65
- orgId: string
66
- namespace: string
67
- relativePath: string
68
- content: string
69
- contentType: string
70
- }): Promise<{ storageKey: string; sizeBytes: number }> {
71
- const storageKey = buildOrganizationDocumentStorageKey({ orgId, namespace, relativePath })
72
- const sizeBytes = Buffer.byteLength(content, 'utf8')
49
+ export function makeAttachmentStorageService(config: ResolvedLotaRuntimeConfig) {
50
+ const client = new S3Client({
51
+ accessKeyId: config.s3.accessKeyId,
52
+ secretAccessKey: config.s3.secretAccessKey,
53
+ bucket: config.s3.bucket,
54
+ endpoint: config.s3.endpoint,
55
+ region: config.s3.region,
56
+ })
57
+
58
+ function getAttachmentUrl(storageKey: string): string {
59
+ return client.file(storageKey).presign({ expiresIn: config.s3.attachmentUrlExpiresIn })
60
+ }
73
61
 
74
- await this.client.file(storageKey).write(new Blob([content]), { type: contentType })
62
+ function readFilenameFromStorageKey(storageKey: string): string | null {
63
+ const slashIndex = storageKey.lastIndexOf('/')
64
+ if (slashIndex < 0) return null
65
+ const rawName = storageKey.slice(slashIndex + 1).trim()
66
+ if (!rawName) return null
75
67
 
76
- return { storageKey, sizeBytes }
68
+ const dashIndex = rawName.indexOf('-')
69
+ if (dashIndex <= 0 || dashIndex === rawName.length - 1) return rawName
70
+ return rawName.slice(dashIndex + 1)
77
71
  }
78
72
 
79
- async uploadOrganizationDocument({
80
- file,
73
+ function collectAttachmentContextCandidates({
74
+ parts,
81
75
  orgId,
82
- namespace,
83
- relativePath,
76
+ userId,
84
77
  }: {
85
- file: File
78
+ parts: readonly MessagePartLike[]
86
79
  orgId: string
87
- namespace: string
88
- relativePath: string
89
- }): Promise<UploadedThreadAttachment> {
90
- const filename = file.name || 'document'
91
- const mediaType = file.type || inferContentType(filename)
92
- const storageKey = buildOrganizationDocumentStorageKey({
93
- orgId,
94
- namespace,
95
- relativePath: relativePath.trim().length > 0 ? relativePath : filename,
96
- })
80
+ userId: string
81
+ }): AttachmentContextCandidate[] {
82
+ const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
83
+ const candidates: AttachmentContextCandidate[] = []
97
84
 
98
- await this.client.file(storageKey).write(file, { type: mediaType })
85
+ for (const part of parts) {
86
+ if (part.type !== 'file') continue
99
87
 
100
- return {
101
- filename,
102
- mediaType,
103
- sizeBytes: file.size,
104
- storageKey,
105
- url: this.getAttachmentUrl(storageKey),
106
- expiresInSeconds: getRuntimeConfig().s3.attachmentUrlExpiresIn,
88
+ const providerMetadata = readRecord(part.providerMetadata)
89
+ const lotaMetadata = readRecord(providerMetadata?.lota)
90
+ const storageKey = readString(lotaMetadata?.attachmentStorageKey)
91
+ if (!storageKey || !storageKey.startsWith(storagePrefix)) continue
92
+
93
+ const name = readString(part.filename) ?? readFilenameFromStorageKey(storageKey) ?? 'Attachment'
94
+ const contentType = readString(part.mediaType) ?? inferContentType(name)
95
+ const sizeBytes = readNonNegativeInteger(lotaMetadata?.attachmentSizeBytes)
96
+ candidates.push({ storageKey, name, contentType, sizeBytes })
107
97
  }
98
+
99
+ return candidates
108
100
  }
109
101
 
110
- async uploadThreadAttachment({
111
- file,
102
+ function assertUploadOwnership({
103
+ storageKey,
112
104
  orgId,
113
105
  userId,
114
106
  }: {
115
- file: File
107
+ storageKey: string
116
108
  orgId: string
117
109
  userId: string
118
- }): Promise<UploadedThreadAttachment> {
119
- const filename = file.name || 'attachment'
120
- const mediaType = file.type || inferContentType(filename)
121
- const sizeBytes = file.size
122
- const storageKey = buildUploadStorageKey({ orgId, userId, filename, uploadId: Bun.randomUUIDv7() })
123
-
124
- await this.client.file(storageKey).write(file, { type: mediaType })
125
-
126
- return {
127
- filename,
128
- mediaType,
129
- sizeBytes,
130
- storageKey,
131
- url: this.getAttachmentUrl(storageKey),
132
- expiresInSeconds: getRuntimeConfig().s3.attachmentUrlExpiresIn,
110
+ }): void {
111
+ const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
112
+ if (!storageKey.startsWith(storagePrefix)) {
113
+ throw new ForbiddenError({ message: 'Upload does not belong to the current organization/user scope.' })
133
114
  }
134
115
  }
135
116
 
136
- hydrateSignedFileUrlsInMessageParts({
137
- parts,
117
+ function writeOrganizationDocumentEffect({
138
118
  orgId,
139
- userId,
119
+ namespace,
120
+ relativePath,
121
+ content,
122
+ contentType,
140
123
  }: {
141
- parts: readonly MessagePartLike[]
142
124
  orgId: string
143
- userId: string
144
- }): MessagePartLike[] {
145
- const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
146
-
147
- return parts.map((part) => {
148
- if (part.type !== 'file') return part
125
+ namespace: string
126
+ relativePath: string
127
+ content: string
128
+ contentType: string
129
+ }) {
130
+ return Effect.gen(function* () {
131
+ const storageKey = buildOrganizationDocumentStorageKey({ orgId, namespace, relativePath })
132
+ const sizeBytes = Buffer.byteLength(content, 'utf8')
149
133
 
150
- const providerMetadata = readRecord(part.providerMetadata)
151
- const lotaMetadata = readRecord(providerMetadata?.lota)
152
- const storageKey = lotaMetadata?.attachmentStorageKey
153
- if (typeof storageKey !== 'string' || !storageKey.startsWith(storagePrefix)) return part
134
+ yield* Effect.tryPromise({
135
+ try: () => client.file(storageKey).write(new Blob([content]), { type: contentType }),
136
+ catch: (error) => toAttachmentStorageError('writeOrganizationDocument', error),
137
+ })
154
138
 
155
- return { ...part, url: this.getAttachmentUrl(storageKey) }
139
+ return { storageKey, sizeBytes }
156
140
  })
157
141
  }
158
142
 
159
- listReadableUploadsFromMessages({
160
- messages,
143
+ function uploadOrganizationDocumentEffect({
144
+ file,
161
145
  orgId,
162
- userId,
146
+ namespace,
147
+ relativePath,
163
148
  }: {
164
- messages: ReadonlyArray<{ parts: readonly MessagePartLike[] }>
149
+ file: File
165
150
  orgId: string
166
- userId: string
167
- }): ReadableUploadMetadata[] {
168
- const uploadsByStorageKey = new Map<
169
- string,
170
- { storageKey: string; filename: string; mediaType: string; sizeBytes: number | null }
171
- >()
172
-
173
- for (const message of messages) {
174
- const candidates = this.collectAttachmentContextCandidates({ parts: message.parts, orgId, userId })
175
- for (const candidate of candidates) {
176
- if (uploadsByStorageKey.has(candidate.storageKey)) continue
177
- uploadsByStorageKey.set(candidate.storageKey, {
178
- storageKey: candidate.storageKey,
179
- filename: candidate.name,
180
- mediaType: candidate.contentType,
181
- sizeBytes: candidate.sizeBytes,
182
- })
151
+ namespace: string
152
+ relativePath: string
153
+ }) {
154
+ return Effect.gen(function* () {
155
+ const filename = file.name || 'document'
156
+ const mediaType = file.type || inferContentType(filename)
157
+ const storageKey = buildOrganizationDocumentStorageKey({
158
+ orgId,
159
+ namespace,
160
+ relativePath: relativePath.trim().length > 0 ? relativePath : filename,
161
+ })
162
+
163
+ yield* Effect.tryPromise({
164
+ try: () => client.file(storageKey).write(file, { type: mediaType }),
165
+ catch: (error) => toAttachmentStorageError('uploadOrganizationDocument', error),
166
+ })
167
+
168
+ return {
169
+ filename,
170
+ mediaType,
171
+ sizeBytes: file.size,
172
+ storageKey,
173
+ url: getAttachmentUrl(storageKey),
174
+ expiresInSeconds: config.s3.attachmentUrlExpiresIn,
183
175
  }
184
- }
176
+ })
177
+ }
185
178
 
186
- return [...uploadsByStorageKey.values()].map((upload) => ({
187
- storageKey: upload.storageKey,
188
- filename: upload.filename,
189
- mediaType: upload.mediaType,
190
- sizeBytes: upload.sizeBytes,
191
- }))
179
+ function uploadThreadAttachmentEffect({ file, orgId, userId }: { file: File; orgId: string; userId: string }) {
180
+ return Effect.gen(function* () {
181
+ const filename = file.name || 'attachment'
182
+ const mediaType = file.type || inferContentType(filename)
183
+ const sizeBytes = file.size
184
+ const storageKey = buildUploadStorageKey({ orgId, userId, filename, uploadId: Bun.randomUUIDv7() })
185
+
186
+ yield* Effect.tryPromise({
187
+ try: () => client.file(storageKey).write(file, { type: mediaType }),
188
+ catch: (error) => toAttachmentStorageError('uploadThreadAttachment', error),
189
+ })
190
+
191
+ return {
192
+ filename,
193
+ mediaType,
194
+ sizeBytes,
195
+ storageKey,
196
+ url: getAttachmentUrl(storageKey),
197
+ expiresInSeconds: config.s3.attachmentUrlExpiresIn,
198
+ }
199
+ })
192
200
  }
193
201
 
194
- async extractStoredAttachmentText({
202
+ function extractStoredAttachmentTextEffect({
195
203
  storageKey,
196
204
  name,
197
205
  contentType,
@@ -199,18 +207,30 @@ export class AttachmentStorageService {
199
207
  storageKey: string
200
208
  name: string
201
209
  contentType: string
202
- }): Promise<string> {
203
- try {
204
- const s3File = this.client.file(storageKey)
205
- const fileLike = new File([await s3File.arrayBuffer()], name, { type: contentType })
206
- return await extractAttachmentText(fileLike)
207
- } catch (error) {
208
- serverLogger.warn`Failed to extract stored attachment text: ${error}`
209
- return ''
210
- }
210
+ }) {
211
+ return Effect.gen(function* () {
212
+ const s3File = client.file(storageKey)
213
+ const bytes = yield* Effect.tryPromise({
214
+ try: () => s3File.arrayBuffer(),
215
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentText:read', error),
216
+ })
217
+ const fileLike = new File([bytes], name, { type: contentType })
218
+
219
+ return yield* Effect.tryPromise({
220
+ try: () => extractAttachmentText(fileLike),
221
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentText:extract', error),
222
+ })
223
+ }).pipe(
224
+ Effect.catch((error) =>
225
+ Effect.sync(() => {
226
+ serverLogger.warn`Failed to extract stored attachment text: ${error}`
227
+ return ''
228
+ }),
229
+ ),
230
+ )
211
231
  }
212
232
 
213
- async extractStoredAttachmentPages({
233
+ function extractStoredAttachmentPagesEffect({
214
234
  storageKey,
215
235
  name,
216
236
  contentType,
@@ -218,25 +238,39 @@ export class AttachmentStorageService {
218
238
  storageKey: string
219
239
  name: string
220
240
  contentType: string
221
- }): Promise<{ pageMode: ReadableUploadPageMode; pages: string[] }> {
222
- try {
223
- const s3File = this.client.file(storageKey)
224
- const fileLike = new File([await s3File.arrayBuffer()], name, { type: contentType })
241
+ }) {
242
+ return Effect.gen(function* () {
243
+ const s3File = client.file(storageKey)
244
+ const bytes = yield* Effect.tryPromise({
245
+ try: () => s3File.arrayBuffer(),
246
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentPages:read', error),
247
+ })
248
+ const fileLike = new File([bytes], name, { type: contentType })
225
249
 
226
250
  if (isPdfAttachmentFile(fileLike)) {
227
- const pages = await extractPdfPages(fileLike)
228
- return { pageMode: 'pdf', pages }
251
+ const pages = yield* Effect.tryPromise({
252
+ try: () => extractPdfPages(fileLike),
253
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentPages:pdf', error),
254
+ })
255
+ return { pageMode: 'pdf' as const, pages }
229
256
  }
230
257
 
231
- const extractedText = await extractAttachmentText(fileLike)
232
- return { pageMode: 'logical', pages: splitExtractedTextIntoPages(extractedText) }
233
- } catch (error) {
234
- serverLogger.warn`Failed to extract attachment pages from storage: ${error}`
235
- return { pageMode: 'logical', pages: [] }
236
- }
258
+ const extractedText = yield* Effect.tryPromise({
259
+ try: () => extractAttachmentText(fileLike),
260
+ catch: (error) => toAttachmentStorageError('extractStoredAttachmentPages:text', error),
261
+ })
262
+ return { pageMode: 'logical' as const, pages: splitExtractedTextIntoPages(extractedText) }
263
+ }).pipe(
264
+ Effect.catch((error) =>
265
+ Effect.sync(() => {
266
+ serverLogger.warn`Failed to extract attachment pages from storage: ${error}`
267
+ return { pageMode: 'logical' as const, pages: [] }
268
+ }),
269
+ ),
270
+ )
237
271
  }
238
272
 
239
- async readFilePartsFromUpload({
273
+ function readFilePartsFromUploadEffect({
240
274
  upload,
241
275
  orgId,
242
276
  userId,
@@ -248,145 +282,167 @@ export class AttachmentStorageService {
248
282
  userId: string
249
283
  part?: number
250
284
  pagesPerPart?: number
251
- }): Promise<{
252
- pageMode: ReadableUploadPageMode
253
- totalPages: number
254
- fullPageLength: number
255
- currentPart: number
256
- totalParts: number
257
- hasNextPart: boolean
258
- hasPreviousPart: boolean
259
- pagesPerPart: number
260
- pageStart: number
261
- pageEnd: number
262
- data: ReadableUploadPagePart[]
263
- }> {
264
- this.assertUploadOwnership({ storageKey: upload.storageKey, orgId, userId })
265
-
266
- const parsed = await this.extractStoredAttachmentPages({
267
- storageKey: upload.storageKey,
268
- name: upload.filename,
269
- contentType: upload.mediaType,
270
- })
271
- const totalPages = parsed.pages.length
272
- if (totalPages === 0) {
273
- return {
274
- pageMode: parsed.pageMode,
275
- totalPages: 0,
276
- fullPageLength: 0,
277
- currentPart: 1,
278
- totalParts: 0,
279
- hasNextPart: false,
280
- hasPreviousPart: false,
281
- pagesPerPart,
282
- pageStart: 1,
283
- pageEnd: 0,
284
- data: [],
285
+ }) {
286
+ return Effect.gen(function* () {
287
+ assertUploadOwnership({ storageKey: upload.storageKey, orgId, userId })
288
+
289
+ const parsed = yield* extractStoredAttachmentPagesEffect({
290
+ storageKey: upload.storageKey,
291
+ name: upload.filename,
292
+ contentType: upload.mediaType,
293
+ })
294
+ const totalPages = parsed.pages.length
295
+ if (totalPages === 0) {
296
+ return {
297
+ pageMode: parsed.pageMode,
298
+ totalPages: 0,
299
+ fullPageLength: 0,
300
+ currentPart: 1,
301
+ totalParts: 0,
302
+ hasNextPart: false,
303
+ hasPreviousPart: false,
304
+ pagesPerPart,
305
+ pageStart: 1,
306
+ pageEnd: 0,
307
+ data: [],
308
+ }
285
309
  }
286
- }
287
310
 
288
- if (!Number.isInteger(pagesPerPart) || pagesPerPart <= 0) {
289
- throw new Error('pagesPerPart must be a positive integer.')
290
- }
291
- if (!Number.isInteger(part) || part <= 0) {
292
- throw new Error('part must be an integer greater than or equal to 1.')
293
- }
311
+ if (!Number.isInteger(pagesPerPart) || pagesPerPart <= 0) {
312
+ return yield* new BadRequestError({ message: 'pagesPerPart must be a positive integer.' })
313
+ }
314
+ if (!Number.isInteger(part) || part <= 0) {
315
+ return yield* new BadRequestError({ message: 'part must be an integer greater than or equal to 1.' })
316
+ }
294
317
 
295
- const totalParts = Math.ceil(totalPages / pagesPerPart)
296
- if (part > totalParts) {
297
- throw new Error(`part ${part} exceeds total parts ${totalParts}.`)
298
- }
318
+ const totalParts = Math.ceil(totalPages / pagesPerPart)
319
+ if (part > totalParts) {
320
+ return yield* new BadRequestError({ message: `part ${part} exceeds total parts ${totalParts}.` })
321
+ }
299
322
 
300
- const pageStart = (part - 1) * pagesPerPart + 1
301
- const pageEnd = Math.min(pageStart + pagesPerPart - 1, totalPages)
302
-
303
- const data = parsed.pages
304
- .slice(pageStart - 1, pageEnd)
305
- .map((text, index) => ({ pageNumber: pageStart + index, text, charCount: text.length }))
306
-
307
- return {
308
- pageMode: parsed.pageMode,
309
- totalPages,
310
- fullPageLength: totalPages,
311
- currentPart: part,
312
- totalParts,
313
- hasNextPart: part < totalParts,
314
- hasPreviousPart: part > 1,
315
- pagesPerPart,
316
- pageStart,
317
- pageEnd,
318
- data,
319
- }
320
- }
323
+ const pageStart = (part - 1) * pagesPerPart + 1
324
+ const pageEnd = Math.min(pageStart + pagesPerPart - 1, totalPages)
321
325
 
322
- private assertUploadOwnership({
323
- storageKey,
324
- orgId,
325
- userId,
326
- }: {
327
- storageKey: string
328
- orgId: string
329
- userId: string
330
- }): void {
331
- const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
332
- if (!storageKey.startsWith(storagePrefix)) {
333
- throw new Error('Upload does not belong to the current organization/user scope.')
334
- }
335
- }
326
+ const data = parsed.pages
327
+ .slice(pageStart - 1, pageEnd)
328
+ .map((text, index) => ({ pageNumber: pageStart + index, text, charCount: text.length }))
336
329
 
337
- private collectAttachmentContextCandidates({
338
- parts,
339
- orgId,
340
- userId,
341
- }: {
342
- parts: readonly MessagePartLike[]
343
- orgId: string
344
- userId: string
345
- }): AttachmentContextCandidate[] {
346
- const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
347
- const candidates: AttachmentContextCandidate[] = []
348
-
349
- for (const part of parts) {
350
- if (part.type !== 'file') continue
351
-
352
- const providerMetadata = readRecord(part.providerMetadata)
353
- const lotaMetadata = readRecord(providerMetadata?.lota)
354
- const storageKey = readString(lotaMetadata?.attachmentStorageKey)
355
- if (!storageKey || !storageKey.startsWith(storagePrefix)) continue
330
+ return {
331
+ pageMode: parsed.pageMode,
332
+ totalPages,
333
+ fullPageLength: totalPages,
334
+ currentPart: part,
335
+ totalParts,
336
+ hasNextPart: part < totalParts,
337
+ hasPreviousPart: part > 1,
338
+ pagesPerPart,
339
+ pageStart,
340
+ pageEnd,
341
+ data,
342
+ }
343
+ })
344
+ }
356
345
 
357
- const name = readString(part.filename) ?? this.readFilenameFromStorageKey(storageKey) ?? 'Attachment'
358
- const contentType = readString(part.mediaType) ?? inferContentType(name)
359
- const sizeBytes = readNonNegativeInteger(lotaMetadata?.attachmentSizeBytes)
360
- candidates.push({ storageKey, name, contentType, sizeBytes })
361
- }
346
+ return {
347
+ getAttachmentUrl,
362
348
 
363
- return candidates
364
- }
349
+ writeOrganizationDocument: (params: {
350
+ orgId: string
351
+ namespace: string
352
+ relativePath: string
353
+ content: string
354
+ contentType: string
355
+ }) => writeOrganizationDocumentEffect(params),
365
356
 
366
- private readFilenameFromStorageKey(storageKey: string): string | null {
367
- const slashIndex = storageKey.lastIndexOf('/')
368
- if (slashIndex < 0) return null
369
- const rawName = storageKey.slice(slashIndex + 1).trim()
370
- if (!rawName) return null
357
+ uploadOrganizationDocument: (params: { file: File; orgId: string; namespace: string; relativePath: string }) =>
358
+ uploadOrganizationDocumentEffect(params),
371
359
 
372
- const dashIndex = rawName.indexOf('-')
373
- if (dashIndex <= 0 || dashIndex === rawName.length - 1) return rawName
374
- return rawName.slice(dashIndex + 1)
375
- }
376
- }
360
+ uploadThreadAttachment: (params: { file: File; orgId: string; userId: string }) =>
361
+ uploadThreadAttachmentEffect(params),
377
362
 
378
- let _attachmentStorageService: AttachmentStorageService | undefined
363
+ hydrateSignedFileUrlsInMessageParts<TPart extends MessagePartLike>({
364
+ parts,
365
+ orgId,
366
+ userId,
367
+ }: {
368
+ parts: readonly TPart[]
369
+ orgId: string
370
+ userId: string
371
+ }): TPart[] {
372
+ const storagePrefix = buildUploadStoragePrefix({ orgId, userId })
373
+
374
+ return parts.map<TPart>((part) => {
375
+ if (part.type !== 'file') return part
376
+
377
+ const providerMetadata = readRecord(part.providerMetadata)
378
+ const lotaMetadata = readRecord(providerMetadata?.lota)
379
+ const storageKey = lotaMetadata?.attachmentStorageKey
380
+ if (typeof storageKey !== 'string' || !storageKey.startsWith(storagePrefix)) return part
381
+
382
+ return { ...part, url: getAttachmentUrl(storageKey) }
383
+ })
384
+ },
385
+
386
+ listReadableUploadsFromMessages<TPart extends MessagePartLike>({
387
+ messages,
388
+ orgId,
389
+ userId,
390
+ }: {
391
+ messages: ReadonlyArray<{ parts: readonly TPart[] }>
392
+ orgId: string
393
+ userId: string
394
+ }): ReadableUploadMetadata[] {
395
+ const uploadsByStorageKey = new Map<
396
+ string,
397
+ { storageKey: string; filename: string; mediaType: string; sizeBytes: number | null }
398
+ >()
399
+
400
+ for (const message of messages) {
401
+ const candidates = collectAttachmentContextCandidates({ parts: message.parts, orgId, userId })
402
+ for (const candidate of candidates) {
403
+ if (uploadsByStorageKey.has(candidate.storageKey)) continue
404
+ uploadsByStorageKey.set(candidate.storageKey, {
405
+ storageKey: candidate.storageKey,
406
+ filename: candidate.name,
407
+ mediaType: candidate.contentType,
408
+ sizeBytes: candidate.sizeBytes,
409
+ })
410
+ }
411
+ }
379
412
 
380
- /** @lintignore */
381
- export function getAttachmentStorageService(): AttachmentStorageService {
382
- if (!_attachmentStorageService) {
383
- _attachmentStorageService = new AttachmentStorageService()
413
+ return [...uploadsByStorageKey.values()].map((upload) => ({
414
+ storageKey: upload.storageKey,
415
+ filename: upload.filename,
416
+ mediaType: upload.mediaType,
417
+ sizeBytes: upload.sizeBytes,
418
+ }))
419
+ },
420
+
421
+ extractStoredAttachmentText: (params: { storageKey: string; name: string; contentType: string }) =>
422
+ extractStoredAttachmentTextEffect(params),
423
+
424
+ extractStoredAttachmentPages: (params: { storageKey: string; name: string; contentType: string }) =>
425
+ extractStoredAttachmentPagesEffect(params),
426
+
427
+ readFilePartsFromUpload: (params: {
428
+ upload: ReadableUploadMetadata
429
+ orgId: string
430
+ userId: string
431
+ part?: number
432
+ pagesPerPart?: number
433
+ }) => readFilePartsFromUploadEffect(params),
384
434
  }
385
- return _attachmentStorageService
386
435
  }
387
436
 
388
- export const attachmentStorageService = new Proxy({} as AttachmentStorageService, {
389
- get(_target, prop: string): unknown {
390
- return Reflect.get(getAttachmentStorageService(), prop)
391
- },
392
- })
437
+ export class AttachmentStorageServiceTag extends Context.Service<
438
+ AttachmentStorageServiceTag,
439
+ ReturnType<typeof makeAttachmentStorageService>
440
+ >()('AttachmentStorageService') {}
441
+
442
+ export const AttachmentStorageServiceLive = Layer.effect(
443
+ AttachmentStorageServiceTag,
444
+ Effect.gen(function* () {
445
+ const config = yield* RuntimeConfigServiceTag
446
+ return makeAttachmentStorageService(config)
447
+ }),
448
+ )