@sixb/pg 0.1.16 → 0.1.17

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 (92) hide show
  1. package/README.md +125 -0
  2. package/dist/index.d.ts +15 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3201 -1473
  5. package/dist/index.js.map +32 -25
  6. package/dist/migrations.d.ts.map +1 -1
  7. package/dist/objects/aggregate-admission.d.ts +10 -0
  8. package/dist/objects/aggregate-admission.d.ts.map +1 -0
  9. package/dist/objects/preparation-lock.d.ts +6 -0
  10. package/dist/objects/preparation-lock.d.ts.map +1 -0
  11. package/dist/objects/prepare-queries.d.ts +5 -0
  12. package/dist/objects/prepare-queries.d.ts.map +1 -0
  13. package/dist/objects/query-compiler.d.ts +19 -0
  14. package/dist/objects/query-compiler.d.ts.map +1 -1
  15. package/dist/objects/query-indexes.d.ts +43 -0
  16. package/dist/objects/query-indexes.d.ts.map +1 -0
  17. package/dist/objects/reader.d.ts +7 -1
  18. package/dist/objects/reader.d.ts.map +1 -1
  19. package/dist/objects/storage.d.ts +1 -1
  20. package/dist/objects/storage.d.ts.map +1 -1
  21. package/dist/objects/text-count-index.d.ts +24 -0
  22. package/dist/objects/text-count-index.d.ts.map +1 -0
  23. package/dist/ontology-storage/index.d.ts +2 -0
  24. package/dist/ontology-storage/index.d.ts.map +1 -1
  25. package/dist/ontology-storage/materialization-session.d.ts +18 -38
  26. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  27. package/dist/ontology-storage/materialization-state.d.ts +3 -26
  28. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  29. package/dist/ontology-storage/materialization-writer.d.ts +51 -9
  30. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts +5 -7
  32. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  33. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  34. package/dist/ontology-storage/replacement-plans.d.ts +36 -0
  35. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
  36. package/dist/ontology-storage/shared.d.ts +11 -5
  37. package/dist/ontology-storage/shared.d.ts.map +1 -1
  38. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  39. package/dist/ontology-storage/source-roots.d.ts +14 -1
  40. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  41. package/dist/ontology-storage/sources.d.ts +1 -2
  42. package/dist/ontology-storage/sources.d.ts.map +1 -1
  43. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  44. package/dist/pg-ai-limit-storage.d.ts.map +1 -1
  45. package/dist/pg-ai-usage-storage.d.ts +2 -0
  46. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  47. package/dist/pg-execution-storage.d.ts +28 -0
  48. package/dist/pg-execution-storage.d.ts.map +1 -1
  49. package/dist/pg-file-download-grant-storage.d.ts +11 -0
  50. package/dist/pg-file-download-grant-storage.d.ts.map +1 -0
  51. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  52. package/dist/pg-rules-storage.d.ts.map +1 -1
  53. package/dist/transactions.d.ts +6 -2
  54. package/dist/transactions.d.ts.map +1 -1
  55. package/package.json +3 -3
  56. package/src/index.ts +53 -3
  57. package/src/migrations/053-replacement-plans.sql +72 -0
  58. package/src/migrations/054-slim-ontology-outbox.sql +108 -0
  59. package/src/migrations/055-object-query-preparation.sql +11 -0
  60. package/src/migrations/056-commit-touches.sql +41 -0
  61. package/src/migrations/057-audio-transcription.sql +10 -0
  62. package/src/migrations/058-reranking-model-kind.sql +11 -0
  63. package/src/migrations/059-file-download-grants.sql +16 -0
  64. package/src/migrations/060-drained-source-versions.sql +11 -0
  65. package/src/migrations.ts +20 -0
  66. package/src/objects/aggregate-admission.ts +21 -0
  67. package/src/objects/preparation-lock.ts +15 -0
  68. package/src/objects/prepare-queries.ts +181 -0
  69. package/src/objects/query-compiler.ts +404 -114
  70. package/src/objects/query-indexes.ts +348 -0
  71. package/src/objects/reader.ts +140 -51
  72. package/src/objects/storage.ts +15 -3
  73. package/src/objects/text-count-index.ts +455 -0
  74. package/src/ontology-storage/index.ts +3 -0
  75. package/src/ontology-storage/materialization-session.ts +116 -275
  76. package/src/ontology-storage/materialization-state.ts +8 -259
  77. package/src/ontology-storage/materialization-writer.ts +618 -508
  78. package/src/ontology-storage/materializations.ts +106 -289
  79. package/src/ontology-storage/outbox.ts +46 -15
  80. package/src/ontology-storage/replacement-plans.ts +1110 -0
  81. package/src/ontology-storage/shared.ts +27 -15
  82. package/src/ontology-storage/source-cleanup.ts +22 -10
  83. package/src/ontology-storage/source-roots.ts +35 -8
  84. package/src/ontology-storage/sources.ts +0 -30
  85. package/src/pg-ai-cost-storage.ts +12 -0
  86. package/src/pg-ai-limit-storage.ts +13 -2
  87. package/src/pg-ai-usage-storage.ts +8 -0
  88. package/src/pg-execution-storage.ts +8 -3
  89. package/src/pg-file-download-grant-storage.ts +76 -0
  90. package/src/pg-projection-run-storage.ts +6 -1
  91. package/src/pg-rules-storage.ts +7 -3
  92. package/src/transactions.ts +10 -3
@@ -1,6 +1,7 @@
1
1
  import { parseSixbFailure } from "@sixb/core/internal/errors"
2
2
  import { MaterializationConflictError } from "@sixb/core/internal/materialization"
3
3
  import {
4
+ type ClaimedOutboxDraft,
4
5
  sourceAssertionFromColumns,
5
6
  sourceEntityFromKey,
6
7
  } from "@sixb/core/internal/ontology-storage-provider"
@@ -8,7 +9,6 @@ import type {
8
9
  AssertSourceMaterializationExecutionInput,
9
10
  OntologyCommitRecord,
10
11
  OntologyCommitWrite,
11
- OntologyOutboxRecord,
12
12
  OntologySourceRecord,
13
13
  StageSourceAssertion,
14
14
  } from "@sixb/core/storage"
@@ -42,8 +42,6 @@ export interface PgOntologyCommitRow {
42
42
  readonly origin_run_id: string | null
43
43
  readonly origin_batch_ordinal: number | string | null
44
44
  readonly origin: unknown
45
- readonly requested_by: unknown | null
46
- readonly executor: unknown
47
45
  readonly ontology_revision: string
48
46
  readonly projection_revision: string | null
49
47
  readonly ownership_hash: string | null
@@ -104,7 +102,10 @@ export interface PgStoredOverrideRow {
104
102
  }
105
103
 
106
104
  export interface PgOntologyOutboxRow {
107
- readonly envelope: unknown
105
+ readonly id: string
106
+ readonly commit_id: string
107
+ readonly commit_ordinal: number | string
108
+ readonly event: unknown
108
109
  readonly available_at: Date | string
109
110
  readonly attempts: number | string
110
111
  readonly lease_id: string | null
@@ -194,14 +195,6 @@ export function commitRecord(row: PgOntologyCommitRow): OntologyCommitRecord {
194
195
  requestHash: row.request_hash,
195
196
  executionId: row.execution_id,
196
197
  origin: structuredClone(row.origin) as OntologyCommitWrite["origin"],
197
- ...(row.requested_by === null
198
- ? {}
199
- : {
200
- requestedBy: structuredClone(row.requested_by) as NonNullable<
201
- OntologyCommitWrite["requestedBy"]
202
- >,
203
- }),
204
- executor: structuredClone(row.executor) as OntologyCommitWrite["executor"],
205
198
  ontologyRevision: row.ontology_revision,
206
199
  ...(row.projection_revision === null ? {} : { projectionRevision: row.projection_revision }),
207
200
  ...(row.ownership_hash === null ? {} : { ownershipHash: row.ownership_hash }),
@@ -212,13 +205,23 @@ export function commitRecord(row: PgOntologyCommitRow): OntologyCommitRecord {
212
205
  return { ...base, intent, result } as OntologyCommitRecord
213
206
  }
214
207
 
215
- export function outboxRecord(row: PgOntologyOutboxRow): OntologyOutboxRecord {
208
+ /** A claimed row as stored: its draft, to rebuild from its commit. */
209
+ export function claimedOutboxDraft(row: PgOntologyOutboxRow): ClaimedOutboxDraft {
210
+ if (row.lease_id === null || row.lease_expires_at === null) {
211
+ throw new MaterializationConflictError(
212
+ "outbox-lease",
213
+ "Ontology outbox claim returned an unpaired lease."
214
+ )
215
+ }
216
216
  return {
217
- envelope: structuredClone(row.envelope) as OntologyOutboxRecord["envelope"],
217
+ id: row.id,
218
+ commitId: row.commit_id,
219
+ commitOrdinal: databaseSafeInteger(row.commit_ordinal, "Ontology outbox commit ordinal"),
220
+ event: structuredClone(row.event) as ClaimedOutboxDraft["event"],
218
221
  availableAt: toIsoString(row.available_at),
219
222
  attempts: databaseSafeInteger(row.attempts, "Ontology outbox attempts"),
220
223
  leaseId: row.lease_id,
221
- leaseExpiresAt: optionalIsoString(row.lease_expires_at),
224
+ leaseExpiresAt: toIsoString(row.lease_expires_at),
222
225
  publishedAt: optionalIsoString(row.published_at),
223
226
  lastFailure:
224
227
  row.last_failure === null
@@ -301,3 +304,12 @@ export async function assertProjectionExecution(
301
304
  export function ontologyLockKey(kind: string, ...parts: readonly string[]): string {
302
305
  return `ontology:${kind}:${JSON.stringify(parts)}`
303
306
  }
307
+
308
+ export function jsonTupleExpression(parts: readonly string[]): string {
309
+ return `concat('[', ${parts.join(", ',', ")}, ']')`
310
+ }
311
+
312
+ /** The canonical JSON key of a row's identity columns, as `objectRefKey`/`linkRefKey` render it. */
313
+ export function columnKeyExpression(columns: readonly string[]): string {
314
+ return jsonTupleExpression(columns.map((column) => `to_jsonb(${column})::text`))
315
+ }
@@ -5,6 +5,11 @@ import type {
5
5
  } from "@sixb/core/storage"
6
6
  import type { SQLClient } from "../pg-client"
7
7
 
8
+ /** A version outlives its replacement plan, which references it. */
9
+ const PLAN_PURGED = `NOT EXISTS (
10
+ SELECT 1 FROM ontology_replacement_plans AS plans WHERE plans.version_id = versions.version_id
11
+ )`
12
+
8
13
  /**
9
14
  * Superseded versions can still hold live roots (a delta keeps unchanged roots where they were),
10
15
  * so they are cleaned root by root once retired. An abandoned candidate was never published:
@@ -19,8 +24,8 @@ export async function purgeAbandonedSourceVersions(
19
24
  let materializationsDeleted = 0
20
25
  while (remaining > 0) {
21
26
  const [candidate] = await sql<{ readonly version_id: string }[]>`
22
- SELECT version_id FROM ontology_sources
23
- WHERE project_id = ${input.projectId} AND status = 'abandoned'
27
+ SELECT version_id FROM ontology_sources AS versions
28
+ WHERE project_id = ${input.projectId} AND status = 'abandoned' AND ${sql.unsafe(PLAN_PURGED)}
24
29
  ORDER BY terminal_at, source_id, materialization_id
25
30
  LIMIT 1 FOR UPDATE SKIP LOCKED
26
31
  `
@@ -75,7 +80,8 @@ export async function cleanupSourceVersions(
75
80
  let rowsDeleted = removedRows.length
76
81
  remaining -= rowsDeleted
77
82
  if (remaining > 0) {
78
- const removed = await sql`
83
+ // Each version's root_count follows its roots down, so a drained version is found by index.
84
+ const [removed] = await sql<{ readonly count: number }[]>`
79
85
  WITH selected AS (
80
86
  SELECT roots.ctid FROM ontology_source_roots AS roots
81
87
  JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
@@ -83,10 +89,18 @@ export async function cleanupSourceVersions(
83
89
  AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id)
84
90
  ORDER BY roots.retired_at, roots.id
85
91
  LIMIT ${remaining} FOR UPDATE OF roots SKIP LOCKED
86
- ) DELETE FROM ontology_source_roots AS roots USING selected WHERE roots.ctid = selected.ctid RETURNING 1
92
+ ), removed AS (
93
+ DELETE FROM ontology_source_roots AS roots USING selected WHERE roots.ctid = selected.ctid
94
+ RETURNING roots.version_id
95
+ ), drained AS (
96
+ UPDATE ontology_sources AS versions SET root_count = versions.root_count - counts.count
97
+ FROM (SELECT version_id, count(*)::int AS count FROM removed GROUP BY version_id) AS counts
98
+ WHERE versions.version_id = counts.version_id
99
+ RETURNING counts.count
100
+ ) SELECT COALESCE(sum(count), 0)::int AS count FROM drained
87
101
  `
88
- rowsDeleted += removed.length
89
- remaining -= removed.length
102
+ rowsDeleted += removed?.count ?? 0
103
+ remaining -= removed?.count ?? 0
90
104
  }
91
105
  const removedManifests =
92
106
  remaining === 0
@@ -94,10 +108,8 @@ export async function cleanupSourceVersions(
94
108
  : await sql`
95
109
  WITH selected AS (
96
110
  SELECT versions.ctid FROM ontology_sources AS versions
97
- WHERE project_id = ${input.projectId} AND status = 'superseded'
98
- AND terminal_at < ${input.terminalBefore}
99
- AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
100
- WHERE roots.version_id = versions.version_id)
111
+ WHERE project_id = ${input.projectId} AND status = 'superseded' AND root_count = 0
112
+ AND terminal_at < ${input.terminalBefore} AND ${sql.unsafe(PLAN_PURGED)}
101
113
  ORDER BY terminal_at, source_id, materialization_id LIMIT ${remaining} FOR UPDATE SKIP LOCKED
102
114
  ) DELETE FROM ontology_sources AS versions USING selected WHERE versions.ctid = selected.ctid RETURNING 1
103
115
  `
@@ -11,7 +11,7 @@ import {
11
11
  import type { SourceActivationWrite, StageSourceRowsInput } from "@sixb/core/storage"
12
12
  import type { SQLClient } from "../pg-client"
13
13
  import type { PgOntologySourceAssertionRow } from "./shared"
14
- import { jsonParameter, type PgOntologySourceRow } from "./shared"
14
+ import { columnKeyExpression, jsonParameter, type PgOntologySourceRow } from "./shared"
15
15
 
16
16
  /**
17
17
  * Source storage in three levels: a version (`ontology_sources`, one per run), its roots (one per
@@ -22,7 +22,7 @@ import { jsonParameter, type PgOntologySourceRow } from "./shared"
22
22
  * version; the candidate's own roots are never rewritten. One live root per source and key holds
23
23
  * because activation, under the source fence, retires every root the candidate replaces.
24
24
  */
25
- const PUBLISHED = "('active', 'superseded')"
25
+ export const PUBLISHED = "('active', 'superseded')"
26
26
 
27
27
  /** The columns `sourceAssertion` reads, over `versions`, `roots` and `rows`. */
28
28
  export function sourceAssertionColumns(sql: SQLClient) {
@@ -178,7 +178,10 @@ export async function stageSourceRoots(
178
178
  )
179
179
  }
180
180
 
181
- /** Retires what the candidate replaces; its own roots go live when its version turns active. */
181
+ /**
182
+ * Retires what the candidate replaces; its own roots go live when its version turns active. Every
183
+ * link those roots assert changes its live source, which the commit records as touched.
184
+ */
182
185
  export async function activateSourceRoots(
183
186
  sql: SQLClient,
184
187
  projectId: string,
@@ -199,19 +202,43 @@ export async function activateSourceRoots(
199
202
  AND versions.version_id = roots.version_id AND versions.project_id = ${projectId}
200
203
  AND versions.source_id = ${activation.source.projectionId}
201
204
  AND versions.status IN ${sql.unsafe(PUBLISHED)}`
202
- if (candidate.base_materialization_id === null) {
203
- await sql`
205
+ const retired =
206
+ candidate.base_materialization_id === null
207
+ ? sql`
204
208
  UPDATE ontology_source_roots AS roots SET retired_at = ${activation.updatedAt}
205
209
  FROM ontology_sources AS versions WHERE ${live}
210
+ RETURNING roots.id
206
211
  `
207
- } else {
208
- await sql`
212
+ : sql`
209
213
  UPDATE ontology_source_roots AS roots SET retired_at = ${activation.updatedAt}
210
214
  FROM ontology_source_roots AS changed, ontology_sources AS versions
211
215
  WHERE changed.version_id = ${candidate.version_id} AND roots.project_id = ${projectId}
212
216
  AND roots.root_key = changed.root_key AND ${live}
217
+ RETURNING roots.id
213
218
  `
214
- }
219
+ await sql`
220
+ WITH retired AS (${retired}), changed AS (
221
+ SELECT id FROM retired
222
+ UNION ALL
223
+ SELECT id FROM ontology_source_roots WHERE version_id = ${candidate.version_id}
224
+ )
225
+ INSERT INTO ontology_commit_touches (project_id, entity_kind, identity_key)
226
+ SELECT DISTINCT ${projectId}, 'link', ${sql.unsafe(
227
+ columnKeyExpression([
228
+ "rows.source_type_id",
229
+ "rows.source_primary_id",
230
+ "rows.link_id",
231
+ "rows.target_type_id",
232
+ "rows.target_primary_id",
233
+ ])
234
+ )}
235
+ FROM changed
236
+ CROSS JOIN LATERAL (
237
+ SELECT * FROM ontology_source_rows AS rows
238
+ WHERE rows.root_id = changed.id AND rows.entity_kind = 'link'
239
+ OFFSET 0
240
+ ) AS rows
241
+ `
215
242
  // Deletions are never live; retiring them lets cleanup remove them with their version.
216
243
  await sql`
217
244
  UPDATE ontology_source_roots SET retired_at = ${activation.updatedAt}
@@ -32,8 +32,6 @@ import type {
32
32
  PurgeAbandonedSourceMaterializationsInput,
33
33
  StageSourceRowsInput,
34
34
  StageSourceRowsResult,
35
- SummarizeTerminalSourceMaterializationsInput,
36
- TerminalSourceMaterializationSummary,
37
35
  } from "@sixb/core/storage"
38
36
  import type { SQLClient } from "../pg-client"
39
37
  import { lockAdvisoryKeys } from "../transactions"
@@ -307,29 +305,6 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
307
305
  })
308
306
  }
309
307
 
310
- async summarizeTerminal(
311
- input: SummarizeTerminalSourceMaterializationsInput
312
- ): Promise<TerminalSourceMaterializationSummary> {
313
- return this.runRootOperation(async (sql) => {
314
- assertNonblank(input.projectId, "Terminal source summary project id")
315
- const [summary] = await sql<
316
- {
317
- readonly terminal_count: number
318
- readonly oldest_terminal_at: Date | string | null
319
- }[]
320
- >`
321
- SELECT COUNT(*)::int AS terminal_count, MIN(terminal_at) AS oldest_terminal_at
322
- FROM ontology_sources
323
- WHERE project_id = ${input.projectId}
324
- AND status IN ('superseded', 'abandoned')
325
- `
326
- return {
327
- count: summary?.terminal_count ?? 0,
328
- oldestTerminalAt: toOptionalIsoString(summary?.oldest_terminal_at),
329
- }
330
- })
331
- }
332
-
333
308
  private async abandonCandidate(
334
309
  sql: SQLClient,
335
310
  input: AbandonSourceMaterializationCandidateInput
@@ -642,11 +617,6 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
642
617
  }
643
618
  }
644
619
 
645
- function toOptionalIsoString(value: Date | string | null | undefined): string | null {
646
- if (value === null || value === undefined) return null
647
- return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
648
- }
649
-
650
620
  function numberOrNull(value: number | string | null): number | null {
651
621
  return value === null ? null : Number(value)
652
622
  }
@@ -111,6 +111,7 @@ export class PgAiCostStorage implements AiCostStorage {
111
111
  usage.cache_write_input_tokens,
112
112
  usage.text_output_tokens,
113
113
  usage.reasoning_output_tokens,
114
+ usage.audio_duration_ms,
114
115
  usage.reporting_status,
115
116
  CASE
116
117
  WHEN direct_agent.kind = 'conversation' THEN 'agent'
@@ -204,6 +205,8 @@ export class PgAiCostStorage implements AiCostStorage {
204
205
  SUM(text_output_tokens)::text AS text_output_tokens_sum,
205
206
  COUNT(reasoning_output_tokens)::text AS reasoning_output_tokens_count,
206
207
  SUM(reasoning_output_tokens)::text AS reasoning_output_tokens_sum,
208
+ COUNT(audio_duration_ms)::text AS audio_duration_ms_count,
209
+ SUM(audio_duration_ms)::text AS audio_duration_ms_sum,
207
210
  COUNT(*) FILTER (WHERE valuation_status = 'rated')::text AS rated_call_count,
208
211
  COUNT(*) FILTER (WHERE valuation_status = 'unpriceable')::text
209
212
  AS unpriceable_call_count,
@@ -331,6 +334,7 @@ interface UsageRow {
331
334
  readonly call_id: string
332
335
  readonly provider_id: string
333
336
  readonly requested_model_id: string
337
+ readonly model_kind: AiModelCallUsageRecord["modelKind"] | null
334
338
  readonly requested_reasoning: string | Record<string, unknown> | null
335
339
  readonly response_model_id: string | null
336
340
  readonly response_id: string
@@ -342,6 +346,7 @@ interface UsageRow {
342
346
  readonly cache_write_input_tokens: number | string | null
343
347
  readonly text_output_tokens: number | string | null
344
348
  readonly reasoning_output_tokens: number | string | null
349
+ readonly audio_duration_ms: number | string | null
345
350
  readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
346
351
  readonly raw_usage: ReadonlyJsonObject | string | null
347
352
  readonly provider_ids: Record<string, unknown> | string | null
@@ -396,6 +401,8 @@ interface AggregateRow {
396
401
  readonly text_output_tokens_sum: string | null
397
402
  readonly reasoning_output_tokens_count: string
398
403
  readonly reasoning_output_tokens_sum: string | null
404
+ readonly audio_duration_ms_count: string
405
+ readonly audio_duration_ms_sum: string | null
399
406
  readonly rated_call_count: string
400
407
  readonly unpriceable_call_count: string
401
408
  readonly unvalued_call_count: string
@@ -478,6 +485,7 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
478
485
  row.reasoning_output_tokens_count,
479
486
  row.reasoning_output_tokens_sum
480
487
  ),
488
+ audioDurationMs: meterFragment(row.audio_duration_ms_count, row.audio_duration_ms_sum),
481
489
  },
482
490
  costs: {
483
491
  ...(row.amount_currency === null || row.amount_nanos === null
@@ -549,6 +557,7 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
549
557
  ),
550
558
  }),
551
559
  requestedModelId: row.requested_model_id,
560
+ ...(row.model_kind === null ? {} : { modelKind: row.model_kind }),
552
561
  ...(row.requested_reasoning === null
553
562
  ? {}
554
563
  : { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) }),
@@ -575,6 +584,9 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
575
584
  ...(number(row.reasoning_output_tokens) === undefined
576
585
  ? {}
577
586
  : { reasoningOutputTokens: number(row.reasoning_output_tokens) }),
587
+ ...(number(row.audio_duration_ms) === undefined
588
+ ? {}
589
+ : { audioDurationMs: number(row.audio_duration_ms) }),
578
590
  reportingStatus: row.reporting_status,
579
591
  },
580
592
  ...(row.raw_usage === null ? {} : { rawUsage: parseJsonObject(row.raw_usage) }),
@@ -2,6 +2,7 @@ import {
2
2
  type AiLimitAccountingEntry,
3
3
  AiLimitOperationLock,
4
4
  aiLimitAmountKey,
5
+ aiLimitMeterApplies,
5
6
  aiLimitQuantityFromAmount,
6
7
  aiLimitReservationBuckets,
7
8
  aiLimitReservationRequestKey,
@@ -172,7 +173,12 @@ export class PgAiLimitStorage implements AiLimitStorage {
172
173
  const unavailablePolicies: AiLimitPolicyStatus[] = []
173
174
  const unavailableReasons = new Set<"missingEstimate" | "incompleteAccounting">()
174
175
  for (const policy of enabledPolicies) {
175
- if (!subjectKeys.has(aiLimitSubjectKey(policy.subject))) continue
176
+ if (
177
+ !subjectKeys.has(aiLimitSubjectKey(policy.subject)) ||
178
+ !aiLimitMeterApplies(policy.limit.meter, request.modelKind)
179
+ ) {
180
+ continue
181
+ }
176
182
  const limit = normalizeAiLimitAmount(policy.limit)
177
183
  const estimate = estimates.get(aiLimitAmountKey(limit))
178
184
  const status = await this.statusForPolicy(tx, policy, request.period)
@@ -212,7 +218,8 @@ export class PgAiLimitStorage implements AiLimitStorage {
212
218
  const buckets = aiLimitReservationBuckets(
213
219
  enabledPolicies,
214
220
  request.subjects,
215
- request.estimates
221
+ request.estimates,
222
+ request.modelKind
216
223
  )
217
224
  if (buckets.length === 0) return { status: "notRequired" }
218
225
  for (const bucket of buckets) {
@@ -587,6 +594,7 @@ export class PgAiLimitStorage implements AiLimitStorage {
587
594
  SELECT usage.project_id, usage.id AS usage_record_id, usage.execution_id,
588
595
  usage.attempt, usage.call_id, usage.occurred_at,
589
596
  usage.total_tokens::text AS total_tokens,
597
+ usage.model_kind,
590
598
  executions.requested_by_user_id,
591
599
  executions.requested_by_service_account_id,
592
600
  valuations.status AS valuation_status,
@@ -632,6 +640,7 @@ export class PgAiLimitStorage implements AiLimitStorage {
632
640
  SELECT usage.project_id, usage.id AS usage_record_id, usage.execution_id,
633
641
  usage.attempt, usage.call_id, usage.occurred_at,
634
642
  usage.total_tokens::text AS total_tokens,
643
+ usage.model_kind,
635
644
  executions.requested_by_user_id,
636
645
  executions.requested_by_service_account_id,
637
646
  valuations.status AS valuation_status,
@@ -717,6 +726,7 @@ interface AiLimitAccountingRow {
717
726
  readonly call_id: string
718
727
  readonly occurred_at: Date | string
719
728
  readonly total_tokens: string | null
729
+ readonly model_kind: AiLimitAccountingEntry["modelKind"] | null
720
730
  readonly requested_by_user_id: string | null
721
731
  readonly requested_by_service_account_id: string | null
722
732
  readonly valuation_status: "rated" | "unpriceable" | null
@@ -804,6 +814,7 @@ function accountingEntryFromRow(
804
814
  attempt: Number(row.attempt),
805
815
  callId: row.call_id,
806
816
  occurredAt: new Date(row.occurred_at),
817
+ ...(row.model_kind === null ? {} : { modelKind: row.model_kind }),
807
818
  ...(totalTokens !== undefined && Number.isSafeInteger(totalTokens) ? { totalTokens } : {}),
808
819
  requesterGroupIds,
809
820
  ...(requester ? { requester } : {}),
@@ -38,6 +38,7 @@ export class PgAiUsageStorage implements AiUsageStorage {
38
38
  provider_id,
39
39
  provider_ids,
40
40
  requested_model_id,
41
+ model_kind,
41
42
  requested_reasoning,
42
43
  response_model_id,
43
44
  response_id,
@@ -49,6 +50,7 @@ export class PgAiUsageStorage implements AiUsageStorage {
49
50
  cache_write_input_tokens,
50
51
  text_output_tokens,
51
52
  reasoning_output_tokens,
53
+ audio_duration_ms,
52
54
  reporting_status,
53
55
  raw_usage,
54
56
  occurred_at,
@@ -62,6 +64,7 @@ export class PgAiUsageStorage implements AiUsageStorage {
62
64
  ${record.providerId},
63
65
  ${record.providerIds === undefined ? null : JSON.stringify(record.providerIds)}::jsonb,
64
66
  ${record.requestedModelId},
67
+ ${record.modelKind ?? null},
65
68
  ${
66
69
  record.requestedReasoning === undefined
67
70
  ? null
@@ -77,6 +80,7 @@ export class PgAiUsageStorage implements AiUsageStorage {
77
80
  ${record.usage.cacheWriteInputTokens ?? null},
78
81
  ${record.usage.textOutputTokens ?? null},
79
82
  ${record.usage.reasoningOutputTokens ?? null},
83
+ ${record.usage.audioDurationMs ?? null},
80
84
  ${record.usage.reportingStatus},
81
85
  ${record.rawUsage === undefined ? null : JSON.stringify(record.rawUsage)}::text::jsonb,
82
86
  ${record.occurredAt},
@@ -197,6 +201,7 @@ export class PgAiUsageStorage implements AiUsageStorage {
197
201
  ),
198
202
  }),
199
203
  requestedModelId: row.requested_model_id,
204
+ ...(row.model_kind === null ? {} : { modelKind: row.model_kind }),
200
205
  ...(row.requested_reasoning === null
201
206
  ? {}
202
207
  : { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) }),
@@ -257,6 +262,7 @@ interface AiUsageRow {
257
262
  readonly provider_id: string
258
263
  readonly provider_ids: Record<string, unknown> | string | null
259
264
  readonly requested_model_id: string
265
+ readonly model_kind: AiModelCallUsageRecord["modelKind"] | null
260
266
  readonly requested_reasoning: string | Record<string, unknown> | null
261
267
  readonly response_model_id: string | null
262
268
  readonly response_id: string
@@ -268,6 +274,7 @@ interface AiUsageRow {
268
274
  readonly cache_write_input_tokens: number | string | null
269
275
  readonly text_output_tokens: number | string | null
270
276
  readonly reasoning_output_tokens: number | string | null
277
+ readonly audio_duration_ms: number | string | null
271
278
  readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
272
279
  readonly raw_usage: ReadonlyJsonObject | string | null
273
280
  readonly occurred_at: Date | string
@@ -311,6 +318,7 @@ function usageFromRow(row: AiUsageRow): AiModelCallUsageInput {
311
318
  ...(row.reasoning_output_tokens === null
312
319
  ? {}
313
320
  : { reasoningOutputTokens: Number(row.reasoning_output_tokens) }),
321
+ ...(row.audio_duration_ms === null ? {} : { audioDurationMs: Number(row.audio_duration_ms) }),
314
322
  }
315
323
  }
316
324
 
@@ -87,7 +87,7 @@ export class PgExecutionStorage implements ExecutionStorage {
87
87
  if (!created) {
88
88
  throw new Error("[SixbPg] Execution insert returned no row.")
89
89
  }
90
- return executionRecordFromStorageRow(toStorageRow(created))
90
+ return pgExecutionRecord(created)
91
91
  } catch (error) {
92
92
  if (isUniqueViolation(error)) {
93
93
  throw new ExecutionStorageError(
@@ -109,10 +109,15 @@ export class PgExecutionStorage implements ExecutionStorage {
109
109
  FROM executions
110
110
  WHERE project_id = ${params.projectId} AND id = ${params.id}
111
111
  `
112
- return row ? executionRecordFromStorageRow(toStorageRow(row)) : null
112
+ return row ? pgExecutionRecord(row) : null
113
113
  }
114
114
  }
115
115
 
116
+ /** An `executions` row as its record, for any store that reads executions alongside its own rows. */
117
+ export function pgExecutionRecord(row: PgExecutionRow): ExecutionRecord {
118
+ return executionRecordFromStorageRow(toStorageRow(row))
119
+ }
120
+
116
121
  function toStorageRow(row: PgExecutionRow): ExecutionStorageRow {
117
122
  return {
118
123
  projectId: row.project_id,
@@ -141,7 +146,7 @@ function toStorageRow(row: PgExecutionRow): ExecutionStorageRow {
141
146
  }
142
147
  }
143
148
 
144
- interface PgExecutionRow {
149
+ export interface PgExecutionRow {
145
150
  readonly project_id: string
146
151
  readonly id: string
147
152
  readonly executor_kind: ExecutionStorageRow["executorKind"]
@@ -0,0 +1,76 @@
1
+ import { parseFileDownloadGrantRow } from "@sixb/core/internal/storage"
2
+ import type {
3
+ DeleteExpiredFileDownloadGrantsInput,
4
+ FileDownloadGrantRecord,
5
+ FileDownloadGrantStorage,
6
+ FindFileDownloadGrantInput,
7
+ RevokeFileDownloadGrantInput,
8
+ } from "@sixb/core/storage"
9
+ import type { PgStoreClient } from "./transactions"
10
+
11
+ export class PgFileDownloadGrantStorage implements FileDownloadGrantStorage {
12
+ constructor(private readonly sql: PgStoreClient) {}
13
+
14
+ async create(record: FileDownloadGrantRecord): Promise<void> {
15
+ await this.sql`
16
+ INSERT INTO file_download_grants (
17
+ project_id, id, token_hash, file, execution_id, created_at, expires_at
18
+ ) VALUES (
19
+ ${record.projectId}, ${record.id}, ${record.tokenHash},
20
+ ${JSON.stringify(record.file)}::text::jsonb, ${record.executionId},
21
+ ${record.createdAt}, ${record.expiresAt}
22
+ )
23
+ `
24
+ }
25
+
26
+ async findByTokenHash(
27
+ input: FindFileDownloadGrantInput
28
+ ): Promise<FileDownloadGrantRecord | null> {
29
+ const [row] = await this.sql<PgFileDownloadGrantRow[]>`
30
+ SELECT * FROM file_download_grants
31
+ WHERE project_id = ${input.projectId} AND token_hash = ${input.tokenHash}
32
+ `
33
+ return row ? grantFromRow(row) : null
34
+ }
35
+
36
+ async revoke(input: RevokeFileDownloadGrantInput): Promise<FileDownloadGrantRecord | null> {
37
+ const [row] = await this.sql<PgFileDownloadGrantRow[]>`
38
+ UPDATE file_download_grants
39
+ SET revoked_at = COALESCE(revoked_at, ${input.revokedAt})
40
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
41
+ RETURNING *
42
+ `
43
+ return row ? grantFromRow(row) : null
44
+ }
45
+
46
+ async deleteExpired(input: DeleteExpiredFileDownloadGrantsInput): Promise<number> {
47
+ const rows = await this.sql`
48
+ DELETE FROM file_download_grants
49
+ WHERE project_id = ${input.projectId} AND id IN (
50
+ SELECT id FROM file_download_grants
51
+ WHERE project_id = ${input.projectId} AND expires_at < ${input.expiredBefore}
52
+ ORDER BY expires_at
53
+ LIMIT ${input.limit}
54
+ )
55
+ RETURNING id
56
+ `
57
+ return rows.length
58
+ }
59
+ }
60
+
61
+ interface PgFileDownloadGrantRow {
62
+ readonly [column: string]: unknown
63
+ }
64
+
65
+ function grantFromRow(row: PgFileDownloadGrantRow): FileDownloadGrantRecord {
66
+ return parseFileDownloadGrantRow({
67
+ id: row.id,
68
+ projectId: row.project_id,
69
+ tokenHash: row.token_hash,
70
+ file: row.file,
71
+ executionId: row.execution_id,
72
+ createdAt: row.created_at,
73
+ expiresAt: row.expires_at,
74
+ revokedAt: row.revoked_at,
75
+ })
76
+ }
@@ -348,7 +348,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
348
348
  async list(input: ListProjectionRunsInput): Promise<ListProjectionRunsResult> {
349
349
  assertProjectionRunNonEmpty(input.projectId, "projectId")
350
350
 
351
- if (input.statuses && input.statuses.length === 0) {
351
+ if (input.statuses?.length === 0 || input.errorCodes?.length === 0) {
352
352
  return { runs: [], hasMore: false, total: 0 }
353
353
  }
354
354
 
@@ -394,6 +394,11 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
394
394
  whereClauses.push(`status IN (${placeholders.join(", ")})`)
395
395
  params.push(...input.statuses)
396
396
  }
397
+ if (input.errorCodes) {
398
+ const placeholders = input.errorCodes.map(() => `$${index++}`)
399
+ whereClauses.push(`error->>'code' IN (${placeholders.join(", ")})`)
400
+ params.push(...input.errorCodes)
401
+ }
397
402
  if (input.startedAfter) {
398
403
  whereClauses.push(`COALESCE(started_at, queued_at) >= $${index++}`)
399
404
  params.push(input.startedAfter)
@@ -151,7 +151,11 @@ export class PgRulesStorage implements RulesStorage {
151
151
  ): Promise<ListRuleStatesReconciliationPageResult> {
152
152
  assertPositiveLimit(input.limit)
153
153
  const params: SqlParameter[] = [input.projectId]
154
- const cursor = input.after ? "AND (rule_id, object_type_id, primary_id) > ($2, $3, $4)" : ""
154
+ // The cursor follows the primary key column for column, so the index scan starts at the
155
+ // cursor instead of at the cursor's rule. `subject_kind` is always 'object' (CHECK).
156
+ const cursor = input.after
157
+ ? "AND (rule_id, subject_kind, object_type_id, primary_id) > ($2, 'object', $3, $4)"
158
+ : ""
155
159
  if (input.after) {
156
160
  params.push(input.after.ruleId, input.after.objectTypeId, input.after.primaryId)
157
161
  }
@@ -161,8 +165,8 @@ export class PgRulesStorage implements RulesStorage {
161
165
  `
162
166
  SELECT project_id, rule_id, subject_kind, object_type_id, primary_id, triggered_at
163
167
  FROM rule_states
164
- WHERE project_id = $1 AND subject_kind = 'object' ${cursor}
165
- ORDER BY rule_id ASC, object_type_id ASC, primary_id ASC
168
+ WHERE project_id = $1 ${cursor}
169
+ ORDER BY rule_id ASC, subject_kind ASC, object_type_id ASC, primary_id ASC
166
170
  LIMIT ${limitParameter}
167
171
  `,
168
172
  params
@@ -71,13 +71,15 @@ export async function runPgTransaction<T>(
71
71
  }
72
72
 
73
73
  /**
74
- * Hold one pool connection for `run`, and hand it back to the pool only while it is still usable.
74
+ * Hold one pool connection for `run`, or reuse a caller-owned reserved connection.
75
+ * Hand an owned connection back to the pool only while it is still usable.
75
76
  * A connection whose session is gone stays out of the pool, which replaces it on its own.
76
77
  */
77
78
  export async function withReservedPgConnection<T>(
78
- sql: SQL,
79
+ sql: SQL | ReservedSQL,
79
80
  run: (connection: ReservedSQL) => Promise<T>
80
81
  ): Promise<T> {
82
+ if (!("reserve" in sql)) return run(sql)
81
83
  const connection = await sql.reserve()
82
84
  let usable = true
83
85
  try {
@@ -184,7 +186,12 @@ function advisoryLockParts(key: string): readonly [number, number] {
184
186
  return [hash.readInt32BE(0), hash.readInt32BE(4)]
185
187
  }
186
188
 
187
- function canStartPgTransaction(sql: PgStoreClient): sql is SQL {
189
+ /** Whether `sql` already runs within a transaction, provider-owned or external. */
190
+ export function isWithinPgTransaction(sql: PgStoreClient): boolean {
191
+ return !canStartPgTransaction(sql)
192
+ }
193
+
194
+ export function canStartPgTransaction(sql: PgStoreClient): sql is SQL {
188
195
  return (
189
196
  !pgTransactionClients.has(sql) &&
190
197
  typeof (sql as { readonly reserve?: unknown }).reserve === "function"