@intelligo-dev/core 1.0.0-beta.1 → 1.0.0-beta.13

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 (355) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +102 -0
  3. package/dist/attachments/errors.d.ts +7 -0
  4. package/dist/attachments/errors.d.ts.map +1 -0
  5. package/dist/attachments/errors.js +11 -0
  6. package/dist/attachments/errors.js.map +1 -0
  7. package/dist/attachments/index.d.ts +8 -0
  8. package/dist/attachments/index.d.ts.map +1 -0
  9. package/dist/attachments/index.js +7 -0
  10. package/dist/attachments/index.js.map +1 -0
  11. package/dist/attachments/service.d.ts +45 -0
  12. package/dist/attachments/service.d.ts.map +1 -0
  13. package/dist/attachments/service.js +100 -0
  14. package/dist/attachments/service.js.map +1 -0
  15. package/dist/attachments/types.d.ts +6 -0
  16. package/dist/attachments/types.d.ts.map +1 -0
  17. package/dist/attachments/types.js +2 -0
  18. package/dist/attachments/types.js.map +1 -0
  19. package/dist/conversations/errors.d.ts +2 -8
  20. package/dist/conversations/errors.d.ts.map +1 -1
  21. package/dist/conversations/errors.js +2 -8
  22. package/dist/conversations/errors.js.map +1 -1
  23. package/dist/conversations/index.d.ts +4 -12
  24. package/dist/conversations/index.d.ts.map +1 -1
  25. package/dist/conversations/index.js +2 -11
  26. package/dist/conversations/index.js.map +1 -1
  27. package/dist/conversations/service.d.ts +49 -61
  28. package/dist/conversations/service.d.ts.map +1 -1
  29. package/dist/conversations/service.js +110 -83
  30. package/dist/conversations/service.js.map +1 -1
  31. package/dist/conversations/types.d.ts +1 -4
  32. package/dist/conversations/types.d.ts.map +1 -1
  33. package/dist/conversations/types.js +0 -3
  34. package/dist/conversations/types.js.map +1 -1
  35. package/dist/db/client.d.ts +21 -10
  36. package/dist/db/client.d.ts.map +1 -1
  37. package/dist/db/client.js +43 -18
  38. package/dist/db/client.js.map +1 -1
  39. package/dist/db/index.d.ts +3 -3
  40. package/dist/db/index.d.ts.map +1 -1
  41. package/dist/db/index.js +3 -7
  42. package/dist/db/index.js.map +1 -1
  43. package/dist/db/schema/agents.d.ts +14 -51
  44. package/dist/db/schema/agents.d.ts.map +1 -1
  45. package/dist/db/schema/agents.js +9 -21
  46. package/dist/db/schema/agents.js.map +1 -1
  47. package/dist/db/schema/ai.d.ts +86 -1556
  48. package/dist/db/schema/ai.d.ts.map +1 -1
  49. package/dist/db/schema/ai.js +32 -236
  50. package/dist/db/schema/ai.js.map +1 -1
  51. package/dist/db/schema/auth.d.ts +5 -34
  52. package/dist/db/schema/auth.d.ts.map +1 -1
  53. package/dist/db/schema/auth.js +25 -56
  54. package/dist/db/schema/auth.js.map +1 -1
  55. package/dist/db/schema/billing.d.ts +136 -107
  56. package/dist/db/schema/billing.d.ts.map +1 -1
  57. package/dist/db/schema/billing.js +72 -60
  58. package/dist/db/schema/billing.js.map +1 -1
  59. package/dist/db/schema/feature-flags.d.ts +3 -12
  60. package/dist/db/schema/feature-flags.d.ts.map +1 -1
  61. package/dist/db/schema/feature-flags.js +3 -12
  62. package/dist/db/schema/feature-flags.js.map +1 -1
  63. package/dist/db/schema/identity.d.ts +12 -274
  64. package/dist/db/schema/identity.d.ts.map +1 -1
  65. package/dist/db/schema/identity.js +19 -104
  66. package/dist/db/schema/identity.js.map +1 -1
  67. package/dist/db/schema/index.d.ts +8 -15
  68. package/dist/db/schema/index.d.ts.map +1 -1
  69. package/dist/db/schema/index.js +12 -42
  70. package/dist/db/schema/index.js.map +1 -1
  71. package/dist/db/schema/notifications.d.ts +3 -17
  72. package/dist/db/schema/notifications.d.ts.map +1 -1
  73. package/dist/db/schema/notifications.js +3 -17
  74. package/dist/db/schema/notifications.js.map +1 -1
  75. package/dist/db/schema/usage.d.ts +94 -77
  76. package/dist/db/schema/usage.d.ts.map +1 -1
  77. package/dist/db/schema/usage.js +59 -89
  78. package/dist/db/schema/usage.js.map +1 -1
  79. package/dist/db/validate.d.ts +2 -11
  80. package/dist/db/validate.d.ts.map +1 -1
  81. package/dist/db/validate.js +6 -17
  82. package/dist/db/validate.js.map +1 -1
  83. package/dist/db/workspace-queries.d.ts +5 -29
  84. package/dist/db/workspace-queries.d.ts.map +1 -1
  85. package/dist/db/workspace-queries.js +5 -29
  86. package/dist/db/workspace-queries.js.map +1 -1
  87. package/dist/documents/classifier.d.ts +5 -12
  88. package/dist/documents/classifier.d.ts.map +1 -1
  89. package/dist/documents/classifier.js +16 -16
  90. package/dist/documents/classifier.js.map +1 -1
  91. package/dist/documents/errors.d.ts +2 -10
  92. package/dist/documents/errors.d.ts.map +1 -1
  93. package/dist/documents/errors.js +2 -10
  94. package/dist/documents/errors.js.map +1 -1
  95. package/dist/documents/index.d.ts +4 -13
  96. package/dist/documents/index.d.ts.map +1 -1
  97. package/dist/documents/index.js +3 -12
  98. package/dist/documents/index.js.map +1 -1
  99. package/dist/documents/service.d.ts +19 -26
  100. package/dist/documents/service.d.ts.map +1 -1
  101. package/dist/documents/service.js +44 -38
  102. package/dist/documents/service.js.map +1 -1
  103. package/dist/documents/types.d.ts +6 -10
  104. package/dist/documents/types.d.ts.map +1 -1
  105. package/dist/documents/types.js +0 -3
  106. package/dist/documents/types.js.map +1 -1
  107. package/dist/email/brand.d.ts +17 -0
  108. package/dist/email/brand.d.ts.map +1 -0
  109. package/dist/email/brand.js +9 -0
  110. package/dist/email/brand.js.map +1 -0
  111. package/dist/email/index.d.ts +4 -4
  112. package/dist/email/index.js +4 -4
  113. package/dist/email/provider.d.ts +1 -7
  114. package/dist/email/provider.d.ts.map +1 -1
  115. package/dist/email/provider.js +10 -19
  116. package/dist/email/provider.js.map +1 -1
  117. package/dist/email/send.d.ts +12 -12
  118. package/dist/email/send.d.ts.map +1 -1
  119. package/dist/email/send.js +5 -19
  120. package/dist/email/send.js.map +1 -1
  121. package/dist/email/senders.d.ts +17 -30
  122. package/dist/email/senders.d.ts.map +1 -1
  123. package/dist/email/senders.js +33 -47
  124. package/dist/email/senders.js.map +1 -1
  125. package/dist/email/templates/base-layout.d.ts +1 -1
  126. package/dist/email/templates/base-layout.d.ts.map +1 -1
  127. package/dist/email/templates/base-layout.js +4 -6
  128. package/dist/email/templates/base-layout.js.map +1 -1
  129. package/dist/email/templates/index.d.ts +10 -10
  130. package/dist/email/templates/index.d.ts.map +1 -1
  131. package/dist/email/templates/index.js +10 -16
  132. package/dist/email/templates/index.js.map +1 -1
  133. package/dist/email/templates/password-reset.d.ts +2 -2
  134. package/dist/email/templates/password-reset.d.ts.map +1 -1
  135. package/dist/email/templates/password-reset.js +10 -22
  136. package/dist/email/templates/password-reset.js.map +1 -1
  137. package/dist/email/templates/payment-failed.d.ts +2 -2
  138. package/dist/email/templates/payment-failed.d.ts.map +1 -1
  139. package/dist/email/templates/payment-failed.js +11 -24
  140. package/dist/email/templates/payment-failed.js.map +1 -1
  141. package/dist/email/templates/quota-warning.d.ts +4 -4
  142. package/dist/email/templates/quota-warning.d.ts.map +1 -1
  143. package/dist/email/templates/quota-warning.js +19 -39
  144. package/dist/email/templates/quota-warning.js.map +1 -1
  145. package/dist/email/templates/subscription-confirmed.d.ts +2 -2
  146. package/dist/email/templates/subscription-confirmed.d.ts.map +1 -1
  147. package/dist/email/templates/subscription-confirmed.js +13 -28
  148. package/dist/email/templates/subscription-confirmed.js.map +1 -1
  149. package/dist/email/templates/trial-expiry.d.ts +2 -2
  150. package/dist/email/templates/trial-expiry.d.ts.map +1 -1
  151. package/dist/email/templates/trial-expiry.js +12 -36
  152. package/dist/email/templates/trial-expiry.js.map +1 -1
  153. package/dist/email/templates/trial-warning.d.ts +2 -2
  154. package/dist/email/templates/trial-warning.d.ts.map +1 -1
  155. package/dist/email/templates/trial-warning.js +15 -32
  156. package/dist/email/templates/trial-warning.js.map +1 -1
  157. package/dist/email/templates/verify-email.d.ts +2 -2
  158. package/dist/email/templates/verify-email.d.ts.map +1 -1
  159. package/dist/email/templates/verify-email.js +10 -22
  160. package/dist/email/templates/verify-email.js.map +1 -1
  161. package/dist/email/templates/welcome.d.ts +2 -2
  162. package/dist/email/templates/welcome.d.ts.map +1 -1
  163. package/dist/email/templates/welcome.js +13 -20
  164. package/dist/email/templates/welcome.js.map +1 -1
  165. package/dist/email/templates/workspace-invitation.d.ts +2 -2
  166. package/dist/email/templates/workspace-invitation.d.ts.map +1 -1
  167. package/dist/email/templates/workspace-invitation.js +12 -26
  168. package/dist/email/templates/workspace-invitation.js.map +1 -1
  169. package/dist/env.d.ts +5 -10
  170. package/dist/env.d.ts.map +1 -1
  171. package/dist/env.js +41 -31
  172. package/dist/env.js.map +1 -1
  173. package/dist/identity/audit.d.ts +11 -24
  174. package/dist/identity/audit.d.ts.map +1 -1
  175. package/dist/identity/audit.js +13 -27
  176. package/dist/identity/audit.js.map +1 -1
  177. package/dist/identity/errors.d.ts +4 -12
  178. package/dist/identity/errors.d.ts.map +1 -1
  179. package/dist/identity/errors.js +2 -8
  180. package/dist/identity/errors.js.map +1 -1
  181. package/dist/identity/index.d.ts +5 -13
  182. package/dist/identity/index.d.ts.map +1 -1
  183. package/dist/identity/index.js +4 -12
  184. package/dist/identity/index.js.map +1 -1
  185. package/dist/identity/service.d.ts +13 -47
  186. package/dist/identity/service.d.ts.map +1 -1
  187. package/dist/identity/service.js +31 -78
  188. package/dist/identity/service.js.map +1 -1
  189. package/dist/identity/types.d.ts +1 -4
  190. package/dist/identity/types.d.ts.map +1 -1
  191. package/dist/identity/types.js +0 -3
  192. package/dist/identity/types.js.map +1 -1
  193. package/dist/index.d.ts +1 -2
  194. package/dist/index.d.ts.map +1 -1
  195. package/dist/index.js +3 -10
  196. package/dist/index.js.map +1 -1
  197. package/dist/logger.d.ts +7 -18
  198. package/dist/logger.d.ts.map +1 -1
  199. package/dist/logger.js +45 -49
  200. package/dist/logger.js.map +1 -1
  201. package/dist/money.d.ts +95 -0
  202. package/dist/money.d.ts.map +1 -0
  203. package/dist/money.js +208 -0
  204. package/dist/money.js.map +1 -0
  205. package/dist/notifications/index.d.ts +9 -43
  206. package/dist/notifications/index.d.ts.map +1 -1
  207. package/dist/notifications/index.js +9 -49
  208. package/dist/notifications/index.js.map +1 -1
  209. package/dist/notifications/triggers.d.ts +9 -32
  210. package/dist/notifications/triggers.d.ts.map +1 -1
  211. package/dist/notifications/triggers.js +21 -58
  212. package/dist/notifications/triggers.js.map +1 -1
  213. package/dist/notifications/types.d.ts +1 -10
  214. package/dist/notifications/types.d.ts.map +1 -1
  215. package/dist/notifications/types.js +0 -9
  216. package/dist/notifications/types.js.map +1 -1
  217. package/dist/prompt.d.ts +38 -0
  218. package/dist/prompt.d.ts.map +1 -0
  219. package/dist/prompt.js +118 -0
  220. package/dist/prompt.js.map +1 -0
  221. package/dist/registry.d.ts +32 -0
  222. package/dist/registry.d.ts.map +1 -0
  223. package/dist/registry.js +60 -0
  224. package/dist/registry.js.map +1 -0
  225. package/dist/request-context.d.ts +49 -0
  226. package/dist/request-context.d.ts.map +1 -0
  227. package/dist/request-context.js +100 -0
  228. package/dist/request-context.js.map +1 -0
  229. package/dist/storage/index.d.ts +65 -0
  230. package/dist/storage/index.d.ts.map +1 -0
  231. package/dist/storage/index.js +109 -0
  232. package/dist/storage/index.js.map +1 -0
  233. package/package.json +74 -24
  234. package/src/attachments/errors.ts +18 -0
  235. package/src/attachments/index.ts +22 -0
  236. package/src/attachments/service.ts +178 -0
  237. package/src/attachments/types.ts +6 -0
  238. package/src/conversations/errors.ts +45 -0
  239. package/src/conversations/index.ts +39 -0
  240. package/src/conversations/service.ts +594 -0
  241. package/src/conversations/types.ts +15 -0
  242. package/src/db/client.ts +91 -0
  243. package/src/db/index.ts +3 -0
  244. package/src/db/migrations/0000_baseline.sql +652 -0
  245. package/src/db/migrations/0001_reconcile_chain_built_databases.sql +53 -0
  246. package/src/db/migrations/0002_webhook_claim_lease.sql +1 -0
  247. package/src/db/migrations/README.md +74 -36
  248. package/src/db/migrations/legacy-chain.json +197 -0
  249. package/src/db/migrations/meta/0000_snapshot.json +4182 -157
  250. package/src/db/migrations/meta/0002_snapshot.json +2478 -368
  251. package/src/db/migrations/meta/_journal.json +6 -279
  252. package/src/db/schema/agents.ts +57 -0
  253. package/src/db/schema/ai.ts +174 -0
  254. package/src/db/schema/auth.ts +183 -0
  255. package/src/db/schema/billing.ts +209 -0
  256. package/src/db/schema/feature-flags.ts +20 -0
  257. package/src/db/schema/identity.ts +250 -0
  258. package/src/db/schema/index.ts +12 -0
  259. package/src/db/schema/notifications.ts +31 -0
  260. package/src/db/schema/usage.ts +269 -0
  261. package/src/db/validate.ts +63 -0
  262. package/src/db/workspace-queries.ts +64 -0
  263. package/src/documents/classifier.ts +61 -0
  264. package/src/documents/errors.ts +44 -0
  265. package/src/documents/index.ts +24 -0
  266. package/src/documents/service.ts +256 -0
  267. package/src/documents/types.ts +24 -0
  268. package/src/email/brand.ts +24 -0
  269. package/src/email/index.ts +19 -0
  270. package/src/email/provider.ts +278 -0
  271. package/src/email/send.ts +169 -0
  272. package/src/email/senders.ts +292 -0
  273. package/src/email/templates/base-layout.tsx +145 -0
  274. package/src/email/templates/index.ts +28 -0
  275. package/src/email/templates/password-reset.tsx +72 -0
  276. package/src/email/templates/payment-failed.tsx +77 -0
  277. package/src/email/templates/quota-warning.tsx +118 -0
  278. package/src/email/templates/subscription-confirmed.tsx +91 -0
  279. package/src/email/templates/trial-expiry.tsx +124 -0
  280. package/src/email/templates/trial-warning.tsx +104 -0
  281. package/src/email/templates/verify-email.tsx +72 -0
  282. package/src/email/templates/welcome.tsx +64 -0
  283. package/src/email/templates/workspace-invitation.tsx +105 -0
  284. package/src/env.ts +180 -0
  285. package/src/identity/audit.ts +65 -0
  286. package/src/identity/errors.ts +44 -0
  287. package/src/identity/index.ts +26 -0
  288. package/src/identity/service.ts +202 -0
  289. package/src/identity/types.ts +28 -0
  290. package/src/index.ts +4 -0
  291. package/src/logger.ts +198 -0
  292. package/src/money.ts +270 -0
  293. package/src/notifications/index.ts +108 -0
  294. package/src/notifications/triggers.ts +158 -0
  295. package/src/notifications/types.ts +18 -0
  296. package/src/prompt.ts +142 -0
  297. package/src/registry.ts +75 -0
  298. package/src/request-context.ts +137 -0
  299. package/src/storage/index.ts +153 -0
  300. package/dist/db/schema/rag.d.ts +0 -171
  301. package/dist/db/schema/rag.d.ts.map +0 -1
  302. package/dist/db/schema/rag.js +0 -42
  303. package/dist/db/schema/rag.js.map +0 -1
  304. package/src/db/migrations/0000_sharp_night_nurse.sql +0 -52
  305. package/src/db/migrations/0001_hesitant_vapor.sql +0 -40
  306. package/src/db/migrations/0002_numerous_lady_vermin.sql +0 -235
  307. package/src/db/migrations/0003_mighty_jean_grey.sql +0 -37
  308. package/src/db/migrations/0004_cynical_grandmaster.sql +0 -2
  309. package/src/db/migrations/0005_careful_switch.sql +0 -8
  310. package/src/db/migrations/0006_tense_blue_marvel.sql +0 -16
  311. package/src/db/migrations/0007_next_squadron_sinister.sql +0 -1
  312. package/src/db/migrations/0008_powerful_scream.sql +0 -39
  313. package/src/db/migrations/0009_sad_ink.sql +0 -1
  314. package/src/db/migrations/0010_shocking_omega_flight.sql +0 -2
  315. package/src/db/migrations/0011_ambiguous_otto_octavius.sql +0 -151
  316. package/src/db/migrations/0011_hnsw_vector_index.sql +0 -17
  317. package/src/db/migrations/0012_user_profiles.sql +0 -30
  318. package/src/db/migrations/0013_trending_seasonal_columns.sql +0 -9
  319. package/src/db/migrations/0014_add_product_type_to_agents.sql +0 -6
  320. package/src/db/migrations/0015_new_v1_tables.sql +0 -149
  321. package/src/db/migrations/0016_document_types.sql +0 -30
  322. package/src/db/migrations/0017_document_metadata.sql +0 -2
  323. package/src/db/migrations/0018_identity_graph.sql +0 -200
  324. package/src/db/migrations/0019_db_driven_prompts.sql +0 -20
  325. package/src/db/migrations/0020_pending_extractions.sql +0 -35
  326. package/src/db/migrations/0021_rag_documents.sql +0 -39
  327. package/src/db/migrations/0022_user_quotas_usage_jsonb.sql +0 -70
  328. package/src/db/migrations/0023_user_facts_user_scoped_unique.sql +0 -17
  329. package/src/db/migrations/0024_user_memories_hnsw.sql +0 -23
  330. package/src/db/migrations/0025_audit_trigger_errcode.sql +0 -21
  331. package/src/db/migrations/0026_user_profile_snapshots_composite_pk.sql +0 -17
  332. package/src/db/migrations/0027_billing_settings_mnt_columns.sql +0 -54
  333. package/src/db/migrations/0028_trial_credits_normalized_email.sql +0 -37
  334. package/src/db/migrations/0029_usage_records_conversation_idx.sql +0 -13
  335. package/src/db/migrations/0030_pending_extractions_composite_idx.sql +0 -11
  336. package/src/db/migrations/0031_rag_documents_hnsw.sql +0 -16
  337. package/src/db/migrations/0032_rate_limit_unique_idx.sql +0 -16
  338. package/src/db/migrations/0033_pending_extractions_partial_idx.sql +0 -18
  339. package/src/db/migrations/0034_rate_limit_count.sql +0 -9
  340. package/src/db/migrations/0035_credit_reservations.sql +0 -24
  341. package/src/db/migrations/0036_executions_audit_jobs.sql +0 -96
  342. package/src/db/migrations/0037_rate_limit_minute_bucket.sql +0 -46
  343. package/src/db/migrations/0038_user_quotas_generic_usage.sql +0 -25
  344. package/src/db/migrations/0039_platform_admin_impersonation.sql +0 -27
  345. package/src/db/migrations/0040_ee_audit_chain.sql +0 -23
  346. package/src/db/migrations/meta/0001_snapshot.json +0 -631
  347. package/src/db/migrations/meta/0003_snapshot.json +0 -2488
  348. package/src/db/migrations/meta/0004_snapshot.json +0 -2500
  349. package/src/db/migrations/meta/0005_snapshot.json +0 -2548
  350. package/src/db/migrations/meta/0006_snapshot.json +0 -2650
  351. package/src/db/migrations/meta/0007_snapshot.json +0 -2657
  352. package/src/db/migrations/meta/0008_snapshot.json +0 -2719
  353. package/src/db/migrations/meta/0009_snapshot.json +0 -2726
  354. package/src/db/migrations/meta/0010_snapshot.json +0 -2739
  355. package/src/db/migrations/meta/0011_snapshot.json +0 -3886
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Server-only persistence for conversations, messages and votes.
3
+ *
4
+ * Title generation, windowing and summarization, agent validation and model
5
+ * selection belong to the product; this module takes an already-resolved
6
+ * agentId and modelId.
7
+ *
8
+ * Callers pass a resolved actor (workspaceId, userId); core cannot depend on
9
+ * `@intelligo-dev/auth`. Conversations are user-private within a workspace,
10
+ * so every query filters by both ids — the caller is never trusted to have
11
+ * done it. Failures throw `ConversationServiceError`.
12
+ */
13
+
14
+ import { and, asc, desc, eq, gt, inArray, lt, sql } from "drizzle-orm";
15
+ import { db } from "../db";
16
+ import { conversations, messages, votes } from "../db/schema";
17
+ import { ConversationServiceError } from "./errors";
18
+ import type {
19
+ Conversation,
20
+ ConversationActor,
21
+ InsertMessage,
22
+ Message,
23
+ Vote,
24
+ } from "./types";
25
+
26
+ /**
27
+ * Returns the conversation if the actor owns it. Throws
28
+ * ConversationServiceError("not_found") otherwise — another user's
29
+ * conversation is indistinguishable from a missing one, so an id never
30
+ * reveals existence across users.
31
+ */
32
+ async function verifyConversation(
33
+ actor: ConversationActor,
34
+ id: string
35
+ ): Promise<Conversation> {
36
+ const [conversation] = await db
37
+ .select()
38
+ .from(conversations)
39
+ .where(
40
+ and(
41
+ eq(conversations.id, id),
42
+ eq(conversations.workspaceId, actor.workspaceId),
43
+ eq(conversations.userId, actor.userId)
44
+ )
45
+ )
46
+ .limit(1);
47
+
48
+ if (!conversation) {
49
+ throw new ConversationServiceError("not_found", "Conversation not found");
50
+ }
51
+
52
+ return conversation;
53
+ }
54
+
55
+ /**
56
+ * Creates a conversation. `agentId` and `modelId` are opaque to this
57
+ * module; `id` defaults to a fresh UUID. An `id` the actor already owns
58
+ * returns that conversation; one another actor owns is `forbidden`.
59
+ */
60
+ export async function createConversation(
61
+ actor: ConversationActor,
62
+ params: {
63
+ agentId: string;
64
+ modelId: string;
65
+ id?: string;
66
+ title?: string | null;
67
+ }
68
+ ): Promise<Conversation> {
69
+ const id = params.id ?? crypto.randomUUID();
70
+ const [conversation] = await db
71
+ .insert(conversations)
72
+ .values({
73
+ id,
74
+ workspaceId: actor.workspaceId,
75
+ userId: actor.userId,
76
+ agentId: params.agentId,
77
+ modelId: params.modelId,
78
+ title: params.title ?? null,
79
+ })
80
+ .onConflictDoNothing({ target: conversations.id })
81
+ .returning();
82
+
83
+ if (conversation) return conversation;
84
+
85
+ // The id is taken: by the actor's own racing first turn, which is the
86
+ // conversation this call wanted, or by someone else's.
87
+ try {
88
+ return await verifyConversation(actor, id);
89
+ } catch {
90
+ throw new ConversationServiceError(
91
+ "forbidden",
92
+ "Conversation id belongs to another actor"
93
+ );
94
+ }
95
+ }
96
+
97
+ /** Get a single conversation, scoped to the actor. */
98
+ export async function getConversation(
99
+ actor: ConversationActor,
100
+ id: string
101
+ ): Promise<Conversation> {
102
+ return verifyConversation(actor, id);
103
+ }
104
+
105
+ /**
106
+ * List conversations for the actor, most recently updated first.
107
+ * Projects only the columns a sidebar/dashboard list needs.
108
+ */
109
+ export async function listConversations(
110
+ actor: ConversationActor,
111
+ options?: { limit?: number; offset?: number }
112
+ ): Promise<
113
+ Array<
114
+ Pick<Conversation, "id" | "title" | "agentId" | "updatedAt" | "metadata">
115
+ >
116
+ > {
117
+ const limit = options?.limit ?? 50;
118
+ const offset = options?.offset ?? 0;
119
+
120
+ return db
121
+ .select({
122
+ id: conversations.id,
123
+ title: conversations.title,
124
+ agentId: conversations.agentId,
125
+ updatedAt: conversations.updatedAt,
126
+ metadata: conversations.metadata,
127
+ })
128
+ .from(conversations)
129
+ .where(
130
+ and(
131
+ eq(conversations.workspaceId, actor.workspaceId),
132
+ eq(conversations.userId, actor.userId)
133
+ )
134
+ )
135
+ .orderBy(desc(conversations.updatedAt))
136
+ .limit(limit)
137
+ .offset(offset);
138
+ }
139
+
140
+ const MAX_HISTORY_LIMIT = 100;
141
+
142
+ /** Paginated conversation history with cursor-based pagination. */
143
+ export async function getConversationHistory(
144
+ actor: ConversationActor,
145
+ params: { limit: number; endingBefore?: string }
146
+ ): Promise<{ chats: Conversation[]; hasMore: boolean }> {
147
+ const limit = Math.min(Math.max(1, params.limit), MAX_HISTORY_LIMIT);
148
+
149
+ let cursorUpdatedAt: Date | undefined;
150
+ if (params.endingBefore) {
151
+ // Scope the cursor lookup too: unscoped, it would answer "does
152
+ // this conversation id exist, and when was it updated?" for any
153
+ // id in any tenant — a cross-tenant existence oracle.
154
+ const [cursorConversation] = await db
155
+ .select({ updatedAt: conversations.updatedAt })
156
+ .from(conversations)
157
+ .where(
158
+ and(
159
+ eq(conversations.id, params.endingBefore),
160
+ eq(conversations.workspaceId, actor.workspaceId),
161
+ eq(conversations.userId, actor.userId)
162
+ )
163
+ )
164
+ .limit(1);
165
+ cursorUpdatedAt = cursorConversation?.updatedAt;
166
+ }
167
+
168
+ const result = await db
169
+ .select()
170
+ .from(conversations)
171
+ .where(
172
+ and(
173
+ eq(conversations.workspaceId, actor.workspaceId),
174
+ eq(conversations.userId, actor.userId),
175
+ ...(cursorUpdatedAt
176
+ ? [lt(conversations.updatedAt, cursorUpdatedAt)]
177
+ : [])
178
+ )
179
+ )
180
+ .orderBy(desc(conversations.updatedAt))
181
+ .limit(limit + 1);
182
+
183
+ const hasMore = result.length > limit;
184
+ const chats = hasMore ? result.slice(0, -1) : result;
185
+
186
+ return { chats, hasMore };
187
+ }
188
+
189
+ const MAX_TITLE_LENGTH = 300;
190
+
191
+ /** Rename a conversation. Throws on an empty title or a title that's too long. */
192
+ export async function renameConversation(
193
+ actor: ConversationActor,
194
+ id: string,
195
+ title: string
196
+ ): Promise<Conversation> {
197
+ const trimmed = title.trim();
198
+ if (trimmed.length === 0) {
199
+ throw new ConversationServiceError(
200
+ "invalid_input",
201
+ "Title cannot be empty"
202
+ );
203
+ }
204
+ if (trimmed.length > MAX_TITLE_LENGTH) {
205
+ throw new ConversationServiceError(
206
+ "invalid_input",
207
+ `Title cannot exceed ${MAX_TITLE_LENGTH} characters`
208
+ );
209
+ }
210
+
211
+ await verifyConversation(actor, id);
212
+
213
+ const [updated] = await db
214
+ .update(conversations)
215
+ .set({ title: trimmed, updatedAt: new Date() })
216
+ .where(eq(conversations.id, id))
217
+ .returning();
218
+
219
+ if (!updated) {
220
+ throw new ConversationServiceError(
221
+ "database_error",
222
+ "Failed to rename conversation"
223
+ );
224
+ }
225
+
226
+ return updated;
227
+ }
228
+
229
+ /**
230
+ * Shallow-merges a patch into the conversation's `metadata`: each top-level
231
+ * key in `patch` replaces the stored one, every other key stays.
232
+ */
233
+ export async function updateConversationMetadata(
234
+ actor: ConversationActor,
235
+ id: string,
236
+ patch: Record<string, unknown>
237
+ ): Promise<Conversation> {
238
+ await verifyConversation(actor, id);
239
+
240
+ const [updated] = await db
241
+ .update(conversations)
242
+ .set({
243
+ metadata: sql`coalesce(${conversations.metadata}, '{}'::jsonb) || ${JSON.stringify(patch)}::jsonb`,
244
+ updatedAt: new Date(),
245
+ })
246
+ .where(eq(conversations.id, id))
247
+ .returning();
248
+
249
+ if (!updated) {
250
+ throw new ConversationServiceError(
251
+ "database_error",
252
+ "Failed to update conversation metadata"
253
+ );
254
+ }
255
+
256
+ return updated;
257
+ }
258
+
259
+ export type ConversationVisibility = "private" | "public";
260
+
261
+ /** Publish or unpublish a conversation. Public means readable by its id, by anyone. */
262
+ export async function setConversationVisibility(
263
+ actor: ConversationActor,
264
+ id: string,
265
+ visibility: ConversationVisibility
266
+ ): Promise<Conversation> {
267
+ await verifyConversation(actor, id);
268
+
269
+ const [updated] = await db
270
+ .update(conversations)
271
+ .set({ visibility, updatedAt: new Date() })
272
+ .where(eq(conversations.id, id))
273
+ .returning();
274
+
275
+ if (!updated) {
276
+ throw new ConversationServiceError(
277
+ "database_error",
278
+ "Failed to update conversation visibility"
279
+ );
280
+ }
281
+
282
+ return updated;
283
+ }
284
+
285
+ /**
286
+ * A conversation its owner published, by id alone — no actor, because
287
+ * the reader has none. Everything else about the row stays private:
288
+ * only what a shared page shows is projected.
289
+ */
290
+ export async function getPublicConversation(id: string): Promise<{
291
+ id: string;
292
+ title: string | null;
293
+ agentId: string;
294
+ createdAt: Date;
295
+ updatedAt: Date;
296
+ }> {
297
+ const [row] = await db
298
+ .select({
299
+ id: conversations.id,
300
+ title: conversations.title,
301
+ agentId: conversations.agentId,
302
+ createdAt: conversations.createdAt,
303
+ updatedAt: conversations.updatedAt,
304
+ })
305
+ .from(conversations)
306
+ .where(
307
+ and(eq(conversations.id, id), eq(conversations.visibility, "public"))
308
+ )
309
+ .limit(1);
310
+
311
+ if (!row) {
312
+ throw new ConversationServiceError("not_found", "Conversation not found");
313
+ }
314
+ return row;
315
+ }
316
+
317
+ /** The messages of a published conversation, oldest first. Empty when it is not public. */
318
+ export async function getPublicMessages(id: string): Promise<Message[]> {
319
+ await getPublicConversation(id);
320
+ return db
321
+ .select()
322
+ .from(messages)
323
+ .where(eq(messages.conversationId, id))
324
+ .orderBy(asc(messages.createdAt));
325
+ }
326
+
327
+ /** Delete a conversation and all its messages (CASCADE). */
328
+ export async function deleteConversation(
329
+ actor: ConversationActor,
330
+ id: string
331
+ ): Promise<void> {
332
+ await verifyConversation(actor, id);
333
+ await db.delete(conversations).where(eq(conversations.id, id));
334
+ }
335
+
336
+ /** Delete all conversations for the actor (user-scoped within the workspace). */
337
+ export async function deleteAllConversations(
338
+ actor: ConversationActor
339
+ ): Promise<{ deletedCount: number }> {
340
+ const deleted = await db
341
+ .delete(conversations)
342
+ .where(
343
+ and(
344
+ eq(conversations.workspaceId, actor.workspaceId),
345
+ eq(conversations.userId, actor.userId)
346
+ )
347
+ )
348
+ .returning();
349
+
350
+ return { deletedCount: deleted.length };
351
+ }
352
+
353
+ /** Get all messages for a conversation, ordered oldest-first. */
354
+ export async function getMessages(
355
+ actor: ConversationActor,
356
+ conversationId: string
357
+ ): Promise<Message[]> {
358
+ await verifyConversation(actor, conversationId);
359
+
360
+ return db
361
+ .select()
362
+ .from(messages)
363
+ .where(eq(messages.conversationId, conversationId))
364
+ .orderBy(asc(messages.createdAt));
365
+ }
366
+
367
+ /**
368
+ * Saves a batch of messages. Every `conversationId` must belong to a
369
+ * conversation the actor owns — a workspace-only guard would let any member
370
+ * write into a colleague's private conversation. Bumps `updatedAt` on the
371
+ * touched conversations.
372
+ */
373
+ export async function saveMessages(
374
+ actor: ConversationActor,
375
+ messagesToSave: InsertMessage[]
376
+ ): Promise<Message[]> {
377
+ if (messagesToSave.length === 0) return [];
378
+
379
+ const conversationIds = [
380
+ ...new Set(messagesToSave.map((m) => m.conversationId)),
381
+ ];
382
+
383
+ const ownedConversations = await db
384
+ .select({ id: conversations.id })
385
+ .from(conversations)
386
+ .where(
387
+ and(
388
+ eq(conversations.workspaceId, actor.workspaceId),
389
+ eq(conversations.userId, actor.userId),
390
+ inArray(conversations.id, conversationIds)
391
+ )
392
+ );
393
+
394
+ const ownedIds = new Set(ownedConversations.map((c) => c.id));
395
+ const unowned = messagesToSave.filter((m) => !ownedIds.has(m.conversationId));
396
+ if (unowned.length > 0) {
397
+ throw new ConversationServiceError(
398
+ "forbidden",
399
+ "One or more messages reference a conversation the actor does not own"
400
+ );
401
+ }
402
+
403
+ // A batch arrives in conversation order, but `defaultNow()` would
404
+ // stamp every row with the same transaction time, and a tie in
405
+ // created_at makes "the messages after this one" undefined — a
406
+ // regenerate could then delete nothing or everything. One millisecond
407
+ // per position keeps the order the caller gave.
408
+ const base = Date.now();
409
+ const stamped = messagesToSave.map((message, index) => ({
410
+ ...message,
411
+ createdAt: message.createdAt ?? new Date(base + index),
412
+ }));
413
+
414
+ const inserted = await db.insert(messages).values(stamped).returning();
415
+
416
+ await db
417
+ .update(conversations)
418
+ .set({ updatedAt: new Date() })
419
+ .where(inArray(conversations.id, conversationIds));
420
+
421
+ return inserted;
422
+ }
423
+
424
+ /**
425
+ * Idempotently upserts streamed messages. Takes no actor: the caller
426
+ * verified ownership once at the top of the already-authorized request.
427
+ *
428
+ * One multi-row `INSERT ... ON CONFLICT (id) DO UPDATE`, so an N-message
429
+ * tool loop pays one round-trip. The `WHERE` on the update keeps the write
430
+ * scoped to `conversationId` even if a message id collided across
431
+ * conversations.
432
+ */
433
+ export async function upsertMessages(
434
+ conversationId: string,
435
+ finishedMessages: Array<{ id: string; role: string; parts: unknown[] }>
436
+ ): Promise<void> {
437
+ if (finishedMessages.length === 0) return;
438
+
439
+ // Increment per row so `ORDER BY created_at` (used by getMessages)
440
+ // preserves insertion order across a batched tool loop. Sharing a
441
+ // single `new Date()` across rows would tie their timestamps and
442
+ // let Postgres return them in heap order.
443
+ const baseMs = Date.now();
444
+ const rows = finishedMessages.map((m, i) => ({
445
+ id: m.id,
446
+ conversationId,
447
+ role: m.role,
448
+ parts: JSON.stringify(m.parts),
449
+ createdAt: new Date(baseMs + i),
450
+ }));
451
+
452
+ await db
453
+ .insert(messages)
454
+ .values(rows)
455
+ .onConflictDoUpdate({
456
+ target: messages.id,
457
+ set: {
458
+ parts: sql`excluded.parts`,
459
+ },
460
+ setWhere: sql`${messages.conversationId} = excluded.conversation_id`,
461
+ });
462
+ }
463
+
464
+ /** Update a message's parts (e.g. for a tool-approval flow). */
465
+ export async function updateMessage(
466
+ actor: ConversationActor,
467
+ params: { id: string; parts: unknown }
468
+ ): Promise<Message> {
469
+ const [message] = await db
470
+ .select()
471
+ .from(messages)
472
+ .where(eq(messages.id, params.id))
473
+ .limit(1);
474
+
475
+ if (!message) {
476
+ throw new ConversationServiceError("not_found", "Message not found");
477
+ }
478
+
479
+ await verifyConversation(actor, message.conversationId);
480
+
481
+ const [updated] = await db
482
+ .update(messages)
483
+ .set({ parts: JSON.stringify(params.parts) })
484
+ .where(eq(messages.id, params.id))
485
+ .returning();
486
+
487
+ if (!updated) {
488
+ throw new ConversationServiceError(
489
+ "database_error",
490
+ "Failed to update message"
491
+ );
492
+ }
493
+
494
+ return updated;
495
+ }
496
+
497
+ /** Delete all messages created after a specific message. */
498
+ export async function deleteTrailingMessages(
499
+ actor: ConversationActor,
500
+ params: { id: string }
501
+ ): Promise<{ deletedCount: number }> {
502
+ const [message] = await db
503
+ .select()
504
+ .from(messages)
505
+ .where(eq(messages.id, params.id))
506
+ .limit(1);
507
+
508
+ if (!message) {
509
+ throw new ConversationServiceError("not_found", "Message not found");
510
+ }
511
+
512
+ await verifyConversation(actor, message.conversationId);
513
+
514
+ // Compare inside SQL rather than against the JS Date read above: the
515
+ // driver truncates Postgres microseconds to milliseconds, so a message
516
+ // whose real timestamp is 12:00:00.123456 reads back as .123, and
517
+ // `created_at > .123` then matches the message itself — deleting the
518
+ // row a regenerate was supposed to keep.
519
+ const deleted = await db
520
+ .delete(messages)
521
+ .where(
522
+ and(
523
+ eq(messages.conversationId, message.conversationId),
524
+ gt(
525
+ messages.createdAt,
526
+ sql`(select ${messages.createdAt} from ${messages} where ${messages.id} = ${params.id})`
527
+ )
528
+ )
529
+ )
530
+ .returning();
531
+
532
+ return { deletedCount: deleted.length };
533
+ }
534
+
535
+ /** Vote on a message (upvote or downvote). */
536
+ export async function voteMessage(
537
+ actor: ConversationActor,
538
+ params: { chatId: string; messageId: string; type: "up" | "down" }
539
+ ): Promise<void> {
540
+ await verifyConversation(actor, params.chatId);
541
+
542
+ const [message] = await db
543
+ .select({ id: messages.id })
544
+ .from(messages)
545
+ .where(
546
+ and(
547
+ eq(messages.id, params.messageId),
548
+ eq(messages.conversationId, params.chatId)
549
+ )
550
+ )
551
+ .limit(1);
552
+
553
+ if (!message) {
554
+ throw new ConversationServiceError("not_found", "Message not found");
555
+ }
556
+
557
+ await db
558
+ .insert(votes)
559
+ .values({
560
+ chatId: params.chatId,
561
+ messageId: params.messageId,
562
+ isUpvoted: params.type === "up",
563
+ })
564
+ .onConflictDoUpdate({
565
+ target: [votes.chatId, votes.messageId],
566
+ set: { isUpvoted: params.type === "up" },
567
+ });
568
+ }
569
+
570
+ /** Withdraw a vote. A missing vote is not an error. */
571
+ export async function clearVote(
572
+ actor: ConversationActor,
573
+ params: { chatId: string; messageId: string }
574
+ ): Promise<void> {
575
+ await verifyConversation(actor, params.chatId);
576
+ await db
577
+ .delete(votes)
578
+ .where(
579
+ and(
580
+ eq(votes.chatId, params.chatId),
581
+ eq(votes.messageId, params.messageId)
582
+ )
583
+ );
584
+ }
585
+
586
+ /** Get all votes for a conversation. */
587
+ export async function getVotes(
588
+ actor: ConversationActor,
589
+ chatId: string
590
+ ): Promise<Vote[]> {
591
+ await verifyConversation(actor, chatId);
592
+
593
+ return db.select().from(votes).where(eq(votes.chatId, chatId));
594
+ }
@@ -0,0 +1,15 @@
1
+ /** Resolved actor identity — every query is scoped to this pair. */
2
+ export type ConversationActor = {
3
+ workspaceId: string;
4
+ userId: string;
5
+ };
6
+
7
+ export type {
8
+ Conversation,
9
+ InsertConversation,
10
+ Message,
11
+ InsertMessage,
12
+ Vote,
13
+ InsertVote,
14
+ ConversationMetadata,
15
+ } from "../db/schema";
@@ -0,0 +1,91 @@
1
+ /**
2
+ * One Drizzle handle, two drivers: `pg` (node-postgres) for ordinary
3
+ * Postgres URLs, and `neon-serverless` (Neon's WebSocket driver) for Neon
4
+ * URLs.
5
+ *
6
+ * Neon's HTTP driver is deliberately not an option: it has no session, so
7
+ * `db.transaction()` throws, and quota admission (`pg_advisory_xact_lock` +
8
+ * reservation insert) and usage settlement are transactions.
9
+ *
10
+ * `INTELLIGO_DB_DRIVER=pg | neon-serverless` forces a driver, for a
11
+ * Neon-compatible proxy or a self-hosted Postgres whose hostname happens to
12
+ * match.
13
+ *
14
+ * The client is created on first access so `next build` can collect route
15
+ * metadata without DATABASE_URL; a missing URL throws on the first query.
16
+ */
17
+
18
+ import { drizzle as drizzleNeon } from "drizzle-orm/neon-serverless";
19
+ import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
20
+ import { Pool as NeonPool, neonConfig } from "@neondatabase/serverless";
21
+ import pkg from "pg";
22
+ const { Pool } = pkg;
23
+ import * as schema from "./schema";
24
+
25
+ export type DbDriver = "pg" | "neon-serverless";
26
+
27
+ type Db = ReturnType<typeof drizzleNeon> | ReturnType<typeof drizzlePostgres>;
28
+
29
+ let cached: Db | null = null;
30
+
31
+ /**
32
+ * Which driver a connection string gets. Pure, so the rule is testable
33
+ * without a database: an explicit `INTELLIGO_DB_DRIVER` wins; otherwise
34
+ * Neon hosts (`neon.tech`) and Neon endpoint ids (`@ep-…`) take the
35
+ * WebSocket driver and everything else takes node-postgres.
36
+ */
37
+ export function selectDriver(url: string, override?: string): DbDriver {
38
+ if (override === "pg" || override === "neon-serverless") return override;
39
+ if (override) {
40
+ throw new Error(
41
+ `INTELLIGO_DB_DRIVER must be "pg" or "neon-serverless", got "${override}". ` +
42
+ `(neon-http is not supported: it cannot run transactions.)`
43
+ );
44
+ }
45
+ const isNeon = url.includes("neon.tech") || url.includes("@ep-");
46
+ return isNeon ? "neon-serverless" : "pg";
47
+ }
48
+
49
+ function initDb(): Db {
50
+ const url = process.env.DATABASE_URL;
51
+ if (!url) {
52
+ throw new Error(
53
+ "DATABASE_URL is not defined. Please set it in your .env file."
54
+ );
55
+ }
56
+
57
+ const driver = selectDriver(url, process.env.INTELLIGO_DB_DRIVER);
58
+
59
+ if (driver === "neon-serverless") {
60
+ // The WebSocket driver needs a WebSocket implementation. Node 22+
61
+ // has one globally; older runtimes must install `ws` and assign
62
+ // `neonConfig.webSocketConstructor` themselves before first use.
63
+ if (!neonConfig.webSocketConstructor && typeof WebSocket !== "undefined") {
64
+ neonConfig.webSocketConstructor = WebSocket;
65
+ }
66
+ const pool = new NeonPool({ connectionString: url });
67
+ console.log("📊 Database: Neon (serverless, WebSocket)");
68
+ return drizzleNeon(pool, { schema });
69
+ }
70
+
71
+ const pool = new Pool({ connectionString: url });
72
+ console.log("📊 Database: PostgreSQL");
73
+ return drizzlePostgres(pool as any, { schema });
74
+ }
75
+
76
+ function resolveDb(): Db {
77
+ if (!cached) cached = initDb();
78
+ return cached;
79
+ }
80
+
81
+ // Proxy keeps the ergonomic `db.select()...` shape for all call
82
+ // sites while deferring actual driver instantiation to first access.
83
+ // `next build` never touches these handlers, so module load is
84
+ // env-independent; first real request materializes the client.
85
+ export const db: Db = new Proxy({} as Db, {
86
+ get(_target, prop, receiver) {
87
+ const real = resolveDb();
88
+ const value = Reflect.get(real, prop, receiver);
89
+ return typeof value === "function" ? value.bind(real) : value;
90
+ },
91
+ }) as Db;
@@ -0,0 +1,3 @@
1
+ export * from "./client";
2
+ export * from "./validate";
3
+ export { workspaceEq, withWorkspaceFilter } from "./workspace-queries";