@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.
- package/README.md +125 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3201 -1473
- package/dist/index.js.map +32 -25
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/aggregate-admission.d.ts +10 -0
- package/dist/objects/aggregate-admission.d.ts.map +1 -0
- package/dist/objects/preparation-lock.d.ts +6 -0
- package/dist/objects/preparation-lock.d.ts.map +1 -0
- package/dist/objects/prepare-queries.d.ts +5 -0
- package/dist/objects/prepare-queries.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +19 -0
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/query-indexes.d.ts +43 -0
- package/dist/objects/query-indexes.d.ts.map +1 -0
- package/dist/objects/reader.d.ts +7 -1
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +1 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/objects/text-count-index.d.ts +24 -0
- package/dist/objects/text-count-index.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +18 -38
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +3 -26
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +51 -9
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts +5 -7
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/outbox.d.ts.map +1 -1
- package/dist/ontology-storage/replacement-plans.d.ts +36 -0
- package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +11 -5
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
- package/dist/ontology-storage/source-roots.d.ts +14 -1
- package/dist/ontology-storage/source-roots.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +1 -2
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-limit-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +2 -0
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +28 -0
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-file-download-grant-storage.d.ts +11 -0
- package/dist/pg-file-download-grant-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -1
- package/dist/pg-rules-storage.d.ts.map +1 -1
- package/dist/transactions.d.ts +6 -2
- package/dist/transactions.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +53 -3
- package/src/migrations/053-replacement-plans.sql +72 -0
- package/src/migrations/054-slim-ontology-outbox.sql +108 -0
- package/src/migrations/055-object-query-preparation.sql +11 -0
- package/src/migrations/056-commit-touches.sql +41 -0
- package/src/migrations/057-audio-transcription.sql +10 -0
- package/src/migrations/058-reranking-model-kind.sql +11 -0
- package/src/migrations/059-file-download-grants.sql +16 -0
- package/src/migrations/060-drained-source-versions.sql +11 -0
- package/src/migrations.ts +20 -0
- package/src/objects/aggregate-admission.ts +21 -0
- package/src/objects/preparation-lock.ts +15 -0
- package/src/objects/prepare-queries.ts +181 -0
- package/src/objects/query-compiler.ts +404 -114
- package/src/objects/query-indexes.ts +348 -0
- package/src/objects/reader.ts +140 -51
- package/src/objects/storage.ts +15 -3
- package/src/objects/text-count-index.ts +455 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-session.ts +116 -275
- package/src/ontology-storage/materialization-state.ts +8 -259
- package/src/ontology-storage/materialization-writer.ts +618 -508
- package/src/ontology-storage/materializations.ts +106 -289
- package/src/ontology-storage/outbox.ts +46 -15
- package/src/ontology-storage/replacement-plans.ts +1110 -0
- package/src/ontology-storage/shared.ts +27 -15
- package/src/ontology-storage/source-cleanup.ts +22 -10
- package/src/ontology-storage/source-roots.ts +35 -8
- package/src/ontology-storage/sources.ts +0 -30
- package/src/pg-ai-cost-storage.ts +12 -0
- package/src/pg-ai-limit-storage.ts +13 -2
- package/src/pg-ai-usage-storage.ts +8 -0
- package/src/pg-execution-storage.ts +8 -3
- package/src/pg-file-download-grant-storage.ts +76 -0
- package/src/pg-projection-run-storage.ts +6 -1
- package/src/pg-rules-storage.ts +7 -3
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
|
89
|
-
remaining -= removed
|
|
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
|
-
/**
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 ?
|
|
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
|
|
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)
|
package/src/pg-rules-storage.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
package/src/transactions.ts
CHANGED
|
@@ -71,13 +71,15 @@ export async function runPgTransaction<T>(
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Hold one pool connection for `run`,
|
|
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
|
-
|
|
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"
|