@cat-factory/node-server 0.6.0

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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/dist/config.d.ts +3 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +297 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/container.d.ts +88 -0
  7. package/dist/container.d.ts.map +1 -0
  8. package/dist/container.js +937 -0
  9. package/dist/container.js.map +1 -0
  10. package/dist/db/client.d.ts +13 -0
  11. package/dist/db/client.d.ts.map +1 -0
  12. package/dist/db/client.js +21 -0
  13. package/dist/db/client.js.map +1 -0
  14. package/dist/db/migrate.d.ts +12 -0
  15. package/dist/db/migrate.d.ts.map +1 -0
  16. package/dist/db/migrate.js +40 -0
  17. package/dist/db/migrate.js.map +1 -0
  18. package/dist/db/schema.d.ts +7858 -0
  19. package/dist/db/schema.d.ts.map +1 -0
  20. package/dist/db/schema.js +928 -0
  21. package/dist/db/schema.js.map +1 -0
  22. package/dist/environments.d.ts +11 -0
  23. package/dist/environments.d.ts.map +1 -0
  24. package/dist/environments.js +31 -0
  25. package/dist/environments.js.map +1 -0
  26. package/dist/execution/bootstrapRunner.d.ts +27 -0
  27. package/dist/execution/bootstrapRunner.d.ts.map +1 -0
  28. package/dist/execution/bootstrapRunner.js +79 -0
  29. package/dist/execution/bootstrapRunner.js.map +1 -0
  30. package/dist/execution/config.d.ts +37 -0
  31. package/dist/execution/config.d.ts.map +1 -0
  32. package/dist/execution/config.js +86 -0
  33. package/dist/execution/config.js.map +1 -0
  34. package/dist/execution/drive.d.ts +6 -0
  35. package/dist/execution/drive.d.ts.map +1 -0
  36. package/dist/execution/drive.js +13 -0
  37. package/dist/execution/drive.js.map +1 -0
  38. package/dist/execution/pgBossRunner.d.ts +82 -0
  39. package/dist/execution/pgBossRunner.d.ts.map +1 -0
  40. package/dist/execution/pgBossRunner.js +163 -0
  41. package/dist/execution/pgBossRunner.js.map +1 -0
  42. package/dist/gateways.d.ts +4 -0
  43. package/dist/gateways.d.ts.map +1 -0
  44. package/dist/gateways.js +91 -0
  45. package/dist/gateways.js.map +1 -0
  46. package/dist/index.d.ts +13 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +22 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/main.d.ts +2 -0
  51. package/dist/main.d.ts.map +1 -0
  52. package/dist/main.js +9 -0
  53. package/dist/main.js.map +1 -0
  54. package/dist/modelProvider.d.ts +6 -0
  55. package/dist/modelProvider.d.ts.map +1 -0
  56. package/dist/modelProvider.js +72 -0
  57. package/dist/modelProvider.js.map +1 -0
  58. package/dist/realtime.d.ts +62 -0
  59. package/dist/realtime.d.ts.map +1 -0
  60. package/dist/realtime.js +171 -0
  61. package/dist/realtime.js.map +1 -0
  62. package/dist/recurring.d.ts +11 -0
  63. package/dist/recurring.d.ts.map +1 -0
  64. package/dist/recurring.js +33 -0
  65. package/dist/recurring.js.map +1 -0
  66. package/dist/repositories/bootstrap.d.ts +25 -0
  67. package/dist/repositories/bootstrap.d.ts.map +1 -0
  68. package/dist/repositories/bootstrap.js +280 -0
  69. package/dist/repositories/bootstrap.js.map +1 -0
  70. package/dist/repositories/containerExecution.d.ts +33 -0
  71. package/dist/repositories/containerExecution.d.ts.map +1 -0
  72. package/dist/repositories/containerExecution.js +199 -0
  73. package/dist/repositories/containerExecution.js.map +1 -0
  74. package/dist/repositories/documents.d.ts +31 -0
  75. package/dist/repositories/documents.d.ts.map +1 -0
  76. package/dist/repositories/documents.js +176 -0
  77. package/dist/repositories/documents.js.map +1 -0
  78. package/dist/repositories/drizzle.d.ts +105 -0
  79. package/dist/repositories/drizzle.d.ts.map +1 -0
  80. package/dist/repositories/drizzle.js +1872 -0
  81. package/dist/repositories/drizzle.js.map +1 -0
  82. package/dist/repositories/environments.d.ts +23 -0
  83. package/dist/repositories/environments.d.ts.map +1 -0
  84. package/dist/repositories/environments.js +162 -0
  85. package/dist/repositories/environments.js.map +1 -0
  86. package/dist/repositories/fragments.d.ts +23 -0
  87. package/dist/repositories/fragments.d.ts.map +1 -0
  88. package/dist/repositories/fragments.js +190 -0
  89. package/dist/repositories/fragments.js.map +1 -0
  90. package/dist/repositories/github.d.ts +53 -0
  91. package/dist/repositories/github.d.ts.map +1 -0
  92. package/dist/repositories/github.js +441 -0
  93. package/dist/repositories/github.js.map +1 -0
  94. package/dist/repositories/localModelEndpoint.d.ts +12 -0
  95. package/dist/repositories/localModelEndpoint.d.ts.map +1 -0
  96. package/dist/repositories/localModelEndpoint.js +75 -0
  97. package/dist/repositories/localModelEndpoint.js.map +1 -0
  98. package/dist/repositories/notifications.d.ts +11 -0
  99. package/dist/repositories/notifications.d.ts.map +1 -0
  100. package/dist/repositories/notifications.js +88 -0
  101. package/dist/repositories/notifications.js.map +1 -0
  102. package/dist/repositories/personalSubscription.d.ts +22 -0
  103. package/dist/repositories/personalSubscription.d.ts.map +1 -0
  104. package/dist/repositories/personalSubscription.js +159 -0
  105. package/dist/repositories/personalSubscription.js.map +1 -0
  106. package/dist/repositories/providerApiKey.d.ts +18 -0
  107. package/dist/repositories/providerApiKey.d.ts.map +1 -0
  108. package/dist/repositories/providerApiKey.js +111 -0
  109. package/dist/repositories/providerApiKey.js.map +1 -0
  110. package/dist/repositories/providerSubscription.d.ts +16 -0
  111. package/dist/repositories/providerSubscription.d.ts.map +1 -0
  112. package/dist/repositories/providerSubscription.js +88 -0
  113. package/dist/repositories/providerSubscription.js.map +1 -0
  114. package/dist/repositories/slack.d.ts +23 -0
  115. package/dist/repositories/slack.d.ts.map +1 -0
  116. package/dist/repositories/slack.js +150 -0
  117. package/dist/repositories/slack.js.map +1 -0
  118. package/dist/repositories/tasks.d.ts +24 -0
  119. package/dist/repositories/tasks.d.ts.map +1 -0
  120. package/dist/repositories/tasks.js +194 -0
  121. package/dist/repositories/tasks.js.map +1 -0
  122. package/dist/retention.d.ts +38 -0
  123. package/dist/retention.d.ts.map +1 -0
  124. package/dist/retention.js +53 -0
  125. package/dist/retention.js.map +1 -0
  126. package/dist/runtime.d.ts +10 -0
  127. package/dist/runtime.d.ts.map +1 -0
  128. package/dist/runtime.js +13 -0
  129. package/dist/runtime.js.map +1 -0
  130. package/dist/server.d.ts +41 -0
  131. package/dist/server.d.ts.map +1 -0
  132. package/dist/server.js +138 -0
  133. package/dist/server.js.map +1 -0
  134. package/dist/tasks/JiraProvider.d.ts +27 -0
  135. package/dist/tasks/JiraProvider.d.ts.map +1 -0
  136. package/dist/tasks/JiraProvider.js +79 -0
  137. package/dist/tasks/JiraProvider.js.map +1 -0
  138. package/drizzle/20260622175812_flashy_maginty/migration.sql +689 -0
  139. package/drizzle/20260622175812_flashy_maginty/snapshot.json +8318 -0
  140. package/drizzle/20260623172634_loud_wallop/migration.sql +11 -0
  141. package/drizzle/20260623172634_loud_wallop/snapshot.json +8439 -0
  142. package/drizzle/20260623174706_acoustic_zemo/migration.sql +16 -0
  143. package/drizzle/20260623174706_acoustic_zemo/snapshot.json +8506 -0
  144. package/drizzle/20260623184400_silent_cardiac/migration.sql +24 -0
  145. package/drizzle/20260623184400_silent_cardiac/snapshot.json +8639 -0
  146. package/drizzle/20260623205323_quick_arclight/migration.sql +1 -0
  147. package/drizzle/20260623205323_quick_arclight/snapshot.json +8963 -0
  148. package/drizzle/20260623221910_black_zombie/migration.sql +22 -0
  149. package/drizzle/20260623221910_black_zombie/snapshot.json +9189 -0
  150. package/drizzle/20260624131343_far_lily_hollister/migration.sql +3 -0
  151. package/drizzle/20260624131343_far_lily_hollister/snapshot.json +9228 -0
  152. package/drizzle/20260624135452_tiny_norman_osborn/migration.sql +11 -0
  153. package/drizzle/20260624135452_tiny_norman_osborn/snapshot.json +9126 -0
  154. package/drizzle/20260624140138_wandering_avengers/migration.sql +1 -0
  155. package/drizzle/20260624140138_wandering_avengers/snapshot.json +9045 -0
  156. package/package.json +62 -0
@@ -0,0 +1,1872 @@
1
+ import { LLM_WARNING_FINISH_REASONS } from '@cat-factory/kernel';
2
+ import { blockInsertValues, blockPatchToColumns, rowToBlock, rowToExecution, executionToDetail, rowToPipeline, rowToWorkspace, } from '@cat-factory/server';
3
+ import { and, desc, eq, gte, inArray, isNull, lt, or, sql } from 'drizzle-orm';
4
+ import { accountInvitations, accounts, agentRuns, blocks, consensusSessions, emailConnections, llmCallMetrics, memberships, mergeThresholdPresets, repoBlueprints, pipelineScheduleRuns, pipelineSchedules, pipelines, requirementReviews, clarityReviews, services, tokenUsage, trackerSettings, userIdentities, users, workspaceFragmentDefaults, workspaceModelDefaults, workspaceServices, workspaces, } from '../db/schema.js';
5
+ // Drizzle/Postgres implementations of the core kernel repository ports. The
6
+ // row<->domain mapping is the SAME shared mapping the Cloudflare D1 repos use
7
+ // (@cat-factory/server), so behaviour matches across stores; this layer only owns
8
+ // the Drizzle queries. This is the single persistence used in dev, test and prod.
9
+ class DrizzleWorkspaceRepository {
10
+ db;
11
+ constructor(db) {
12
+ this.db = db;
13
+ }
14
+ async listVisible(scope) {
15
+ if (scope === null) {
16
+ const rows = await this.db.select().from(workspaces).orderBy(desc(workspaces.created_at));
17
+ return rows.map(rowToWorkspace);
18
+ }
19
+ const legacy = and(isNull(workspaces.account_id), eq(workspaces.owner_user_id, scope.ownerUserId));
20
+ const where = scope.accountIds.length > 0
21
+ ? or(inArray(workspaces.account_id, scope.accountIds), legacy)
22
+ : legacy;
23
+ const rows = await this.db
24
+ .select()
25
+ .from(workspaces)
26
+ .where(where)
27
+ .orderBy(desc(workspaces.created_at));
28
+ return rows.map(rowToWorkspace);
29
+ }
30
+ async get(id) {
31
+ const [row] = await this.db.select().from(workspaces).where(eq(workspaces.id, id));
32
+ return row ? rowToWorkspace(row) : null;
33
+ }
34
+ async ownerOf(id) {
35
+ const [row] = await this.db
36
+ .select({ owner: workspaces.owner_user_id })
37
+ .from(workspaces)
38
+ .where(eq(workspaces.id, id));
39
+ return row ? row.owner : undefined;
40
+ }
41
+ async accountOf(id) {
42
+ const [row] = await this.db
43
+ .select({ account: workspaces.account_id })
44
+ .from(workspaces)
45
+ .where(eq(workspaces.id, id));
46
+ return row ? row.account : undefined;
47
+ }
48
+ async create(workspace, ownerUserId, accountId) {
49
+ await this.db.insert(workspaces).values({
50
+ id: workspace.id,
51
+ name: workspace.name,
52
+ description: workspace.description,
53
+ created_at: workspace.createdAt,
54
+ owner_user_id: ownerUserId,
55
+ account_id: accountId,
56
+ });
57
+ }
58
+ async rename(id, name) {
59
+ await this.db.update(workspaces).set({ name }).where(eq(workspaces.id, id));
60
+ }
61
+ async setDescription(id, description) {
62
+ await this.db.update(workspaces).set({ description }).where(eq(workspaces.id, id));
63
+ }
64
+ async delete(id) {
65
+ await this.db.transaction(async (tx) => {
66
+ await tx.delete(agentRuns).where(eq(agentRuns.workspace_id, id));
67
+ await tx.delete(blocks).where(eq(blocks.workspace_id, id));
68
+ await tx.delete(pipelines).where(eq(pipelines.workspace_id, id));
69
+ await tx.delete(workspaces).where(eq(workspaces.id, id));
70
+ });
71
+ }
72
+ }
73
+ class DrizzleBlockRepository {
74
+ db;
75
+ constructor(db) {
76
+ this.db = db;
77
+ }
78
+ async listByWorkspace(workspaceId) {
79
+ const rows = await this.db.select().from(blocks).where(eq(blocks.workspace_id, workspaceId));
80
+ return rows.map(rowToBlock);
81
+ }
82
+ async listByService(serviceId) {
83
+ const rows = await this.db.select().from(blocks).where(eq(blocks.service_id, serviceId));
84
+ return rows.map(rowToBlock);
85
+ }
86
+ async listByServices(serviceIds) {
87
+ if (serviceIds.length === 0)
88
+ return [];
89
+ const out = [];
90
+ // Chunk the IN list to stay well under the bind-parameter limit.
91
+ for (let i = 0; i < serviceIds.length; i += 500) {
92
+ const rows = await this.db
93
+ .select()
94
+ .from(blocks)
95
+ .where(inArray(blocks.service_id, serviceIds.slice(i, i + 500)));
96
+ for (const row of rows)
97
+ out.push(rowToBlock(row));
98
+ }
99
+ return out;
100
+ }
101
+ async get(workspaceId, id) {
102
+ const [row] = await this.db
103
+ .select()
104
+ .from(blocks)
105
+ .where(and(eq(blocks.workspace_id, workspaceId), eq(blocks.id, id)));
106
+ return row ? rowToBlock(row) : null;
107
+ }
108
+ async findById(blockId) {
109
+ const [row] = await this.db.select().from(blocks).where(eq(blocks.id, blockId)).limit(1);
110
+ if (!row)
111
+ return null;
112
+ return {
113
+ workspaceId: row.workspace_id,
114
+ serviceId: row.service_id ?? null,
115
+ block: rowToBlock(row),
116
+ };
117
+ }
118
+ async insert(workspaceId, block, serviceId) {
119
+ await this.db.insert(blocks).values({
120
+ workspace_id: workspaceId,
121
+ service_id: serviceId ?? null,
122
+ ...blockInsertValues(block),
123
+ });
124
+ }
125
+ async update(workspaceId, id, patch) {
126
+ const set = blockPatchToColumns(patch);
127
+ if (Object.keys(set).length === 0)
128
+ return;
129
+ await this.db
130
+ .update(blocks)
131
+ .set(set)
132
+ .where(and(eq(blocks.workspace_id, workspaceId), eq(blocks.id, id)));
133
+ }
134
+ async setService(workspaceId, ids, serviceId) {
135
+ if (ids.length === 0)
136
+ return;
137
+ await this.db
138
+ .update(blocks)
139
+ .set({ service_id: serviceId })
140
+ .where(and(eq(blocks.workspace_id, workspaceId), inArray(blocks.id, ids)));
141
+ }
142
+ async deleteMany(workspaceId, ids) {
143
+ if (ids.length === 0)
144
+ return;
145
+ await this.db
146
+ .delete(blocks)
147
+ .where(and(eq(blocks.workspace_id, workspaceId), inArray(blocks.id, ids)));
148
+ }
149
+ }
150
+ class DrizzlePipelineRepository {
151
+ db;
152
+ constructor(db) {
153
+ this.db = db;
154
+ }
155
+ async listByWorkspace(workspaceId) {
156
+ const rows = await this.db
157
+ .select()
158
+ .from(pipelines)
159
+ .where(eq(pipelines.workspace_id, workspaceId))
160
+ // Order by the monotonic insert `seq` so the catalog comes back in the curated
161
+ // `seedPipelines()` order it was inserted in (Postgres gives no row order without
162
+ // ORDER BY) — deterministic snapshots, a stable default `pipelines[0]`, and parity
163
+ // with the Cloudflare facade's `ORDER BY rowid`.
164
+ .orderBy(pipelines.seq);
165
+ return rows.map(rowToPipeline);
166
+ }
167
+ async get(workspaceId, id) {
168
+ const [row] = await this.db
169
+ .select()
170
+ .from(pipelines)
171
+ .where(and(eq(pipelines.workspace_id, workspaceId), eq(pipelines.id, id)));
172
+ return row ? rowToPipeline(row) : null;
173
+ }
174
+ async insert(workspaceId, pipeline) {
175
+ await this.db.insert(pipelines).values({
176
+ workspace_id: workspaceId,
177
+ id: pipeline.id,
178
+ name: pipeline.name,
179
+ agent_kinds: JSON.stringify(pipeline.agentKinds),
180
+ gates: pipeline.gates ? JSON.stringify(pipeline.gates) : null,
181
+ thresholds: pipeline.thresholds ? JSON.stringify(pipeline.thresholds) : null,
182
+ enabled: pipeline.enabled ? JSON.stringify(pipeline.enabled) : null,
183
+ consensus: pipeline.consensus ? JSON.stringify(pipeline.consensus) : null,
184
+ builtin: pipeline.builtin ? 1 : null,
185
+ });
186
+ }
187
+ async update(workspaceId, pipeline) {
188
+ // UPDATE in place preserves the row's `seq`, so an edited pipeline keeps its place
189
+ // in the catalog order. `builtin` is immutable, so it is not rewritten.
190
+ await this.db
191
+ .update(pipelines)
192
+ .set({
193
+ name: pipeline.name,
194
+ agent_kinds: JSON.stringify(pipeline.agentKinds),
195
+ gates: pipeline.gates ? JSON.stringify(pipeline.gates) : null,
196
+ thresholds: pipeline.thresholds ? JSON.stringify(pipeline.thresholds) : null,
197
+ enabled: pipeline.enabled ? JSON.stringify(pipeline.enabled) : null,
198
+ consensus: pipeline.consensus ? JSON.stringify(pipeline.consensus) : null,
199
+ })
200
+ .where(and(eq(pipelines.workspace_id, workspaceId), eq(pipelines.id, pipeline.id)));
201
+ }
202
+ async delete(workspaceId, id) {
203
+ await this.db
204
+ .delete(pipelines)
205
+ .where(and(eq(pipelines.workspace_id, workspaceId), eq(pipelines.id, id)));
206
+ }
207
+ }
208
+ /** Execution runs live as `kind='execution'` rows of the unified agent_runs table. */
209
+ class DrizzleExecutionRepository {
210
+ db;
211
+ clock;
212
+ constructor(db, clock) {
213
+ this.db = db;
214
+ this.clock = clock;
215
+ }
216
+ isExecution = eq(agentRuns.kind, 'execution');
217
+ async listByWorkspace(workspaceId) {
218
+ const rows = await this.db
219
+ .select()
220
+ .from(agentRuns)
221
+ .where(and(eq(agentRuns.workspace_id, workspaceId), this.isExecution))
222
+ .orderBy(agentRuns.created_at);
223
+ return rows.map((r) => rowToExecution(r));
224
+ }
225
+ async listByService(serviceId) {
226
+ const rows = await this.db
227
+ .select()
228
+ .from(agentRuns)
229
+ .where(and(eq(agentRuns.service_id, serviceId), this.isExecution))
230
+ .orderBy(agentRuns.created_at);
231
+ return rows.map((r) => rowToExecution(r));
232
+ }
233
+ async listByServices(serviceIds) {
234
+ if (serviceIds.length === 0)
235
+ return [];
236
+ const out = [];
237
+ // Chunk the IN list to stay well under the bind-parameter limit.
238
+ for (let i = 0; i < serviceIds.length; i += 500) {
239
+ const rows = await this.db
240
+ .select()
241
+ .from(agentRuns)
242
+ .where(and(inArray(agentRuns.service_id, serviceIds.slice(i, i + 500)), this.isExecution))
243
+ .orderBy(agentRuns.created_at);
244
+ for (const r of rows)
245
+ out.push(rowToExecution(r));
246
+ }
247
+ return out;
248
+ }
249
+ async get(workspaceId, id) {
250
+ const [row] = await this.db
251
+ .select()
252
+ .from(agentRuns)
253
+ .where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, id), this.isExecution));
254
+ return row ? rowToExecution(row) : null;
255
+ }
256
+ async getByBlock(workspaceId, blockId) {
257
+ const [row] = await this.db
258
+ .select()
259
+ .from(agentRuns)
260
+ .where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.block_id, blockId), this.isExecution));
261
+ return row ? rowToExecution(row) : null;
262
+ }
263
+ async upsert(workspaceId, execution) {
264
+ const now = this.clock.now();
265
+ const detail = executionToDetail(execution);
266
+ // Stamp `service_id` from the run's block (subquery) so a shared service's runs surface on
267
+ // every board that mounts it via `listByService`; refreshed on every write so it follows a
268
+ // reparent that re-homes the block. Mirrors the D1 repo.
269
+ const serviceIdSub = sql `(SELECT ${blocks.service_id} FROM ${blocks} WHERE ${blocks.workspace_id} = ${workspaceId} AND ${blocks.id} = ${execution.blockId})`;
270
+ await this.db
271
+ .insert(agentRuns)
272
+ .values({
273
+ workspace_id: workspaceId,
274
+ id: execution.id,
275
+ kind: 'execution',
276
+ block_id: execution.blockId,
277
+ status: execution.status,
278
+ detail,
279
+ created_at: now,
280
+ updated_at: now,
281
+ workflow_instance_id: execution.id,
282
+ service_id: serviceIdSub,
283
+ })
284
+ // error/failure/workflow_instance_id are left out of the update so they survive
285
+ // normal step writes (see markFailed) — mirrors the D1 repo.
286
+ .onConflictDoUpdate({
287
+ target: [agentRuns.workspace_id, agentRuns.id],
288
+ set: {
289
+ block_id: execution.blockId,
290
+ status: execution.status,
291
+ detail,
292
+ updated_at: now,
293
+ service_id: serviceIdSub,
294
+ },
295
+ });
296
+ }
297
+ async deleteByBlock(workspaceId, blockId) {
298
+ await this.db
299
+ .delete(agentRuns)
300
+ .where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.block_id, blockId), this.isExecution));
301
+ }
302
+ async listStale(olderThanEpochMs) {
303
+ const rows = await this.db
304
+ .select({ workspaceId: agentRuns.workspace_id, id: agentRuns.id })
305
+ .from(agentRuns)
306
+ .where(and(this.isExecution, eq(agentRuns.status, 'running'), lt(agentRuns.updated_at, olderThanEpochMs)))
307
+ .orderBy(agentRuns.updated_at);
308
+ return rows;
309
+ }
310
+ async markFailed(workspaceId, id, failure) {
311
+ await this.db
312
+ .update(agentRuns)
313
+ .set({
314
+ status: 'failed',
315
+ error: failure.message,
316
+ failure: JSON.stringify(failure),
317
+ updated_at: this.clock.now(),
318
+ })
319
+ .where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, id), this.isExecution));
320
+ }
321
+ }
322
+ class DrizzleAgentRunRepository {
323
+ db;
324
+ constructor(db) {
325
+ this.db = db;
326
+ }
327
+ async getRef(workspaceId, id) {
328
+ const [row] = await this.db
329
+ .select({ kind: agentRuns.kind })
330
+ .from(agentRuns)
331
+ .where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, id)));
332
+ return row ? { workspaceId, id, kind: row.kind } : null;
333
+ }
334
+ async listStale(olderThanEpochMs) {
335
+ const rows = await this.db
336
+ .select({ workspaceId: agentRuns.workspace_id, id: agentRuns.id, kind: agentRuns.kind })
337
+ .from(agentRuns)
338
+ .where(and(eq(agentRuns.status, 'running'), lt(agentRuns.updated_at, olderThanEpochMs)))
339
+ .orderBy(agentRuns.updated_at);
340
+ return rows.map((r) => ({ workspaceId: r.workspaceId, id: r.id, kind: r.kind }));
341
+ }
342
+ }
343
+ function rowToAccount(row) {
344
+ return {
345
+ id: row.id,
346
+ type: row.type === 'org' ? 'org' : 'personal',
347
+ name: row.name,
348
+ githubAccountLogin: row.github_account_login,
349
+ ownerUserId: row.owner_user_id,
350
+ createdAt: row.created_at,
351
+ ...(row.default_cloud_provider
352
+ ? { defaultCloudProvider: row.default_cloud_provider }
353
+ : {}),
354
+ };
355
+ }
356
+ class DrizzleAccountRepository {
357
+ db;
358
+ constructor(db) {
359
+ this.db = db;
360
+ }
361
+ async get(id) {
362
+ const [row] = await this.db.select().from(accounts).where(eq(accounts.id, id));
363
+ return row ? rowToAccount(row) : null;
364
+ }
365
+ async create(account) {
366
+ await this.db.insert(accounts).values({
367
+ id: account.id,
368
+ type: account.type,
369
+ name: account.name,
370
+ github_account_login: account.githubAccountLogin,
371
+ owner_user_id: account.ownerUserId,
372
+ created_at: account.createdAt,
373
+ default_cloud_provider: account.defaultCloudProvider ?? null,
374
+ });
375
+ }
376
+ async rename(id, name) {
377
+ await this.db.update(accounts).set({ name }).where(eq(accounts.id, id));
378
+ }
379
+ async updateSettings(id, patch) {
380
+ if (!('defaultCloudProvider' in patch))
381
+ return;
382
+ await this.db
383
+ .update(accounts)
384
+ .set({ default_cloud_provider: patch.defaultCloudProvider ?? null })
385
+ .where(eq(accounts.id, id));
386
+ }
387
+ async findPersonalByUser(userId) {
388
+ const [row] = await this.db
389
+ .select()
390
+ .from(accounts)
391
+ .where(and(eq(accounts.type, 'personal'), eq(accounts.owner_user_id, userId)));
392
+ return row ? rowToAccount(row) : null;
393
+ }
394
+ }
395
+ /** Parse the CSV `roles` column into a non-empty role set (defaults to developer). */
396
+ function parseRoles(csv) {
397
+ const roles = (csv ?? '')
398
+ .split(',')
399
+ .map((r) => r.trim())
400
+ .filter((r) => r === 'admin' || r === 'developer' || r === 'product');
401
+ return roles.length > 0 ? [...new Set(roles)] : ['developer'];
402
+ }
403
+ function rowToMembership(row) {
404
+ return {
405
+ accountId: row.account_id,
406
+ userId: row.user_id,
407
+ roles: parseRoles(row.roles),
408
+ createdAt: row.created_at,
409
+ };
410
+ }
411
+ class DrizzleMembershipRepository {
412
+ db;
413
+ constructor(db) {
414
+ this.db = db;
415
+ }
416
+ async listByUser(userId) {
417
+ const rows = await this.db
418
+ .select()
419
+ .from(memberships)
420
+ .where(eq(memberships.user_id, userId))
421
+ .orderBy(memberships.created_at);
422
+ return rows.map(rowToMembership);
423
+ }
424
+ async listByAccount(accountId) {
425
+ const rows = await this.db
426
+ .select()
427
+ .from(memberships)
428
+ .where(eq(memberships.account_id, accountId))
429
+ .orderBy(memberships.created_at);
430
+ return rows.map(rowToMembership);
431
+ }
432
+ async get(accountId, userId) {
433
+ const [row] = await this.db
434
+ .select()
435
+ .from(memberships)
436
+ .where(and(eq(memberships.account_id, accountId), eq(memberships.user_id, userId)));
437
+ return row ? rowToMembership(row) : null;
438
+ }
439
+ async upsert(membership) {
440
+ await this.db
441
+ .insert(memberships)
442
+ .values({
443
+ account_id: membership.accountId,
444
+ user_id: membership.userId,
445
+ roles: membership.roles.join(','),
446
+ created_at: membership.createdAt,
447
+ })
448
+ .onConflictDoUpdate({
449
+ target: [memberships.account_id, memberships.user_id],
450
+ set: { roles: membership.roles.join(',') },
451
+ });
452
+ }
453
+ async remove(accountId, userId) {
454
+ await this.db
455
+ .delete(memberships)
456
+ .where(and(eq(memberships.account_id, accountId), eq(memberships.user_id, userId)));
457
+ }
458
+ }
459
+ function rowToUser(row) {
460
+ return {
461
+ id: row.id,
462
+ name: row.name,
463
+ email: row.email,
464
+ avatarUrl: row.avatar_url,
465
+ createdAt: row.created_at,
466
+ };
467
+ }
468
+ function rowToIdentity(row) {
469
+ return {
470
+ userId: row.user_id,
471
+ provider: row.provider,
472
+ subject: row.subject,
473
+ secret: row.secret,
474
+ metadata: row.metadata,
475
+ createdAt: row.created_at,
476
+ };
477
+ }
478
+ class DrizzleUserRepository {
479
+ db;
480
+ constructor(db) {
481
+ this.db = db;
482
+ }
483
+ async get(id) {
484
+ const [row] = await this.db.select().from(users).where(eq(users.id, id));
485
+ return row ? rowToUser(row) : null;
486
+ }
487
+ async create(user) {
488
+ await this.db.insert(users).values({
489
+ id: user.id,
490
+ name: user.name,
491
+ email: user.email,
492
+ avatar_url: user.avatarUrl,
493
+ created_at: user.createdAt,
494
+ });
495
+ }
496
+ async update(id, patch) {
497
+ const set = {};
498
+ if ('name' in patch)
499
+ set.name = patch.name;
500
+ if ('email' in patch)
501
+ set.email = patch.email;
502
+ if ('avatarUrl' in patch)
503
+ set.avatar_url = patch.avatarUrl;
504
+ if (Object.keys(set).length === 0)
505
+ return;
506
+ await this.db.update(users).set(set).where(eq(users.id, id));
507
+ }
508
+ async findByIdentity(provider, subject) {
509
+ const [row] = await this.db
510
+ .select()
511
+ .from(users)
512
+ .innerJoin(userIdentities, eq(userIdentities.user_id, users.id))
513
+ .where(and(eq(userIdentities.provider, provider), eq(userIdentities.subject, subject)));
514
+ return row ? rowToUser(row.users) : null;
515
+ }
516
+ async findByEmail(email) {
517
+ const [row] = await this.db
518
+ .select()
519
+ .from(users)
520
+ .where(eq(users.email, email.toLowerCase().trim()));
521
+ return row ? rowToUser(row) : null;
522
+ }
523
+ async listByIds(ids) {
524
+ if (ids.length === 0)
525
+ return [];
526
+ const rows = await this.db.select().from(users).where(inArray(users.id, ids));
527
+ return rows.map(rowToUser);
528
+ }
529
+ async getIdentity(provider, subject) {
530
+ const [row] = await this.db
531
+ .select()
532
+ .from(userIdentities)
533
+ .where(and(eq(userIdentities.provider, provider), eq(userIdentities.subject, subject)));
534
+ return row ? rowToIdentity(row) : null;
535
+ }
536
+ async linkIdentity(identity) {
537
+ await this.db
538
+ .insert(userIdentities)
539
+ .values({
540
+ user_id: identity.userId,
541
+ provider: identity.provider,
542
+ subject: identity.subject,
543
+ secret: identity.secret,
544
+ metadata: identity.metadata,
545
+ created_at: identity.createdAt,
546
+ })
547
+ .onConflictDoUpdate({
548
+ target: [userIdentities.provider, userIdentities.subject],
549
+ set: { user_id: identity.userId, secret: identity.secret, metadata: identity.metadata },
550
+ });
551
+ }
552
+ async listIdentities(userId) {
553
+ const rows = await this.db
554
+ .select()
555
+ .from(userIdentities)
556
+ .where(eq(userIdentities.user_id, userId));
557
+ return rows.map(rowToIdentity);
558
+ }
559
+ }
560
+ function rowToInvitation(row) {
561
+ return {
562
+ id: row.id,
563
+ accountId: row.account_id,
564
+ email: row.email,
565
+ roles: parseRoles(row.roles),
566
+ tokenHash: row.token_hash,
567
+ invitedBy: row.invited_by,
568
+ status: row.status,
569
+ expiresAt: row.expires_at,
570
+ createdAt: row.created_at,
571
+ };
572
+ }
573
+ class DrizzleAccountInvitationRepository {
574
+ db;
575
+ constructor(db) {
576
+ this.db = db;
577
+ }
578
+ async create(record) {
579
+ await this.db.insert(accountInvitations).values({
580
+ id: record.id,
581
+ account_id: record.accountId,
582
+ email: record.email,
583
+ roles: record.roles.join(','),
584
+ token_hash: record.tokenHash,
585
+ invited_by: record.invitedBy,
586
+ status: record.status,
587
+ expires_at: record.expiresAt,
588
+ created_at: record.createdAt,
589
+ });
590
+ }
591
+ async get(id) {
592
+ const [row] = await this.db
593
+ .select()
594
+ .from(accountInvitations)
595
+ .where(eq(accountInvitations.id, id));
596
+ return row ? rowToInvitation(row) : null;
597
+ }
598
+ async findByTokenHash(tokenHash) {
599
+ const [row] = await this.db
600
+ .select()
601
+ .from(accountInvitations)
602
+ .where(eq(accountInvitations.token_hash, tokenHash));
603
+ return row ? rowToInvitation(row) : null;
604
+ }
605
+ async listByAccount(accountId) {
606
+ const rows = await this.db
607
+ .select()
608
+ .from(accountInvitations)
609
+ .where(eq(accountInvitations.account_id, accountId))
610
+ .orderBy(desc(accountInvitations.created_at));
611
+ return rows.map(rowToInvitation);
612
+ }
613
+ async setStatus(id, status) {
614
+ await this.db.update(accountInvitations).set({ status }).where(eq(accountInvitations.id, id));
615
+ }
616
+ }
617
+ function rowToEmailConnection(row) {
618
+ return {
619
+ accountId: row.account_id,
620
+ provider: row.provider,
621
+ fromAddress: row.from_address,
622
+ apiKeyCipher: row.api_key_cipher,
623
+ createdAt: row.created_at,
624
+ updatedAt: row.updated_at,
625
+ deletedAt: row.deleted_at,
626
+ };
627
+ }
628
+ class DrizzleEmailConnectionRepository {
629
+ db;
630
+ constructor(db) {
631
+ this.db = db;
632
+ }
633
+ async getByAccount(accountId) {
634
+ const [row] = await this.db
635
+ .select()
636
+ .from(emailConnections)
637
+ .where(and(eq(emailConnections.account_id, accountId), isNull(emailConnections.deleted_at)));
638
+ return row ? rowToEmailConnection(row) : null;
639
+ }
640
+ async upsert(record) {
641
+ await this.db
642
+ .insert(emailConnections)
643
+ .values({
644
+ account_id: record.accountId,
645
+ provider: record.provider,
646
+ from_address: record.fromAddress,
647
+ api_key_cipher: record.apiKeyCipher,
648
+ created_at: record.createdAt,
649
+ updated_at: record.updatedAt,
650
+ deleted_at: record.deletedAt,
651
+ })
652
+ .onConflictDoUpdate({
653
+ target: emailConnections.account_id,
654
+ set: {
655
+ provider: record.provider,
656
+ from_address: record.fromAddress,
657
+ api_key_cipher: record.apiKeyCipher,
658
+ updated_at: record.updatedAt,
659
+ deleted_at: record.deletedAt,
660
+ },
661
+ });
662
+ }
663
+ async softDelete(accountId, at) {
664
+ await this.db
665
+ .update(emailConnections)
666
+ .set({ deleted_at: at, updated_at: at })
667
+ .where(eq(emailConnections.account_id, accountId));
668
+ }
669
+ }
670
+ class DrizzleTokenUsageRepository {
671
+ db;
672
+ constructor(db) {
673
+ this.db = db;
674
+ }
675
+ async record(usage) {
676
+ await this.db.insert(tokenUsage).values({
677
+ id: usage.id,
678
+ workspace_id: usage.workspaceId,
679
+ execution_id: usage.executionId,
680
+ agent_kind: usage.agentKind,
681
+ provider: usage.provider,
682
+ model: usage.model,
683
+ input_tokens: usage.inputTokens,
684
+ output_tokens: usage.outputTokens,
685
+ cost_estimate: usage.costEstimate,
686
+ created_at: usage.createdAt,
687
+ });
688
+ }
689
+ async totalsSince(epochMs) {
690
+ // sum() of int columns is bigint in Postgres — cast to bigint (NOT int4, which
691
+ // overflows past ~2.1B tokens) and coerce: node-postgres returns bigint as a
692
+ // string to avoid precision loss, and token totals stay well within Number's
693
+ // safe-integer range. Matches the 64-bit sum the D1/SQLite store returns.
694
+ const [row] = await this.db
695
+ .select({
696
+ input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
697
+ output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
698
+ cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
699
+ })
700
+ .from(tokenUsage)
701
+ .where(gte(tokenUsage.created_at, epochMs));
702
+ return {
703
+ inputTokens: Number(row?.input ?? 0),
704
+ outputTokens: Number(row?.output ?? 0),
705
+ costEstimate: row?.cost ?? 0,
706
+ };
707
+ }
708
+ async deleteOlderThan(epochMs) {
709
+ const deleted = await this.db
710
+ .delete(tokenUsage)
711
+ .where(lt(tokenUsage.created_at, epochMs))
712
+ .returning({ id: tokenUsage.id });
713
+ return deleted.length;
714
+ }
715
+ }
716
+ function rowToLlmMetric(row) {
717
+ return {
718
+ id: row.id,
719
+ workspaceId: row.workspace_id,
720
+ executionId: row.execution_id,
721
+ agentKind: row.agent_kind,
722
+ provider: row.provider,
723
+ model: row.model,
724
+ createdAt: row.created_at,
725
+ streaming: row.streaming === 1,
726
+ messageCount: row.message_count,
727
+ toolCount: row.tool_count,
728
+ requestMaxTokens: row.request_max_tokens,
729
+ promptTokens: row.prompt_tokens,
730
+ cachedPromptTokens: row.cached_prompt_tokens,
731
+ completionTokens: row.completion_tokens,
732
+ totalTokens: row.total_tokens,
733
+ finishReason: row.finish_reason,
734
+ upstreamMs: row.upstream_ms,
735
+ overheadMs: row.overhead_ms,
736
+ totalMs: row.total_ms,
737
+ ok: row.ok === 1,
738
+ httpStatus: row.http_status,
739
+ errorMessage: row.error_message,
740
+ promptText: row.prompt_text,
741
+ promptPrefixCount: row.prompt_prefix_count,
742
+ promptHash: row.prompt_hash,
743
+ responseText: row.response_text,
744
+ reasoningText: row.reasoning_text,
745
+ };
746
+ }
747
+ class DrizzleLlmCallMetricRepository {
748
+ db;
749
+ constructor(db) {
750
+ this.db = db;
751
+ }
752
+ async record(metric) {
753
+ await this.db.insert(llmCallMetrics).values({
754
+ id: metric.id,
755
+ workspace_id: metric.workspaceId,
756
+ execution_id: metric.executionId,
757
+ agent_kind: metric.agentKind,
758
+ provider: metric.provider,
759
+ model: metric.model,
760
+ created_at: metric.createdAt,
761
+ streaming: metric.streaming ? 1 : 0,
762
+ message_count: metric.messageCount,
763
+ tool_count: metric.toolCount,
764
+ request_max_tokens: metric.requestMaxTokens,
765
+ prompt_tokens: metric.promptTokens,
766
+ cached_prompt_tokens: metric.cachedPromptTokens,
767
+ completion_tokens: metric.completionTokens,
768
+ total_tokens: metric.totalTokens,
769
+ finish_reason: metric.finishReason,
770
+ upstream_ms: metric.upstreamMs,
771
+ overhead_ms: metric.overheadMs,
772
+ total_ms: metric.totalMs,
773
+ ok: metric.ok ? 1 : 0,
774
+ http_status: metric.httpStatus,
775
+ error_message: metric.errorMessage,
776
+ prompt_text: metric.promptText,
777
+ prompt_prefix_count: metric.promptPrefixCount,
778
+ prompt_hash: metric.promptHash,
779
+ response_text: metric.responseText,
780
+ reasoning_text: metric.reasoningText,
781
+ });
782
+ }
783
+ async latestChainTip(workspaceId, executionId, agentKind) {
784
+ // The newest call for the conversation; one indexed row, no text columns.
785
+ const rows = await this.db
786
+ .select({
787
+ messageCount: llmCallMetrics.message_count,
788
+ promptHash: llmCallMetrics.prompt_hash,
789
+ })
790
+ .from(llmCallMetrics)
791
+ .where(and(eq(llmCallMetrics.workspace_id, workspaceId), eq(llmCallMetrics.execution_id, executionId), eq(llmCallMetrics.agent_kind, agentKind)))
792
+ // message_count breaks a same-millisecond createdAt tie in chain order (it grows
793
+ // monotonically as the conversation appends); id is the last resort.
794
+ .orderBy(desc(llmCallMetrics.created_at), desc(llmCallMetrics.message_count), desc(llmCallMetrics.id))
795
+ .limit(1);
796
+ const row = rows[0];
797
+ return row ? { messageCount: row.messageCount, promptHash: row.promptHash } : null;
798
+ }
799
+ async listByExecution(workspaceId, executionId, limit) {
800
+ const base = this.db
801
+ .select()
802
+ .from(llmCallMetrics)
803
+ .where(and(eq(llmCallMetrics.workspace_id, workspaceId), eq(llmCallMetrics.execution_id, executionId)))
804
+ .orderBy(desc(llmCallMetrics.created_at), desc(llmCallMetrics.id));
805
+ const rows = await (limit == null ? base : base.limit(limit));
806
+ return rows.map(rowToLlmMetric);
807
+ }
808
+ async summarizeByExecution(workspaceId, executionId) {
809
+ // Aggregate-only: selects no prompt/response text, so it stays cheap on every
810
+ // execution emit (it backs the live board rollups). int sums fit Number's safe
811
+ // range here (per-run call counts/tokens are small), so a plain ::bigint cast
812
+ // matching the SQLite 64-bit sum is unnecessary — totals are coerced below.
813
+ const reasons = [...LLM_WARNING_FINISH_REASONS];
814
+ const rows = await this.db
815
+ .select({
816
+ agentKind: llmCallMetrics.agent_kind,
817
+ calls: sql `count(*)::int`,
818
+ promptTokens: sql `coalesce(sum(${llmCallMetrics.prompt_tokens}), 0)::int`,
819
+ completionTokens: sql `coalesce(sum(${llmCallMetrics.completion_tokens}), 0)::int`,
820
+ peakCompletionTokens: sql `coalesce(max(${llmCallMetrics.completion_tokens}), 0)::int`,
821
+ maxOutputTokens: sql `max(${llmCallMetrics.request_max_tokens})`,
822
+ truncatedCalls: sql `coalesce(sum(case when ${llmCallMetrics.finish_reason} = 'length' then 1 else 0 end), 0)::int`,
823
+ upstreamMs: sql `coalesce(sum(${llmCallMetrics.upstream_ms}), 0)::int`,
824
+ overheadMs: sql `coalesce(sum(${llmCallMetrics.overhead_ms}), 0)::int`,
825
+ errors: sql `coalesce(sum(case when ${llmCallMetrics.ok} = 0 then 1 else 0 end), 0)::int`,
826
+ // `inArray` builds the IN-list membership: idiomatic, type-checked, and tied to
827
+ // the shared constant. (A raw `${...finish_reason} in ${reasons}` renders the same
828
+ // `in ($1, $2)` on this drizzle version; inArray just documents intent and can't
829
+ // silently mis-bind the array.)
830
+ warnings: sql `coalesce(sum(case when ${llmCallMetrics.ok} = 1 and ${inArray(llmCallMetrics.finish_reason, reasons)} then 1 else 0 end), 0)::int`,
831
+ })
832
+ .from(llmCallMetrics)
833
+ .where(and(eq(llmCallMetrics.workspace_id, workspaceId), eq(llmCallMetrics.execution_id, executionId)))
834
+ .groupBy(llmCallMetrics.agent_kind);
835
+ return rows.map((r) => ({
836
+ agentKind: r.agentKind,
837
+ calls: Number(r.calls),
838
+ promptTokens: Number(r.promptTokens),
839
+ completionTokens: Number(r.completionTokens),
840
+ peakCompletionTokens: Number(r.peakCompletionTokens),
841
+ maxOutputTokens: r.maxOutputTokens == null ? null : Number(r.maxOutputTokens),
842
+ truncatedCalls: Number(r.truncatedCalls),
843
+ upstreamMs: Number(r.upstreamMs),
844
+ overheadMs: Number(r.overheadMs),
845
+ errors: Number(r.errors),
846
+ warnings: Number(r.warnings),
847
+ }));
848
+ }
849
+ async deleteOlderThan(epochMs) {
850
+ const deleted = await this.db
851
+ .delete(llmCallMetrics)
852
+ .where(lt(llmCallMetrics.created_at, epochMs))
853
+ .returning({ id: llmCallMetrics.id });
854
+ return deleted.length;
855
+ }
856
+ }
857
+ /**
858
+ * A workspace's per-agent-kind default models, one row per (workspace, agent kind)
859
+ * in `workspace_model_defaults`. `replace` rewrites the whole map for a workspace
860
+ * in a transaction (delete-all then insert-each), so a kind omitted is cleared.
861
+ */
862
+ class DrizzleModelDefaultsRepository {
863
+ db;
864
+ constructor(db) {
865
+ this.db = db;
866
+ }
867
+ async get(workspaceId) {
868
+ const rows = await this.db
869
+ .select({
870
+ agentKind: workspaceModelDefaults.agent_kind,
871
+ modelId: workspaceModelDefaults.model_id,
872
+ })
873
+ .from(workspaceModelDefaults)
874
+ .where(eq(workspaceModelDefaults.workspace_id, workspaceId));
875
+ const map = {};
876
+ for (const row of rows)
877
+ map[row.agentKind] = row.modelId;
878
+ return map;
879
+ }
880
+ async getForKind(workspaceId, agentKind) {
881
+ const [row] = await this.db
882
+ .select({ modelId: workspaceModelDefaults.model_id })
883
+ .from(workspaceModelDefaults)
884
+ .where(and(eq(workspaceModelDefaults.workspace_id, workspaceId), eq(workspaceModelDefaults.agent_kind, agentKind)));
885
+ return row ? row.modelId : null;
886
+ }
887
+ async replace(workspaceId, defaults) {
888
+ const updatedAt = Date.now();
889
+ const values = Object.entries(defaults).map(([agentKind, modelId]) => ({
890
+ workspace_id: workspaceId,
891
+ agent_kind: agentKind,
892
+ model_id: modelId,
893
+ updated_at: updatedAt,
894
+ }));
895
+ // Rewrite the whole per-kind map atomically: clear the workspace's rows, then
896
+ // insert one per entry, so a reader never sees a partial map.
897
+ await this.db.transaction(async (tx) => {
898
+ await tx
899
+ .delete(workspaceModelDefaults)
900
+ .where(eq(workspaceModelDefaults.workspace_id, workspaceId));
901
+ if (values.length > 0)
902
+ await tx.insert(workspaceModelDefaults).values(values);
903
+ });
904
+ }
905
+ }
906
+ /**
907
+ * A workspace's default service-fragment selection — one row per workspace in
908
+ * `workspace_fragment_defaults`, the fragment ids stored as a JSON array (mirror of the
909
+ * D1 `D1ServiceFragmentDefaultsRepository`). `set` upserts the whole list.
910
+ */
911
+ class DrizzleServiceFragmentDefaultsRepository {
912
+ db;
913
+ constructor(db) {
914
+ this.db = db;
915
+ }
916
+ async get(workspaceId) {
917
+ const [row] = await this.db
918
+ .select({ fragmentIds: workspaceFragmentDefaults.fragment_ids })
919
+ .from(workspaceFragmentDefaults)
920
+ .where(eq(workspaceFragmentDefaults.workspace_id, workspaceId));
921
+ return row ? JSON.parse(row.fragmentIds) : [];
922
+ }
923
+ async set(workspaceId, fragmentIds) {
924
+ await this.db
925
+ .insert(workspaceFragmentDefaults)
926
+ .values({
927
+ workspace_id: workspaceId,
928
+ fragment_ids: JSON.stringify(fragmentIds),
929
+ updated_at: Date.now(),
930
+ })
931
+ .onConflictDoUpdate({
932
+ target: workspaceFragmentDefaults.workspace_id,
933
+ set: { fragment_ids: JSON.stringify(fragmentIds), updated_at: Date.now() },
934
+ });
935
+ }
936
+ }
937
+ function rowToSchedule(row) {
938
+ const recurrence = {
939
+ intervalHours: row.interval_hours,
940
+ weekdays: safeWeekdays(row.weekdays),
941
+ windowStartHour: row.window_start_hour,
942
+ windowEndHour: row.window_end_hour,
943
+ timezone: row.timezone,
944
+ };
945
+ return {
946
+ id: row.id,
947
+ serviceId: row.service_id,
948
+ blockId: row.block_id,
949
+ frameId: row.frame_id,
950
+ pipelineId: row.pipeline_id,
951
+ template: row.template,
952
+ name: row.name,
953
+ recurrence,
954
+ enabled: row.enabled === 1,
955
+ lastRunAt: row.last_run_at,
956
+ nextRunAt: row.next_run_at,
957
+ createdAt: row.created_at,
958
+ };
959
+ }
960
+ function safeWeekdays(value) {
961
+ try {
962
+ const parsed = JSON.parse(value);
963
+ return Array.isArray(parsed) ? parsed.filter((n) => typeof n === 'number') : [];
964
+ }
965
+ catch {
966
+ return [];
967
+ }
968
+ }
969
+ function rowToRun(row) {
970
+ return {
971
+ id: row.id,
972
+ scheduleId: row.schedule_id,
973
+ executionId: row.execution_id,
974
+ status: row.status,
975
+ startedAt: row.started_at,
976
+ finishedAt: row.finished_at,
977
+ outcome: row.outcome,
978
+ };
979
+ }
980
+ class DrizzlePipelineScheduleRepository {
981
+ db;
982
+ constructor(db) {
983
+ this.db = db;
984
+ }
985
+ values(workspaceId, schedule) {
986
+ const r = schedule.recurrence;
987
+ return {
988
+ workspace_id: workspaceId,
989
+ id: schedule.id,
990
+ service_id: schedule.serviceId,
991
+ block_id: schedule.blockId,
992
+ frame_id: schedule.frameId,
993
+ pipeline_id: schedule.pipelineId,
994
+ template: schedule.template,
995
+ name: schedule.name,
996
+ interval_hours: r.intervalHours,
997
+ weekdays: JSON.stringify(r.weekdays),
998
+ window_start_hour: r.windowStartHour,
999
+ window_end_hour: r.windowEndHour,
1000
+ timezone: r.timezone,
1001
+ enabled: schedule.enabled ? 1 : 0,
1002
+ last_run_at: schedule.lastRunAt,
1003
+ next_run_at: schedule.nextRunAt,
1004
+ created_at: schedule.createdAt,
1005
+ };
1006
+ }
1007
+ async get(workspaceId, id) {
1008
+ const [row] = await this.db
1009
+ .select()
1010
+ .from(pipelineSchedules)
1011
+ .where(and(eq(pipelineSchedules.workspace_id, workspaceId), eq(pipelineSchedules.id, id)));
1012
+ return row ? rowToSchedule(row) : null;
1013
+ }
1014
+ async getByBlock(workspaceId, blockId) {
1015
+ const [row] = await this.db
1016
+ .select()
1017
+ .from(pipelineSchedules)
1018
+ .where(and(eq(pipelineSchedules.workspace_id, workspaceId), eq(pipelineSchedules.block_id, blockId)));
1019
+ return row ? rowToSchedule(row) : null;
1020
+ }
1021
+ async list(workspaceId) {
1022
+ const rows = await this.db
1023
+ .select()
1024
+ .from(pipelineSchedules)
1025
+ .where(eq(pipelineSchedules.workspace_id, workspaceId))
1026
+ .orderBy(pipelineSchedules.created_at);
1027
+ return rows.map(rowToSchedule);
1028
+ }
1029
+ async listByService(serviceId) {
1030
+ const rows = await this.db
1031
+ .select()
1032
+ .from(pipelineSchedules)
1033
+ .where(eq(pipelineSchedules.service_id, serviceId))
1034
+ .orderBy(pipelineSchedules.created_at);
1035
+ return rows.map(rowToSchedule);
1036
+ }
1037
+ async listByServices(serviceIds) {
1038
+ if (serviceIds.length === 0)
1039
+ return [];
1040
+ const out = [];
1041
+ // Chunk the IN list to stay well under the bind-parameter limit.
1042
+ for (let i = 0; i < serviceIds.length; i += 500) {
1043
+ const rows = await this.db
1044
+ .select()
1045
+ .from(pipelineSchedules)
1046
+ .where(inArray(pipelineSchedules.service_id, serviceIds.slice(i, i + 500)))
1047
+ .orderBy(pipelineSchedules.created_at);
1048
+ for (const row of rows)
1049
+ out.push(rowToSchedule(row));
1050
+ }
1051
+ return out;
1052
+ }
1053
+ async listDue(asOf) {
1054
+ const rows = await this.db
1055
+ .select()
1056
+ .from(pipelineSchedules)
1057
+ .where(and(eq(pipelineSchedules.enabled, 1), lt(pipelineSchedules.next_run_at, asOf + 1)))
1058
+ .orderBy(pipelineSchedules.next_run_at);
1059
+ return rows.map((row) => ({ workspaceId: row.workspace_id, schedule: rowToSchedule(row) }));
1060
+ }
1061
+ async upsert(workspaceId, schedule) {
1062
+ const values = this.values(workspaceId, schedule);
1063
+ await this.db
1064
+ .insert(pipelineSchedules)
1065
+ .values(values)
1066
+ .onConflictDoUpdate({
1067
+ target: [pipelineSchedules.workspace_id, pipelineSchedules.id],
1068
+ set: {
1069
+ service_id: values.service_id,
1070
+ block_id: values.block_id,
1071
+ frame_id: values.frame_id,
1072
+ pipeline_id: values.pipeline_id,
1073
+ template: values.template,
1074
+ name: values.name,
1075
+ interval_hours: values.interval_hours,
1076
+ weekdays: values.weekdays,
1077
+ window_start_hour: values.window_start_hour,
1078
+ window_end_hour: values.window_end_hour,
1079
+ timezone: values.timezone,
1080
+ enabled: values.enabled,
1081
+ last_run_at: values.last_run_at,
1082
+ next_run_at: values.next_run_at,
1083
+ },
1084
+ });
1085
+ }
1086
+ async remove(workspaceId, id) {
1087
+ await this.db
1088
+ .delete(pipelineSchedules)
1089
+ .where(and(eq(pipelineSchedules.workspace_id, workspaceId), eq(pipelineSchedules.id, id)));
1090
+ }
1091
+ async insertRun(workspaceId, run) {
1092
+ await this.db.insert(pipelineScheduleRuns).values({
1093
+ workspace_id: workspaceId,
1094
+ id: run.id,
1095
+ schedule_id: run.scheduleId,
1096
+ execution_id: run.executionId,
1097
+ status: run.status,
1098
+ started_at: run.startedAt,
1099
+ finished_at: run.finishedAt,
1100
+ outcome: run.outcome,
1101
+ });
1102
+ }
1103
+ async updateRun(workspaceId, runId, patch) {
1104
+ const set = {};
1105
+ if (patch.status !== undefined)
1106
+ set.status = patch.status;
1107
+ if (patch.finishedAt !== undefined)
1108
+ set.finished_at = patch.finishedAt;
1109
+ if (patch.outcome !== undefined)
1110
+ set.outcome = patch.outcome;
1111
+ if (patch.executionId !== undefined)
1112
+ set.execution_id = patch.executionId;
1113
+ if (Object.keys(set).length === 0)
1114
+ return;
1115
+ await this.db
1116
+ .update(pipelineScheduleRuns)
1117
+ .set(set)
1118
+ .where(and(eq(pipelineScheduleRuns.workspace_id, workspaceId), eq(pipelineScheduleRuns.id, runId)));
1119
+ }
1120
+ async listRuns(workspaceId, scheduleId) {
1121
+ const rows = await this.db
1122
+ .select()
1123
+ .from(pipelineScheduleRuns)
1124
+ .where(and(eq(pipelineScheduleRuns.workspace_id, workspaceId), eq(pipelineScheduleRuns.schedule_id, scheduleId)))
1125
+ .orderBy(desc(pipelineScheduleRuns.started_at));
1126
+ return rows.map(rowToRun);
1127
+ }
1128
+ async pruneRunsBefore(before) {
1129
+ const rows = await this.db
1130
+ .delete(pipelineScheduleRuns)
1131
+ .where(lt(pipelineScheduleRuns.started_at, before))
1132
+ .returning({ id: pipelineScheduleRuns.id });
1133
+ return rows.length;
1134
+ }
1135
+ }
1136
+ class DrizzleTrackerSettingsRepository {
1137
+ db;
1138
+ constructor(db) {
1139
+ this.db = db;
1140
+ }
1141
+ async get(workspaceId) {
1142
+ const [row] = await this.db
1143
+ .select()
1144
+ .from(trackerSettings)
1145
+ .where(eq(trackerSettings.workspace_id, workspaceId));
1146
+ if (!row)
1147
+ return null;
1148
+ return {
1149
+ tracker: row.tracker ?? null,
1150
+ jiraProjectKey: row.jira_project_key,
1151
+ updatedAt: row.updated_at,
1152
+ };
1153
+ }
1154
+ async put(workspaceId, settings) {
1155
+ await this.db
1156
+ .insert(trackerSettings)
1157
+ .values({
1158
+ workspace_id: workspaceId,
1159
+ tracker: settings.tracker,
1160
+ jira_project_key: settings.jiraProjectKey,
1161
+ updated_at: settings.updatedAt,
1162
+ })
1163
+ .onConflictDoUpdate({
1164
+ target: trackerSettings.workspace_id,
1165
+ set: {
1166
+ tracker: settings.tracker,
1167
+ jira_project_key: settings.jiraProjectKey,
1168
+ updated_at: settings.updatedAt,
1169
+ },
1170
+ });
1171
+ }
1172
+ }
1173
+ function rowToService(row) {
1174
+ return {
1175
+ id: row.id,
1176
+ accountId: row.account_id,
1177
+ frameBlockId: row.frame_block_id,
1178
+ installationId: row.installation_id,
1179
+ repoGithubId: row.repo_github_id,
1180
+ directory: row.directory,
1181
+ createdAt: row.created_at,
1182
+ };
1183
+ }
1184
+ /** Account-owned services (migration 0030). The canonical, shareable board unit. */
1185
+ class DrizzleServiceRepository {
1186
+ db;
1187
+ constructor(db) {
1188
+ this.db = db;
1189
+ }
1190
+ async get(id) {
1191
+ const [row] = await this.db.select().from(services).where(eq(services.id, id));
1192
+ return row ? rowToService(row) : null;
1193
+ }
1194
+ async getByFrameBlock(frameBlockId) {
1195
+ const [row] = await this.db
1196
+ .select()
1197
+ .from(services)
1198
+ .where(eq(services.frame_block_id, frameBlockId));
1199
+ return row ? rowToService(row) : null;
1200
+ }
1201
+ async listByAccount(accountId) {
1202
+ // NULL-safe match so the legacy/unscoped org (accountId null) lists cleanly.
1203
+ const rows = await this.db
1204
+ .select()
1205
+ .from(services)
1206
+ .where(sql `${services.account_id} IS NOT DISTINCT FROM ${accountId}`)
1207
+ .orderBy(services.created_at);
1208
+ return rows.map(rowToService);
1209
+ }
1210
+ async listByIds(ids) {
1211
+ if (ids.length === 0)
1212
+ return [];
1213
+ const out = [];
1214
+ // Chunk the IN list to stay well under the bind-parameter limit.
1215
+ for (let i = 0; i < ids.length; i += 500) {
1216
+ const rows = await this.db
1217
+ .select()
1218
+ .from(services)
1219
+ .where(inArray(services.id, ids.slice(i, i + 500)));
1220
+ for (const row of rows)
1221
+ out.push(rowToService(row));
1222
+ }
1223
+ return out;
1224
+ }
1225
+ async getByRepo(installationId, repoGithubId) {
1226
+ const [row] = await this.db
1227
+ .select()
1228
+ .from(services)
1229
+ .where(and(eq(services.installation_id, installationId), eq(services.repo_github_id, repoGithubId)));
1230
+ return row ? rowToService(row) : null;
1231
+ }
1232
+ async insert(service) {
1233
+ await this.db.insert(services).values({
1234
+ id: service.id,
1235
+ account_id: service.accountId,
1236
+ frame_block_id: service.frameBlockId,
1237
+ installation_id: service.installationId,
1238
+ repo_github_id: service.repoGithubId,
1239
+ directory: service.directory ?? null,
1240
+ created_at: service.createdAt,
1241
+ });
1242
+ }
1243
+ async update(id, patch) {
1244
+ const set = {};
1245
+ if ('accountId' in patch)
1246
+ set.account_id = patch.accountId ?? null;
1247
+ if ('installationId' in patch)
1248
+ set.installation_id = patch.installationId ?? null;
1249
+ if ('repoGithubId' in patch)
1250
+ set.repo_github_id = patch.repoGithubId ?? null;
1251
+ if ('directory' in patch)
1252
+ set.directory = patch.directory ?? null;
1253
+ if (Object.keys(set).length === 0)
1254
+ return;
1255
+ await this.db.update(services).set(set).where(eq(services.id, id));
1256
+ }
1257
+ async delete(id) {
1258
+ await this.db.delete(services).where(eq(services.id, id));
1259
+ }
1260
+ async deleteMany(ids) {
1261
+ if (ids.length === 0)
1262
+ return;
1263
+ // Chunk the IN list to stay well under the bind-parameter limit.
1264
+ for (let i = 0; i < ids.length; i += 500) {
1265
+ await this.db.delete(services).where(inArray(services.id, ids.slice(i, i + 500)));
1266
+ }
1267
+ }
1268
+ }
1269
+ function rowToMount(row) {
1270
+ return {
1271
+ workspaceId: row.workspace_id,
1272
+ serviceId: row.service_id,
1273
+ position: { x: row.pos_x, y: row.pos_y },
1274
+ size: row.width !== null && row.height !== null ? { w: row.width, h: row.height } : null,
1275
+ createdAt: row.created_at,
1276
+ };
1277
+ }
1278
+ /** A service mounted onto a workspace board + its per-workspace layout (migration 0030). */
1279
+ class DrizzleWorkspaceMountRepository {
1280
+ db;
1281
+ constructor(db) {
1282
+ this.db = db;
1283
+ }
1284
+ async listByWorkspace(workspaceId) {
1285
+ const rows = await this.db
1286
+ .select()
1287
+ .from(workspaceServices)
1288
+ .where(eq(workspaceServices.workspace_id, workspaceId))
1289
+ .orderBy(workspaceServices.created_at);
1290
+ return rows.map(rowToMount);
1291
+ }
1292
+ async listByService(serviceId) {
1293
+ const rows = await this.db
1294
+ .select()
1295
+ .from(workspaceServices)
1296
+ .where(eq(workspaceServices.service_id, serviceId))
1297
+ .orderBy(workspaceServices.created_at);
1298
+ return rows.map(rowToMount);
1299
+ }
1300
+ async listWorkspaceIdsMountingBlock(originWorkspaceId, blockId) {
1301
+ // One join: the service owning the block → the workspaces that mount it. A block with no
1302
+ // service makes the subquery NULL, which matches no rows (`service_id = NULL`) → empty.
1303
+ const rows = await this.db
1304
+ .select({ workspaceId: workspaceServices.workspace_id })
1305
+ .from(workspaceServices)
1306
+ .where(sql `${workspaceServices.service_id} = (SELECT ${blocks.service_id} FROM ${blocks} WHERE ${blocks.workspace_id} = ${originWorkspaceId} AND ${blocks.id} = ${blockId})`);
1307
+ return rows.map((r) => r.workspaceId);
1308
+ }
1309
+ async countByServiceIds(serviceIds) {
1310
+ if (serviceIds.length === 0)
1311
+ return {};
1312
+ const rows = await this.db
1313
+ .select({ serviceId: workspaceServices.service_id, n: sql `count(*)` })
1314
+ .from(workspaceServices)
1315
+ .where(inArray(workspaceServices.service_id, serviceIds))
1316
+ .groupBy(workspaceServices.service_id);
1317
+ const counts = {};
1318
+ for (const row of rows)
1319
+ counts[row.serviceId] = Number(row.n);
1320
+ return counts;
1321
+ }
1322
+ async get(workspaceId, serviceId) {
1323
+ const [row] = await this.db
1324
+ .select()
1325
+ .from(workspaceServices)
1326
+ .where(and(eq(workspaceServices.workspace_id, workspaceId), eq(workspaceServices.service_id, serviceId)));
1327
+ return row ? rowToMount(row) : null;
1328
+ }
1329
+ async upsert(mount) {
1330
+ await this.db
1331
+ .insert(workspaceServices)
1332
+ .values({
1333
+ workspace_id: mount.workspaceId,
1334
+ service_id: mount.serviceId,
1335
+ pos_x: mount.position.x,
1336
+ pos_y: mount.position.y,
1337
+ width: mount.size?.w ?? null,
1338
+ height: mount.size?.h ?? null,
1339
+ created_at: mount.createdAt,
1340
+ })
1341
+ .onConflictDoUpdate({
1342
+ target: [workspaceServices.workspace_id, workspaceServices.service_id],
1343
+ set: {
1344
+ pos_x: mount.position.x,
1345
+ pos_y: mount.position.y,
1346
+ width: mount.size?.w ?? null,
1347
+ height: mount.size?.h ?? null,
1348
+ },
1349
+ });
1350
+ }
1351
+ async update(workspaceId, serviceId, patch) {
1352
+ const set = {};
1353
+ if (patch.position) {
1354
+ set.pos_x = patch.position.x;
1355
+ set.pos_y = patch.position.y;
1356
+ }
1357
+ if ('size' in patch) {
1358
+ set.width = patch.size?.w ?? null;
1359
+ set.height = patch.size?.h ?? null;
1360
+ }
1361
+ if (Object.keys(set).length === 0)
1362
+ return;
1363
+ await this.db
1364
+ .update(workspaceServices)
1365
+ .set(set)
1366
+ .where(and(eq(workspaceServices.workspace_id, workspaceId), eq(workspaceServices.service_id, serviceId)));
1367
+ }
1368
+ async remove(workspaceId, serviceId) {
1369
+ await this.db
1370
+ .delete(workspaceServices)
1371
+ .where(and(eq(workspaceServices.workspace_id, workspaceId), eq(workspaceServices.service_id, serviceId)));
1372
+ }
1373
+ async removeByServices(serviceIds) {
1374
+ if (serviceIds.length === 0)
1375
+ return;
1376
+ // Chunk the IN list to stay well under the bind-parameter limit.
1377
+ for (let i = 0; i < serviceIds.length; i += 500) {
1378
+ await this.db
1379
+ .delete(workspaceServices)
1380
+ .where(inArray(workspaceServices.service_id, serviceIds.slice(i, i + 500)));
1381
+ }
1382
+ }
1383
+ }
1384
+ function rowToRequirementReview(row) {
1385
+ let items = [];
1386
+ try {
1387
+ const parsed = JSON.parse(row.items);
1388
+ if (Array.isArray(parsed))
1389
+ items = parsed;
1390
+ }
1391
+ catch {
1392
+ items = [];
1393
+ }
1394
+ return {
1395
+ id: row.id,
1396
+ blockId: row.block_id,
1397
+ status: row.status,
1398
+ items,
1399
+ model: row.model,
1400
+ incorporatedRequirements: row.incorporated_requirements,
1401
+ iteration: row.iteration,
1402
+ maxIterations: row.max_iterations,
1403
+ createdAt: row.created_at,
1404
+ updatedAt: row.updated_at,
1405
+ };
1406
+ }
1407
+ /**
1408
+ * Requirements reviews over Postgres (the Drizzle mirror of the Worker's
1409
+ * `D1RequirementReviewRepository`, migration 0021). The reviewed items live as a JSON
1410
+ * array in `items`; the service keeps at most one live review per block (it deletes
1411
+ * the block's prior review before inserting a fresh one), so `getByBlock` returns the
1412
+ * latest. Behaviourally identical to the D1 repo so the cross-runtime conformance
1413
+ * suite asserts the same requirements-rework substitution against both stores.
1414
+ */
1415
+ export class DrizzleRequirementReviewRepository {
1416
+ db;
1417
+ constructor(db) {
1418
+ this.db = db;
1419
+ }
1420
+ async getByBlock(workspaceId, blockId) {
1421
+ const rows = await this.db
1422
+ .select()
1423
+ .from(requirementReviews)
1424
+ .where(and(eq(requirementReviews.workspace_id, workspaceId), eq(requirementReviews.block_id, blockId)))
1425
+ .orderBy(desc(requirementReviews.created_at))
1426
+ .limit(1);
1427
+ return rows[0] ? rowToRequirementReview(rows[0]) : null;
1428
+ }
1429
+ async get(workspaceId, id) {
1430
+ const rows = await this.db
1431
+ .select()
1432
+ .from(requirementReviews)
1433
+ .where(and(eq(requirementReviews.workspace_id, workspaceId), eq(requirementReviews.id, id)))
1434
+ .limit(1);
1435
+ return rows[0] ? rowToRequirementReview(rows[0]) : null;
1436
+ }
1437
+ async upsert(workspaceId, review) {
1438
+ const values = {
1439
+ workspace_id: workspaceId,
1440
+ id: review.id,
1441
+ block_id: review.blockId,
1442
+ status: review.status,
1443
+ items: JSON.stringify(review.items),
1444
+ model: review.model,
1445
+ incorporated_requirements: review.incorporatedRequirements,
1446
+ iteration: review.iteration ?? 1,
1447
+ max_iterations: review.maxIterations ?? 1,
1448
+ created_at: review.createdAt,
1449
+ updated_at: review.updatedAt,
1450
+ };
1451
+ await this.db
1452
+ .insert(requirementReviews)
1453
+ .values(values)
1454
+ .onConflictDoUpdate({
1455
+ target: [requirementReviews.workspace_id, requirementReviews.id],
1456
+ set: {
1457
+ block_id: values.block_id,
1458
+ status: values.status,
1459
+ items: values.items,
1460
+ model: values.model,
1461
+ incorporated_requirements: values.incorporated_requirements,
1462
+ iteration: values.iteration,
1463
+ max_iterations: values.max_iterations,
1464
+ updated_at: values.updated_at,
1465
+ },
1466
+ });
1467
+ }
1468
+ async deleteByBlock(workspaceId, blockId) {
1469
+ await this.db
1470
+ .delete(requirementReviews)
1471
+ .where(and(eq(requirementReviews.workspace_id, workspaceId), eq(requirementReviews.block_id, blockId)));
1472
+ }
1473
+ }
1474
+ function parseJsonArray(raw) {
1475
+ try {
1476
+ const parsed = JSON.parse(raw);
1477
+ return Array.isArray(parsed) ? parsed : [];
1478
+ }
1479
+ catch {
1480
+ return [];
1481
+ }
1482
+ }
1483
+ function rowToConsensusSession(row) {
1484
+ return {
1485
+ id: row.id,
1486
+ blockId: row.block_id,
1487
+ executionId: row.execution_id,
1488
+ stepIndex: row.step_index,
1489
+ agentKind: row.agent_kind,
1490
+ strategy: row.strategy,
1491
+ status: row.status,
1492
+ participants: parseJsonArray(row.participants),
1493
+ rounds: parseJsonArray(row.rounds),
1494
+ synthesis: row.synthesis,
1495
+ confidence: row.confidence,
1496
+ dissent: parseJsonArray(row.dissent),
1497
+ error: row.error,
1498
+ createdAt: row.created_at,
1499
+ updatedAt: row.updated_at,
1500
+ };
1501
+ }
1502
+ function rowToClarityReview(row) {
1503
+ let items = [];
1504
+ try {
1505
+ const parsed = JSON.parse(row.items);
1506
+ if (Array.isArray(parsed))
1507
+ items = parsed;
1508
+ }
1509
+ catch {
1510
+ items = [];
1511
+ }
1512
+ return {
1513
+ id: row.id,
1514
+ blockId: row.block_id,
1515
+ status: row.status,
1516
+ items,
1517
+ model: row.model,
1518
+ clarifiedReport: row.clarified_report,
1519
+ iteration: row.iteration,
1520
+ maxIterations: row.max_iterations,
1521
+ createdAt: row.created_at,
1522
+ updatedAt: row.updated_at,
1523
+ };
1524
+ }
1525
+ /**
1526
+ * Consensus session transcripts (`consensus_sessions`), the Drizzle/Postgres mirror of
1527
+ * {@link D1ConsensusSessionRepository}. One row per (execution, step); the
1528
+ * participants/rounds/dissent live as JSON columns, upserted as the process streams.
1529
+ */
1530
+ export class DrizzleConsensusSessionRepository {
1531
+ db;
1532
+ constructor(db) {
1533
+ this.db = db;
1534
+ }
1535
+ async get(workspaceId, id) {
1536
+ const rows = await this.db
1537
+ .select()
1538
+ .from(consensusSessions)
1539
+ .where(and(eq(consensusSessions.workspace_id, workspaceId), eq(consensusSessions.id, id)))
1540
+ .limit(1);
1541
+ return rows[0] ? rowToConsensusSession(rows[0]) : null;
1542
+ }
1543
+ async getByStep(workspaceId, executionId, stepIndex) {
1544
+ const rows = await this.db
1545
+ .select()
1546
+ .from(consensusSessions)
1547
+ .where(and(eq(consensusSessions.workspace_id, workspaceId), eq(consensusSessions.execution_id, executionId), eq(consensusSessions.step_index, stepIndex)))
1548
+ .orderBy(desc(consensusSessions.created_at))
1549
+ .limit(1);
1550
+ return rows[0] ? rowToConsensusSession(rows[0]) : null;
1551
+ }
1552
+ async getByBlock(workspaceId, blockId) {
1553
+ const rows = await this.db
1554
+ .select()
1555
+ .from(consensusSessions)
1556
+ .where(and(eq(consensusSessions.workspace_id, workspaceId), eq(consensusSessions.block_id, blockId)))
1557
+ .orderBy(desc(consensusSessions.created_at))
1558
+ .limit(1);
1559
+ return rows[0] ? rowToConsensusSession(rows[0]) : null;
1560
+ }
1561
+ async upsert(workspaceId, session) {
1562
+ const values = {
1563
+ workspace_id: workspaceId,
1564
+ id: session.id,
1565
+ block_id: session.blockId,
1566
+ execution_id: session.executionId,
1567
+ step_index: session.stepIndex,
1568
+ agent_kind: session.agentKind,
1569
+ strategy: session.strategy,
1570
+ status: session.status,
1571
+ participants: JSON.stringify(session.participants),
1572
+ rounds: JSON.stringify(session.rounds),
1573
+ synthesis: session.synthesis,
1574
+ confidence: session.confidence ?? null,
1575
+ dissent: JSON.stringify(session.dissent ?? []),
1576
+ error: session.error ?? null,
1577
+ created_at: session.createdAt,
1578
+ updated_at: session.updatedAt,
1579
+ };
1580
+ await this.db
1581
+ .insert(consensusSessions)
1582
+ .values(values)
1583
+ .onConflictDoUpdate({
1584
+ target: [consensusSessions.workspace_id, consensusSessions.id],
1585
+ set: {
1586
+ block_id: values.block_id,
1587
+ execution_id: values.execution_id,
1588
+ step_index: values.step_index,
1589
+ agent_kind: values.agent_kind,
1590
+ strategy: values.strategy,
1591
+ status: values.status,
1592
+ participants: values.participants,
1593
+ rounds: values.rounds,
1594
+ synthesis: values.synthesis,
1595
+ confidence: values.confidence,
1596
+ dissent: values.dissent,
1597
+ error: values.error,
1598
+ updated_at: values.updated_at,
1599
+ },
1600
+ });
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Clarity (bug-report triage) reviews over Postgres — the Drizzle mirror of the Worker's
1605
+ * `D1ClarityReviewRepository`. Behaviourally identical to the D1 repo so the cross-runtime
1606
+ * conformance suite asserts the same clarified-brief substitution against both stores.
1607
+ */
1608
+ export class DrizzleClarityReviewRepository {
1609
+ db;
1610
+ constructor(db) {
1611
+ this.db = db;
1612
+ }
1613
+ async getByBlock(workspaceId, blockId) {
1614
+ const rows = await this.db
1615
+ .select()
1616
+ .from(clarityReviews)
1617
+ .where(and(eq(clarityReviews.workspace_id, workspaceId), eq(clarityReviews.block_id, blockId)))
1618
+ .orderBy(desc(clarityReviews.created_at))
1619
+ .limit(1);
1620
+ return rows[0] ? rowToClarityReview(rows[0]) : null;
1621
+ }
1622
+ async get(workspaceId, id) {
1623
+ const rows = await this.db
1624
+ .select()
1625
+ .from(clarityReviews)
1626
+ .where(and(eq(clarityReviews.workspace_id, workspaceId), eq(clarityReviews.id, id)))
1627
+ .limit(1);
1628
+ return rows[0] ? rowToClarityReview(rows[0]) : null;
1629
+ }
1630
+ async upsert(workspaceId, review) {
1631
+ const values = {
1632
+ workspace_id: workspaceId,
1633
+ id: review.id,
1634
+ block_id: review.blockId,
1635
+ status: review.status,
1636
+ items: JSON.stringify(review.items),
1637
+ model: review.model,
1638
+ clarified_report: review.clarifiedReport,
1639
+ iteration: review.iteration ?? 1,
1640
+ max_iterations: review.maxIterations ?? 1,
1641
+ created_at: review.createdAt,
1642
+ updated_at: review.updatedAt,
1643
+ };
1644
+ await this.db
1645
+ .insert(clarityReviews)
1646
+ .values(values)
1647
+ .onConflictDoUpdate({
1648
+ target: [clarityReviews.workspace_id, clarityReviews.id],
1649
+ set: {
1650
+ block_id: values.block_id,
1651
+ status: values.status,
1652
+ items: values.items,
1653
+ model: values.model,
1654
+ clarified_report: values.clarified_report,
1655
+ iteration: values.iteration,
1656
+ max_iterations: values.max_iterations,
1657
+ updated_at: values.updated_at,
1658
+ },
1659
+ });
1660
+ }
1661
+ async deleteByBlock(workspaceId, blockId) {
1662
+ await this.db
1663
+ .delete(clarityReviews)
1664
+ .where(and(eq(clarityReviews.workspace_id, workspaceId), eq(clarityReviews.block_id, blockId)));
1665
+ }
1666
+ }
1667
+ function rowToMergePreset(row) {
1668
+ return {
1669
+ id: row.id,
1670
+ name: row.name,
1671
+ maxComplexity: row.max_complexity,
1672
+ maxRisk: row.max_risk,
1673
+ maxImpact: row.max_impact,
1674
+ ciMaxAttempts: row.ci_max_attempts,
1675
+ maxRequirementIterations: row.max_requirement_iterations,
1676
+ maxRequirementConcernAllowed: row.max_requirement_concern_allowed,
1677
+ isDefault: row.is_default === 1,
1678
+ createdAt: row.created_at,
1679
+ };
1680
+ }
1681
+ /**
1682
+ * Per-workspace merge threshold presets over Postgres (the Drizzle mirror of the
1683
+ * Worker's `D1MergePresetRepository`, migration 0024). Enforces the single-default
1684
+ * invariant: promoting a preset to default demotes every other in the workspace
1685
+ * before the upsert. The default preset cannot be removed (the service keeps that
1686
+ * rule too; the DELETE also guards `is_default = 0`). Behaviourally identical to the
1687
+ * D1 repo so the cross-runtime conformance suite asserts the same preset resolution.
1688
+ */
1689
+ export class DrizzleMergePresetRepository {
1690
+ db;
1691
+ constructor(db) {
1692
+ this.db = db;
1693
+ }
1694
+ async get(workspaceId, id) {
1695
+ const rows = await this.db
1696
+ .select()
1697
+ .from(mergeThresholdPresets)
1698
+ .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), eq(mergeThresholdPresets.id, id)))
1699
+ .limit(1);
1700
+ return rows[0] ? rowToMergePreset(rows[0]) : null;
1701
+ }
1702
+ async list(workspaceId) {
1703
+ const rows = await this.db
1704
+ .select()
1705
+ .from(mergeThresholdPresets)
1706
+ .where(eq(mergeThresholdPresets.workspace_id, workspaceId))
1707
+ .orderBy(mergeThresholdPresets.created_at);
1708
+ return rows.map(rowToMergePreset);
1709
+ }
1710
+ async getDefault(workspaceId) {
1711
+ const rows = await this.db
1712
+ .select()
1713
+ .from(mergeThresholdPresets)
1714
+ .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), eq(mergeThresholdPresets.is_default, 1)))
1715
+ .orderBy(mergeThresholdPresets.created_at)
1716
+ .limit(1);
1717
+ return rows[0] ? rowToMergePreset(rows[0]) : null;
1718
+ }
1719
+ async upsert(workspaceId, preset) {
1720
+ const values = {
1721
+ workspace_id: workspaceId,
1722
+ id: preset.id,
1723
+ name: preset.name,
1724
+ max_complexity: preset.maxComplexity,
1725
+ max_risk: preset.maxRisk,
1726
+ max_impact: preset.maxImpact,
1727
+ ci_max_attempts: preset.ciMaxAttempts,
1728
+ max_requirement_iterations: preset.maxRequirementIterations,
1729
+ max_requirement_concern_allowed: preset.maxRequirementConcernAllowed,
1730
+ is_default: preset.isDefault ? 1 : 0,
1731
+ created_at: preset.createdAt,
1732
+ };
1733
+ // Demote + upsert run in one transaction so the single-default invariant can never
1734
+ // be observed broken (zero or two defaults) by a concurrent reader or a partial failure.
1735
+ await this.db.transaction(async (tx) => {
1736
+ // Promoting this preset to default demotes any other default first.
1737
+ if (preset.isDefault) {
1738
+ await tx
1739
+ .update(mergeThresholdPresets)
1740
+ .set({ is_default: 0 })
1741
+ .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), sql `${mergeThresholdPresets.id} <> ${preset.id}`));
1742
+ }
1743
+ await tx
1744
+ .insert(mergeThresholdPresets)
1745
+ .values(values)
1746
+ .onConflictDoUpdate({
1747
+ target: [mergeThresholdPresets.workspace_id, mergeThresholdPresets.id],
1748
+ set: {
1749
+ name: values.name,
1750
+ max_complexity: values.max_complexity,
1751
+ max_risk: values.max_risk,
1752
+ max_impact: values.max_impact,
1753
+ ci_max_attempts: values.ci_max_attempts,
1754
+ max_requirement_iterations: values.max_requirement_iterations,
1755
+ max_requirement_concern_allowed: values.max_requirement_concern_allowed,
1756
+ is_default: values.is_default,
1757
+ },
1758
+ });
1759
+ });
1760
+ }
1761
+ async remove(workspaceId, id) {
1762
+ await this.db
1763
+ .delete(mergeThresholdPresets)
1764
+ .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), eq(mergeThresholdPresets.id, id), eq(mergeThresholdPresets.is_default, 0)));
1765
+ }
1766
+ }
1767
+ function rowToBlueprintRecord(row) {
1768
+ return {
1769
+ id: row.id,
1770
+ workspaceId: row.workspace_id,
1771
+ repoOwner: row.repo_owner,
1772
+ repoName: row.repo_name,
1773
+ source: row.source,
1774
+ service: JSON.parse(row.service_json),
1775
+ createdAt: row.created_at,
1776
+ updatedAt: row.updated_at,
1777
+ };
1778
+ }
1779
+ /**
1780
+ * Repository blueprints over Postgres (the Drizzle mirror of the Worker's
1781
+ * `D1RepoBlueprintRepository`, migration 0011). One row per (workspace, repo):
1782
+ * `upsert` replaces the existing blueprint in place, keyed by the unique
1783
+ * `(workspace_id, repo_owner, repo_name)` index, so the row is always the single
1784
+ * current decomposition. The service → modules tree is persisted whole as JSON.
1785
+ */
1786
+ export class DrizzleRepoBlueprintRepository {
1787
+ db;
1788
+ constructor(db) {
1789
+ this.db = db;
1790
+ }
1791
+ async upsert(record) {
1792
+ const values = {
1793
+ id: record.id,
1794
+ workspace_id: record.workspaceId,
1795
+ repo_owner: record.repoOwner,
1796
+ repo_name: record.repoName,
1797
+ source: record.source,
1798
+ service_json: JSON.stringify(record.service),
1799
+ created_at: record.createdAt,
1800
+ updated_at: record.updatedAt,
1801
+ };
1802
+ await this.db
1803
+ .insert(repoBlueprints)
1804
+ .values(values)
1805
+ .onConflictDoUpdate({
1806
+ target: [repoBlueprints.workspace_id, repoBlueprints.repo_owner, repoBlueprints.repo_name],
1807
+ set: {
1808
+ source: values.source,
1809
+ service_json: values.service_json,
1810
+ updated_at: values.updated_at,
1811
+ },
1812
+ });
1813
+ }
1814
+ async get(workspaceId, id) {
1815
+ const rows = await this.db
1816
+ .select()
1817
+ .from(repoBlueprints)
1818
+ .where(and(eq(repoBlueprints.workspace_id, workspaceId), eq(repoBlueprints.id, id)))
1819
+ .limit(1);
1820
+ return rows[0] ? rowToBlueprintRecord(rows[0]) : null;
1821
+ }
1822
+ async getByRepo(workspaceId, repoOwner, repoName) {
1823
+ const rows = await this.db
1824
+ .select()
1825
+ .from(repoBlueprints)
1826
+ .where(and(eq(repoBlueprints.workspace_id, workspaceId), eq(repoBlueprints.repo_owner, repoOwner), eq(repoBlueprints.repo_name, repoName)))
1827
+ .limit(1);
1828
+ return rows[0] ? rowToBlueprintRecord(rows[0]) : null;
1829
+ }
1830
+ async listByWorkspace(workspaceId) {
1831
+ const rows = await this.db
1832
+ .select()
1833
+ .from(repoBlueprints)
1834
+ .where(eq(repoBlueprints.workspace_id, workspaceId))
1835
+ .orderBy(desc(repoBlueprints.updated_at));
1836
+ return rows.map(rowToBlueprintRecord);
1837
+ }
1838
+ async delete(workspaceId, id) {
1839
+ await this.db
1840
+ .delete(repoBlueprints)
1841
+ .where(and(eq(repoBlueprints.workspace_id, workspaceId), eq(repoBlueprints.id, id)));
1842
+ }
1843
+ }
1844
+ /** Build the Drizzle/Postgres-backed core repositories. */
1845
+ export function createDrizzleRepositories(db, clock) {
1846
+ return {
1847
+ workspaceRepository: new DrizzleWorkspaceRepository(db),
1848
+ accountRepository: new DrizzleAccountRepository(db),
1849
+ membershipRepository: new DrizzleMembershipRepository(db),
1850
+ userRepository: new DrizzleUserRepository(db),
1851
+ invitationRepository: new DrizzleAccountInvitationRepository(db),
1852
+ emailConnectionRepository: new DrizzleEmailConnectionRepository(db),
1853
+ blockRepository: new DrizzleBlockRepository(db),
1854
+ pipelineRepository: new DrizzlePipelineRepository(db),
1855
+ executionRepository: new DrizzleExecutionRepository(db, clock),
1856
+ tokenUsageRepository: new DrizzleTokenUsageRepository(db),
1857
+ llmCallMetricRepository: new DrizzleLlmCallMetricRepository(db),
1858
+ agentRunRepository: new DrizzleAgentRunRepository(db),
1859
+ modelDefaultsRepository: new DrizzleModelDefaultsRepository(db),
1860
+ serviceFragmentDefaultsRepository: new DrizzleServiceFragmentDefaultsRepository(db),
1861
+ pipelineScheduleRepository: new DrizzlePipelineScheduleRepository(db),
1862
+ trackerSettingsRepository: new DrizzleTrackerSettingsRepository(db),
1863
+ serviceRepository: new DrizzleServiceRepository(db),
1864
+ workspaceMountRepository: new DrizzleWorkspaceMountRepository(db),
1865
+ requirementReviewRepository: new DrizzleRequirementReviewRepository(db),
1866
+ consensusSessionRepository: new DrizzleConsensusSessionRepository(db),
1867
+ clarityReviewRepository: new DrizzleClarityReviewRepository(db),
1868
+ mergePresetRepository: new DrizzleMergePresetRepository(db),
1869
+ repoBlueprintRepository: new DrizzleRepoBlueprintRepository(db),
1870
+ };
1871
+ }
1872
+ //# sourceMappingURL=drizzle.js.map