@opengeni/db 0.27.11 → 0.28.9
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/dist/{chunk-JYQPJ6Y5.js → chunk-P6CUHXQZ.js} +2967 -2272
- package/dist/chunk-P6CUHXQZ.js.map +1 -0
- package/dist/{chunk-RPHPNVWW.js → chunk-VHBFHMPC.js} +15 -1
- package/dist/chunk-VHBFHMPC.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/index.d.ts +272 -26
- package/dist/index.js +7798 -5008
- package/dist/index.js.map +1 -1
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +1420 -271
- package/dist/schema.js +25 -1
- package/dist/session-realtime-terminal.d.ts +7 -0
- package/dist/transcription-recordings-schema.d.ts +1264 -0
- package/dist/transcription-recordings.d.ts +228 -0
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0170_resumable_transcription_recordings.sql +440 -0
- package/drizzle/0175_resumable_transcription_provider_deadline.sql +24 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/package.json +4 -4
- package/src/database.ts +7 -1
- package/src/environment-crypto.ts +22 -9
- package/src/index.ts +4368 -1678
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/persistence-errors.ts +29 -34
- package/src/runtime-posture.ts +14 -0
- package/src/schema.ts +264 -42
- package/src/session-control.ts +125 -76
- package/src/session-queue-commands.ts +325 -234
- package/src/session-realtime-context.ts +18 -5
- package/src/session-realtime-ledger.ts +29 -7
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +3 -2
- package/src/session-tool-call-settlement.ts +29 -15
- package/src/transcription-recordings-schema.ts +253 -0
- package/src/transcription-recordings.ts +1538 -0
- package/dist/chunk-JYQPJ6Y5.js.map +0 -1
- package/dist/chunk-RPHPNVWW.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- package/src/event-payload-sanitizer.ts +0 -377
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
|
|
3
|
+
CREATE TABLE "transcription_recordings" (
|
|
4
|
+
"id" uuid PRIMARY KEY,
|
|
5
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
6
|
+
"workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
|
|
7
|
+
"subject_id" text NOT NULL,
|
|
8
|
+
"mime_type" text NOT NULL,
|
|
9
|
+
"state" text NOT NULL DEFAULT 'uploading',
|
|
10
|
+
"next_chunk_number" integer NOT NULL DEFAULT 0,
|
|
11
|
+
"chunk_count" integer NOT NULL DEFAULT 0,
|
|
12
|
+
"total_bytes" integer NOT NULL DEFAULT 0,
|
|
13
|
+
"total_duration_milliseconds" integer NOT NULL DEFAULT 0,
|
|
14
|
+
"segment_count" integer NOT NULL DEFAULT 0,
|
|
15
|
+
"completed_segment_count" integer NOT NULL DEFAULT 0,
|
|
16
|
+
"transcript_text" text,
|
|
17
|
+
"languages" jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
18
|
+
"error_code" text,
|
|
19
|
+
"retryable" boolean NOT NULL DEFAULT false,
|
|
20
|
+
"provider_id" text,
|
|
21
|
+
"processing_generation" integer NOT NULL DEFAULT 0,
|
|
22
|
+
"processing_owner" uuid,
|
|
23
|
+
"processing_started_at" timestamptz,
|
|
24
|
+
"objects_cleaned_at" timestamptz,
|
|
25
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
27
|
+
"expires_at" timestamptz NOT NULL,
|
|
28
|
+
CONSTRAINT "transcription_recordings_workspace_account_fk"
|
|
29
|
+
FOREIGN KEY ("workspace_id", "account_id")
|
|
30
|
+
REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
|
|
31
|
+
CONSTRAINT "transcription_recordings_exact_authority_uq"
|
|
32
|
+
UNIQUE ("account_id", "workspace_id", "subject_id", "id"),
|
|
33
|
+
CONSTRAINT "transcription_recordings_state_check"
|
|
34
|
+
CHECK ("state" IN (
|
|
35
|
+
'uploading', 'segmenting', 'ready', 'transcribing', 'complete', 'failed', 'discarded'
|
|
36
|
+
)),
|
|
37
|
+
CONSTRAINT "transcription_recordings_values_check"
|
|
38
|
+
CHECK (
|
|
39
|
+
"next_chunk_number" >= 0
|
|
40
|
+
AND "chunk_count" >= 0
|
|
41
|
+
AND "total_bytes" >= 0
|
|
42
|
+
AND "total_duration_milliseconds" >= 0
|
|
43
|
+
AND "segment_count" >= 0
|
|
44
|
+
AND "completed_segment_count" >= 0
|
|
45
|
+
AND "completed_segment_count" <= "segment_count"
|
|
46
|
+
AND octet_length("subject_id") BETWEEN 1 AND 1024
|
|
47
|
+
AND octet_length("mime_type") BETWEEN 1 AND 128
|
|
48
|
+
AND ("provider_id" IS NULL OR octet_length("provider_id") BETWEEN 1 AND 128)
|
|
49
|
+
AND ("transcript_text" IS NULL OR octet_length("transcript_text") <= 4000000)
|
|
50
|
+
AND jsonb_typeof("languages") = 'array'
|
|
51
|
+
),
|
|
52
|
+
CONSTRAINT "transcription_recordings_error_code_check"
|
|
53
|
+
CHECK ("error_code" IS NULL OR "error_code" IN (
|
|
54
|
+
'permission_denied', 'not_supported', 'network', 'provider', 'policy_blocked',
|
|
55
|
+
'timeout', 'cancelled', 'unavailable', 'too_large', 'invalid_audio', 'unknown'
|
|
56
|
+
)),
|
|
57
|
+
CONSTRAINT "transcription_recordings_processing_check"
|
|
58
|
+
CHECK (
|
|
59
|
+
("processing_owner" IS NULL AND "processing_started_at" IS NULL)
|
|
60
|
+
OR ("processing_owner" IS NOT NULL AND "processing_started_at" IS NOT NULL)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX "transcription_recordings_subject_created_idx"
|
|
65
|
+
ON "transcription_recordings" ("workspace_id", "subject_id", "created_at");
|
|
66
|
+
CREATE INDEX "transcription_recordings_expiry_idx"
|
|
67
|
+
ON "transcription_recordings" ("expires_at", "id");
|
|
68
|
+
|
|
69
|
+
CREATE TABLE "transcription_recording_objects" (
|
|
70
|
+
"account_id" uuid NOT NULL,
|
|
71
|
+
"workspace_id" uuid NOT NULL,
|
|
72
|
+
"subject_id" text NOT NULL,
|
|
73
|
+
"recording_id" uuid NOT NULL,
|
|
74
|
+
"object_key" text PRIMARY KEY,
|
|
75
|
+
"kind" text NOT NULL,
|
|
76
|
+
"cleanup_after" timestamptz NOT NULL,
|
|
77
|
+
"cleanup_claim_id" uuid,
|
|
78
|
+
"cleanup_claimed_at" timestamptz,
|
|
79
|
+
"cleaned_at" timestamptz,
|
|
80
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
81
|
+
CONSTRAINT "transcription_recording_objects_authority_fk"
|
|
82
|
+
FOREIGN KEY ("account_id", "workspace_id", "subject_id", "recording_id")
|
|
83
|
+
REFERENCES "transcription_recordings"("account_id", "workspace_id", "subject_id", "id")
|
|
84
|
+
ON DELETE CASCADE,
|
|
85
|
+
CONSTRAINT "transcription_recording_objects_kind_check"
|
|
86
|
+
CHECK ("kind" IN ('chunk', 'segment')),
|
|
87
|
+
CONSTRAINT "transcription_recording_objects_values_check"
|
|
88
|
+
CHECK (
|
|
89
|
+
octet_length("object_key") BETWEEN 1 AND 1024
|
|
90
|
+
AND (
|
|
91
|
+
("cleanup_claim_id" IS NULL AND "cleanup_claimed_at" IS NULL)
|
|
92
|
+
OR ("cleanup_claim_id" IS NOT NULL AND "cleanup_claimed_at" IS NOT NULL)
|
|
93
|
+
)
|
|
94
|
+
AND (
|
|
95
|
+
"cleaned_at" IS NULL
|
|
96
|
+
OR ("cleanup_claim_id" IS NULL AND "cleanup_claimed_at" IS NULL)
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX "transcription_recording_objects_due_cleanup_idx"
|
|
102
|
+
ON "transcription_recording_objects" ("cleanup_after", "object_key")
|
|
103
|
+
WHERE "cleaned_at" IS NULL;
|
|
104
|
+
CREATE INDEX "transcription_recording_objects_claim_recovery_idx"
|
|
105
|
+
ON "transcription_recording_objects" ("cleanup_claimed_at", "object_key")
|
|
106
|
+
WHERE "cleaned_at" IS NULL AND "cleanup_claim_id" IS NOT NULL;
|
|
107
|
+
|
|
108
|
+
CREATE TABLE "transcription_recording_chunks" (
|
|
109
|
+
"account_id" uuid NOT NULL,
|
|
110
|
+
"workspace_id" uuid NOT NULL,
|
|
111
|
+
"subject_id" text NOT NULL,
|
|
112
|
+
"recording_id" uuid NOT NULL,
|
|
113
|
+
"chunk_number" integer NOT NULL,
|
|
114
|
+
"state" text NOT NULL DEFAULT 'uploading',
|
|
115
|
+
"byte_length" integer NOT NULL,
|
|
116
|
+
"sha256" text NOT NULL,
|
|
117
|
+
"start_milliseconds" integer NOT NULL,
|
|
118
|
+
"duration_milliseconds" integer NOT NULL,
|
|
119
|
+
"object_key" text NOT NULL,
|
|
120
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
121
|
+
"completed_at" timestamptz,
|
|
122
|
+
CONSTRAINT "transcription_recording_chunks_pk" PRIMARY KEY ("recording_id", "chunk_number"),
|
|
123
|
+
CONSTRAINT "transcription_recording_chunks_authority_fk"
|
|
124
|
+
FOREIGN KEY ("account_id", "workspace_id", "subject_id", "recording_id")
|
|
125
|
+
REFERENCES "transcription_recordings"("account_id", "workspace_id", "subject_id", "id")
|
|
126
|
+
ON DELETE CASCADE,
|
|
127
|
+
CONSTRAINT "transcription_recording_chunks_object_fk"
|
|
128
|
+
FOREIGN KEY ("object_key")
|
|
129
|
+
REFERENCES "transcription_recording_objects"("object_key") ON DELETE RESTRICT,
|
|
130
|
+
CONSTRAINT "transcription_recording_chunks_state_check"
|
|
131
|
+
CHECK ("state" IN ('uploading', 'complete')),
|
|
132
|
+
CONSTRAINT "transcription_recording_chunks_values_check"
|
|
133
|
+
CHECK (
|
|
134
|
+
"chunk_number" >= 0
|
|
135
|
+
AND "byte_length" > 0
|
|
136
|
+
AND "start_milliseconds" >= 0
|
|
137
|
+
AND "duration_milliseconds" >= 0
|
|
138
|
+
AND "sha256" ~ '^[0-9a-f]{64}$'
|
|
139
|
+
AND octet_length("object_key") BETWEEN 1 AND 1024
|
|
140
|
+
AND (
|
|
141
|
+
("state" = 'uploading' AND "completed_at" IS NULL)
|
|
142
|
+
OR ("state" = 'complete' AND "completed_at" IS NOT NULL)
|
|
143
|
+
)
|
|
144
|
+
),
|
|
145
|
+
CONSTRAINT "transcription_recording_chunks_object_key_uq" UNIQUE ("object_key")
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE INDEX "transcription_recording_chunks_order_idx"
|
|
149
|
+
ON "transcription_recording_chunks" ("workspace_id", "recording_id", "chunk_number");
|
|
150
|
+
|
|
151
|
+
CREATE TABLE "transcription_recording_segments" (
|
|
152
|
+
"account_id" uuid NOT NULL,
|
|
153
|
+
"workspace_id" uuid NOT NULL,
|
|
154
|
+
"subject_id" text NOT NULL,
|
|
155
|
+
"recording_id" uuid NOT NULL,
|
|
156
|
+
"segment_number" integer NOT NULL,
|
|
157
|
+
"generation" integer NOT NULL,
|
|
158
|
+
"state" text NOT NULL DEFAULT 'preparing',
|
|
159
|
+
"byte_length" integer NOT NULL,
|
|
160
|
+
"sha256" text NOT NULL,
|
|
161
|
+
"start_milliseconds" integer NOT NULL,
|
|
162
|
+
"duration_milliseconds" integer NOT NULL,
|
|
163
|
+
"object_key" text NOT NULL,
|
|
164
|
+
"attempt_id" uuid,
|
|
165
|
+
"attempt_started_at" timestamptz,
|
|
166
|
+
"transcript_text" text,
|
|
167
|
+
"languages" jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
168
|
+
"provider_id" text,
|
|
169
|
+
"error_code" text,
|
|
170
|
+
"retryable" boolean NOT NULL DEFAULT false,
|
|
171
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
172
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
173
|
+
CONSTRAINT "transcription_recording_segments_pk"
|
|
174
|
+
PRIMARY KEY ("recording_id", "segment_number"),
|
|
175
|
+
CONSTRAINT "transcription_recording_segments_authority_fk"
|
|
176
|
+
FOREIGN KEY ("account_id", "workspace_id", "subject_id", "recording_id")
|
|
177
|
+
REFERENCES "transcription_recordings"("account_id", "workspace_id", "subject_id", "id")
|
|
178
|
+
ON DELETE CASCADE,
|
|
179
|
+
CONSTRAINT "transcription_recording_segments_object_fk"
|
|
180
|
+
FOREIGN KEY ("object_key")
|
|
181
|
+
REFERENCES "transcription_recording_objects"("object_key") ON DELETE RESTRICT,
|
|
182
|
+
CONSTRAINT "transcription_recording_segments_state_check"
|
|
183
|
+
CHECK ("state" IN ('preparing', 'pending', 'transcribing', 'complete', 'failed')),
|
|
184
|
+
CONSTRAINT "transcription_recording_segments_values_check"
|
|
185
|
+
CHECK (
|
|
186
|
+
"segment_number" >= 0
|
|
187
|
+
AND "generation" > 0
|
|
188
|
+
AND "byte_length" > 0
|
|
189
|
+
AND "start_milliseconds" >= 0
|
|
190
|
+
AND "duration_milliseconds" > 0
|
|
191
|
+
AND "sha256" ~ '^[0-9a-f]{64}$'
|
|
192
|
+
AND octet_length("object_key") BETWEEN 1 AND 1024
|
|
193
|
+
AND ("transcript_text" IS NULL OR octet_length("transcript_text") <= 1000000)
|
|
194
|
+
AND jsonb_typeof("languages") = 'array'
|
|
195
|
+
AND (
|
|
196
|
+
("attempt_id" IS NULL AND "attempt_started_at" IS NULL)
|
|
197
|
+
OR ("attempt_id" IS NOT NULL AND "attempt_started_at" IS NOT NULL)
|
|
198
|
+
)
|
|
199
|
+
),
|
|
200
|
+
CONSTRAINT "transcription_recording_segments_error_code_check"
|
|
201
|
+
CHECK ("error_code" IS NULL OR "error_code" IN (
|
|
202
|
+
'permission_denied', 'not_supported', 'network', 'provider', 'policy_blocked',
|
|
203
|
+
'timeout', 'cancelled', 'unavailable', 'too_large', 'invalid_audio', 'unknown'
|
|
204
|
+
)),
|
|
205
|
+
CONSTRAINT "transcription_recording_segments_object_key_uq" UNIQUE ("object_key")
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
CREATE INDEX "transcription_recording_segments_order_idx"
|
|
209
|
+
ON "transcription_recording_segments" ("workspace_id", "recording_id", "segment_number");
|
|
210
|
+
|
|
211
|
+
ALTER TABLE "transcription_recordings" ENABLE ROW LEVEL SECURITY;
|
|
212
|
+
ALTER TABLE "transcription_recordings" FORCE ROW LEVEL SECURITY;
|
|
213
|
+
ALTER TABLE "transcription_recording_objects" ENABLE ROW LEVEL SECURITY;
|
|
214
|
+
ALTER TABLE "transcription_recording_objects" FORCE ROW LEVEL SECURITY;
|
|
215
|
+
ALTER TABLE "transcription_recording_chunks" ENABLE ROW LEVEL SECURITY;
|
|
216
|
+
ALTER TABLE "transcription_recording_chunks" FORCE ROW LEVEL SECURITY;
|
|
217
|
+
ALTER TABLE "transcription_recording_segments" ENABLE ROW LEVEL SECURITY;
|
|
218
|
+
ALTER TABLE "transcription_recording_segments" FORCE ROW LEVEL SECURITY;
|
|
219
|
+
|
|
220
|
+
CREATE POLICY transcription_recordings_subject_isolation ON "transcription_recordings"
|
|
221
|
+
USING (
|
|
222
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
223
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
224
|
+
)
|
|
225
|
+
WITH CHECK (
|
|
226
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
227
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
CREATE POLICY transcription_recording_objects_subject_isolation
|
|
231
|
+
ON "transcription_recording_objects"
|
|
232
|
+
USING (
|
|
233
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
234
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
235
|
+
)
|
|
236
|
+
WITH CHECK (
|
|
237
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
238
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
CREATE POLICY transcription_recording_chunks_subject_isolation
|
|
242
|
+
ON "transcription_recording_chunks"
|
|
243
|
+
USING (
|
|
244
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
245
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
246
|
+
)
|
|
247
|
+
WITH CHECK (
|
|
248
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
249
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
-- The existing provider-neutral object reaper needs one bounded cross-workspace
|
|
253
|
+
-- claim seam. The function returns only exact authority ids plus opaque storage
|
|
254
|
+
-- keys; no transcript, audio, credential, or provider metadata crosses the RLS
|
|
255
|
+
-- boundary. Recording rows are locked before object rows, matching request-side
|
|
256
|
+
-- mutation order. Expired recordings are fenced to discarded in the same claim.
|
|
257
|
+
-- The migration may target public or a dedicated embedded schema, so bind
|
|
258
|
+
-- current_schema() into every relation at creation time and expose only
|
|
259
|
+
-- pg_catalog at execution time.
|
|
260
|
+
DO $privileged_functions$
|
|
261
|
+
DECLARE data_schema text := current_schema();
|
|
262
|
+
BEGIN
|
|
263
|
+
EXECUTE format($ddl$
|
|
264
|
+
CREATE OR REPLACE FUNCTION opengeni_private.claim_due_transcription_recording_object_cleanup(
|
|
265
|
+
p_grace_ms bigint,
|
|
266
|
+
p_claim_timeout_ms bigint,
|
|
267
|
+
p_limit integer
|
|
268
|
+
)
|
|
269
|
+
RETURNS TABLE (
|
|
270
|
+
account_id uuid,
|
|
271
|
+
workspace_id uuid,
|
|
272
|
+
subject_id text,
|
|
273
|
+
recording_id uuid,
|
|
274
|
+
object_key text,
|
|
275
|
+
cleanup_claim_id uuid
|
|
276
|
+
)
|
|
277
|
+
LANGUAGE sql
|
|
278
|
+
SECURITY DEFINER
|
|
279
|
+
STRICT
|
|
280
|
+
SET search_path = pg_catalog
|
|
281
|
+
AS $function$
|
|
282
|
+
WITH due_recordings AS MATERIALIZED (
|
|
283
|
+
SELECT R.id, R.account_id, R.workspace_id, R.subject_id, R.expires_at
|
|
284
|
+
FROM %1$I.transcription_recordings R
|
|
285
|
+
WHERE EXISTS (
|
|
286
|
+
SELECT 1
|
|
287
|
+
FROM %1$I.transcription_recording_objects O
|
|
288
|
+
WHERE O.recording_id = R.id
|
|
289
|
+
AND O.cleaned_at IS NULL
|
|
290
|
+
AND O.cleanup_after <=
|
|
291
|
+
clock_timestamp() - greatest(p_grace_ms, 0) * interval '1 millisecond'
|
|
292
|
+
AND (
|
|
293
|
+
O.cleanup_claim_id IS NULL
|
|
294
|
+
OR O.cleanup_claimed_at <=
|
|
295
|
+
clock_timestamp() - greatest(p_claim_timeout_ms, 0) * interval '1 millisecond'
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
ORDER BY R.expires_at, R.id
|
|
299
|
+
FOR UPDATE OF R SKIP LOCKED
|
|
300
|
+
LIMIT least(greatest(p_limit, 0), 1000)
|
|
301
|
+
), expired_recordings AS (
|
|
302
|
+
UPDATE %1$I.transcription_recordings R
|
|
303
|
+
SET state = 'discarded',
|
|
304
|
+
processing_owner = NULL,
|
|
305
|
+
processing_started_at = NULL,
|
|
306
|
+
error_code = NULL,
|
|
307
|
+
retryable = false,
|
|
308
|
+
updated_at = clock_timestamp()
|
|
309
|
+
FROM due_recordings D
|
|
310
|
+
WHERE R.id = D.id
|
|
311
|
+
AND D.expires_at <=
|
|
312
|
+
clock_timestamp() - greatest(p_grace_ms, 0) * interval '1 millisecond'
|
|
313
|
+
AND R.state <> 'discarded'
|
|
314
|
+
RETURNING R.id
|
|
315
|
+
), candidates AS MATERIALIZED (
|
|
316
|
+
SELECT O.object_key
|
|
317
|
+
FROM %1$I.transcription_recording_objects O
|
|
318
|
+
JOIN due_recordings R ON R.id = O.recording_id
|
|
319
|
+
WHERE O.cleaned_at IS NULL
|
|
320
|
+
AND O.cleanup_after <=
|
|
321
|
+
clock_timestamp() - greatest(p_grace_ms, 0) * interval '1 millisecond'
|
|
322
|
+
AND (
|
|
323
|
+
O.cleanup_claim_id IS NULL
|
|
324
|
+
OR O.cleanup_claimed_at <=
|
|
325
|
+
clock_timestamp() - greatest(p_claim_timeout_ms, 0) * interval '1 millisecond'
|
|
326
|
+
)
|
|
327
|
+
ORDER BY O.cleanup_after, O.object_key
|
|
328
|
+
FOR UPDATE OF O SKIP LOCKED
|
|
329
|
+
LIMIT least(greatest(p_limit, 0), 1000)
|
|
330
|
+
), claimed AS (
|
|
331
|
+
UPDATE %1$I.transcription_recording_objects O
|
|
332
|
+
SET cleanup_claim_id = gen_random_uuid(), cleanup_claimed_at = clock_timestamp()
|
|
333
|
+
FROM candidates C
|
|
334
|
+
WHERE O.object_key = C.object_key
|
|
335
|
+
RETURNING
|
|
336
|
+
O.account_id,
|
|
337
|
+
O.workspace_id,
|
|
338
|
+
O.subject_id,
|
|
339
|
+
O.recording_id,
|
|
340
|
+
O.object_key,
|
|
341
|
+
O.cleanup_claim_id
|
|
342
|
+
)
|
|
343
|
+
SELECT
|
|
344
|
+
C.account_id,
|
|
345
|
+
C.workspace_id,
|
|
346
|
+
C.subject_id,
|
|
347
|
+
C.recording_id,
|
|
348
|
+
C.object_key,
|
|
349
|
+
C.cleanup_claim_id
|
|
350
|
+
FROM claimed C, (SELECT count(*) FROM expired_recordings) AS fence;
|
|
351
|
+
$function$;
|
|
352
|
+
$ddl$, data_schema);
|
|
353
|
+
|
|
354
|
+
-- Once every object for an expired recording has been confirmed deleted,
|
|
355
|
+
-- purge the remaining manifest, chunk/segment metadata, provider pin, and
|
|
356
|
+
-- transcript. Provider deletion settles one object at a time first, so
|
|
357
|
+
-- metadata is never removed while an opaque object key still needs retryable
|
|
358
|
+
-- cleanup.
|
|
359
|
+
EXECUTE format($ddl$
|
|
360
|
+
CREATE OR REPLACE FUNCTION opengeni_private.purge_expired_transcription_recordings(
|
|
361
|
+
p_grace_ms bigint,
|
|
362
|
+
p_limit integer
|
|
363
|
+
)
|
|
364
|
+
RETURNS integer
|
|
365
|
+
LANGUAGE plpgsql
|
|
366
|
+
SECURITY DEFINER
|
|
367
|
+
STRICT
|
|
368
|
+
SET search_path = pg_catalog
|
|
369
|
+
AS $function$
|
|
370
|
+
DECLARE
|
|
371
|
+
v_recording_id uuid;
|
|
372
|
+
v_purged integer := 0;
|
|
373
|
+
BEGIN
|
|
374
|
+
IF p_grace_ms < 0 OR p_limit <= 0 THEN
|
|
375
|
+
RAISE EXCEPTION 'invalid transcription recording purge bounds';
|
|
376
|
+
END IF;
|
|
377
|
+
|
|
378
|
+
FOR v_recording_id IN
|
|
379
|
+
SELECT R.id
|
|
380
|
+
FROM %1$I.transcription_recordings R
|
|
381
|
+
WHERE R.expires_at <=
|
|
382
|
+
clock_timestamp() - p_grace_ms * interval '1 millisecond'
|
|
383
|
+
AND NOT EXISTS (
|
|
384
|
+
SELECT 1
|
|
385
|
+
FROM %1$I.transcription_recording_objects O
|
|
386
|
+
WHERE O.recording_id = R.id
|
|
387
|
+
AND O.cleaned_at IS NULL
|
|
388
|
+
)
|
|
389
|
+
ORDER BY R.expires_at, R.id
|
|
390
|
+
FOR UPDATE OF R SKIP LOCKED
|
|
391
|
+
LIMIT least(p_limit, 1000)
|
|
392
|
+
LOOP
|
|
393
|
+
DELETE FROM %1$I.transcription_recording_chunks
|
|
394
|
+
WHERE recording_id = v_recording_id;
|
|
395
|
+
DELETE FROM %1$I.transcription_recording_segments
|
|
396
|
+
WHERE recording_id = v_recording_id;
|
|
397
|
+
DELETE FROM %1$I.transcription_recording_objects
|
|
398
|
+
WHERE recording_id = v_recording_id;
|
|
399
|
+
DELETE FROM %1$I.transcription_recordings
|
|
400
|
+
WHERE id = v_recording_id;
|
|
401
|
+
v_purged := v_purged + 1;
|
|
402
|
+
END LOOP;
|
|
403
|
+
|
|
404
|
+
RETURN v_purged;
|
|
405
|
+
END;
|
|
406
|
+
$function$;
|
|
407
|
+
$ddl$, data_schema);
|
|
408
|
+
END
|
|
409
|
+
$privileged_functions$;
|
|
410
|
+
|
|
411
|
+
REVOKE ALL ON FUNCTION
|
|
412
|
+
opengeni_private.claim_due_transcription_recording_object_cleanup(bigint, bigint, integer)
|
|
413
|
+
FROM PUBLIC;
|
|
414
|
+
|
|
415
|
+
REVOKE ALL ON FUNCTION
|
|
416
|
+
opengeni_private.purge_expired_transcription_recordings(bigint, integer)
|
|
417
|
+
FROM PUBLIC;
|
|
418
|
+
|
|
419
|
+
DO $$
|
|
420
|
+
BEGIN
|
|
421
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
422
|
+
GRANT EXECUTE ON FUNCTION
|
|
423
|
+
opengeni_private.claim_due_transcription_recording_object_cleanup(bigint, bigint, integer)
|
|
424
|
+
TO opengeni_app;
|
|
425
|
+
GRANT EXECUTE ON FUNCTION
|
|
426
|
+
opengeni_private.purge_expired_transcription_recordings(bigint, integer)
|
|
427
|
+
TO opengeni_app;
|
|
428
|
+
END IF;
|
|
429
|
+
END $$;
|
|
430
|
+
|
|
431
|
+
CREATE POLICY transcription_recording_segments_subject_isolation
|
|
432
|
+
ON "transcription_recording_segments"
|
|
433
|
+
USING (
|
|
434
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
435
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
436
|
+
)
|
|
437
|
+
WITH CHECK (
|
|
438
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
439
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
440
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
|
|
3
|
+
ALTER TABLE "transcription_recording_segments"
|
|
4
|
+
ADD COLUMN "attempt_deadline_at" timestamptz;
|
|
5
|
+
|
|
6
|
+
-- Rows already in flight when this migration rolls out retain the original
|
|
7
|
+
-- fifteen-minute reclaim fence. New attempts receive the shorter provider
|
|
8
|
+
-- deadline from the API claim path.
|
|
9
|
+
UPDATE "transcription_recording_segments"
|
|
10
|
+
SET "attempt_deadline_at" = "attempt_started_at" + interval '10 minutes'
|
|
11
|
+
WHERE "attempt_id" IS NOT NULL
|
|
12
|
+
AND "attempt_started_at" IS NOT NULL
|
|
13
|
+
AND "attempt_deadline_at" IS NULL;
|
|
14
|
+
|
|
15
|
+
ALTER TABLE "transcription_recording_segments"
|
|
16
|
+
ADD CONSTRAINT "transcription_recording_segments_attempt_deadline_check"
|
|
17
|
+
CHECK (
|
|
18
|
+
("attempt_id" IS NULL AND "attempt_started_at" IS NULL AND "attempt_deadline_at" IS NULL)
|
|
19
|
+
OR (
|
|
20
|
+
"attempt_id" IS NOT NULL
|
|
21
|
+
AND "attempt_started_at" IS NOT NULL
|
|
22
|
+
AND "attempt_deadline_at" IS NOT NULL
|
|
23
|
+
)
|
|
24
|
+
);
|