@@ -4,21 +4,17 @@ import {
44 index ,
55 integer ,
66 jsonb ,
7- pgSchema ,
87 primaryKey ,
98 smallint ,
109 text ,
1110 timestamp ,
12- uniqueIndex ,
1311} from "drizzle-orm/pg-core" ;
14- import type {
15- WatchObservedOutcome ,
16- WatchResolution ,
17- WatchSpec ,
18- } from "@internal/dashboard-agent-contracts" ;
12+ import { dashboardAgentSchema } from "./schema-base.js" ;
1913
20- // Tables are schema-qualified explicitly, so the connection needs no `search_path`.
21- export const dashboardAgentSchema = pgSchema ( "trigger_dashboard_agent" ) ;
14+ // drizzle-kit reads this file, so the watch tables are re-exported here: the
15+ // generated SQL and the public surface stay exactly as they were.
16+ export * from "./schema-base.js" ;
17+ export * from "./watch-schema.js" ;
2218
2319/**
2420 * Scoped to org + user. `organizationId` and `userId` are main-DB ids with no FK:
@@ -143,117 +139,6 @@ export const investigations = dashboardAgentSchema.table(
143139 ]
144140) ;
145141
146- /** `active` is the only non-terminal status; the other three are immutable. */
147- export type WatchStatus = "active" | "fired" | "expired" | "cancelled" ;
148- /**
149- * `not_required` while active and for every cancellation. `delivering` is the
150- * in-flight claim, one deliverer at a time. See `claimWatchDelivery`.
151- */
152- export type WatchDeliveryStatus = "not_required" | "pending" | "delivering" | "delivered" ;
153- /** Cancellations are silent: no resolution, no wake. */
154- export type WatchCancelReason = "user" | "access_revoked" | "chat_deleted" | "scheduling_failed" ;
155-
156- /** `since` is server-set at creation, so `error_recurrence` can't match older errors. */
157- export type PersistedWatchSpec = WatchSpec & { since ?: string } ;
158-
159- /**
160- * The initiating identity is snapshotted at creation, so a membership change can only
161- * cancel a watch, never widen its scope. Main-DB ids, FK-free.
162- */
163- export const watches = dashboardAgentSchema . table (
164- "watches" ,
165- {
166- id : text ( "id" ) . primaryKey ( ) , // = watchId (`watch_…`)
167- chatId : text ( "chat_id" ) . notNull ( ) , // = chats.id
168- identity : text ( "identity" ) . notNull ( ) ,
169- spec : jsonb ( "spec" ) . $type < PersistedWatchSpec > ( ) . notNull ( ) ,
170- status : text ( "status" ) . $type < WatchStatus > ( ) . notNull ( ) . default ( "active" ) ,
171- deliveryStatus : text ( "delivery_status" )
172- . $type < WatchDeliveryStatus > ( )
173- . notNull ( )
174- . default ( "not_required" ) ,
175- cancelReason : text ( "cancel_reason" ) . $type < WatchCancelReason > ( ) ,
176- /**
177- * The meaning; `status` above stays the two-value transport encoding so persisted
178- * wake ids and dedup keys remain valid. NULL while active and on cancellation.
179- */
180- resolution : text ( "resolution" ) . $type < WatchResolution > ( ) ,
181- /** Written in the same statement as `resolution` and `lastResult`. */
182- observedOutcome : jsonb ( "observed_outcome" ) . $type < WatchObservedOutcome > ( ) ,
183- /** Consent given at creation. Never part of `identity`. */
184- investigateOnAttention : boolean ( "investigate_on_attention" ) . notNull ( ) . default ( false ) ,
185- // Immutable initiating identity, snapshotted at creation.
186- organizationId : text ( "organization_id" ) . notNull ( ) ,
187- projectId : text ( "project_id" ) . notNull ( ) ,
188- /** Nullable: rows created before this column don't carry it. */
189- projectRef : text ( "project_ref" ) ,
190- environmentId : text ( "environment_id" ) . notNull ( ) ,
191- userId : text ( "user_id" ) . notNull ( ) ,
192- createdAt : timestamp ( "created_at" , { withTimezone : true } ) . notNull ( ) . defaultNow ( ) ,
193- expiresAt : timestamp ( "expires_at" , { withTimezone : true } ) . notNull ( ) ,
194- lastCheckedAt : timestamp ( "last_checked_at" , { withTimezone : true } ) ,
195- firedAt : timestamp ( "fired_at" , { withTimezone : true } ) ,
196- // A claim older than the delivery grace is abandoned and may be re-claimed.
197- deliveryClaimedAt : timestamp ( "delivery_claimed_at" , { withTimezone : true } ) ,
198- // Fencing token, written fresh on every claim and required by the release and
199- // delivered marks, so a revived deliverer can't touch the claim that replaced it.
200- deliveryClaimId : text ( "delivery_claim_id" ) ,
201- deliveredAt : timestamp ( "delivered_at" , { withTimezone : true } ) ,
202- cancelledAt : timestamp ( "cancelled_at" , { withTimezone : true } ) ,
203- // On fire or expire this is the notification's payload.
204- lastResult : jsonb ( "last_result" ) . $type < Record < string , unknown > > ( ) ,
205- // Check idempotency keys are `watch:{id}:tick:{n}`.
206- tickCount : integer ( "tick_count" ) . notNull ( ) . default ( 0 ) ,
207- } ,
208- ( t ) => [
209- index ( "watches_chat_idx" ) . on ( t . chatId ) ,
210- // UNIQUE is the dedup guarantee: a read-then-insert check can't be race-proof
211- // under READ COMMITTED. Leading `chat_id` also serves the active-watches lookup.
212- uniqueIndex ( "watches_chat_active_identity_key" )
213- . on ( t . chatId , t . projectId , t . environmentId , t . identity )
214- . where ( sql `${ t . status } = 'active'` ) ,
215- // Sweep: active watches due to be checked / past their expiry.
216- index ( "watches_status_expires_idx" ) . on ( t . status , t . expiresAt ) ,
217- // Sweep: resolved watches whose wake is still owed.
218- index ( "watches_pending_delivery_idx" )
219- . on ( t . firedAt , t . lastCheckedAt )
220- . where ( sql `${ t . deliveryStatus } in ('pending', 'delivering')` ) ,
221- // Tenant columns must lead so the index, not the `chats` join, narrows to this
222- // user first. The trailing expression is what the wake queries filter and order on.
223- index ( "watches_org_user_wake_idx" )
224- . on ( t . organizationId , t . userId , sql `coalesce(${ t . firedAt } , ${ t . lastCheckedAt } ) desc` )
225- . where ( sql `${ t . deliveryStatus } = 'delivered' and ${ t . status } in ('fired', 'expired')` ) ,
226- // Covers `listActiveWatchesForBatch`.
227- index ( "watches_active_env_idx" )
228- . on ( t . environmentId , t . expiresAt )
229- . where ( sql `${ t . status } = 'active'` ) ,
230- ]
231- ) ;
232-
233- export type WatchBatchStatus = "running" | "stopped" ;
234-
235- /**
236- * `epoch` and `generation` are claimed together by {@link claimWatchBatchTick}, so a
237- * duplicated schedule can't fork the chain. FK-free.
238- */
239- export const watchBatches = dashboardAgentSchema . table (
240- "watch_batches" ,
241- {
242- environmentId : text ( "environment_id" ) . notNull ( ) ,
243- // 1 | 5 | 15 | 60.
244- cadenceMinutes : integer ( "cadence_minutes" ) . notNull ( ) ,
245- // Bumped by every arm. Runs carry it, and a claim requires it to match.
246- epoch : integer ( "epoch" ) . notNull ( ) . default ( 0 ) ,
247- // Inside the current epoch. The claim is its only writer.
248- generation : integer ( "generation" ) . notNull ( ) . default ( 0 ) ,
249- status : text ( "status" ) . $type < WatchBatchStatus > ( ) . notNull ( ) . default ( "running" ) ,
250- armedAt : timestamp ( "armed_at" , { withTimezone : true } ) . notNull ( ) . defaultNow ( ) ,
251- // Heartbeat. NULL between an arm and the first run landing.
252- lastTickAt : timestamp ( "last_tick_at" , { withTimezone : true } ) ,
253- } ,
254- ( t ) => [ primaryKey ( { columns : [ t . environmentId , t . cadenceMinutes ] } ) ]
255- ) ;
256-
257142export type Chat = typeof chats . $inferSelect ;
258143export type NewChat = typeof chats . $inferInsert ;
259144export type ChatSession = typeof chatSessions . $inferSelect ;
@@ -262,6 +147,3 @@ export type ChatTurnEval = typeof chatTurnEvals.$inferSelect;
262147export type NewChatTurnEval = typeof chatTurnEvals . $inferInsert ;
263148export type Investigation = typeof investigations . $inferSelect ;
264149export type NewInvestigation = typeof investigations . $inferInsert ;
265- export type Watch = typeof watches . $inferSelect ;
266- export type NewWatch = typeof watches . $inferInsert ;
267- export type WatchBatch = typeof watchBatches . $inferSelect ;
0 commit comments