@opengeni/db 0.27.11 → 0.28.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "0.27.11",
3
+ "version": "0.28.1",
4
4
  "description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -51,8 +51,8 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@opengeni/codex": "^0.2.11",
54
- "@opengeni/config": "^0.10.14",
55
- "@opengeni/contracts": "^0.38.3",
54
+ "@opengeni/config": "^0.11.0",
55
+ "@opengeni/contracts": "^0.39.0",
56
56
  "@opengeni/network": "^0.2.0",
57
57
  "drizzle-orm": "^0.45.2",
58
58
  "postgres": "^3.4.7"
package/src/index.ts CHANGED
@@ -21967,7 +21967,6 @@ export async function applyContextCompaction(
21967
21967
  expectedAttemptId: string;
21968
21968
  replacementItems: Array<Record<string, unknown>>;
21969
21969
  summaryItem: Record<string, unknown>;
21970
- replacementInputTokens: number;
21971
21970
  clearRequestedCompaction?: boolean;
21972
21971
  eventPayload?: Record<string, unknown>;
21973
21972
  },
@@ -22056,7 +22055,10 @@ export async function applyContextCompaction(
22056
22055
  await tx
22057
22056
  .update(schema.sessions)
22058
22057
  .set({
22059
- lastInputTokens: Math.max(0, Math.floor(input.replacementInputTokens)),
22058
+ // The active model input changed without an ordinary provider call.
22059
+ // Keep this field provider-only and let the first successful
22060
+ // post-compaction response install the next authoritative count.
22061
+ lastInputTokens: null,
22060
22062
  ...(input.clearRequestedCompaction ? { compactRequested: false } : {}),
22061
22063
  ...(insertedEvents.length > 0
22062
22064
  ? {
@@ -22279,8 +22281,9 @@ export async function nextSessionHistoryPosition(
22279
22281
  }
22280
22282
 
22281
22283
  /**
22282
- * Record the actual input-token count of the most recent turn's final model
22283
- * call, for the next turn's pre-read compaction trigger.
22284
+ * Replace the input-token count for the most recent authoritative terminal
22285
+ * response. Null means that response supplied no usable final-call count, so
22286
+ * the next turn must not reuse an older response's value for compaction.
22284
22287
  */
22285
22288
  export async function setSessionLastInputTokensForTurnAttempt(
22286
22289
  db: Database,
@@ -22290,7 +22293,7 @@ export async function setSessionLastInputTokensForTurnAttempt(
22290
22293
  turnId: string;
22291
22294
  expectedExecutionGeneration: number;
22292
22295
  expectedAttemptId: string;
22293
- lastInputTokens: number;
22296
+ lastInputTokens: number | null;
22294
22297
  },
22295
22298
  ): Promise<boolean> {
22296
22299
  return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
@@ -22348,8 +22351,8 @@ export class SessionContextBusyError extends Error {
22348
22351
  * reserved for an approval that paused mid-turn, and the API forbids a
22349
22352
  * clear while such an approval or active turn exists.
22350
22353
  *
22351
- * Also resets last_input_tokens to 0 so the next turn's compaction trigger
22352
- * starts fresh against the now-short context.
22354
+ * Also clears last_input_tokens because no provider has observed the new
22355
+ * context yet.
22353
22356
  *
22354
22357
  * Idempotent: a re-run supersedes the (now sole, already-marker) active row,
22355
22358
  * inserts another marker at the next position. The post-condition (one active
@@ -22441,7 +22444,7 @@ export async function clearSessionContext(
22441
22444
 
22442
22445
  await tx
22443
22446
  .update(schema.sessions)
22444
- .set({ lastInputTokens: 0, updatedAt: new Date() })
22447
+ .set({ lastInputTokens: null, updatedAt: new Date() })
22445
22448
  .where(
22446
22449
  and(
22447
22450
  eq(schema.sessions.workspaceId, input.workspaceId),
@@ -29167,67 +29170,87 @@ async function verifyWorkspaceMutationSettlementForAuthority(
29167
29170
  },
29168
29171
  ): Promise<void> {
29169
29172
  const operation = normalizeWorkspaceMutationOperation(input.operation);
29170
- const settlement: SandboxWorkspaceMutationSettlementResult = await withRlsContext(
29171
- db,
29173
+ const settleOnce = async (): Promise<SandboxWorkspaceMutationSettlementResult> =>
29174
+ await withRlsContext(
29175
+ db,
29176
+ {
29177
+ accountId: authorityInput.accountId,
29178
+ workspaceId: authorityInput.workspaceId,
29179
+ },
29180
+ async (scopedDb) =>
29181
+ await scopedDb.transaction(async (txRaw) => {
29182
+ const tx = txRaw as unknown as Database;
29183
+ // Admission and settlement must take the same canonical ownership
29184
+ // prefix before either touches the lease. The former order
29185
+ // (admission row -> authority -> lease) deadlocked a completed
29186
+ // parallel exec settlement against retained-process promotion
29187
+ // (admission row -> lease -> authority). PostgreSQL then rolled
29188
+ // back the losing admission settlement and permanently blocked
29189
+ // checkpoint capture.
29190
+ //
29191
+ // Preserve physical-settlement semantics when mutable authority
29192
+ // is stale: retain the typed fence, settle the exact immutable
29193
+ // admission below, commit, and only then reject its output.
29194
+ let authority: LockedWorkspaceMutationAuthority | null = null;
29195
+ let authorityFailure: SandboxWorkspaceMutationSettlementResult | null = null;
29196
+ try {
29197
+ authority = await lockWorkspaceMutationAuthorityTx(tx, authorityInput);
29198
+ } catch (error) {
29199
+ const failure = workspaceMutationAuthorityFailure(error);
29200
+ if (!failure) throw error;
29201
+ authorityFailure = failure;
29202
+ }
29203
+
29204
+ const actorKind = authorityInput.kind;
29205
+ const actorId =
29206
+ authorityInput.kind === "turn"
29207
+ ? authorityInput.attemptId
29208
+ : authorityInput.kind === "direct"
29209
+ ? authorityInput.requestId
29210
+ : authorityInput.processId;
29211
+ const admission = await selectExactAdmissionForUpdate(tx, {
29212
+ accountId: authorityInput.accountId,
29213
+ workspaceId: authorityInput.workspaceId,
29214
+ admissionId: input.admission.id,
29215
+ actorKind,
29216
+ actorId,
29217
+ sessionId: authorityInput.sessionId,
29218
+ admittedWorkspaceGeneration: input.admission.workspaceGeneration,
29219
+ operation,
29220
+ });
29221
+ if (
29222
+ !admission ||
29223
+ !admissionMatchesSnapshot(admission, input.admission) ||
29224
+ !admissionSnapshotMatchesAuthorityInput(input.admission, authorityInput) ||
29225
+ admission.provider_outcome === "retained" ||
29226
+ (admission.provider_outcome && admission.provider_outcome !== input.outcome)
29227
+ ) {
29228
+ return {
29229
+ failure: "admission_fenced" as const,
29230
+ detail: "Workspace mutation settlement did not match its exact durable admission",
29231
+ };
29232
+ }
29233
+ if (!admission.settled_at) {
29234
+ await tx.execute(sql`
29235
+ update sandbox_workspace_mutation_admissions set
29236
+ provider_outcome = ${input.outcome}, settled_at = now()
29237
+ where id = ${input.admission.id} and settled_at is null
29238
+ `);
29239
+ }
29240
+ if (input.outcome === "rejected") return { failure: null };
29241
+ if (authorityFailure) return authorityFailure;
29242
+ if (!authority) {
29243
+ throw new Error("Workspace mutation settlement lost its locked authority");
29244
+ }
29245
+ return await verifyResolvedAdmissionAuthority(tx, authority, admission);
29246
+ }),
29247
+ );
29248
+ const settlement = await runIdempotentPersistenceTransaction(
29172
29249
  {
29173
- accountId: authorityInput.accountId,
29174
- workspaceId: authorityInput.workspaceId,
29250
+ stage: "sandbox_workspace_mutation_settlement",
29251
+ maxAttempts: 5,
29175
29252
  },
29176
- async (scopedDb) =>
29177
- await scopedDb.transaction(async (txRaw) => {
29178
- const tx = txRaw as unknown as Database;
29179
- const actorKind = authorityInput.kind;
29180
- const actorId =
29181
- authorityInput.kind === "turn"
29182
- ? authorityInput.attemptId
29183
- : authorityInput.kind === "direct"
29184
- ? authorityInput.requestId
29185
- : authorityInput.processId;
29186
- const admission = await selectExactAdmissionForUpdate(tx, {
29187
- accountId: authorityInput.accountId,
29188
- workspaceId: authorityInput.workspaceId,
29189
- admissionId: input.admission.id,
29190
- actorKind,
29191
- actorId,
29192
- sessionId: authorityInput.sessionId,
29193
- admittedWorkspaceGeneration: input.admission.workspaceGeneration,
29194
- operation,
29195
- });
29196
- if (
29197
- !admission ||
29198
- !admissionMatchesSnapshot(admission, input.admission) ||
29199
- !admissionSnapshotMatchesAuthorityInput(input.admission, authorityInput) ||
29200
- admission.provider_outcome === "retained" ||
29201
- (admission.provider_outcome && admission.provider_outcome !== input.outcome)
29202
- ) {
29203
- return {
29204
- failure: "admission_fenced" as const,
29205
- detail: "Workspace mutation settlement did not match its exact durable admission",
29206
- };
29207
- }
29208
- if (!admission.settled_at) {
29209
- await tx.execute(sql`
29210
- update sandbox_workspace_mutation_admissions set
29211
- provider_outcome = ${input.outcome}, settled_at = now()
29212
- where id = ${input.admission.id} and settled_at is null
29213
- `);
29214
- }
29215
- if (input.outcome === "rejected") return { failure: null };
29216
- // The provider has already returned. Lock and settle its immutable
29217
- // admission before consulting mutable turn/route/process authority, so
29218
- // a stale-authority rejection cannot roll the physical settlement back
29219
- // and strand archive capture. Only authority-fence errors are converted
29220
- // to a post-commit rejection; database failures still abort normally.
29221
- let authority: LockedWorkspaceMutationAuthority;
29222
- try {
29223
- authority = await lockWorkspaceMutationAuthorityTx(tx, authorityInput);
29224
- } catch (error) {
29225
- const failure = workspaceMutationAuthorityFailure(error);
29226
- if (failure) return failure;
29227
- throw error;
29228
- }
29229
- return await verifyResolvedAdmissionAuthority(tx, authority, admission);
29230
- }),
29253
+ settleOnce,
29231
29254
  );
29232
29255
  if (settlement.failure !== null) {
29233
29256
  throw new SandboxWorkspaceMutationFencedError(settlement.failure, settlement.detail);
@@ -29426,6 +29449,19 @@ export async function retainWorkspaceMutationProcess(
29426
29449
  async (scopedDb) =>
29427
29450
  await scopedDb.transaction(async (txRaw) => {
29428
29451
  const tx = txRaw as unknown as Database;
29452
+ // Use the same ownership prefix as mutation admission and settlement.
29453
+ // A stale authority still cannot strand a provider process: remember
29454
+ // the fence, durably promote the exact process, then reject its output
29455
+ // after this transaction commits.
29456
+ let authority: LockedWorkspaceMutationAuthority | null = null;
29457
+ let authorityFailure: SandboxWorkspaceMutationSettlementResult | null = null;
29458
+ try {
29459
+ authority = await lockWorkspaceMutationAuthorityTx(tx, authorityInput);
29460
+ } catch (error) {
29461
+ const failure = workspaceMutationAuthorityFailure(error);
29462
+ if (!failure) throw error;
29463
+ authorityFailure = failure;
29464
+ }
29429
29465
  const actorKind = authorityInput.kind;
29430
29466
  const actorId =
29431
29467
  authorityInput.kind === "turn" ? authorityInput.attemptId : authorityInput.requestId;
@@ -29586,16 +29622,13 @@ export async function retainWorkspaceMutationProcess(
29586
29622
  }
29587
29623
 
29588
29624
  // A yielded provider process is already a physical outcome. Persist its
29589
- // exact route and non-TTL holder before consulting mutable authority, so
29590
- // a route/turn race cannot leave an untracked process or open parent
29591
- // admission. Report staleness only after this transaction commits.
29592
- let authority: LockedWorkspaceMutationAuthority;
29593
- try {
29594
- authority = await lockWorkspaceMutationAuthorityTx(tx, authorityInput);
29595
- } catch (error) {
29596
- const failure = workspaceMutationAuthorityFailure(error);
29597
- if (failure) return { process: mapRetainedProcess(process!), failure };
29598
- throw error;
29625
+ // exact route and non-TTL holder even when the earlier authority check
29626
+ // found a stale route/turn, then report staleness after commit.
29627
+ if (authorityFailure) {
29628
+ return { process: mapRetainedProcess(process!), failure: authorityFailure };
29629
+ }
29630
+ if (!authority) {
29631
+ throw new Error("Retained process promotion lost its locked authority");
29599
29632
  }
29600
29633
  const identity = await verifyResolvedAdmissionAuthority(tx, authority, admission);
29601
29634
  return { process: mapRetainedProcess(process!), failure: identity };
@@ -39494,8 +39527,10 @@ export async function peekSessionWork(
39494
39527
  attemptId: interruption.attemptId,
39495
39528
  };
39496
39529
  }
39497
- if (effectiveControl.state !== "active") return { kind: "idle" };
39498
-
39530
+ // Physical quiescence finishes an already-accepted interruption; it is not
39531
+ // new session work. Reconcile the missing receipt even while control stays
39532
+ // paused, otherwise the pause itself can strand this session and every
39533
+ // ancestor behind a permanent `settlement: stopping` projection.
39499
39534
  const awaitingQuiescence = await nextSessionAttemptAwaitingQuiescence(
39500
39535
  scopedDb,
39501
39536
  workspaceId,
@@ -39508,6 +39543,8 @@ export async function peekSessionWork(
39508
39543
  };
39509
39544
  }
39510
39545
 
39546
+ if (effectiveControl.state !== "active") return { kind: "idle" };
39547
+
39511
39548
  const [capacityWait] = await scopedDb
39512
39549
  .select()
39513
39550
  .from(schema.codexCapacityWaiters)
@@ -45137,3 +45174,4 @@ export {
45137
45174
  } from "./connection-token-resolver";
45138
45175
 
45139
45176
  export * from "./workspace-artifacts";
45177
+ export * from "./transcription-recordings";
@@ -118,6 +118,10 @@ export const FORCE_RLS_TABLES = [
118
118
  "slack_interactions",
119
119
  "social_connections",
120
120
  "social_posts",
121
+ "transcription_recording_chunks",
122
+ "transcription_recording_objects",
123
+ "transcription_recording_segments",
124
+ "transcription_recordings",
121
125
  "usage_events",
122
126
  "workspace_artifact_events",
123
127
  "workspace_artifact_versions",
@@ -248,6 +252,10 @@ export const RUNTIME_FULL_DML_TABLES = [
248
252
  "social_connections",
249
253
  "social_posts",
250
254
  "stripe_webhook_events",
255
+ "transcription_recording_chunks",
256
+ "transcription_recording_objects",
257
+ "transcription_recording_segments",
258
+ "transcription_recordings",
251
259
  "usage_events",
252
260
  "workspace_artifacts",
253
261
  "workspace_captures",
package/src/schema.ts CHANGED
@@ -1499,10 +1499,9 @@ export const sessions = pgTable(
1499
1499
  nestedAgentDepthPolicySessionId: uuid("nested_agent_depth_policy_session_id"),
1500
1500
  temporalWorkflowId: text("temporal_workflow_id"),
1501
1501
  activeTurnId: uuid("active_turn_id"),
1502
- // Actual input tokens reported for the last model call of the most recent
1503
- // turn. The pre-turn portable compaction trigger reads this as its budget
1504
- // signal (char/4 estimate is the same-turn fallback). Null until a turn with
1505
- // usage has completed.
1502
+ // Actual input tokens reported for the latest authoritative ordinary model
1503
+ // call. Compaction/context clearing invalidates it to null; local history
1504
+ // estimates must never be stored here.
1506
1505
  lastInputTokens: integer("last_input_tokens"),
1507
1506
  // Operator /compact request flag. The API sets
1508
1507
  // it true; the worker honors it BEFORE the next turn's model call by forcing
@@ -6037,3 +6036,4 @@ export * from "./workspace-instruction-policies-schema";
6037
6036
  export * from "./preference-registry-schema";
6038
6037
  export * from "./memory-governance-schema";
6039
6038
  export * from "./scoped-knowledge-schema";
6039
+ export * from "./transcription-recordings-schema";
@@ -0,0 +1,250 @@
1
+ import { sql } from "drizzle-orm";
2
+ import {
3
+ boolean,
4
+ check,
5
+ index,
6
+ integer,
7
+ jsonb,
8
+ pgTable,
9
+ primaryKey,
10
+ text,
11
+ timestamp,
12
+ uniqueIndex,
13
+ uuid,
14
+ } from "drizzle-orm/pg-core";
15
+
16
+ export const transcriptionRecordingStateValues = [
17
+ "uploading",
18
+ "segmenting",
19
+ "ready",
20
+ "transcribing",
21
+ "complete",
22
+ "failed",
23
+ "discarded",
24
+ ] as const;
25
+
26
+ export const transcriptionRecordingChunkStateValues = ["uploading", "complete"] as const;
27
+ export const transcriptionRecordingObjectKindValues = ["chunk", "segment"] as const;
28
+
29
+ export const transcriptionRecordingSegmentStateValues = [
30
+ "preparing",
31
+ "pending",
32
+ "transcribing",
33
+ "complete",
34
+ "failed",
35
+ ] as const;
36
+
37
+ export const transcriptionRecordings = pgTable(
38
+ "transcription_recordings",
39
+ {
40
+ id: uuid("id").primaryKey(),
41
+ accountId: uuid("account_id").notNull(),
42
+ workspaceId: uuid("workspace_id").notNull(),
43
+ subjectId: text("subject_id").notNull(),
44
+ mimeType: text("mime_type").notNull(),
45
+ state: text("state", { enum: transcriptionRecordingStateValues })
46
+ .notNull()
47
+ .default("uploading"),
48
+ nextChunkNumber: integer("next_chunk_number").notNull().default(0),
49
+ chunkCount: integer("chunk_count").notNull().default(0),
50
+ totalBytes: integer("total_bytes").notNull().default(0),
51
+ totalDurationMilliseconds: integer("total_duration_milliseconds").notNull().default(0),
52
+ segmentCount: integer("segment_count").notNull().default(0),
53
+ completedSegmentCount: integer("completed_segment_count").notNull().default(0),
54
+ transcriptText: text("transcript_text"),
55
+ languages: jsonb("languages").$type<string[]>().notNull().default([]),
56
+ errorCode: text("error_code"),
57
+ retryable: boolean("retryable").notNull().default(false),
58
+ providerId: text("provider_id"),
59
+ processingGeneration: integer("processing_generation").notNull().default(0),
60
+ processingOwner: uuid("processing_owner"),
61
+ processingStartedAt: timestamp("processing_started_at", { withTimezone: true }),
62
+ objectsCleanedAt: timestamp("objects_cleaned_at", { withTimezone: true }),
63
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
64
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
65
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
66
+ },
67
+ (table) => ({
68
+ exactAuthority: uniqueIndex("transcription_recordings_exact_authority_uq").on(
69
+ table.accountId,
70
+ table.workspaceId,
71
+ table.subjectId,
72
+ table.id,
73
+ ),
74
+ subjectCreated: index("transcription_recordings_subject_created_idx").on(
75
+ table.workspaceId,
76
+ table.subjectId,
77
+ table.createdAt,
78
+ ),
79
+ expiry: index("transcription_recordings_expiry_idx").on(table.expiresAt, table.id),
80
+ valuesValid: check(
81
+ "transcription_recordings_values_check",
82
+ sql`${table.nextChunkNumber} >= 0
83
+ and ${table.chunkCount} >= 0
84
+ and ${table.totalBytes} >= 0
85
+ and ${table.totalDurationMilliseconds} >= 0
86
+ and ${table.segmentCount} >= 0
87
+ and ${table.completedSegmentCount} >= 0
88
+ and ${table.completedSegmentCount} <= ${table.segmentCount}
89
+ and octet_length(${table.subjectId}) between 1 and 1024
90
+ and octet_length(${table.mimeType}) between 1 and 128
91
+ and (
92
+ ${table.providerId} is null
93
+ or octet_length(${table.providerId}) between 1 and 128
94
+ )`,
95
+ ),
96
+ processingValid: check(
97
+ "transcription_recordings_processing_check",
98
+ sql`(
99
+ ${table.processingOwner} is null
100
+ and ${table.processingStartedAt} is null
101
+ ) or (
102
+ ${table.processingOwner} is not null
103
+ and ${table.processingStartedAt} is not null
104
+ )`,
105
+ ),
106
+ }),
107
+ );
108
+
109
+ export const transcriptionRecordingObjects = pgTable(
110
+ "transcription_recording_objects",
111
+ {
112
+ accountId: uuid("account_id").notNull(),
113
+ workspaceId: uuid("workspace_id").notNull(),
114
+ subjectId: text("subject_id").notNull(),
115
+ recordingId: uuid("recording_id").notNull(),
116
+ objectKey: text("object_key").primaryKey(),
117
+ kind: text("kind", { enum: transcriptionRecordingObjectKindValues }).notNull(),
118
+ cleanupAfter: timestamp("cleanup_after", { withTimezone: true }).notNull(),
119
+ cleanupClaimId: uuid("cleanup_claim_id"),
120
+ cleanupClaimedAt: timestamp("cleanup_claimed_at", { withTimezone: true }),
121
+ cleanedAt: timestamp("cleaned_at", { withTimezone: true }),
122
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
123
+ },
124
+ (table) => ({
125
+ dueCleanup: index("transcription_recording_objects_due_cleanup_idx")
126
+ .on(table.cleanupAfter, table.objectKey)
127
+ .where(sql`${table.cleanedAt} is null`),
128
+ claimRecovery: index("transcription_recording_objects_claim_recovery_idx")
129
+ .on(table.cleanupClaimedAt, table.objectKey)
130
+ .where(sql`${table.cleanedAt} is null and ${table.cleanupClaimId} is not null`),
131
+ valuesValid: check(
132
+ "transcription_recording_objects_values_check",
133
+ sql`octet_length(${table.objectKey}) between 1 and 1024
134
+ and (
135
+ (${table.cleanupClaimId} is null and ${table.cleanupClaimedAt} is null)
136
+ or (${table.cleanupClaimId} is not null and ${table.cleanupClaimedAt} is not null)
137
+ )
138
+ and (
139
+ ${table.cleanedAt} is null
140
+ or (${table.cleanupClaimId} is null and ${table.cleanupClaimedAt} is null)
141
+ )`,
142
+ ),
143
+ }),
144
+ );
145
+
146
+ export const transcriptionRecordingChunks = pgTable(
147
+ "transcription_recording_chunks",
148
+ {
149
+ accountId: uuid("account_id").notNull(),
150
+ workspaceId: uuid("workspace_id").notNull(),
151
+ subjectId: text("subject_id").notNull(),
152
+ recordingId: uuid("recording_id").notNull(),
153
+ chunkNumber: integer("chunk_number").notNull(),
154
+ state: text("state", { enum: transcriptionRecordingChunkStateValues })
155
+ .notNull()
156
+ .default("uploading"),
157
+ byteLength: integer("byte_length").notNull(),
158
+ sha256: text("sha256").notNull(),
159
+ startMilliseconds: integer("start_milliseconds").notNull(),
160
+ durationMilliseconds: integer("duration_milliseconds").notNull(),
161
+ objectKey: text("object_key").notNull(),
162
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
163
+ completedAt: timestamp("completed_at", { withTimezone: true }),
164
+ },
165
+ (table) => ({
166
+ pk: primaryKey({
167
+ name: "transcription_recording_chunks_pk",
168
+ columns: [table.recordingId, table.chunkNumber],
169
+ }),
170
+ recordingOrder: index("transcription_recording_chunks_order_idx").on(
171
+ table.workspaceId,
172
+ table.recordingId,
173
+ table.chunkNumber,
174
+ ),
175
+ objectKey: uniqueIndex("transcription_recording_chunks_object_key_uq").on(table.objectKey),
176
+ valuesValid: check(
177
+ "transcription_recording_chunks_values_check",
178
+ sql`${table.chunkNumber} >= 0
179
+ and ${table.byteLength} > 0
180
+ and ${table.startMilliseconds} >= 0
181
+ and ${table.durationMilliseconds} >= 0
182
+ and ${table.sha256} ~ '^[0-9a-f]{64}$'
183
+ and octet_length(${table.objectKey}) between 1 and 1024
184
+ and (
185
+ (${table.state} = 'uploading' and ${table.completedAt} is null)
186
+ or (${table.state} = 'complete' and ${table.completedAt} is not null)
187
+ )`,
188
+ ),
189
+ }),
190
+ );
191
+
192
+ export const transcriptionRecordingSegments = pgTable(
193
+ "transcription_recording_segments",
194
+ {
195
+ accountId: uuid("account_id").notNull(),
196
+ workspaceId: uuid("workspace_id").notNull(),
197
+ subjectId: text("subject_id").notNull(),
198
+ recordingId: uuid("recording_id").notNull(),
199
+ segmentNumber: integer("segment_number").notNull(),
200
+ generation: integer("generation").notNull(),
201
+ state: text("state", { enum: transcriptionRecordingSegmentStateValues })
202
+ .notNull()
203
+ .default("preparing"),
204
+ byteLength: integer("byte_length").notNull(),
205
+ sha256: text("sha256").notNull(),
206
+ startMilliseconds: integer("start_milliseconds").notNull(),
207
+ durationMilliseconds: integer("duration_milliseconds").notNull(),
208
+ objectKey: text("object_key").notNull(),
209
+ attemptId: uuid("attempt_id"),
210
+ attemptStartedAt: timestamp("attempt_started_at", { withTimezone: true }),
211
+ attemptDeadlineAt: timestamp("attempt_deadline_at", { withTimezone: true }),
212
+ transcriptText: text("transcript_text"),
213
+ languages: jsonb("languages").$type<string[]>().notNull().default([]),
214
+ providerId: text("provider_id"),
215
+ errorCode: text("error_code"),
216
+ retryable: boolean("retryable").notNull().default(false),
217
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
218
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
219
+ },
220
+ (table) => ({
221
+ pk: primaryKey({
222
+ name: "transcription_recording_segments_pk",
223
+ columns: [table.recordingId, table.segmentNumber],
224
+ }),
225
+ recordingOrder: index("transcription_recording_segments_order_idx").on(
226
+ table.workspaceId,
227
+ table.recordingId,
228
+ table.segmentNumber,
229
+ ),
230
+ objectKey: uniqueIndex("transcription_recording_segments_object_key_uq").on(table.objectKey),
231
+ valuesValid: check(
232
+ "transcription_recording_segments_values_check",
233
+ sql`${table.segmentNumber} >= 0
234
+ and ${table.generation} > 0
235
+ and ${table.byteLength} > 0
236
+ and ${table.startMilliseconds} >= 0
237
+ and ${table.durationMilliseconds} > 0
238
+ and ${table.sha256} ~ '^[0-9a-f]{64}$'
239
+ and octet_length(${table.objectKey}) between 1 and 1024
240
+ and (
241
+ (${table.attemptId} is null
242
+ and ${table.attemptStartedAt} is null
243
+ and ${table.attemptDeadlineAt} is null)
244
+ or (${table.attemptId} is not null
245
+ and ${table.attemptStartedAt} is not null
246
+ and ${table.attemptDeadlineAt} is not null)
247
+ )`,
248
+ ),
249
+ }),
250
+ );