@opengeni/db 0.14.3 → 0.17.1

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 (67) hide show
  1. package/dist/{chunk-Y5WZZVQK.js → chunk-SK7YYHBI.js} +5 -2
  2. package/dist/chunk-SK7YYHBI.js.map +1 -0
  3. package/dist/{chunk-ELMUCYZF.js → chunk-T6RSJT6C.js} +202 -43
  4. package/dist/chunk-T6RSJT6C.js.map +1 -0
  5. package/dist/chunk-TLAC622R.js +4990 -0
  6. package/dist/chunk-TLAC622R.js.map +1 -0
  7. package/dist/codex-token-resolver.d.ts +64 -0
  8. package/dist/connection-token-resolver.d.ts +90 -0
  9. package/dist/environment-crypto.d.ts +11 -0
  10. package/dist/event-payload-sanitizer.d.ts +32 -0
  11. package/dist/index.d.ts +6374 -10
  12. package/dist/index.js +8075 -2776
  13. package/dist/index.js.map +1 -1
  14. package/dist/insights.d.ts +157 -0
  15. package/dist/memory-domain.d.ts +44 -0
  16. package/dist/migrate.d.ts +5 -7
  17. package/dist/migrate.js +1 -1
  18. package/dist/new-session-drafts.d.ts +49 -0
  19. package/dist/persistence-errors.d.ts +69 -0
  20. package/dist/preference-registry-schema.d.ts +1147 -0
  21. package/dist/preference-registry.d.ts +878 -0
  22. package/dist/provision-roles.d.ts +4 -6489
  23. package/dist/provision-roles.js +1 -1
  24. package/dist/role-relationships.d.ts +35 -0
  25. package/dist/runtime-posture-cli.d.ts +1 -0
  26. package/dist/runtime-posture.d.ts +103 -0
  27. package/dist/schema.d.ts +25330 -3
  28. package/dist/schema.js +31 -1
  29. package/dist/session-control.d.ts +331 -0
  30. package/dist/session-queue-commands.d.ts +186 -0
  31. package/dist/session-tool-call-settlement.d.ts +32 -0
  32. package/dist/turn-initiator.d.ts +28 -0
  33. package/dist/workspace-artifacts.d.ts +62 -0
  34. package/dist/workspace-instruction-policies-schema.d.ts +735 -0
  35. package/dist/workspace-instruction-policies.d.ts +64 -0
  36. package/drizzle/0136_unified_session_tool_policy.sql +1 -1
  37. package/drizzle/0137_preference_registry.sql +1347 -0
  38. package/drizzle/0138_sandbox_checkpoint_artifacts_and_deadlines.sql +1074 -0
  39. package/drizzle/0139_codex_provider_artifact_invalidations.sql +51 -0
  40. package/drizzle/0140_sandbox_restore_and_reaper_fences.sql +276 -0
  41. package/drizzle/0142_sandbox_archive_capture_gate.sql +80 -0
  42. package/drizzle/0143_session_codex_compaction_mode.sql +20 -0
  43. package/drizzle/0144_sandbox_viewer_force_drain_gate.sql +95 -0
  44. package/drizzle/0145_model_call_facts.sql +96 -0
  45. package/drizzle/0146_slack_bot_delete_idempotency.sql +105 -0
  46. package/drizzle/0147_draft_latency_mode.sql +12 -0
  47. package/drizzle/0148_session_turn_latency_mode.sql +12 -0
  48. package/drizzle/0149_workspace_artifacts.sql +348 -0
  49. package/drizzle/0150_slack_task_interactions.sql +344 -0
  50. package/package.json +6 -6
  51. package/src/index.ts +5766 -275
  52. package/src/insights.ts +837 -0
  53. package/src/migrate.ts +9 -1
  54. package/src/new-session-drafts.ts +4 -0
  55. package/src/preference-registry-schema.ts +170 -0
  56. package/src/preference-registry.ts +1220 -0
  57. package/src/provision-roles.ts +95 -22
  58. package/src/role-relationships.ts +132 -0
  59. package/src/runtime-posture.ts +47 -27
  60. package/src/schema.ts +875 -19
  61. package/src/session-queue-commands.ts +39 -6
  62. package/src/workspace-artifacts.ts +751 -0
  63. package/dist/chunk-DW24CKCT.js +0 -4046
  64. package/dist/chunk-DW24CKCT.js.map +0 -1
  65. package/dist/chunk-ELMUCYZF.js.map +0 -1
  66. package/dist/chunk-Y5WZZVQK.js.map +0 -1
  67. package/dist/schema-DhkRhcuQ.d.ts +0 -22666
@@ -0,0 +1,837 @@
1
+ import { and, desc, eq, gte, inArray, lt, sql } from "drizzle-orm";
2
+ import { alias } from "drizzle-orm/pg-core";
3
+ import type { Database } from "./index";
4
+ import { rlsContextForWorkspace, withRlsContext } from "./index";
5
+ import * as schema from "./schema";
6
+
7
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
8
+
9
+ export type InsightsTimeWindow = {
10
+ since: Date;
11
+ until: Date;
12
+ };
13
+
14
+ export type ModelCallFactAggregateRow = {
15
+ provider: string;
16
+ model: string;
17
+ billingPath: string;
18
+ calls: number;
19
+ inputTokens: number;
20
+ outputTokens: number;
21
+ cachedTokens: number;
22
+ cacheWriteTokens: number;
23
+ reasoningTokens: number;
24
+ totalTokens: number;
25
+ pricedCostMicros: number;
26
+ };
27
+
28
+ export type InsightsDayBucket = {
29
+ day: string;
30
+ modelCostMicros: number;
31
+ warmSeconds: number;
32
+ inputTokens: number;
33
+ cachedTokens: number;
34
+ calls: number;
35
+ };
36
+
37
+ export type WarmGroupAggregate = {
38
+ groupId: string;
39
+ warmSeconds: number;
40
+ };
41
+
42
+ export type LiveWarmLeaseRow = {
43
+ id: string;
44
+ groupId: string;
45
+ backend: string;
46
+ turnHolders: number;
47
+ viewerHolders: number;
48
+ lastMeterAt: Date | null;
49
+ };
50
+
51
+ export type RootSessionDriverRow = {
52
+ rootSessionId: string;
53
+ title: string | null;
54
+ pricedCostMicros: number;
55
+ inputTokens: number;
56
+ cachedTokens: number;
57
+ };
58
+
59
+ export type ScheduleFactAggregate = {
60
+ scheduledTaskId: string;
61
+ pricedCostMicros: number;
62
+ inputTokens: number;
63
+ cachedTokens: number;
64
+ calls: number;
65
+ /** Prefer credits whenever any credits-path call exists in the window. */
66
+ billingPath: string;
67
+ };
68
+
69
+ export type ModelCallFacetRow = {
70
+ provider: string;
71
+ model: string;
72
+ };
73
+
74
+ export type SessionDepthAggregate = {
75
+ depth: number;
76
+ sessions: number;
77
+ };
78
+
79
+ export type FloorSessionRow = {
80
+ id: string;
81
+ title: string | null;
82
+ status: string;
83
+ directControlState: string;
84
+ nestedAgentDepth: number;
85
+ model: string;
86
+ sandboxBackend: string;
87
+ updatedAt: Date;
88
+ createdAt: Date;
89
+ };
90
+
91
+ function dayKeyUtc(value: Date): string {
92
+ return value.toISOString().slice(0, 10);
93
+ }
94
+
95
+ export async function sumUsageQuantityInRange(
96
+ db: Database,
97
+ input: {
98
+ workspaceId: string;
99
+ eventType: string;
100
+ since: Date;
101
+ until: Date;
102
+ },
103
+ ): Promise<number> {
104
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
105
+ return await withRlsContext(db, context, async (scopedDb) => {
106
+ const [{ total } = { total: 0 }] = await scopedDb
107
+ .select({
108
+ total: sql<number>`coalesce(sum(${schema.usageEvents.quantity}), 0)`,
109
+ })
110
+ .from(schema.usageEvents)
111
+ .where(
112
+ and(
113
+ eq(schema.usageEvents.workspaceId, input.workspaceId),
114
+ eq(schema.usageEvents.eventType, input.eventType),
115
+ gte(schema.usageEvents.occurredAt, input.since),
116
+ lt(schema.usageEvents.occurredAt, input.until),
117
+ ),
118
+ );
119
+ return Number(total);
120
+ });
121
+ }
122
+
123
+ export async function sumUsageQuantityByDay(
124
+ db: Database,
125
+ input: {
126
+ workspaceId: string;
127
+ eventType: string;
128
+ since: Date;
129
+ until: Date;
130
+ },
131
+ ): Promise<Map<string, number>> {
132
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
133
+ return await withRlsContext(db, context, async (scopedDb) => {
134
+ const rows = await scopedDb
135
+ .select({
136
+ day: sql<string>`to_char(date_trunc('day', ${schema.usageEvents.occurredAt} at time zone 'UTC'), 'YYYY-MM-DD')`,
137
+ total: sql<number>`coalesce(sum(${schema.usageEvents.quantity}), 0)`,
138
+ })
139
+ .from(schema.usageEvents)
140
+ .where(
141
+ and(
142
+ eq(schema.usageEvents.workspaceId, input.workspaceId),
143
+ eq(schema.usageEvents.eventType, input.eventType),
144
+ gte(schema.usageEvents.occurredAt, input.since),
145
+ lt(schema.usageEvents.occurredAt, input.until),
146
+ ),
147
+ )
148
+ .groupBy(sql`date_trunc('day', ${schema.usageEvents.occurredAt} at time zone 'UTC')`);
149
+ return new Map(rows.map((row) => [row.day, Number(row.total)]));
150
+ });
151
+ }
152
+
153
+ export async function aggregateModelCallFacts(
154
+ db: Database,
155
+ input: {
156
+ workspaceId: string;
157
+ since: Date;
158
+ until: Date;
159
+ provider?: string | null;
160
+ model?: string | null;
161
+ },
162
+ ): Promise<ModelCallFactAggregateRow[]> {
163
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
164
+ return await withRlsContext(db, context, async (scopedDb) => {
165
+ const clauses = [
166
+ eq(schema.modelCallFacts.workspaceId, input.workspaceId),
167
+ gte(schema.modelCallFacts.occurredAt, input.since),
168
+ lt(schema.modelCallFacts.occurredAt, input.until),
169
+ ...(input.provider ? [eq(schema.modelCallFacts.provider, input.provider)] : []),
170
+ ...(input.model ? [eq(schema.modelCallFacts.model, input.model)] : []),
171
+ ];
172
+ const rows = await scopedDb
173
+ .select({
174
+ provider: schema.modelCallFacts.provider,
175
+ model: schema.modelCallFacts.model,
176
+ billingPath: schema.modelCallFacts.billingPath,
177
+ calls: sql<number>`count(*)::int`,
178
+ inputTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.inputTokens}), 0)`,
179
+ outputTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.outputTokens}), 0)`,
180
+ cachedTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.cachedTokens}), 0)`,
181
+ cacheWriteTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.cacheWriteTokens}), 0)`,
182
+ reasoningTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.reasoningTokens}), 0)`,
183
+ totalTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.totalTokens}), 0)`,
184
+ pricedCostMicros: sql<number>`coalesce(sum(${schema.modelCallFacts.pricedCostMicros}), 0)`,
185
+ })
186
+ .from(schema.modelCallFacts)
187
+ .where(and(...clauses))
188
+ .groupBy(
189
+ schema.modelCallFacts.provider,
190
+ schema.modelCallFacts.model,
191
+ schema.modelCallFacts.billingPath,
192
+ );
193
+ return rows.map((row) => ({
194
+ provider: row.provider,
195
+ model: row.model,
196
+ billingPath: row.billingPath,
197
+ calls: Number(row.calls),
198
+ inputTokens: Number(row.inputTokens),
199
+ outputTokens: Number(row.outputTokens),
200
+ cachedTokens: Number(row.cachedTokens),
201
+ cacheWriteTokens: Number(row.cacheWriteTokens),
202
+ reasoningTokens: Number(row.reasoningTokens),
203
+ totalTokens: Number(row.totalTokens),
204
+ pricedCostMicros: Number(row.pricedCostMicros),
205
+ }));
206
+ });
207
+ }
208
+
209
+ export async function aggregateModelCallFactsByDay(
210
+ db: Database,
211
+ input: {
212
+ workspaceId: string;
213
+ since: Date;
214
+ until: Date;
215
+ provider?: string | null;
216
+ model?: string | null;
217
+ },
218
+ ): Promise<
219
+ Map<string, { costMicros: number; inputTokens: number; cachedTokens: number; calls: number }>
220
+ > {
221
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
222
+ return await withRlsContext(db, context, async (scopedDb) => {
223
+ const clauses = [
224
+ eq(schema.modelCallFacts.workspaceId, input.workspaceId),
225
+ gte(schema.modelCallFacts.occurredAt, input.since),
226
+ lt(schema.modelCallFacts.occurredAt, input.until),
227
+ ...(input.provider ? [eq(schema.modelCallFacts.provider, input.provider)] : []),
228
+ ...(input.model ? [eq(schema.modelCallFacts.model, input.model)] : []),
229
+ ];
230
+ const rows = await scopedDb
231
+ .select({
232
+ day: sql<string>`to_char(date_trunc('day', ${schema.modelCallFacts.occurredAt} at time zone 'UTC'), 'YYYY-MM-DD')`,
233
+ costMicros: sql<number>`coalesce(sum(${schema.modelCallFacts.pricedCostMicros}), 0)`,
234
+ inputTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.inputTokens}), 0)`,
235
+ cachedTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.cachedTokens}), 0)`,
236
+ calls: sql<number>`count(*)::int`,
237
+ })
238
+ .from(schema.modelCallFacts)
239
+ .where(and(...clauses))
240
+ .groupBy(sql`date_trunc('day', ${schema.modelCallFacts.occurredAt} at time zone 'UTC')`);
241
+ return new Map(
242
+ rows.map((row) => [
243
+ row.day,
244
+ {
245
+ costMicros: Number(row.costMicros),
246
+ inputTokens: Number(row.inputTokens),
247
+ cachedTokens: Number(row.cachedTokens),
248
+ calls: Number(row.calls),
249
+ },
250
+ ]),
251
+ );
252
+ });
253
+ }
254
+
255
+ export async function aggregateWarmSecondsByGroup(
256
+ db: Database,
257
+ input: InsightsTimeWindow & { workspaceId: string; limit?: number },
258
+ ): Promise<WarmGroupAggregate[]> {
259
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
260
+ const limit = input.limit ?? 24;
261
+ return await withRlsContext(db, context, async (scopedDb) => {
262
+ const rows = await scopedDb
263
+ .select({
264
+ groupId: sql<string>`split_part(${schema.usageEvents.sourceResourceId}, ':', 1)`,
265
+ warmSeconds: sql<number>`coalesce(sum(${schema.usageEvents.quantity}), 0)`,
266
+ })
267
+ .from(schema.usageEvents)
268
+ .where(
269
+ and(
270
+ eq(schema.usageEvents.workspaceId, input.workspaceId),
271
+ eq(schema.usageEvents.eventType, "sandbox.warm_seconds"),
272
+ gte(schema.usageEvents.occurredAt, input.since),
273
+ lt(schema.usageEvents.occurredAt, input.until),
274
+ sql`${schema.usageEvents.sourceResourceId} is not null`,
275
+ ),
276
+ )
277
+ .groupBy(sql`split_part(${schema.usageEvents.sourceResourceId}, ':', 1)`)
278
+ .orderBy(sql`coalesce(sum(${schema.usageEvents.quantity}), 0) desc`)
279
+ .limit(Math.max(limit * 4, limit));
280
+ return rows
281
+ .filter((row) => UUID_RE.test(row.groupId))
282
+ .slice(0, limit)
283
+ .map((row) => ({
284
+ groupId: row.groupId,
285
+ warmSeconds: Number(row.warmSeconds),
286
+ }));
287
+ });
288
+ }
289
+
290
+ export async function listLiveWarmLeases(
291
+ db: Database,
292
+ workspaceId: string,
293
+ ): Promise<LiveWarmLeaseRow[]> {
294
+ const context = await rlsContextForWorkspace(db, workspaceId);
295
+ return await withRlsContext(db, context, async (scopedDb) => {
296
+ const rows = await scopedDb
297
+ .select({
298
+ id: schema.sandboxLeases.id,
299
+ groupId: schema.sandboxLeases.sandboxGroupId,
300
+ backend: schema.sandboxLeases.backend,
301
+ turnHolders: schema.sandboxLeases.turnHolders,
302
+ viewerHolders: schema.sandboxLeases.viewerHolders,
303
+ lastMeterAt: schema.sandboxLeases.lastMeterAt,
304
+ })
305
+ .from(schema.sandboxLeases)
306
+ .where(
307
+ and(
308
+ eq(schema.sandboxLeases.workspaceId, workspaceId),
309
+ // Only ready warm boxes — warming leases are not yet idle/usable compute.
310
+ eq(schema.sandboxLeases.liveness, "warm"),
311
+ ),
312
+ )
313
+ .orderBy(desc(schema.sandboxLeases.updatedAt));
314
+ return rows.map((row) => ({
315
+ id: row.id,
316
+ groupId: row.groupId,
317
+ backend: row.backend,
318
+ turnHolders: row.turnHolders,
319
+ viewerHolders: row.viewerHolders,
320
+ lastMeterAt: row.lastMeterAt,
321
+ }));
322
+ });
323
+ }
324
+
325
+ export async function countSessionsAttachedToGroups(
326
+ db: Database,
327
+ workspaceId: string,
328
+ groupIds: string[],
329
+ ): Promise<Map<string, number>> {
330
+ if (groupIds.length === 0) return new Map();
331
+ const context = await rlsContextForWorkspace(db, workspaceId);
332
+ return await withRlsContext(db, context, async (scopedDb) => {
333
+ const rows = await scopedDb
334
+ .select({
335
+ groupId: schema.sessions.sandboxGroupId,
336
+ n: sql<number>`count(*)::int`,
337
+ })
338
+ .from(schema.sessions)
339
+ .where(
340
+ and(
341
+ eq(schema.sessions.workspaceId, workspaceId),
342
+ inArray(schema.sessions.sandboxGroupId, groupIds),
343
+ ),
344
+ )
345
+ .groupBy(schema.sessions.sandboxGroupId);
346
+ return new Map(
347
+ rows
348
+ .filter((row): row is { groupId: string; n: number } => typeof row.groupId === "string")
349
+ .map((row) => [row.groupId, Number(row.n)]),
350
+ );
351
+ });
352
+ }
353
+
354
+ export async function aggregateRootSessionDrivers(
355
+ db: Database,
356
+ input: InsightsTimeWindow & {
357
+ workspaceId: string;
358
+ provider?: string | null;
359
+ model?: string | null;
360
+ rootSessionIds?: string[];
361
+ limit?: number;
362
+ },
363
+ ): Promise<RootSessionDriverRow[]> {
364
+ if (input.rootSessionIds && input.rootSessionIds.length === 0) return [];
365
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
366
+ const childSessions = alias(schema.sessions, "insight_child_sessions");
367
+ const rootSessions = alias(schema.sessions, "insight_root_sessions");
368
+ return await withRlsContext(db, context, async (scopedDb) => {
369
+ const clauses = [
370
+ eq(schema.modelCallFacts.workspaceId, input.workspaceId),
371
+ gte(schema.modelCallFacts.occurredAt, input.since),
372
+ lt(schema.modelCallFacts.occurredAt, input.until),
373
+ ...(input.provider ? [eq(schema.modelCallFacts.provider, input.provider)] : []),
374
+ ...(input.model ? [eq(schema.modelCallFacts.model, input.model)] : []),
375
+ ...(input.rootSessionIds ? [inArray(childSessions.rootSessionId, input.rootSessionIds)] : []),
376
+ ];
377
+ const query = scopedDb
378
+ .select({
379
+ rootSessionId: childSessions.rootSessionId,
380
+ title: rootSessions.title,
381
+ pricedCostMicros: sql<number>`coalesce(sum(${schema.modelCallFacts.pricedCostMicros}), 0)`,
382
+ inputTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.inputTokens}), 0)`,
383
+ cachedTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.cachedTokens}), 0)`,
384
+ })
385
+ .from(schema.modelCallFacts)
386
+ .innerJoin(
387
+ childSessions,
388
+ and(
389
+ eq(childSessions.workspaceId, schema.modelCallFacts.workspaceId),
390
+ eq(childSessions.id, schema.modelCallFacts.sessionId),
391
+ ),
392
+ )
393
+ .leftJoin(
394
+ rootSessions,
395
+ and(
396
+ eq(rootSessions.workspaceId, childSessions.workspaceId),
397
+ eq(rootSessions.id, childSessions.rootSessionId),
398
+ ),
399
+ )
400
+ .where(and(...clauses))
401
+ .groupBy(childSessions.rootSessionId, rootSessions.title)
402
+ .orderBy(sql`coalesce(sum(${schema.modelCallFacts.pricedCostMicros}), 0) desc`);
403
+ const rows = input.rootSessionIds ? await query : await query.limit(input.limit ?? 8);
404
+ return rows.map((row) => ({
405
+ rootSessionId: row.rootSessionId,
406
+ title: row.title,
407
+ pricedCostMicros: Number(row.pricedCostMicros),
408
+ inputTokens: Number(row.inputTokens),
409
+ cachedTokens: Number(row.cachedTokens),
410
+ }));
411
+ });
412
+ }
413
+
414
+ export async function listModelCallFacets(
415
+ db: Database,
416
+ input: InsightsTimeWindow & { workspaceId: string },
417
+ ): Promise<ModelCallFacetRow[]> {
418
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
419
+ return await withRlsContext(db, context, async (scopedDb) => {
420
+ const rows = await scopedDb
421
+ .selectDistinct({
422
+ provider: schema.modelCallFacts.provider,
423
+ model: schema.modelCallFacts.model,
424
+ })
425
+ .from(schema.modelCallFacts)
426
+ .where(
427
+ and(
428
+ eq(schema.modelCallFacts.workspaceId, input.workspaceId),
429
+ gte(schema.modelCallFacts.occurredAt, input.since),
430
+ lt(schema.modelCallFacts.occurredAt, input.until),
431
+ ),
432
+ )
433
+ .orderBy(schema.modelCallFacts.provider, schema.modelCallFacts.model)
434
+ .limit(500);
435
+ return rows;
436
+ });
437
+ }
438
+
439
+ export async function aggregateScheduleFacts(
440
+ db: Database,
441
+ input: InsightsTimeWindow & {
442
+ workspaceId: string;
443
+ provider?: string | null;
444
+ model?: string | null;
445
+ },
446
+ ): Promise<ScheduleFactAggregate[]> {
447
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
448
+ return await withRlsContext(db, context, async (scopedDb) => {
449
+ const clauses = [
450
+ eq(schema.modelCallFacts.workspaceId, input.workspaceId),
451
+ gte(schema.modelCallFacts.occurredAt, input.since),
452
+ lt(schema.modelCallFacts.occurredAt, input.until),
453
+ sql`${schema.modelCallFacts.scheduledTaskId} is not null`,
454
+ ...(input.provider ? [eq(schema.modelCallFacts.provider, input.provider)] : []),
455
+ ...(input.model ? [eq(schema.modelCallFacts.model, input.model)] : []),
456
+ ];
457
+ const rows = await scopedDb
458
+ .select({
459
+ scheduledTaskId: schema.modelCallFacts.scheduledTaskId,
460
+ pricedCostMicros: sql<number>`coalesce(sum(${schema.modelCallFacts.pricedCostMicros}), 0)`,
461
+ inputTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.inputTokens}), 0)`,
462
+ cachedTokens: sql<number>`coalesce(sum(${schema.modelCallFacts.cachedTokens}), 0)`,
463
+ calls: sql<number>`count(*)::int`,
464
+ billingPath: sql<string>`case
465
+ when bool_or(${schema.modelCallFacts.billingPath} = 'opengeni_credits')
466
+ then 'opengeni_credits'
467
+ else 'external'
468
+ end`,
469
+ })
470
+ .from(schema.modelCallFacts)
471
+ .where(and(...clauses))
472
+ .groupBy(schema.modelCallFacts.scheduledTaskId);
473
+ return rows
474
+ .filter((row): row is typeof row & { scheduledTaskId: string } => row.scheduledTaskId != null)
475
+ .map((row) => ({
476
+ scheduledTaskId: row.scheduledTaskId,
477
+ pricedCostMicros: Number(row.pricedCostMicros),
478
+ inputTokens: Number(row.inputTokens),
479
+ cachedTokens: Number(row.cachedTokens),
480
+ calls: Number(row.calls),
481
+ billingPath: row.billingPath,
482
+ }));
483
+ });
484
+ }
485
+
486
+ export async function countScheduledTaskFires(
487
+ db: Database,
488
+ input: InsightsTimeWindow & { workspaceId: string; taskIds: string[] },
489
+ ): Promise<Map<string, number>> {
490
+ if (input.taskIds.length === 0) return new Map();
491
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
492
+ return await withRlsContext(db, context, async (scopedDb) => {
493
+ const rows = await scopedDb
494
+ .select({
495
+ taskId: schema.scheduledTaskRuns.taskId,
496
+ n: sql<number>`count(*)::int`,
497
+ })
498
+ .from(schema.scheduledTaskRuns)
499
+ .where(
500
+ and(
501
+ eq(schema.scheduledTaskRuns.workspaceId, input.workspaceId),
502
+ inArray(schema.scheduledTaskRuns.taskId, input.taskIds),
503
+ gte(schema.scheduledTaskRuns.createdAt, input.since),
504
+ lt(schema.scheduledTaskRuns.createdAt, input.until),
505
+ ),
506
+ )
507
+ .groupBy(schema.scheduledTaskRuns.taskId);
508
+ return new Map(rows.map((row) => [row.taskId, Number(row.n)]));
509
+ });
510
+ }
511
+
512
+ export async function aggregateSessionDepth(
513
+ db: Database,
514
+ workspaceId: string,
515
+ ): Promise<{
516
+ buckets: SessionDepthAggregate[];
517
+ sessionsTouched: number;
518
+ rootSessions: number;
519
+ deepestDepth: number;
520
+ deepestSessionTitle: string;
521
+ avgDepth: number;
522
+ goalsActive: number;
523
+ goalsCompleted: number;
524
+ }> {
525
+ const context = await rlsContextForWorkspace(db, workspaceId);
526
+ return await withRlsContext(db, context, async (scopedDb) => {
527
+ const buckets = await scopedDb
528
+ .select({
529
+ depth: schema.sessions.nestedAgentDepth,
530
+ sessions: sql<number>`count(*)::int`,
531
+ })
532
+ .from(schema.sessions)
533
+ .where(eq(schema.sessions.workspaceId, workspaceId))
534
+ .groupBy(schema.sessions.nestedAgentDepth)
535
+ .orderBy(schema.sessions.nestedAgentDepth);
536
+ const [stats] = await scopedDb
537
+ .select({
538
+ sessionsTouched: sql<number>`count(*)::int`,
539
+ rootSessions: sql<number>`count(*) filter (where ${schema.sessions.nestedAgentDepth} = 0)::int`,
540
+ deepestDepth: sql<number>`coalesce(max(${schema.sessions.nestedAgentDepth}), 0)`,
541
+ avgDepth: sql<number>`coalesce(avg(${schema.sessions.nestedAgentDepth}), 0)`,
542
+ })
543
+ .from(schema.sessions)
544
+ .where(eq(schema.sessions.workspaceId, workspaceId));
545
+ const [deepest] = await scopedDb
546
+ .select({
547
+ title: schema.sessions.title,
548
+ depth: schema.sessions.nestedAgentDepth,
549
+ })
550
+ .from(schema.sessions)
551
+ .where(eq(schema.sessions.workspaceId, workspaceId))
552
+ .orderBy(desc(schema.sessions.nestedAgentDepth), desc(schema.sessions.updatedAt))
553
+ .limit(1);
554
+ const [goals] = await scopedDb
555
+ .select({
556
+ active: sql<number>`count(*) filter (where ${schema.sessionGoals.status} = 'active')::int`,
557
+ completed: sql<number>`count(*) filter (where ${schema.sessionGoals.status} = 'completed')::int`,
558
+ })
559
+ .from(schema.sessionGoals)
560
+ .where(eq(schema.sessionGoals.workspaceId, workspaceId));
561
+ return {
562
+ buckets: buckets.map((row) => ({
563
+ depth: row.depth,
564
+ sessions: Number(row.sessions),
565
+ })),
566
+ sessionsTouched: Number(stats?.sessionsTouched ?? 0),
567
+ rootSessions: Number(stats?.rootSessions ?? 0),
568
+ deepestDepth: Number(stats?.deepestDepth ?? 0),
569
+ deepestSessionTitle: deepest?.title?.trim() || "",
570
+ avgDepth: Number(stats?.avgDepth ?? 0),
571
+ goalsActive: Number(goals?.active ?? 0),
572
+ goalsCompleted: Number(goals?.completed ?? 0),
573
+ };
574
+ });
575
+ }
576
+
577
+ export async function listFloorSessions(
578
+ db: Database,
579
+ workspaceId: string,
580
+ limit = 24,
581
+ ): Promise<FloorSessionRow[]> {
582
+ const context = await rlsContextForWorkspace(db, workspaceId);
583
+ return await withRlsContext(db, context, async (scopedDb) => {
584
+ const rows = await scopedDb
585
+ .select({
586
+ id: schema.sessions.id,
587
+ title: schema.sessions.title,
588
+ status: schema.sessions.status,
589
+ directControlState: schema.sessions.directControlState,
590
+ nestedAgentDepth: schema.sessions.nestedAgentDepth,
591
+ model: schema.sessions.model,
592
+ sandboxBackend: schema.sessions.sandboxBackend,
593
+ updatedAt: schema.sessions.updatedAt,
594
+ createdAt: schema.sessions.createdAt,
595
+ })
596
+ .from(schema.sessions)
597
+ .where(eq(schema.sessions.workspaceId, workspaceId))
598
+ .orderBy(desc(schema.sessions.updatedAt))
599
+ .limit(limit);
600
+ return rows;
601
+ });
602
+ }
603
+
604
+ export async function countOnlineMachines(
605
+ db: Database,
606
+ workspaceId: string,
607
+ heartbeatFreshMs: number,
608
+ ): Promise<number> {
609
+ const context = await rlsContextForWorkspace(db, workspaceId);
610
+ return await withRlsContext(db, context, async (scopedDb) => {
611
+ const cutoff = new Date(Date.now() - heartbeatFreshMs);
612
+ const [{ n } = { n: 0 }] = await scopedDb
613
+ .select({ n: sql<number>`count(*)::int` })
614
+ .from(schema.enrollments)
615
+ .where(
616
+ and(
617
+ eq(schema.enrollments.workspaceId, workspaceId),
618
+ eq(schema.enrollments.status, "active"),
619
+ gte(schema.enrollments.lastSeenAt, cutoff),
620
+ ),
621
+ );
622
+ return Number(n);
623
+ });
624
+ }
625
+
626
+ export function enumerateUtcDays(since: Date, until: Date): string[] {
627
+ const days: string[] = [];
628
+ const cursor = new Date(
629
+ Date.UTC(since.getUTCFullYear(), since.getUTCMonth(), since.getUTCDate()),
630
+ );
631
+ const end = until.getTime();
632
+ while (cursor.getTime() < end) {
633
+ days.push(dayKeyUtc(cursor));
634
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
635
+ }
636
+ return days;
637
+ }
638
+
639
+ /**
640
+ * Idempotent YTD backfill from authoritative agent.model.usage into model_call_facts.
641
+ * Never rewrites billing rows or existing live facts (onConflictDoNothing).
642
+ * Commits in bounded batches so a long YTD run cannot pin one multi-hour txn.
643
+ */
644
+ export async function backfillModelCallFactsFromSessionEvents(
645
+ db: Database,
646
+ input: {
647
+ workspaceId: string;
648
+ since: Date;
649
+ until?: Date;
650
+ limit?: number;
651
+ batchSize?: number;
652
+ },
653
+ ): Promise<{ considered: number; upserted: number }> {
654
+ const until = input.until ?? new Date();
655
+ const limit = input.limit ?? 50_000;
656
+ const batchSize = Math.max(1, Math.min(input.batchSize ?? 500, limit));
657
+ const context = await rlsContextForWorkspace(db, input.workspaceId);
658
+
659
+ let considered = 0;
660
+ let upserted = 0;
661
+ let cursorOccurredAt = input.since;
662
+ let cursorId = "00000000-0000-0000-0000-000000000000";
663
+
664
+ while (considered < limit) {
665
+ const remaining = limit - considered;
666
+ const page = await withRlsContext(db, context, async (scopedDb) => {
667
+ return await scopedDb
668
+ .select({
669
+ id: schema.sessionEvents.id,
670
+ accountId: schema.sessionEvents.accountId,
671
+ workspaceId: schema.sessionEvents.workspaceId,
672
+ sessionId: schema.sessionEvents.sessionId,
673
+ turnId: schema.sessionEvents.turnId,
674
+ turnAttemptId: schema.sessionEvents.turnAttemptId,
675
+ payload: schema.sessionEvents.payload,
676
+ occurredAt: schema.sessionEvents.occurredAt,
677
+ })
678
+ .from(schema.sessionEvents)
679
+ .where(
680
+ and(
681
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
682
+ eq(schema.sessionEvents.type, "agent.model.usage"),
683
+ eq(schema.sessionEvents.turnAssociation, "current"),
684
+ lt(schema.sessionEvents.occurredAt, until),
685
+ sql`${schema.sessionEvents.turnId} is not null`,
686
+ // Keyset on (occurred_at, id) so same-millisecond bursts cannot be skipped.
687
+ sql`(${schema.sessionEvents.occurredAt}, ${schema.sessionEvents.id}) > (${cursorOccurredAt}, ${cursorId}::uuid)`,
688
+ ),
689
+ )
690
+ .orderBy(schema.sessionEvents.occurredAt, schema.sessionEvents.id)
691
+ .limit(Math.min(batchSize, remaining));
692
+ });
693
+ if (page.length === 0) break;
694
+ considered += page.length;
695
+ const last = page[page.length - 1]!;
696
+ cursorOccurredAt = last.occurredAt;
697
+ cursorId = last.id;
698
+
699
+ const batchUpserted = await withRlsContext(db, context, async (scopedDb) => {
700
+ let inserted = 0;
701
+ for (const event of page) {
702
+ if (!event.turnId) continue;
703
+ const payload =
704
+ event.payload && typeof event.payload === "object" && !Array.isArray(event.payload)
705
+ ? (event.payload as Record<string, unknown>)
706
+ : null;
707
+ if (!payload) continue;
708
+ const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey : null;
709
+ const provider = typeof payload.provider === "string" ? payload.provider : null;
710
+ const providerApi = typeof payload.providerApi === "string" ? payload.providerApi : null;
711
+ const model = typeof payload.model === "string" ? payload.model : null;
712
+ if (!sourceKey || !provider || !providerApi || !model) continue;
713
+
714
+ const sourceResourceId = `${event.turnId}:${sourceKey}`;
715
+ const [cost] = await scopedDb
716
+ .select({ quantity: schema.usageEvents.quantity })
717
+ .from(schema.usageEvents)
718
+ .where(
719
+ and(
720
+ eq(schema.usageEvents.workspaceId, input.workspaceId),
721
+ eq(schema.usageEvents.eventType, "model.cost"),
722
+ eq(schema.usageEvents.sourceResourceId, sourceResourceId),
723
+ ),
724
+ )
725
+ .limit(1);
726
+ const [tokenRow] = await scopedDb
727
+ .select({ id: schema.usageEvents.id })
728
+ .from(schema.usageEvents)
729
+ .where(
730
+ and(
731
+ eq(schema.usageEvents.workspaceId, input.workspaceId),
732
+ eq(schema.usageEvents.eventType, "model.tokens"),
733
+ eq(schema.usageEvents.sourceResourceId, sourceResourceId),
734
+ ),
735
+ )
736
+ .limit(1);
737
+
738
+ // External turns always write model.cost=0 and never model.tokens.
739
+ // Credits turns with totalTokens=0 write neither — keep those as credits.
740
+ const pricedCostMicros = cost ? Number(cost.quantity) : 0;
741
+ const billingPath =
742
+ cost != null && pricedCostMicros === 0 && !tokenRow ? "external" : "opengeni_credits";
743
+
744
+ const [turn] = await scopedDb
745
+ .select({
746
+ source: schema.sessionTurns.source,
747
+ initiatorKind: schema.sessionTurns.initiatorKind,
748
+ initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
749
+ initiatorContext: schema.sessionTurns.initiatorContext,
750
+ })
751
+ .from(schema.sessionTurns)
752
+ .where(
753
+ and(
754
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
755
+ eq(schema.sessionTurns.id, event.turnId),
756
+ ),
757
+ )
758
+ .limit(1);
759
+
760
+ let scheduledTaskId: string | null = null;
761
+ const runIds =
762
+ turn?.initiatorContext &&
763
+ typeof turn.initiatorContext === "object" &&
764
+ !Array.isArray(turn.initiatorContext) &&
765
+ Array.isArray((turn.initiatorContext as Record<string, unknown>).scheduledRunIds)
766
+ ? (
767
+ (turn.initiatorContext as Record<string, unknown>).scheduledRunIds as unknown[]
768
+ ).filter((value): value is string => typeof value === "string")
769
+ : [];
770
+ if (runIds.length > 0) {
771
+ const [run] = await scopedDb
772
+ .select({ taskId: schema.scheduledTaskRuns.taskId })
773
+ .from(schema.scheduledTaskRuns)
774
+ .where(
775
+ and(
776
+ eq(schema.scheduledTaskRuns.workspaceId, input.workspaceId),
777
+ inArray(schema.scheduledTaskRuns.id, runIds),
778
+ ),
779
+ )
780
+ .orderBy(schema.scheduledTaskRuns.createdAt)
781
+ .limit(1);
782
+ scheduledTaskId = run?.taskId ?? null;
783
+ }
784
+
785
+ const insertedRows = await scopedDb
786
+ .insert(schema.modelCallFacts)
787
+ .values({
788
+ accountId: event.accountId,
789
+ workspaceId: event.workspaceId,
790
+ sessionId: event.sessionId,
791
+ turnId: event.turnId,
792
+ turnAttemptId: event.turnAttemptId,
793
+ sourceKey,
794
+ provider,
795
+ providerApi,
796
+ model,
797
+ billingPath,
798
+ turnSource: turn?.source ?? null,
799
+ initiatorKind: turn?.initiatorKind ?? null,
800
+ initiatorSubjectId: turn?.initiatorSubjectId ?? null,
801
+ scheduledTaskId,
802
+ inputTokens: numberOrNull(payload.inputTokens),
803
+ outputTokens: numberOrNull(payload.outputTokens),
804
+ cachedTokens: numberOrNull(payload.cachedTokens),
805
+ cacheWriteTokens: numberOrNull(payload.cacheWriteTokens),
806
+ reasoningTokens: numberOrNull(payload.reasoningTokens),
807
+ totalTokens:
808
+ numberOrNull(payload.inputTokens) !== null ||
809
+ numberOrNull(payload.outputTokens) !== null
810
+ ? (numberOrNull(payload.inputTokens) ?? 0) +
811
+ (numberOrNull(payload.outputTokens) ?? 0)
812
+ : null,
813
+ pricedCostMicros,
814
+ occurredAt: event.occurredAt,
815
+ })
816
+ .onConflictDoNothing({
817
+ target: [
818
+ schema.modelCallFacts.workspaceId,
819
+ schema.modelCallFacts.turnId,
820
+ schema.modelCallFacts.sourceKey,
821
+ ],
822
+ })
823
+ .returning({ id: schema.modelCallFacts.id });
824
+ if (insertedRows.length > 0) inserted += 1;
825
+ }
826
+ return inserted;
827
+ });
828
+ upserted += batchUpserted;
829
+ if (page.length < Math.min(batchSize, remaining)) break;
830
+ }
831
+
832
+ return { considered, upserted };
833
+ }
834
+
835
+ function numberOrNull(value: unknown): number | null {
836
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
837
+ }