From 33f55939bb09f26936fccfb7487262bcb16e3a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 15:28:45 +0200 Subject: [PATCH 01/43] feat(mobile): deliver glanceable snapshot updates Add activity-token registration and background delivery so eligible work updates the iOS Live Activity, Android ongoing notification, and Home widgets without opening the app. Logout unregisters tokens. Old clients ignore the new payload type. --- ENVIRONMENT.md | 7 + .../src/app/(app)/(tabs)/(2_agents)/index.tsx | 11 + apps/mobile/src/lib/auth/logout-cleanup.ts | 7 + .../push-registration-reconciliation.test.ts | 5 + .../auth/push-registration-reconciliation.ts | 4 + .../src/lib/glanceable/activity-kit-prompt.ts | 35 + .../lib/glanceable/delivery-registration.ts | 98 + apps/mobile/src/lib/notification-path.ts | 5 + apps/mobile/src/lib/notifications.ts | 60 +- .../glanceable-agents-snapshot/route.ts | 61 + apps/web/src/lib/active-sessions-list.ts | 462 + .../lib/glanceable-agents-snapshot-server.ts | 39 + apps/web/src/lib/user/index.test.ts | 44 + apps/web/src/lib/user/index.ts | 5 + .../web/src/routers/active-sessions-router.ts | 463 +- apps/web/src/routers/user-router.ts | 55 + .../0233_square_daimon_hellstrom.sql | 14 + .../db/src/migrations/meta/0233_snapshot.json | 39690 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema.ts | 40 + packages/notifications/src/locales/en.json | 3 +- packages/notifications/src/push-data.ts | 20 + .../notifications/src/push-presentation.ts | 15 + services/notifications/src/bindings.d.ts | 12 + services/notifications/src/index.ts | 186 +- .../src/lib/apns-live-activity.test.ts | 153 + .../src/lib/apns-live-activity.ts | 148 + .../src/lib/glanceable-delivery.test.ts | 181 + .../src/lib/glanceable-delivery.ts | 108 + 29 files changed, 41492 insertions(+), 446 deletions(-) create mode 100644 apps/mobile/src/lib/glanceable/activity-kit-prompt.ts create mode 100644 apps/mobile/src/lib/glanceable/delivery-registration.ts create mode 100644 apps/web/src/app/api/internal/glanceable-agents-snapshot/route.ts create mode 100644 apps/web/src/lib/active-sessions-list.ts create mode 100644 apps/web/src/lib/glanceable-agents-snapshot-server.ts create mode 100644 packages/db/src/migrations/0233_square_daimon_hellstrom.sql create mode 100644 packages/db/src/migrations/meta/0233_snapshot.json create mode 100644 services/notifications/src/lib/apns-live-activity.test.ts create mode 100644 services/notifications/src/lib/apns-live-activity.ts create mode 100644 services/notifications/src/lib/glanceable-delivery.test.ts create mode 100644 services/notifications/src/lib/glanceable-delivery.ts diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index 2b843cc7b2..c3208de065 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -322,6 +322,13 @@ When `VERCEL_TARGET_ENV` is absent in local development or a script process, tra ## Services +### Notifications Worker + +- `APNS_TEAM_ID` - Apple Developer team ID for the token-based APNs key used to send Live Activity pushes. [SERVER] +- `APNS_KEY_ID` - APNs key identifier (`kid`) for the Live Activity push key. [SERVER] +- `APNS_PRIVATE_KEY` - PKCS#8 ES256 `.p8` private key contents for APNs provider-token signing. `[SECRET]` +- `APNS_TOPIC` - iOS app bundle id (`com.kilocode.kiloapp`); Live Activity pushes use `.push-type.liveactivity`. [SERVER] + ### KiloClaw Controller - `KILOCODE_API_KEY` - API key used by the KiloClaw controller for internal gateway identity. `[SECRET]` diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx index 31ce78b6d5..5f03d58c56 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; +import { useFocusEffect } from 'expo-router'; import { Alert, Platform } from 'react-native'; import { i18n } from '@/i18n'; @@ -11,6 +12,7 @@ import { type GitHubInstallReturnOutcome, subscribeToGitHubInstallReturnOutcome, } from '@/lib/github-install-return'; +import { showActivityKitDisabledAlertOnce } from '@/lib/glanceable/activity-kit-prompt'; import { trpcClient } from '@/lib/trpc'; export type GitHubInstallOutcomeAlertButton = { @@ -135,5 +137,14 @@ export default function AgentSessionList() { return subscribeToGitHubInstallReturnOutcome(consumeReturnOutcome); }, [consumeReturnOutcome]); + // Show the one-time "turn on Live Activities" alert when the Agents tab + // regains focus and ActivityKit is unavailable. Never auto-alerts from the + // publisher; this tab focus is the single prompt site. + useFocusEffect( + useCallback(() => { + showActivityKitDisabledAlertOnce(); + }, []) + ); + return ; } diff --git a/apps/mobile/src/lib/auth/logout-cleanup.ts b/apps/mobile/src/lib/auth/logout-cleanup.ts index edef6541d9..c5ad0bf386 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.ts @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/react-native'; import * as z from 'zod'; import { getDevicePushTokenOutcome } from '@/lib/notifications'; +import { getGlanceableDelivery } from '@/lib/glanceable/sink-registry'; import { readCachedUserId } from '@/lib/persist/read-cache'; import { queryClient } from '@/lib/query-client'; import { LOGOUT_CLEANUP_TOMBSTONE_KEY } from '@/lib/storage-keys'; @@ -110,6 +111,12 @@ export async function runLogoutCleanup(): Promise { : Promise.resolve(), ]); + // Unregister activity tokens (Live Activity / push-to-start) before the + // epoch bump. Best-effort: the delivery re-registers tokens on the next + // activity start, so a failed unregister is not tombstoned — a tombstone + // has no reconciliation retry for activity tokens. + getGlanceableDelivery().unregisterTokens(); + const unregister = results[1]; let needsPushUnregister = false; diff --git a/apps/mobile/src/lib/auth/push-registration-reconciliation.test.ts b/apps/mobile/src/lib/auth/push-registration-reconciliation.test.ts index 0dd9ee5646..2d1238b4a0 100644 --- a/apps/mobile/src/lib/auth/push-registration-reconciliation.test.ts +++ b/apps/mobile/src/lib/auth/push-registration-reconciliation.test.ts @@ -49,6 +49,11 @@ vi.mock('@/lib/query-client', () => ({ queryClient: queryClientMock, })); vi.mock('@/lib/hooks/use-language-preference', () => languageMock); +// The slice's side-effect import registers iOS activity-token delivery and +// transitively loads expo-widgets / @expo/ui; this suite exercises +// push-token reconciliation only, so stub the side effect instead of +// mocking every iOS native module. +vi.mock('@/lib/glanceable/delivery-registration', () => ({})); vi.mock('expo-notifications', () => ({ addPushTokenListener: expoNotificationsMock.addPushTokenListener, })); diff --git a/apps/mobile/src/lib/auth/push-registration-reconciliation.ts b/apps/mobile/src/lib/auth/push-registration-reconciliation.ts index 3c4552a5f1..d054d81dd3 100644 --- a/apps/mobile/src/lib/auth/push-registration-reconciliation.ts +++ b/apps/mobile/src/lib/auth/push-registration-reconciliation.ts @@ -13,6 +13,10 @@ import { getDevicePushTokenOutcome, getPlatform } from '@/lib/notifications'; import { queryClient } from '@/lib/query-client'; import { trpcClient } from '@/lib/trpc'; +// Import side effect: registers the iOS activity-token delivery with the +// glanceable sink registry so the publisher can register/unregister tokens. +import '@/lib/glanceable/delivery-registration'; + const trpcOptions = createTRPCOptionsProxy({ client: trpcClient, queryClient }); /** diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts new file mode 100644 index 0000000000..aa69e6a4d8 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -0,0 +1,35 @@ +import { Alert, Linking, Platform } from 'react-native'; + +import { getActivityKitDenied } from '@/glanceable-ios/ios-sink'; +import { i18n } from '@/i18n'; + +/** + * The one in-app Open Settings alert for an ActivityKit-unavailable surface. + * Shown at most once per process when the Agents tab regains focus — never + * auto-alerted from the publisher, so the publisher stays a pure state machine. + */ + +let alertShown = false; + +export function showActivityKitDisabledAlertOnce(): void { + if (Platform.OS !== 'ios' || alertShown) { + return; + } + if (!getActivityKitDenied()) { + return; + } + alertShown = true; + Alert.alert( + i18n.t('glanceable.activityKitDisabledTitle'), + i18n.t('glanceable.activityKitDisabledBody'), + [ + { text: i18n.t('common.cancel'), style: 'cancel' }, + { text: i18n.t('common.openSettings'), onPress: () => void Linking.openSettings() }, + ] + ); +} + +/** Test-only: drop the once-per-process latch between cases. */ +export function _resetActivityKitPromptForTests(): void { + alertShown = false; +} diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.ts b/apps/mobile/src/lib/glanceable/delivery-registration.ts new file mode 100644 index 0000000000..77c8542acf --- /dev/null +++ b/apps/mobile/src/lib/glanceable/delivery-registration.ts @@ -0,0 +1,98 @@ +import { Platform } from 'react-native'; + +import { addPushToStartTokenListener } from 'expo-widgets'; + +import { ActiveAgentsLiveActivity } from '@/glanceable-ios/active-agents-live-activity'; +import { trpcClient } from '@/lib/trpc'; + +import { type GlanceableDelivery, setGlanceableDelivery } from './sink-registry'; + +/** + * iOS activity-token registrar. Wires the glanceable publisher's delivery + * hooks to `user.registerActivityToken`/`user.unregisterActivityToken` so the + * server can reach this device's Live Activity and push-to-start token via + * APNs. Android uses Expo push tokens and never calls this delivery. + */ + +let pushToStartToken: string | null = null; + +async function register( + token: string, + kind: 'ios_push_to_start' | 'ios_activity', + organizationId: string | null +): Promise { + try { + await trpcClient.user.registerActivityToken.mutate({ + token, + kind, + platform: 'ios', + organizationId, + }); + } catch { + // Best effort: a failed registration is retried on the next start. + } +} + +async function unregister(token: string): Promise { + try { + await trpcClient.user.unregisterActivityToken.mutate({ token }); + } catch { + // Best effort: a stale token row is pruned server-side. + } +} + +if (Platform.OS === 'ios') { + // Push-to-start token events are emitted whenever the system rotates the + // token; cache the latest and register on the next activity start. + addPushToStartTokenListener(({ activityPushToStartToken }) => { + pushToStartToken = activityPushToStartToken; + }); +} + +const delivery: GlanceableDelivery = { + registerTokens(_snapshot, organizationId) { + if (Platform.OS !== 'ios') { + return; + } + void (async () => { + if (pushToStartToken !== null) { + await register(pushToStartToken, 'ios_push_to_start', organizationId); + } + try { + const activity = ActiveAgentsLiveActivity.getInstances().at(-1); + if (activity) { + const token = await activity.getPushToken(); + if (token) { + await register(token, 'ios_activity', organizationId); + } + } + } catch { + // getInstances can throw on unsupported surfaces; the sink owns retry. + } + })(); + }, + + unregisterTokens() { + if (Platform.OS !== 'ios') { + return; + } + void (async () => { + if (pushToStartToken !== null) { + await unregister(pushToStartToken); + } + try { + const activity = ActiveAgentsLiveActivity.getInstances().at(-1); + if (activity) { + const token = await activity.getPushToken(); + if (token) { + await unregister(token); + } + } + } catch { + // Nothing to unregister when no activity survives. + } + })(); + }, +}; + +setGlanceableDelivery(delivery); diff --git a/apps/mobile/src/lib/notification-path.ts b/apps/mobile/src/lib/notification-path.ts index 740aa31ae8..728e90cdb0 100644 --- a/apps/mobile/src/lib/notification-path.ts +++ b/apps/mobile/src/lib/notification-path.ts @@ -29,6 +29,11 @@ export function notificationPathForData(data: PushData): string { case 'scheduled-action': { return chatSandboxRoute(data.sandboxId); } + case 'active_agents_glanceable': { + // The aggregate glanceable payload never opens a session chat; it lands + // on the agents tab. + return '/(app)/(tabs)/(2_agents)'; + } default: { // Exhaustiveness: new PushData variants must be handled above. const _exhaustive: never = data; diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index 71a8377b8d..1e26752e11 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -10,7 +10,15 @@ import { type PushData, pushDataSchema, } from '@kilocode/notifications'; - +import { + type GlanceableAgentsSnapshot, + isEligibleGlanceableWork, + shouldDiscardGlanceableRevision, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; +import { getLastGlanceableSnapshot, getLocalScopeKey } from '@/lib/glanceable/persist'; +import { getGlanceableSinks } from '@/lib/glanceable/sink-registry'; import { i18n } from '@/i18n'; import { setPendingDeepLink } from './deep-link-launch'; import { notificationPathForData } from './notification-path'; @@ -46,6 +54,47 @@ export function parseNotificationData(data: unknown): PushData | null { return parsed.success ? parsed.data : null; } +/** + * Apply an `active_agents_glanceable` background push to the glanceable sinks + * (widgets, Android ongoing, iOS Live Activity). Returns false when the push + * must be dropped: its opaque scope key does not match the persisted local + * scope key, or its revision is older than the last applied snapshot. + * + * The server omits `accountEpoch`, so it is set to the current local epoch + * before publishing. Never opens a session chat. + */ +export function applyGlanceablePushData( + data: Extract +): boolean { + if (data.scopeKey !== getLocalScopeKey()) { + return false; + } + + const { type: _type, ...fields } = data; + const snapshot: GlanceableAgentsSnapshot = { + ...fields, + accountEpoch: currentAuthEpoch(), + }; + + const current = getLastGlanceableSnapshot(); + if (current !== null && shouldDiscardGlanceableRevision(snapshot, current)) { + return false; + } + + const ctx = { organizationId: null }; + if (isEligibleGlanceableWork(snapshot)) { + for (const sink of getGlanceableSinks()) { + sink.publish(snapshot); + sink.startOrUpdate(snapshot, ctx); + } + } else { + for (const sink of getGlanceableSinks()) { + sink.publish(snapshot); + } + } + return true; +} + const shown = { shouldPlaySound: true, shouldSetBadge: true, @@ -66,6 +115,14 @@ export function setupNotificationHandler() { handleNotification: async notification => { const data = parseNotificationData(notification.request.content.data); + if (data?.type === 'active_agents_glanceable') { + // The aggregate glanceable push is a data carrier for the ongoing + // notification/widgets, never a visible banner: the local ongoing owns + // the display. Apply it to the sinks regardless of the discard outcome. + applyGlanceablePushData(data); + return suppressed; + } + if ( data?.type === 'chat.message' && activeChatLocation?.sandboxId === data.sandboxId && @@ -158,6 +215,7 @@ const CHANNEL_NAME_KEYS = { kiloclaw: 'notifications.channel.kiloclaw', balance: 'notifications.channel.balance', security: 'notifications.channel.security', + 'active-agents': 'glanceable.channelName', } as const satisfies Record; /** diff --git a/apps/web/src/app/api/internal/glanceable-agents-snapshot/route.ts b/apps/web/src/app/api/internal/glanceable-agents-snapshot/route.ts new file mode 100644 index 0000000000..a3543e2488 --- /dev/null +++ b/apps/web/src/app/api/internal/glanceable-agents-snapshot/route.ts @@ -0,0 +1,61 @@ +import { createHmac, timingSafeEqual } from 'crypto'; +import { NextResponse, type NextRequest } from 'next/server'; +import { z } from 'zod'; + +import { INTERNAL_API_SECRET } from '@/lib/config.server'; +import { buildGlanceableSnapshotForUser } from '@/lib/glanceable-agents-snapshot-server'; +import { ensureOrganizationAccess } from '@/routers/organizations/utils'; +import type { TRPCContext } from '@/lib/trpc/init'; + +const SECRET_COMPARE_HMAC_KEY = Buffer.from('glanceable-agents-snapshot-secret-compare'); + +const BodySchema = z + .object({ + userId: z.string().min(1), + organizationId: z.string().min(1).nullable(), + }) + .strict(); + +function secretMatches(provided: string | null, expected: string): boolean { + if (!provided) return false; + const left = createHmac('sha256', SECRET_COMPARE_HMAC_KEY).update(provided).digest(); + const right = createHmac('sha256', SECRET_COMPARE_HMAC_KEY).update(expected).digest(); + return timingSafeEqual(left, right); +} + +/** + * Internal server-to-server snapshot builder for background glanceable + * delivery. The notifications worker is the only caller; mobile never calls + * this route. Requires the internal secret, and — when `organizationId` is a + * string — re-checks that `userId` is a member of that organization with the + * same helper the active-sessions router uses, so a compromised worker cannot + * read another user's org snapshot. + */ +export async function POST(req: NextRequest) { + const secret = req.headers.get('X-Internal-Secret'); + if (!INTERNAL_API_SECRET || !secretMatches(secret, INTERNAL_API_SECRET)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const rawBody: unknown = await req.json().catch(() => null); + const parsedBody = BodySchema.safeParse(rawBody); + if (!parsedBody.success) { + return NextResponse.json({ error: 'Invalid body' }, { status: 400 }); + } + + const { userId, organizationId } = parsedBody.data; + + if (typeof organizationId === 'string') { + try { + await ensureOrganizationAccess( + { user: { id: userId, is_admin: false } } as unknown as TRPCContext, + organizationId + ); + } catch { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }); + } + } + + const snapshot = await buildGlanceableSnapshotForUser({ userId, organizationId }); + return NextResponse.json(snapshot, { status: 200 }); +} diff --git a/apps/web/src/lib/active-sessions-list.ts b/apps/web/src/lib/active-sessions-list.ts new file mode 100644 index 0000000000..8da46f1b06 --- /dev/null +++ b/apps/web/src/lib/active-sessions-list.ts @@ -0,0 +1,462 @@ +import 'server-only'; +import { TRPCError } from '@trpc/server'; +import { z } from 'zod'; +import { SESSION_INGEST_WORKER_URL } from '@/lib/config.server'; +import { generateInternalServiceToken } from '@/lib/tokens'; +import { db } from '@/lib/drizzle'; +import { + cli_sessions_v2, + cloud_agent_session_runs, + github_branch_pull_requests, +} from '@kilocode/db/schema'; +import { and, desc, eq, gt, inArray, isNotNull, isNull, or, sql } from 'drizzle-orm'; +import { + associatedPrSchema, + formatAssociatedPr, + sessionPrJoinPredicate, +} from '@/routers/cli-sessions-v2-router'; + +export const activeSessionSchema = z.object({ + id: z.string(), + status: z.string(), + title: z.string(), + connectionId: z.string(), + gitUrl: z.string().optional(), + gitBranch: z.string().optional(), + createdOnPlatform: z.string().optional(), + createdAt: z.string().optional(), + updatedAt: z.string().optional(), + /** + * Latest agent activity timestamp from `cli_sessions_v2.last_activity_at` + * (raw DB text, same treatment as `createdAt`/`updatedAt`). Omitted when + * the column is NULL or the row was never enriched. + */ + lastActivityAt: z.string().optional(), + /** + * Capabilities advertised by the CLI connection that owns this session. + * Omitted when the owning connection's latest heartbeat did not include a + * capabilities object (legacy CLI, or a CLI that predates the field). + */ + capabilities: z.object({ attachments: z.boolean().optional() }).optional(), + // Optional: legacy CLIs (predating the `kilo remote` spawner) never + // report a platform. Only present in the response when the CLI supplied it. + platform: z.string().optional(), + /** + * Optional total session cost from `cli_sessions_v2.total_cost_microdollars` + * (microdollars, bigint). Only present when the DB row carries a non-null + * value — null never goes on the wire. Unenriched heartbeat rows (no + * `cli_sessions_v2` join) omit the key. The wire may legitimately carry + * zero; display still omits it via `formatSessionTotalCost`. + */ + totalCostMicrodollars: z.number().optional(), + /** + * Associated pull request for this session's branch, merged from the + * per-tenant PR cache during enrichment. Old clients omit this key; + * remove optional when every client is past this release. + */ + associatedPr: associatedPrSchema.optional(), +}); + +const activeSessionsResponseSchema = z.object({ + sessions: z.array(activeSessionSchema), +}); + +/** + * A live session as this router returns it: the worker's wire row plus the + * fields enriched from `cli_sessions_v2`. + */ +export type ActiveSession = z.infer & { + /** + * Owning organization from `cli_sessions_v2`; `null` = personal, which + * also covers a live session with no `cli_sessions_v2` row (an + * unattributable session — the server attributes it to personal). + */ + organizationId?: string | null; +}; + +/** Sentinel `connectionId` for cloud-agent rows merged when the flag is on. */ +export const CLOUD_AGENT_CONNECTION_ID = 'cloud-agent'; + +/** + * Warm-idle window for live cloud sessions. Mirrors + * `KILO_SERVER_IDLE_TIMEOUT_MS_DEFAULT` in + * services/cloud-agent-next/src/persistence/CloudAgentSession.ts:189-190. + * Env override drift is accepted (A2). + */ +const CLOUD_AGENT_WARM_IDLE_CUTOFF = sql`now() - interval '15 minutes'`; + +type EnrichmentRow = { + session_id: string; + created_on_platform: string | null; + created_at: string; + updated_at: string; + status: string | null; + title: string | null; + organization_id: string | null; + last_activity_at: string | null; + total_cost_microdollars: number | null; + // Session's own stored PR link, aliased so it never collides with the + // cache keys below. + session_pr_platform: string | null; + session_pr_url: string | null; + session_pr_number: number | null; + // Per-tenant PR cache columns from the LEFT JOIN. + pr_url: string | null; + pr_number: number | null; + pr_state: string | null; + pr_title: string | null; + pr_head_sha: string | null; + pr_last_synced_at: string | null; + pr_review_decision: string | null; + review_decision_pending: boolean | null; +}; + +type CloudCandidateRow = EnrichmentRow & { + git_url: string | null; + git_branch: string | null; + cloud_agent_session_id: string | null; +}; + +/** + * Fold an enriched row's flat PR columns into the `associatedPr` shape. + * Returns `null` when there is no cache PR and no stored session link, so + * callers can omit the key entirely instead of emitting `associatedPr: null`. + */ +function associatedPrFromRow(row: EnrichmentRow): z.infer | null { + return formatAssociatedPr( + { + platform: row.session_pr_platform, + pr_url: row.session_pr_url, + pr_number: row.session_pr_number, + updated_at: row.updated_at, + }, + { + pr_url: row.pr_url, + pr_number: row.pr_number, + pr_state: row.pr_state, + pr_title: row.pr_title, + pr_head_sha: row.pr_head_sha, + pr_last_synced_at: row.pr_last_synced_at, + pr_review_decision: row.pr_review_decision, + review_decision_pending: row.review_decision_pending, + } + ); +} + +/** + * Overlay stored attention (question/permission) onto a live heartbeat + * status. Non-attention DB values yield to live so busy/idle remain + * authoritative while the CLI is connected. + * + * Must run in the router: client fetchQuery replaces the cache wholesale, + * so sticky attention held only in client helpers is wiped on every + * enrichment / reconnect / cli.connected refresh. + */ +export function resolveActiveSessionStatus( + liveStatus: string, + storedStatus: string | null | undefined +): string { + if (storedStatus === 'question' || storedStatus === 'permission') { + return storedStatus; + } + return liveStatus; +} + +function mapEnrichedHeartbeatSession( + session: ActiveSession, + row: EnrichmentRow | undefined +): ActiveSession { + if (!row) { + // Always emit the field, `null` included: an absent `organizationId` on + // a client-cached row must mean "never server-attributed" and nothing + // else, or the client filter cannot tell a heartbeat-inserted row apart + // from a server-attributed personal one (D4/D6). + return { ...session, organizationId: null }; + } + const mapped: ActiveSession = { + ...session, + status: resolveActiveSessionStatus(session.status, row.status), + // The tray title must be what a rename wrote, not what the CLI still + // reports: nothing propagates a cloud rename back to the CLI, so the + // heartbeat title stays stale forever. A NULL title (never-ingested + // placeholder row) falls back to the live one. + title: row.title ?? session.title, + organizationId: row.organization_id, + createdOnPlatform: row.created_on_platform ?? undefined, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; + if (row.last_activity_at != null) { + mapped.lastActivityAt = row.last_activity_at; + } + if (row.total_cost_microdollars != null) { + mapped.totalCostMicrodollars = row.total_cost_microdollars; + } + const associatedPr = associatedPrFromRow(row); + if (associatedPr) { + mapped.associatedPr = associatedPr; + } + return mapped; +} + +function mapCloudCandidateRow(row: CloudCandidateRow): ActiveSession { + const mapped: ActiveSession = { + id: row.session_id, + // Cloud rows have no live heartbeat source — use the stored status as-is + // (do NOT run resolveActiveSessionStatus). + status: row.status ?? '', + title: row.title ?? '', + connectionId: CLOUD_AGENT_CONNECTION_ID, + gitUrl: row.git_url ?? undefined, + gitBranch: row.git_branch ?? undefined, + createdOnPlatform: row.created_on_platform ?? undefined, + createdAt: row.created_at, + updatedAt: row.updated_at, + // Key ALWAYS emitted, null included (D21) — mobile filter treats an + // absent key as never-attributed and would hide personal cloud rows. + organizationId: row.organization_id ?? null, + }; + if (row.last_activity_at != null) { + mapped.lastActivityAt = row.last_activity_at; + } + if (row.total_cost_microdollars != null) { + mapped.totalCostMicrodollars = row.total_cost_microdollars; + } + const associatedPr = associatedPrFromRow(row); + if (associatedPr) { + mapped.associatedPr = associatedPr; + } + return mapped; +} + +function throwOrgContextFailure(error: unknown): never { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to resolve the organization context for active sessions', + cause: error, + }); +} + +export type ListActiveSessionsInput = { + userId: string; + /** + * Personal/organization context. `undefined` = no context filter (the + * liveness-resolution callers), `null` = personal only, a uuid = that + * organization. Mirrors `addOrganizationCondition` in + * `cli-sessions-v2-router.ts`. + */ + organizationId: string | null | undefined; + /** When true, also merge live cloud-agent root sessions from Postgres. */ + includeCloudAgentSessions: boolean; +}; + +/** + * Fetch + parse + enrich the active sessions list for a user. This is the + * extracted core of the tRPC `activeSessions.list` procedure: it takes no + * tRPC context so the snapshot builder and other server-side callers can use + * it directly. The router owns `ensureOrganizationAccess` before calling. + */ +export async function listActiveSessions({ + userId, + organizationId, + includeCloudAgentSessions, +}: ListActiveSessionsInput): Promise<{ sessions: ActiveSession[] }> { + // Phase 1: fetch + parse the worker response. Any failure here + // (HTTP error, malformed JSON, schema mismatch) degrades to an empty + // list exactly as before — these are "no data" outcomes from the + // mobile client's point of view. With includeCloudAgentSessions, all + // three early exits fall through to the cloud-candidates query (D11). + let parsed: { sessions: ActiveSession[] } = { sessions: [] }; + + if (!SESSION_INGEST_WORKER_URL) { + if (!includeCloudAgentSessions) { + return { sessions: [] as ActiveSession[] }; + } + } else { + const token = generateInternalServiceToken(userId); + const url = `${SESSION_INGEST_WORKER_URL}/api/sessions/active`; + + try { + const response = await fetch(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + console.warn( + `[active-sessions] fetch failed: ${response.status} ${response.statusText}`, + await response.text().catch(() => '') + ); + if (!includeCloudAgentSessions) { + return { sessions: [] as ActiveSession[] }; + } + } else { + const raw = await response.json(); + parsed = activeSessionsResponseSchema.parse(raw); + } + } catch (error) { + console.warn('[active-sessions] error:', error); + if (!includeCloudAgentSessions) { + return { sessions: [] as ActiveSession[] }; + } + } + } + + // Phase 2a — Query 1: enrich heartbeat sessions from cli_sessions_v2. + // Independent try/catch from Query 2 (D16). Skipped when there are no + // heartbeat ids (never an empty inArray). + const ids = parsed.sessions.map(s => s.id); + let enrichmentRows: EnrichmentRow[] = []; + let enrichmentFailed = false; + + if (ids.length > 0) { + try { + enrichmentRows = await db + .select({ + session_id: cli_sessions_v2.session_id, + created_on_platform: cli_sessions_v2.created_on_platform, + created_at: cli_sessions_v2.created_at, + updated_at: cli_sessions_v2.updated_at, + status: cli_sessions_v2.status, + title: cli_sessions_v2.title, + organization_id: cli_sessions_v2.organization_id, + last_activity_at: cli_sessions_v2.last_activity_at, + total_cost_microdollars: cli_sessions_v2.total_cost_microdollars, + session_pr_platform: cli_sessions_v2.platform, + session_pr_url: cli_sessions_v2.pr_url, + session_pr_number: cli_sessions_v2.pr_number, + pr_url: github_branch_pull_requests.pr_url, + pr_number: github_branch_pull_requests.pr_number, + pr_state: github_branch_pull_requests.pr_state, + pr_title: github_branch_pull_requests.pr_title, + pr_head_sha: github_branch_pull_requests.pr_head_sha, + pr_last_synced_at: github_branch_pull_requests.pr_last_synced_at, + pr_review_decision: github_branch_pull_requests.pr_review_decision, + review_decision_pending: github_branch_pull_requests.review_decision_pending, + }) + .from(cli_sessions_v2) + .leftJoin(github_branch_pull_requests, sessionPrJoinPredicate) + .where( + and(eq(cli_sessions_v2.kilo_user_id, userId), inArray(cli_sessions_v2.session_id, ids)) + ); + } catch (error) { + console.warn('[active-sessions] enrichment db query failed:', error); + // Attribution is unknowable without the join. An unfiltered caller (web, + // `resolveSession`) keeps the existing best-effort unenriched passthrough — + // a DB blip must not collapse its list. A filtered caller cannot be + // answered at all: calling every row personal would lie (breaking AC 1) + // and returning an empty list would silently blank the tray with no + // explanation. So fail the query and let the client's already-shipped + // retryable state handle it (D11). + if (organizationId === undefined) { + enrichmentFailed = true; + if (!includeCloudAgentSessions) { + return parsed; + } + } else { + throwOrgContextFailure(error); + } + } + } else if (!includeCloudAgentSessions) { + // Flag-off empty heartbeats: today's short-circuit (no DB). + return parsed; + } + + let sessions: ActiveSession[]; + if (enrichmentFailed) { + // Unfiltered + flag on: keep wire rows unenriched, still attempt cloud. + sessions = [...parsed.sessions]; + } else { + const byId = new Map(enrichmentRows.map(r => [r.session_id, r])); + sessions = []; + for (const session of parsed.sessions) { + const row = byId.get(session.id); + // No `cli_sessions_v2` row → unattributable → personal. An SQL-side filter + // could not tell this case apart from "belongs to another organization". + const rowOrganizationId = row?.organization_id ?? null; + if (organizationId !== undefined && rowOrganizationId !== organizationId) { + continue; + } + sessions.push(mapEnrichedHeartbeatSession(session, row)); + } + } + + // Phase 2b — Query 2: live cloud-agent candidates (flag-on only). + // Own try/catch; failure semantics mirror Query 1 (D16). + if (includeCloudAgentSessions) { + try { + const orgPredicate = + organizationId === null + ? isNull(cli_sessions_v2.organization_id) + : typeof organizationId === 'string' + ? eq(cli_sessions_v2.organization_id, organizationId) + : undefined; + + const livePredicate = or( + sql`EXISTS ( + SELECT 1 FROM ${cloud_agent_session_runs} + WHERE ${cloud_agent_session_runs.cloud_agent_session_id} = ${cli_sessions_v2.cloud_agent_session_id} + AND ${cloud_agent_session_runs.terminal_at} IS NULL + )`, + and( + eq(cli_sessions_v2.status, 'idle'), + gt(cli_sessions_v2.status_updated_at, CLOUD_AGENT_WARM_IDLE_CUTOFF) + ) + ); + + const cloudRows: CloudCandidateRow[] = await db + .select({ + session_id: cli_sessions_v2.session_id, + created_on_platform: cli_sessions_v2.created_on_platform, + created_at: cli_sessions_v2.created_at, + updated_at: cli_sessions_v2.updated_at, + status: cli_sessions_v2.status, + title: cli_sessions_v2.title, + organization_id: cli_sessions_v2.organization_id, + git_url: cli_sessions_v2.git_url, + git_branch: cli_sessions_v2.git_branch, + last_activity_at: cli_sessions_v2.last_activity_at, + total_cost_microdollars: cli_sessions_v2.total_cost_microdollars, + cloud_agent_session_id: cli_sessions_v2.cloud_agent_session_id, + session_pr_platform: cli_sessions_v2.platform, + session_pr_url: cli_sessions_v2.pr_url, + session_pr_number: cli_sessions_v2.pr_number, + pr_url: github_branch_pull_requests.pr_url, + pr_number: github_branch_pull_requests.pr_number, + pr_state: github_branch_pull_requests.pr_state, + pr_title: github_branch_pull_requests.pr_title, + pr_head_sha: github_branch_pull_requests.pr_head_sha, + pr_last_synced_at: github_branch_pull_requests.pr_last_synced_at, + pr_review_decision: github_branch_pull_requests.pr_review_decision, + review_decision_pending: github_branch_pull_requests.review_decision_pending, + }) + .from(cli_sessions_v2) + .leftJoin(github_branch_pull_requests, sessionPrJoinPredicate) + .where( + and( + eq(cli_sessions_v2.kilo_user_id, userId), + isNull(cli_sessions_v2.parent_session_id), + isNotNull(cli_sessions_v2.cloud_agent_session_id), + orgPredicate, + livePredicate + ) + ) + .orderBy(desc(cli_sessions_v2.created_at)) + .limit(50); + + const heartbeatIds = new Set(sessions.map(s => s.id)); + for (const row of cloudRows) { + // CLI adoption wins: keep the worker row's real connectionId/status. + if (heartbeatIds.has(row.session_id)) continue; + sessions.push(mapCloudCandidateRow(row)); + } + } catch (error) { + console.warn('[active-sessions] cloud candidates db query failed:', error); + if (organizationId !== undefined) { + throwOrgContextFailure(error); + } + // Unfiltered: skip cloud merge, return heartbeat rows as built. + } + } + + return { sessions }; +} diff --git a/apps/web/src/lib/glanceable-agents-snapshot-server.ts b/apps/web/src/lib/glanceable-agents-snapshot-server.ts new file mode 100644 index 0000000000..f1940b311e --- /dev/null +++ b/apps/web/src/lib/glanceable-agents-snapshot-server.ts @@ -0,0 +1,39 @@ +import 'server-only'; +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { listActiveSessions } from '@/lib/active-sessions-list'; + +/** + * Server-side snapshot for background glanceable delivery (Live Activity, + * widgets, Android ongoing). Builds the same versioned, privacy-minimal shape + * the mobile publisher derives from its own tray cache — the snapshot is the + * extracted active-sessions list, never a second session source. + * + * The shared `buildGlanceableSnapshot` reads only each session's `status`, so + * title, git, id, and every other raw field are structurally excluded from the + * output. `accountEpoch` is intentionally omitted: the mobile client applies + * its own local epoch when it adopts a remote snapshot. + */ +export async function buildGlanceableSnapshotForUser({ + userId, + organizationId, +}: { + userId: string; + organizationId: string | null; +}): Promise { + const { sessions } = await listActiveSessions({ + userId, + organizationId, + includeCloudAgentSessions: true, + }); + + return buildGlanceableSnapshot({ + sessions, + userId, + organizationId, + now: Date.now(), + }); +} diff --git a/apps/web/src/lib/user/index.test.ts b/apps/web/src/lib/user/index.test.ts index 0825031cdf..21e09b728d 100644 --- a/apps/web/src/lib/user/index.test.ts +++ b/apps/web/src/lib/user/index.test.ts @@ -63,6 +63,7 @@ import { kiloclaw_scheduled_action_stages, kiloclaw_scheduled_action_targets, user_push_tokens, + user_activity_tokens, user_notification_preferences, user_data_export_object_deletions, security_advisor_scans, @@ -1342,6 +1343,49 @@ describe('User', () => { ).toEqual([expect.objectContaining({ id: otherOutbox.id })]); }); + it("deletes the user's activity tokens and leaves other users' tokens", async () => { + const user = await insertTestUser({ google_user_email: 'activity-token-user@example.com' }); + const otherUser = await insertTestUser(); + + const [userToken, otherToken] = await db + .insert(user_activity_tokens) + .values([ + { + user_id: user.id, + token: `ios-activity-${crypto.randomUUID()}`, + kind: 'ios_activity', + platform: 'ios', + organization_id: null, + }, + { + user_id: otherUser.id, + token: `android-ongoing-${crypto.randomUUID()}`, + kind: 'android_ongoing', + platform: 'android', + organization_id: null, + }, + ]) + .returning(); + if (!userToken || !otherToken) { + throw new Error('Failed to seed activity token rows'); + } + + await softDeleteUser(user.id); + + expect( + await db + .select() + .from(user_activity_tokens) + .where(eq(user_activity_tokens.id, userToken.id)) + ).toHaveLength(0); + expect( + await db + .select() + .from(user_activity_tokens) + .where(eq(user_activity_tokens.id, otherToken.id)) + ).toHaveLength(1); + }); + it("deletes the user's cloud agent pending-upload rows and leaves other users' rows", async () => { const user = await insertTestUser({ google_user_email: 'pending-upload-user@example.com' }); const otherUser = await insertTestUser(); diff --git a/apps/web/src/lib/user/index.ts b/apps/web/src/lib/user/index.ts index b66ef2c748..cb3d155cf2 100644 --- a/apps/web/src/lib/user/index.ts +++ b/apps/web/src/lib/user/index.ts @@ -79,6 +79,7 @@ import { kiloclaw_admin_audit_logs, kiloclaw_cli_runs, user_push_tokens, + user_activity_tokens, user_notification_preferences, contributor_champion_events, contributor_champion_memberships, @@ -1454,6 +1455,10 @@ export async function anonymizeCloudUserData( ); // Locale is account-adjacent and is removed with the token row. await tx.delete(user_push_tokens).where(eq(user_push_tokens.user_id, userId)); + // Activity tokens (Live Activity / push-to-start / Android ongoing) are + // account-owned device identifiers; a signed-out or deleted user must stop + // receiving glanceable deliveries. + await tx.delete(user_activity_tokens).where(eq(user_activity_tokens.user_id, userId)); await tx .delete(user_notification_preferences) .where(eq(user_notification_preferences.user_id, userId)); diff --git a/apps/web/src/routers/active-sessions-router.ts b/apps/web/src/routers/active-sessions-router.ts index 4384173e48..ab76ace7bd 100644 --- a/apps/web/src/routers/active-sessions-router.ts +++ b/apps/web/src/routers/active-sessions-router.ts @@ -4,64 +4,22 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import { SESSION_INGEST_WORKER_URL } from '@/lib/config.server'; import { generateInternalServiceToken } from '@/lib/tokens'; -import { db } from '@/lib/drizzle'; -import { - cli_sessions_v2, - cloud_agent_session_runs, - github_branch_pull_requests, -} from '@kilocode/db/schema'; -import { and, desc, eq, gt, inArray, isNotNull, isNull, or, sql } from 'drizzle-orm'; import { ensureOrganizationAccess } from '@/routers/organizations/utils'; import { - associatedPrSchema, - formatAssociatedPr, - sessionPrJoinPredicate, -} from './cli-sessions-v2-router'; - -export const activeSessionSchema = z.object({ - id: z.string(), - status: z.string(), - title: z.string(), - connectionId: z.string(), - gitUrl: z.string().optional(), - gitBranch: z.string().optional(), - createdOnPlatform: z.string().optional(), - createdAt: z.string().optional(), - updatedAt: z.string().optional(), - /** - * Latest agent activity timestamp from `cli_sessions_v2.last_activity_at` - * (raw DB text, same treatment as `createdAt`/`updatedAt`). Omitted when - * the column is NULL or the row was never enriched. - */ - lastActivityAt: z.string().optional(), - /** - * Capabilities advertised by the CLI connection that owns this session. - * Omitted when the owning connection's latest heartbeat did not include a - * capabilities object (legacy CLI, or a CLI that predates the field). - */ - capabilities: z.object({ attachments: z.boolean().optional() }).optional(), - // Optional: legacy CLIs (predating the `kilo remote` spawner) never - // report a platform. Only present in the response when the CLI supplied it. - platform: z.string().optional(), - /** - * Optional total session cost from `cli_sessions_v2.total_cost_microdollars` - * (microdollars, bigint). Only present when the DB row carries a non-null - * value — null never goes on the wire. Unenriched heartbeat rows (no - * `cli_sessions_v2` join) omit the key. The wire may legitimately carry - * zero; display still omits it via `formatSessionTotalCost`. - */ - totalCostMicrodollars: z.number().optional(), - /** - * Associated pull request for this session's branch, merged from the - * per-tenant PR cache during enrichment. Old clients omit this key; - * remove optional when every client is past this release. - */ - associatedPr: associatedPrSchema.optional(), -}); - -const activeSessionsResponseSchema = z.object({ - sessions: z.array(activeSessionSchema), -}); + activeSessionSchema, + listActiveSessions, + resolveActiveSessionStatus, + CLOUD_AGENT_CONNECTION_ID, + type ActiveSession, +} from '@/lib/active-sessions-list'; + +// Re-exported for existing consumers and tests. +export { + activeSessionSchema, + resolveActiveSessionStatus, + CLOUD_AGENT_CONNECTION_ID, + type ActiveSession, +}; const connectedInstanceSchema = z.object({ connectionId: z.string(), @@ -80,6 +38,8 @@ const connectedInstancesResponseSchema = z.object({ instances: z.array(connectedInstanceSchema), }); +export type ConnectedInstance = z.infer; + /** * Session Ingest `/api/user/web-ticket` mint response. Parsed at runtime so a * malformed 200 fails the mutation instead of returning undefined fields. @@ -89,38 +49,6 @@ const webTicketResponseSchema = z.object({ expiresAt: z.number(), }); -/** - * A live session as this router returns it: the worker's wire row plus the - * fields enriched from `cli_sessions_v2`. - */ -export type ActiveSession = z.infer & { - /** - * Owning organization from `cli_sessions_v2`; `null` = personal, which - * also covers a live session with no `cli_sessions_v2` row (an - * unattributable session — the server attributes it to personal). - * - * This router sets the field on EVERY row it returns, so an absent value - * on a client-cached row means exactly one thing: that row entered the - * cache from a WS payload and has never been server-attributed. The - * client filter relies on that (see the mobile - * `filterActiveSessionsByOrganization`). The field stays optional in the - * type only because those WS-inserted cached rows share it. - */ - organizationId?: string | null; -}; -export type ConnectedInstance = z.infer; - -/** Sentinel `connectionId` for cloud-agent rows merged when the flag is on. */ -export const CLOUD_AGENT_CONNECTION_ID = 'cloud-agent'; - -/** - * Warm-idle window for live cloud sessions. Mirrors - * `KILO_SERVER_IDLE_TIMEOUT_MS_DEFAULT` in - * services/cloud-agent-next/src/persistence/CloudAgentSession.ts:189-190. - * Env override drift is accepted (A2). - */ -const CLOUD_AGENT_WARM_IDLE_CUTOFF = sql`now() - interval '15 minutes'`; - const listInputSchema = z .object({ /** @@ -139,158 +67,6 @@ const listInputSchema = z }) .optional(); -type EnrichmentRow = { - session_id: string; - created_on_platform: string | null; - created_at: string; - updated_at: string; - status: string | null; - title: string | null; - organization_id: string | null; - last_activity_at: string | null; - total_cost_microdollars: number | null; - // Session's own stored PR link, aliased so it never collides with the - // cache keys below. - session_pr_platform: string | null; - session_pr_url: string | null; - session_pr_number: number | null; - // Per-tenant PR cache columns from the LEFT JOIN. - pr_url: string | null; - pr_number: number | null; - pr_state: string | null; - pr_title: string | null; - pr_head_sha: string | null; - pr_last_synced_at: string | null; - pr_review_decision: string | null; - review_decision_pending: boolean | null; -}; - -type CloudCandidateRow = EnrichmentRow & { - git_url: string | null; - git_branch: string | null; - cloud_agent_session_id: string | null; -}; - -/** - * Fold an enriched row's flat PR columns into the `associatedPr` shape. - * Returns `null` when there is no cache PR and no stored session link, so - * callers can omit the key entirely instead of emitting `associatedPr: null`. - */ -function associatedPrFromRow(row: EnrichmentRow): z.infer | null { - return formatAssociatedPr( - { - platform: row.session_pr_platform, - pr_url: row.session_pr_url, - pr_number: row.session_pr_number, - updated_at: row.updated_at, - }, - { - pr_url: row.pr_url, - pr_number: row.pr_number, - pr_state: row.pr_state, - pr_title: row.pr_title, - pr_head_sha: row.pr_head_sha, - pr_last_synced_at: row.pr_last_synced_at, - pr_review_decision: row.pr_review_decision, - review_decision_pending: row.review_decision_pending, - } - ); -} - -/** - * Overlay stored attention (question/permission) onto a live heartbeat - * status. Non-attention DB values yield to live so busy/idle remain - * authoritative while the CLI is connected. - * - * Must run in the router: client fetchQuery replaces the cache wholesale, - * so sticky attention held only in client helpers is wiped on every - * enrichment / reconnect / cli.connected refresh. - */ -export function resolveActiveSessionStatus( - liveStatus: string, - storedStatus: string | null | undefined -): string { - if (storedStatus === 'question' || storedStatus === 'permission') { - return storedStatus; - } - return liveStatus; -} - -function mapEnrichedHeartbeatSession( - session: ActiveSession, - row: EnrichmentRow | undefined -): ActiveSession { - if (!row) { - // Always emit the field, `null` included: an absent `organizationId` on - // a client-cached row must mean "never server-attributed" and nothing - // else, or the client filter cannot tell a heartbeat-inserted row apart - // from a server-attributed personal one (D4/D6). - return { ...session, organizationId: null }; - } - const mapped: ActiveSession = { - ...session, - status: resolveActiveSessionStatus(session.status, row.status), - // The tray title must be what a rename wrote, not what the CLI still - // reports: nothing propagates a cloud rename back to the CLI, so the - // heartbeat title stays stale forever. A NULL title (never-ingested - // placeholder row) falls back to the live one. - title: row.title ?? session.title, - organizationId: row.organization_id, - createdOnPlatform: row.created_on_platform ?? undefined, - createdAt: row.created_at, - updatedAt: row.updated_at, - }; - if (row.last_activity_at != null) { - mapped.lastActivityAt = row.last_activity_at; - } - if (row.total_cost_microdollars != null) { - mapped.totalCostMicrodollars = row.total_cost_microdollars; - } - const associatedPr = associatedPrFromRow(row); - if (associatedPr) { - mapped.associatedPr = associatedPr; - } - return mapped; -} - -function mapCloudCandidateRow(row: CloudCandidateRow): ActiveSession { - const mapped: ActiveSession = { - id: row.session_id, - // Cloud rows have no live heartbeat source — use the stored status as-is - // (do NOT run resolveActiveSessionStatus). - status: row.status ?? '', - title: row.title ?? '', - connectionId: CLOUD_AGENT_CONNECTION_ID, - gitUrl: row.git_url ?? undefined, - gitBranch: row.git_branch ?? undefined, - createdOnPlatform: row.created_on_platform ?? undefined, - createdAt: row.created_at, - updatedAt: row.updated_at, - // Key ALWAYS emitted, null included (D21) — mobile filter treats an - // absent key as never-attributed and would hide personal cloud rows. - organizationId: row.organization_id ?? null, - }; - if (row.last_activity_at != null) { - mapped.lastActivityAt = row.last_activity_at; - } - if (row.total_cost_microdollars != null) { - mapped.totalCostMicrodollars = row.total_cost_microdollars; - } - const associatedPr = associatedPrFromRow(row); - if (associatedPr) { - mapped.associatedPr = associatedPr; - } - return mapped; -} - -function throwOrgContextFailure(error: unknown): never { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to resolve the organization context for active sessions', - cause: error, - }); -} - /** * Mint a one-use web ticket from Session Ingest for the given user. The * returned `token` is the opaque ticket; `expiresAt` is the Unix-seconds @@ -365,208 +141,11 @@ export const activeSessionsRouter = createTRPCRouter({ if (typeof organizationId === 'string') { await ensureOrganizationAccess(ctx, organizationId); } - - // Phase 1: fetch + parse the worker response. Any failure here - // (HTTP error, malformed JSON, schema mismatch) degrades to an empty - // list exactly as before — these are "no data" outcomes from the - // mobile client's point of view. With includeCloudAgentSessions, all - // three early exits fall through to the cloud-candidates query (D11). - let parsed: { sessions: ActiveSession[] } = { sessions: [] }; - - if (!SESSION_INGEST_WORKER_URL) { - if (!includeCloudAgentSessions) { - return { sessions: [] as ActiveSession[] }; - } - } else { - const token = generateInternalServiceToken(ctx.user.id); - const url = `${SESSION_INGEST_WORKER_URL}/api/sessions/active`; - - try { - const response = await fetch(url, { - headers: { Authorization: `Bearer ${token}` }, - }); - - if (!response.ok) { - console.warn( - `[active-sessions] fetch failed: ${response.status} ${response.statusText}`, - await response.text().catch(() => '') - ); - if (!includeCloudAgentSessions) { - return { sessions: [] as ActiveSession[] }; - } - } else { - const raw = await response.json(); - parsed = activeSessionsResponseSchema.parse(raw); - } - } catch (error) { - console.warn('[active-sessions] error:', error); - if (!includeCloudAgentSessions) { - return { sessions: [] as ActiveSession[] }; - } - } - } - - // Phase 2a — Query 1: enrich heartbeat sessions from cli_sessions_v2. - // Independent try/catch from Query 2 (D16). Skipped when there are no - // heartbeat ids (never an empty inArray). - const ids = parsed.sessions.map(s => s.id); - let enrichmentRows: EnrichmentRow[] = []; - let enrichmentFailed = false; - - if (ids.length > 0) { - try { - enrichmentRows = await db - .select({ - session_id: cli_sessions_v2.session_id, - created_on_platform: cli_sessions_v2.created_on_platform, - created_at: cli_sessions_v2.created_at, - updated_at: cli_sessions_v2.updated_at, - status: cli_sessions_v2.status, - title: cli_sessions_v2.title, - organization_id: cli_sessions_v2.organization_id, - last_activity_at: cli_sessions_v2.last_activity_at, - total_cost_microdollars: cli_sessions_v2.total_cost_microdollars, - session_pr_platform: cli_sessions_v2.platform, - session_pr_url: cli_sessions_v2.pr_url, - session_pr_number: cli_sessions_v2.pr_number, - pr_url: github_branch_pull_requests.pr_url, - pr_number: github_branch_pull_requests.pr_number, - pr_state: github_branch_pull_requests.pr_state, - pr_title: github_branch_pull_requests.pr_title, - pr_head_sha: github_branch_pull_requests.pr_head_sha, - pr_last_synced_at: github_branch_pull_requests.pr_last_synced_at, - pr_review_decision: github_branch_pull_requests.pr_review_decision, - review_decision_pending: github_branch_pull_requests.review_decision_pending, - }) - .from(cli_sessions_v2) - .leftJoin(github_branch_pull_requests, sessionPrJoinPredicate) - .where( - and( - eq(cli_sessions_v2.kilo_user_id, ctx.user.id), - inArray(cli_sessions_v2.session_id, ids) - ) - ); - } catch (error) { - console.warn('[active-sessions] enrichment db query failed:', error); - // Attribution is unknowable without the join. An unfiltered caller (web, - // `resolveSession`) keeps the existing best-effort unenriched passthrough — - // a DB blip must not collapse its list. A filtered caller cannot be - // answered at all: calling every row personal would lie (breaking AC 1) - // and returning an empty list would silently blank the tray with no - // explanation. So fail the query and let the client's already-shipped - // retryable state handle it (D11). - if (organizationId === undefined) { - enrichmentFailed = true; - if (!includeCloudAgentSessions) { - return parsed; - } - } else { - throwOrgContextFailure(error); - } - } - } else if (!includeCloudAgentSessions) { - // Flag-off empty heartbeats: today's short-circuit (no DB). - return parsed; - } - - let sessions: ActiveSession[]; - if (enrichmentFailed) { - // Unfiltered + flag on: keep wire rows unenriched, still attempt cloud. - sessions = [...parsed.sessions]; - } else { - const byId = new Map(enrichmentRows.map(r => [r.session_id, r])); - sessions = []; - for (const session of parsed.sessions) { - const row = byId.get(session.id); - // No `cli_sessions_v2` row → unattributable → personal. An SQL-side filter - // could not tell this case apart from "belongs to another organization". - const rowOrganizationId = row?.organization_id ?? null; - if (organizationId !== undefined && rowOrganizationId !== organizationId) { - continue; - } - sessions.push(mapEnrichedHeartbeatSession(session, row)); - } - } - - // Phase 2b — Query 2: live cloud-agent candidates (flag-on only). - // Own try/catch; failure semantics mirror Query 1 (D16). - if (includeCloudAgentSessions) { - try { - const orgPredicate = - organizationId === null - ? isNull(cli_sessions_v2.organization_id) - : typeof organizationId === 'string' - ? eq(cli_sessions_v2.organization_id, organizationId) - : undefined; - - const livePredicate = or( - sql`EXISTS ( - SELECT 1 FROM ${cloud_agent_session_runs} - WHERE ${cloud_agent_session_runs.cloud_agent_session_id} = ${cli_sessions_v2.cloud_agent_session_id} - AND ${cloud_agent_session_runs.terminal_at} IS NULL - )`, - and( - eq(cli_sessions_v2.status, 'idle'), - gt(cli_sessions_v2.status_updated_at, CLOUD_AGENT_WARM_IDLE_CUTOFF) - ) - ); - - const cloudRows: CloudCandidateRow[] = await db - .select({ - session_id: cli_sessions_v2.session_id, - created_on_platform: cli_sessions_v2.created_on_platform, - created_at: cli_sessions_v2.created_at, - updated_at: cli_sessions_v2.updated_at, - status: cli_sessions_v2.status, - title: cli_sessions_v2.title, - organization_id: cli_sessions_v2.organization_id, - git_url: cli_sessions_v2.git_url, - git_branch: cli_sessions_v2.git_branch, - last_activity_at: cli_sessions_v2.last_activity_at, - total_cost_microdollars: cli_sessions_v2.total_cost_microdollars, - cloud_agent_session_id: cli_sessions_v2.cloud_agent_session_id, - session_pr_platform: cli_sessions_v2.platform, - session_pr_url: cli_sessions_v2.pr_url, - session_pr_number: cli_sessions_v2.pr_number, - pr_url: github_branch_pull_requests.pr_url, - pr_number: github_branch_pull_requests.pr_number, - pr_state: github_branch_pull_requests.pr_state, - pr_title: github_branch_pull_requests.pr_title, - pr_head_sha: github_branch_pull_requests.pr_head_sha, - pr_last_synced_at: github_branch_pull_requests.pr_last_synced_at, - pr_review_decision: github_branch_pull_requests.pr_review_decision, - review_decision_pending: github_branch_pull_requests.review_decision_pending, - }) - .from(cli_sessions_v2) - .leftJoin(github_branch_pull_requests, sessionPrJoinPredicate) - .where( - and( - eq(cli_sessions_v2.kilo_user_id, ctx.user.id), - isNull(cli_sessions_v2.parent_session_id), - isNotNull(cli_sessions_v2.cloud_agent_session_id), - orgPredicate, - livePredicate - ) - ) - .orderBy(desc(cli_sessions_v2.created_at)) - .limit(50); - - const heartbeatIds = new Set(sessions.map(s => s.id)); - for (const row of cloudRows) { - // CLI adoption wins: keep the worker row's real connectionId/status. - if (heartbeatIds.has(row.session_id)) continue; - sessions.push(mapCloudCandidateRow(row)); - } - } catch (error) { - console.warn('[active-sessions] cloud candidates db query failed:', error); - if (organizationId !== undefined) { - throwOrgContextFailure(error); - } - // Unfiltered: skip cloud merge, return heartbeat rows as built. - } - } - - return { sessions }; + return listActiveSessions({ + userId: ctx.user.id, + organizationId, + includeCloudAgentSessions, + }); }), /** diff --git a/apps/web/src/routers/user-router.ts b/apps/web/src/routers/user-router.ts index d91192264a..bb1c53ae31 100644 --- a/apps/web/src/routers/user-router.ts +++ b/apps/web/src/routers/user-router.ts @@ -36,6 +36,7 @@ import { kiloclaw_subscriptions, user_notification_preferences, user_push_tokens, + user_activity_tokens, agent_configs, } from '@kilocode/db/schema'; import { eq, and, isNull, inArray, or, sql, gte, gt, desc, isNotNull } from 'drizzle-orm'; @@ -1190,6 +1191,60 @@ export const userRouter = createTRPCRouter({ return { success: true }; }), + // Activity tokens for glanceable surfaces (Live Activity / push-to-start / + // Android ongoing). Upsert on `token` so a re-registration of the same + // device token replaces the row instead of failing the unique index. + + registerActivityToken: baseProcedure + .input( + z.object({ + token: z.string().min(1), + kind: z.enum(['ios_push_to_start', 'ios_activity', 'android_ongoing']), + platform: z.enum(['ios', 'android']), + organizationId: z.string().min(1).nullable(), + }) + ) + .mutation(async ({ ctx, input }) => { + await db + .insert(user_activity_tokens) + .values({ + user_id: ctx.user.id, + token: input.token, + kind: input.kind, + platform: input.platform, + organization_id: input.organizationId, + }) + .onConflictDoUpdate({ + target: [user_activity_tokens.token], + set: { + user_id: ctx.user.id, + kind: input.kind, + platform: input.platform, + organization_id: input.organizationId, + updated_at: sql`now()`, + }, + }); + return { success: true }; + }), + + unregisterActivityToken: baseProcedure + .input( + z.object({ + token: z.string().min(1), + }) + ) + .mutation(async ({ ctx, input }) => { + await db + .delete(user_activity_tokens) + .where( + and( + eq(user_activity_tokens.user_id, ctx.user.id), + eq(user_activity_tokens.token, input.token) + ) + ); + return { success: true }; + }), + getMyPushTokens: baseProcedure.query(async ({ ctx }) => { return db .select({ diff --git a/packages/db/src/migrations/0233_square_daimon_hellstrom.sql b/packages/db/src/migrations/0233_square_daimon_hellstrom.sql new file mode 100644 index 0000000000..de6246d0e5 --- /dev/null +++ b/packages/db/src/migrations/0233_square_daimon_hellstrom.sql @@ -0,0 +1,14 @@ +CREATE TABLE "user_activity_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "token" text NOT NULL, + "kind" text NOT NULL, + "platform" text NOT NULL, + "organization_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "user_activity_tokens" ADD CONSTRAINT "user_activity_tokens_user_id_kilocode_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_user_activity_tokens_token" ON "user_activity_tokens" USING btree ("token");--> statement-breakpoint +CREATE INDEX "IDX_user_activity_tokens_user_org" ON "user_activity_tokens" USING btree ("user_id","organization_id"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0233_snapshot.json b/packages/db/src/migrations/meta/0233_snapshot.json new file mode 100644 index 0000000000..883a2c2c2f --- /dev/null +++ b/packages/db/src/migrations/meta/0233_snapshot.json @@ -0,0 +1,39690 @@ +{ + "id": "47f92c1d-a8a0-48fc-b98b-58090ebcd700", + "prevId": "f163064f-7b94-4a48-90ce-f052c836dd79", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_configs": { + "name": "agent_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "runtime_state": { + "name": "runtime_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "IDX_agent_configs_org_id": { + "name": "IDX_agent_configs_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_owned_by_user_id": { + "name": "IDX_agent_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_agent_type": { + "name": "IDX_agent_configs_agent_type", + "columns": [ + { + "expression": "agent_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_platform": { + "name": "IDX_agent_configs_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_configs_owned_by_organization_id_organizations_id_fk": { + "name": "agent_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_configs_org_agent_platform": { + "name": "UQ_agent_configs_org_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "agent_type", + "platform" + ] + }, + "UQ_agent_configs_user_agent_platform": { + "name": "UQ_agent_configs_user_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_user_id", + "agent_type", + "platform" + ] + } + }, + "policies": {}, + "checkConstraints": { + "agent_configs_owner_check": { + "name": "agent_configs_owner_check", + "value": "(\n (\"agent_configs\".\"owned_by_user_id\" IS NOT NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_configs\".\"owned_by_user_id\" IS NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "agent_configs_agent_type_check": { + "name": "agent_configs_agent_type_check", + "value": "\"agent_configs\".\"agent_type\" IN ('code_review', 'auto_triage', 'auto_fix', 'security_scan')" + }, + "agent_configs_config_revision_check": { + "name": "agent_configs_config_revision_check", + "value": "\"agent_configs\".\"config_revision\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_agents": { + "name": "agent_environment_profile_agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_agents_profile_id": { + "name": "IDX_agent_env_profile_agents_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_agents", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_agents_profile_slug": { + "name": "UQ_agent_env_profile_agents_profile_slug", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_commands": { + "name": "agent_environment_profile_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_commands_profile_id": { + "name": "IDX_agent_env_profile_commands_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_commands_profile_sequence": { + "name": "UQ_agent_env_profile_commands_profile_sequence", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_kilo_commands": { + "name": "agent_environment_profile_kilo_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtask": { + "name": "subtask", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_kilo_cmds_profile_id": { + "name": "IDX_agent_env_profile_kilo_cmds_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_kilo_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_kilo_cmds_profile_name": { + "name": "UQ_agent_env_profile_kilo_cmds_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_mcp_servers": { + "name": "agent_environment_profile_mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_mcp_servers_profile_id": { + "name": "IDX_agent_env_profile_mcp_servers_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_mcp_servers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_mcp_servers_profile_name": { + "name": "UQ_agent_env_profile_mcp_servers_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_repo_bindings": { + "name": "agent_environment_profile_repo_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profile_repo_bindings_user": { + "name": "UQ_agent_env_profile_repo_bindings_user", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profile_repo_bindings_org": { + "name": "UQ_agent_env_profile_repo_bindings_org", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profile_repo_bindings_owner_check": { + "name": "agent_env_profile_repo_bindings_owner_check", + "value": "(\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_skills": { + "name": "agent_environment_profile_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_markdown": { + "name": "raw_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_skills_profile_id": { + "name": "IDX_agent_env_profile_skills_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_skills", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_skills_profile_name": { + "name": "UQ_agent_env_profile_skills_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_vars": { + "name": "agent_environment_profile_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_vars_profile_id": { + "name": "IDX_agent_env_profile_vars_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_vars", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_vars_profile_key": { + "name": "UQ_agent_env_profile_vars_profile_key", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profiles": { + "name": "agent_environment_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profiles_org_name": { + "name": "UQ_agent_env_profiles_org_name", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_name": { + "name": "UQ_agent_env_profiles_user_name", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_org_default": { + "name": "UQ_agent_env_profiles_org_default", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_default": { + "name": "UQ_agent_env_profiles_user_default", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_org_id": { + "name": "IDX_agent_env_profiles_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_user_id": { + "name": "IDX_agent_env_profiles_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_created_by_user_id": { + "name": "IDX_agent_env_profiles_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profiles_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profiles_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profiles_owner_check": { + "name": "agent_env_profiles_owner_check", + "value": "(\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.analytics_event_outbox": { + "name": "analytics_event_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "event_uuid": { + "name": "event_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_name": { + "name": "event_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "distinct_id": { + "name": "distinct_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_analytics_event_outbox_event_uuid": { + "name": "UQ_analytics_event_outbox_event_uuid", + "columns": [ + { + "expression": "event_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_analytics_event_outbox_status_next_attempt_at": { + "name": "IDX_analytics_event_outbox_status_next_attempt_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_kind": { + "name": "api_kind", + "schema": "", + "columns": { + "api_kind_id": { + "name": "api_kind_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_api_kind": { + "name": "UQ_api_kind", + "columns": [ + { + "expression": "api_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_log": { + "name": "api_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_request_id": { + "name": "vercel_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_api_request_log_created_at": { + "name": "idx_api_request_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_feedback": { + "name": "app_builder_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_status": { + "name": "preview_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_feedback_created_at": { + "name": "IDX_app_builder_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_kilo_user_id": { + "name": "IDX_app_builder_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_project_id": { + "name": "IDX_app_builder_feedback_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "app_builder_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "app_builder_feedback_project_id_app_builder_projects_id_fk": { + "name": "app_builder_feedback_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_project_sessions": { + "name": "app_builder_project_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "worker_version": { + "name": "worker_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'v2'" + } + }, + "indexes": { + "IDX_app_builder_project_sessions_project_id": { + "name": "IDX_app_builder_project_sessions_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_project_sessions_project_id_app_builder_projects_id_fk": { + "name": "app_builder_project_sessions_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_project_sessions", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_app_builder_project_sessions_cloud_agent_session_id": { + "name": "UQ_app_builder_project_sessions_cloud_agent_session_id", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_projects": { + "name": "app_builder_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "git_repo_full_name": { + "name": "git_repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_platform_integration_id": { + "name": "git_platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_projects_created_by_user_id": { + "name": "IDX_app_builder_projects_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_user_id": { + "name": "IDX_app_builder_projects_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_organization_id": { + "name": "IDX_app_builder_projects_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_created_at": { + "name": "IDX_app_builder_projects_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_last_message_at": { + "name": "IDX_app_builder_projects_last_message_at", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_git_repo_integration": { + "name": "IDX_app_builder_projects_git_repo_integration", + "columns": [ + { + "expression": "git_repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"app_builder_projects\".\"git_repo_full_name\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_projects_owned_by_user_id_kilocode_users_id_fk": { + "name": "app_builder_projects_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_owned_by_organization_id_organizations_id_fk": { + "name": "app_builder_projects_owned_by_organization_id_organizations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_deployment_id_deployments_id_fk": { + "name": "app_builder_projects_deployment_id_deployments_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk": { + "name": "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "platform_integrations", + "columnsFrom": [ + "git_platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "app_builder_projects_owner_check": { + "name": "app_builder_projects_owner_check", + "value": "(\n (\"app_builder_projects\".\"owned_by_user_id\" IS NOT NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NULL) OR\n (\"app_builder_projects\".\"owned_by_user_id\" IS NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.app_min_versions": { + "name": "app_min_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ios_min_version": { + "name": "ios_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "android_min_version": { + "name": "android_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_reported_messages": { + "name": "app_reported_messages", + "schema": "", + "columns": { + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "app_reported_messages_cli_session_id_cli_sessions_session_id_fk": { + "name": "app_reported_messages_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "app_reported_messages", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_fix_tickets": { + "name": "auto_fix_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "triage_ticket_id": { + "name": "triage_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'label'" + }, + "review_comment_id": { + "name": "review_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_comment_body": { + "name": "review_comment_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "diff_hunk": { + "name": "diff_hunk", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_ref": { + "name": "pr_head_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_branch": { + "name": "pr_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_fix_tickets_repo_issue": { + "name": "UQ_auto_fix_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"trigger_source\" = 'label'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_fix_tickets_repo_review_comment": { + "name": "UQ_auto_fix_tickets_repo_review_comment", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "review_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"review_comment_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_org": { + "name": "IDX_auto_fix_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_user": { + "name": "IDX_auto_fix_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_status": { + "name": "IDX_auto_fix_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_created_at": { + "name": "IDX_auto_fix_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_triage_ticket_id": { + "name": "IDX_auto_fix_tickets_triage_ticket_id", + "columns": [ + { + "expression": "triage_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_session_id": { + "name": "IDX_auto_fix_tickets_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_fix_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_fix_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "triage_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk": { + "name": "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_fix_tickets_owner_check": { + "name": "auto_fix_tickets_owner_check", + "value": "(\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_fix_tickets_status_check": { + "name": "auto_fix_tickets_status_check", + "value": "\"auto_fix_tickets\".\"status\" IN ('pending', 'running', 'completed', 'failed', 'cancelled')" + }, + "auto_fix_tickets_classification_check": { + "name": "auto_fix_tickets_classification_check", + "value": "\"auto_fix_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'unclear')" + }, + "auto_fix_tickets_confidence_check": { + "name": "auto_fix_tickets_confidence_check", + "value": "\"auto_fix_tickets\".\"confidence\" >= 0 AND \"auto_fix_tickets\".\"confidence\" <= 1" + }, + "auto_fix_tickets_trigger_source_check": { + "name": "auto_fix_tickets_trigger_source_check", + "value": "\"auto_fix_tickets\".\"trigger_source\" IN ('label', 'review_comment')" + } + }, + "isRLSEnabled": false + }, + "public.auto_model": { + "name": "auto_model", + "schema": "", + "columns": { + "auto_model_id": { + "name": "auto_model_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_auto_model": { + "name": "UQ_auto_model", + "columns": [ + { + "expression": "auto_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_top_up_configs": { + "name": "auto_top_up_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_method_id": { + "name": "stripe_payment_method_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "last_auto_top_up_at": { + "name": "last_auto_top_up_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_started_at": { + "name": "attempt_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_top_up_configs_owned_by_user_id": { + "name": "UQ_auto_top_up_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_top_up_configs_owned_by_organization_id": { + "name": "UQ_auto_top_up_configs_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "auto_top_up_configs_owned_by_organization_id_organizations_id_fk": { + "name": "auto_top_up_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_top_up_configs_exactly_one_owner": { + "name": "auto_top_up_configs_exactly_one_owner", + "value": "(\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NULL) OR (\"auto_top_up_configs\".\"owned_by_user_id\" IS NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.auto_triage_tickets": { + "name": "auto_triage_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_duplicate": { + "name": "is_duplicate", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duplicate_of_ticket_id": { + "name": "duplicate_of_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "similarity_score": { + "name": "similarity_score", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "qdrant_point_id": { + "name": "qdrant_point_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "should_auto_fix": { + "name": "should_auto_fix", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "action_taken": { + "name": "action_taken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_metadata": { + "name": "action_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_triage_tickets_repo_issue": { + "name": "UQ_auto_triage_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_org": { + "name": "IDX_auto_triage_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_user": { + "name": "IDX_auto_triage_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_status": { + "name": "IDX_auto_triage_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_created_at": { + "name": "IDX_auto_triage_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_qdrant_point_id": { + "name": "IDX_auto_triage_tickets_qdrant_point_id", + "columns": [ + { + "expression": "qdrant_point_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owner_status_created": { + "name": "IDX_auto_triage_tickets_owner_status_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_user_status_created": { + "name": "IDX_auto_triage_tickets_user_status_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_repo_classification": { + "name": "IDX_auto_triage_tickets_repo_classification", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "classification", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_triage_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_triage_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "duplicate_of_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_triage_tickets_owner_check": { + "name": "auto_triage_tickets_owner_check", + "value": "(\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_triage_tickets_issue_type_check": { + "name": "auto_triage_tickets_issue_type_check", + "value": "\"auto_triage_tickets\".\"issue_type\" IN ('issue', 'pull_request')" + }, + "auto_triage_tickets_classification_check": { + "name": "auto_triage_tickets_classification_check", + "value": "\"auto_triage_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'duplicate', 'unclear')" + }, + "auto_triage_tickets_confidence_check": { + "name": "auto_triage_tickets_confidence_check", + "value": "\"auto_triage_tickets\".\"confidence\" >= 0 AND \"auto_triage_tickets\".\"confidence\" <= 1" + }, + "auto_triage_tickets_similarity_score_check": { + "name": "auto_triage_tickets_similarity_score_check", + "value": "\"auto_triage_tickets\".\"similarity_score\" >= 0 AND \"auto_triage_tickets\".\"similarity_score\" <= 1" + }, + "auto_triage_tickets_status_check": { + "name": "auto_triage_tickets_status_check", + "value": "\"auto_triage_tickets\".\"status\" IN ('pending', 'analyzing', 'actioned', 'failed', 'skipped')" + }, + "auto_triage_tickets_action_taken_check": { + "name": "auto_triage_tickets_action_taken_check", + "value": "\"auto_triage_tickets\".\"action_taken\" IN ('pr_created', 'comment_posted', 'closed_duplicate', 'needs_clarification')" + } + }, + "isRLSEnabled": false + }, + "public.bot_request_cloud_agent_sessions": { + "name": "bot_request_cloud_agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "bot_request_id": { + "name": "bot_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spawn_group_id": { + "name": "spawn_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlab_project": { + "name": "gitlab_project", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_step": { + "name": "callback_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message": { + "name": "final_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message_fetched_at": { + "name": "final_message_fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "final_message_error": { + "name": "final_message_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "continuation_started_at": { + "name": "continuation_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_bot_request_cas_cloud_agent_session_id": { + "name": "UQ_bot_request_cas_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id": { + "name": "IDX_bot_request_cas_bot_request_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id_status": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id_status", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk": { + "name": "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk", + "tableFrom": "bot_request_cloud_agent_sessions", + "tableTo": "bot_requests", + "columnsFrom": [ + "bot_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_requests": { + "name": "bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_thread_id": { + "name": "platform_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_message_id": { + "name": "platform_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_bot_requests_created_at": { + "name": "IDX_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_created_by": { + "name": "IDX_bot_requests_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_organization_id": { + "name": "IDX_bot_requests_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_platform_integration_id": { + "name": "IDX_bot_requests_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_status": { + "name": "IDX_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_requests_created_by_kilocode_users_id_fk": { + "name": "bot_requests_created_by_kilocode_users_id_fk", + "tableFrom": "bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_organization_id_organizations_id_fk": { + "name": "bot_requests_organization_id_organizations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.byok_api_keys": { + "name": "byok_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "management_source": { + "name": "management_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_byok_api_keys_organization_id": { + "name": "IDX_byok_api_keys_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_kilo_user_id": { + "name": "IDX_byok_api_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_provider_id": { + "name": "IDX_byok_api_keys_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "byok_api_keys_organization_id_organizations_id_fk": { + "name": "byok_api_keys_organization_id_organizations_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "byok_api_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "byok_api_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_byok_api_keys_org_provider": { + "name": "UQ_byok_api_keys_org_provider", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "provider_id" + ] + }, + "UQ_byok_api_keys_user_provider": { + "name": "UQ_byok_api_keys_user_provider", + "nullsNotDistinct": false, + "columns": [ + "kilo_user_id", + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "byok_api_keys_management_source_check": { + "name": "byok_api_keys_management_source_check", + "value": "\"byok_api_keys\".\"management_source\" IN ('user', 'coding_plan')" + }, + "byok_api_keys_owner_check": { + "name": "byok_api_keys_owner_check", + "value": "(\n (\"byok_api_keys\".\"kilo_user_id\" IS NOT NULL AND \"byok_api_keys\".\"organization_id\" IS NULL) OR\n (\"byok_api_keys\".\"kilo_user_id\" IS NULL AND \"byok_api_keys\".\"organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cli_sessions": { + "name": "cli_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_mode": { + "name": "last_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_kilo_user_id": { + "name": "IDX_cli_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_created_at": { + "name": "IDX_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_updated_at": { + "name": "IDX_cli_sessions_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_organization_id": { + "name": "IDX_cli_sessions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_user_updated": { + "name": "IDX_cli_sessions_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_forked_from_cli_sessions_session_id_fk": { + "name": "cli_sessions_forked_from_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "forked_from" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_parent_session_id_cli_sessions_session_id_fk": { + "name": "cli_sessions_parent_session_id_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "parent_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_organization_id_organizations_id_fk": { + "name": "cli_sessions_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cli_sessions_cloud_agent_session_id_unique": { + "name": "cli_sessions_cloud_agent_session_id_unique", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_sessions_v2": { + "name": "cli_sessions_v2", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_scope_id": { + "name": "cloud_agent_session_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_updated_at": { + "name": "status_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_v2_parent_session_id_kilo_user_id": { + "name": "IDX_cli_sessions_v2_parent_session_id_kilo_user_id", + "columns": [ + { + "expression": "parent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_public_id": { + "name": "UQ_cli_sessions_v2_public_id", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"public_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_cloud_agent_session_id": { + "name": "UQ_cli_sessions_v2_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"cloud_agent_session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_organization_id": { + "name": "IDX_cli_sessions_v2_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_updated": { + "name": "IDX_cli_sessions_v2_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_created": { + "name": "IDX_cli_sessions_v2_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "cli_sessions_v2_git_url_branch_idx": { + "name": "cli_sessions_v2_git_url_branch_idx", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_v2_organization_id_organizations_id_fk": { + "name": "cli_sessions_v2_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_v2_parent_session_id_kilo_user_id_fk": { + "name": "cli_sessions_v2_parent_session_id_kilo_user_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "cli_sessions_v2", + "columnsFrom": [ + "parent_session_id", + "kilo_user_id" + ], + "columnsTo": [ + "session_id", + "kilo_user_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cli_sessions_v2_session_id_kilo_user_id_pk": { + "name": "cli_sessions_v2_session_id_kilo_user_id_pk", + "columns": [ + "session_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_code_review_attempts": { + "name": "cloud_agent_code_review_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retry_reason": { + "name": "retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analytics_enabled_at_dispatch": { + "name": "analytics_enabled_at_dispatch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_review_attempts_review_attempt_number": { + "name": "UQ_cloud_agent_code_review_attempts_review_attempt_number", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_code_review_id": { + "name": "idx_cloud_agent_code_review_attempts_code_review_id", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_session_id": { + "name": "idx_cloud_agent_code_review_attempts_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_cli_session_id": { + "name": "idx_cloud_agent_code_review_attempts_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_status": { + "name": "idx_cloud_agent_code_review_attempts_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_retry_reason": { + "name": "idx_cloud_agent_code_review_attempts_retry_reason", + "columns": [ + { + "expression": "retry_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "retry_of_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_review_attempts_attempt_number_check": { + "name": "cloud_agent_code_review_attempts_attempt_number_check", + "value": "\"cloud_agent_code_review_attempts\".\"attempt_number\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_code_reviews": { + "name": "cloud_agent_code_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "manual_config": { + "name": "manual_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "review_type": { + "name": "review_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "council_result": { + "name": "council_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author": { + "name": "pr_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author_github_id": { + "name": "pr_author_github_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_ref": { + "name": "head_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "dispatch_reservation_id": { + "name": "dispatch_reservation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'v1'" + }, + "check_run_id": { + "name": "check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_used": { + "name": "repository_review_instructions_used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "repository_review_instructions_ref": { + "name": "repository_review_instructions_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_truncated": { + "name": "repository_review_instructions_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previous_summary_body": { + "name": "previous_summary_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_summary_head_sha": { + "name": "previous_summary_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_tokens_in": { + "name": "total_tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens_out": { + "name": "total_tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cost_musd": { + "name": "total_cost_musd", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha": { + "name": "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"manual_config\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_code_reviews_active_provider_publisher": { + "name": "UQ_cloud_agent_code_reviews_active_provider_publisher", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"platform_integration_id\" IS NOT NULL\n AND \"cloud_agent_code_reviews\".\"status\" IN ('pending', 'queued', 'running')\n AND (\"cloud_agent_code_reviews\".\"manual_config\" IS NULL OR \"cloud_agent_code_reviews\".\"manual_config\"->>'outputMode' = 'provider')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_org_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_user_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_session_id": { + "name": "idx_cloud_agent_code_reviews_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_cli_session_id": { + "name": "idx_cloud_agent_code_reviews_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_status": { + "name": "idx_cloud_agent_code_reviews_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_repo": { + "name": "idx_cloud_agent_code_reviews_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_number": { + "name": "idx_cloud_agent_code_reviews_pr_number", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_created_at": { + "name": "idx_cloud_agent_code_reviews_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_author_github_id": { + "name": "idx_cloud_agent_code_reviews_pr_author_github_id", + "columns": [ + { + "expression": "pr_author_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk": { + "name": "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_reviews_owner_check": { + "name": "cloud_agent_code_reviews_owner_check", + "value": "(\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NULL) OR\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_feedback": { + "name": "cloud_agent_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_type": { + "name": "session_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cloud_agent_feedback_created_at": { + "name": "IDX_cloud_agent_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_kilo_user_id": { + "name": "IDX_cloud_agent_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_cloud_agent_session_id": { + "name": "IDX_cloud_agent_feedback_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "cloud_agent_feedback_organization_id_organizations_id_fk": { + "name": "cloud_agent_feedback_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_pending_uploads": { + "name": "cloud_agent_pending_uploads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_uuid": { + "name": "message_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_cloud_agent_pending_uploads_user_message_status": { + "name": "IDX_cloud_agent_pending_uploads_user_message_status", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_pending_uploads_expired": { + "name": "IDX_cloud_agent_pending_uploads_expired", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_pending_uploads\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cloud_agent_pending_uploads_object_key_unique": { + "name": "cloud_agent_pending_uploads_object_key_unique", + "nullsNotDistinct": false, + "columns": [ + "object_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "cloud_agent_pending_uploads_status_check": { + "name": "cloud_agent_pending_uploads_status_check", + "value": "\"cloud_agent_pending_uploads\".\"status\" IN ('pending', 'linked', 'reaped')" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_session_runs": { + "name": "cloud_agent_session_runs", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapper_run_id": { + "name": "wrapper_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dispatch_accepted_at": { + "name": "dispatch_accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_activity_observed_at": { + "name": "agent_activity_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_responsibility": { + "name": "failure_responsibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_cloud_agent_session_runs_wrapper_run_id": { + "name": "IDX_cloud_agent_session_runs_wrapper_run_id", + "columns": [ + { + "expression": "wrapper_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"wrapper_run_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_session_queued": { + "name": "IDX_cloud_agent_session_runs_session_queued", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_queued_at": { + "name": "IDX_cloud_agent_session_runs_queued_at", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_terminal_at": { + "name": "IDX_cloud_agent_session_runs_terminal_at", + "columns": [ + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_status_terminal": { + "name": "IDX_cloud_agent_session_runs_status_terminal", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_failure_terminal": { + "name": "IDX_cloud_agent_session_runs_failure_terminal", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_responsibility_reason_terminal": { + "name": "IDX_cloud_agent_session_runs_responsibility_reason_terminal", + "columns": [ + { + "expression": "failure_responsibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"status\" = 'failed'", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_error_expires_at": { + "name": "IDX_cloud_agent_session_runs_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk", + "tableFrom": "cloud_agent_session_runs", + "tableTo": "cloud_agent_sessions", + "columnsFrom": [ + "cloud_agent_session_id" + ], + "columnsTo": [ + "cloud_agent_session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk", + "columns": [ + "cloud_agent_session_id", + "message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_session_runs_status_check": { + "name": "cloud_agent_session_runs_status_check", + "value": "\"cloud_agent_session_runs\".\"status\" IN ('queued', 'accepted', 'completed', 'failed', 'interrupted')" + }, + "cloud_agent_session_runs_error_message_bounded_check": { + "name": "cloud_agent_session_runs_error_message_bounded_check", + "value": "\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_session_runs\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_session_runs_error_expiry_check": { + "name": "cloud_agent_session_runs_error_expiry_check", + "value": "(\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_session_runs\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_sessions": { + "name": "cloud_agent_sessions", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initial_message_id": { + "name": "initial_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "failure_at": { + "name": "failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_responsibility": { + "name": "failure_responsibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_cloud_agent_sessions_kilo_session_id": { + "name": "UQ_cloud_agent_sessions_kilo_session_id", + "columns": [ + { + "expression": "kilo_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_sessions_initial_message_id": { + "name": "UQ_cloud_agent_sessions_initial_message_id", + "columns": [ + { + "expression": "initial_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_sandbox_id": { + "name": "IDX_cloud_agent_sessions_sandbox_id", + "columns": [ + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"sandbox_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_created_at": { + "name": "IDX_cloud_agent_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_created": { + "name": "IDX_cloud_agent_sessions_failure_created", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_at": { + "name": "IDX_cloud_agent_sessions_failure_at", + "columns": [ + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_classification_at": { + "name": "IDX_cloud_agent_sessions_failure_classification_at", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_error_expires_at": { + "name": "IDX_cloud_agent_sessions_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_sessions_failure_classification_check": { + "name": "cloud_agent_sessions_failure_classification_check", + "value": "(\"cloud_agent_sessions\".\"failure_at\" IS NULL AND \"cloud_agent_sessions\".\"failure_stage\" IS NULL AND \"cloud_agent_sessions\".\"failure_code\" IS NULL) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'sandbox_identity' AND \"cloud_agent_sessions\".\"failure_code\" = 'sandbox_id_derivation_failed') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'registration' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_registration_rejected') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'initial_admission' AND \"cloud_agent_sessions\".\"failure_code\" IN ('initial_admission_rejected', 'initial_queue_full', 'invalid_initial_intent')) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'transport' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_rpc_outcome_unknown')" + }, + "cloud_agent_sessions_error_message_bounded_check": { + "name": "cloud_agent_sessions_error_message_bounded_check", + "value": "\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_sessions\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_sessions_error_expiry_check": { + "name": "cloud_agent_sessions_error_expiry_check", + "value": "(\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_sessions\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_webhook_triggers": { + "name": "cloud_agent_webhook_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cloud_agent'" + }, + "kiloclaw_instance_id": { + "name": "kiloclaw_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "activation_mode": { + "name": "activation_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'webhook'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_timezone": { + "name": "cron_timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'UTC'" + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_webhook_triggers_user_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_user_trigger", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_webhook_triggers_org_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_org_trigger", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_user": { + "name": "IDX_cloud_agent_webhook_triggers_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_org": { + "name": "IDX_cloud_agent_webhook_triggers_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_active": { + "name": "IDX_cloud_agent_webhook_triggers_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_profile": { + "name": "IDX_cloud_agent_webhook_triggers_profile", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_organization_id_organizations_id_fk": { + "name": "cloud_agent_webhook_triggers_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk": { + "name": "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "kiloclaw_instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk": { + "name": "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_cloud_agent_webhook_triggers_owner": { + "name": "CHK_cloud_agent_webhook_triggers_owner", + "value": "(\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NULL) OR\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_cloud_agent_fields": { + "name": "CHK_cloud_agent_webhook_triggers_cloud_agent_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'cloud_agent' OR\n (\"cloud_agent_webhook_triggers\".\"github_repo\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"profile_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_kiloclaw_fields": { + "name": "CHK_cloud_agent_webhook_triggers_kiloclaw_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'kiloclaw_chat' OR\n \"cloud_agent_webhook_triggers\".\"kiloclaw_instance_id\" IS NOT NULL\n )" + }, + "CHK_cloud_agent_webhook_triggers_scheduled_fields": { + "name": "CHK_cloud_agent_webhook_triggers_scheduled_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"activation_mode\" != 'scheduled' OR\n \"cloud_agent_webhook_triggers\".\"cron_expression\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_billing_sku": { + "name": "cloud_billing_sku", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "accepts_new_usage": { + "name": "accepts_new_usage", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cloud_billing_sku_created_by_user_id_kilocode_users_id_fk": { + "name": "cloud_billing_sku_created_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_billing_sku", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_billing_sku_id_format": { + "name": "cloud_billing_sku_id_format", + "value": "\"cloud_billing_sku\".\"id\" ~ '^[a-z0-9][a-z0-9-]{2,79}$'" + }, + "cloud_billing_sku_name_nonempty": { + "name": "cloud_billing_sku_name_nonempty", + "value": "length(btrim(\"cloud_billing_sku\".\"name\")) > 0" + }, + "cloud_billing_sku_rate_positive": { + "name": "cloud_billing_sku_rate_positive", + "value": "\"cloud_billing_sku\".\"rate_cents_per_unit\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.code_indexing_manifest": { + "name": "code_indexing_manifest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_lines": { + "name": "total_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_ai_lines": { + "name": "total_ai_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_manifest_organization_id": { + "name": "IDX_code_indexing_manifest_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_kilo_user_id": { + "name": "IDX_code_indexing_manifest_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_project_id": { + "name": "IDX_code_indexing_manifest_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_git_branch": { + "name": "IDX_code_indexing_manifest_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_created_at": { + "name": "IDX_code_indexing_manifest_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_manifest", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_indexing_manifest_org_user_project_hash_branch": { + "name": "UQ_code_indexing_manifest_org_user_project_hash_branch", + "nullsNotDistinct": true, + "columns": [ + "organization_id", + "kilo_user_id", + "project_id", + "file_path", + "git_branch" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_indexing_search": { + "name": "code_indexing_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_search_organization_id": { + "name": "IDX_code_indexing_search_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_kilo_user_id": { + "name": "IDX_code_indexing_search_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_project_id": { + "name": "IDX_code_indexing_search_project_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_created_at": { + "name": "IDX_code_indexing_search_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_search_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_search_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_search", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_review_analytics_findings": { + "name": "code_review_analytics_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "analytics_result_id": { + "name": "analytics_result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "security_class": { + "name": "security_class", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk": { + "name": "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk", + "tableFrom": "code_review_analytics_findings", + "tableTo": "code_review_analytics_results", + "columnsFrom": [ + "analytics_result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_findings_result_ordinal": { + "name": "UQ_code_review_analytics_findings_result_ordinal", + "nullsNotDistinct": false, + "columns": [ + "analytics_result_id", + "ordinal" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_findings_severity_check": { + "name": "code_review_analytics_findings_severity_check", + "value": "\"code_review_analytics_findings\".\"severity\" IN ('critical', 'warning', 'suggestion')" + }, + "code_review_analytics_findings_category_check": { + "name": "code_review_analytics_findings_category_check", + "value": "\"code_review_analytics_findings\".\"category\" IN ('security', 'correctness', 'reliability', 'data_integrity', 'performance', 'compatibility', 'maintainability', 'test_quality', 'documentation', 'accessibility', 'other')" + }, + "code_review_analytics_findings_security_class_check": { + "name": "code_review_analytics_findings_security_class_check", + "value": "\"code_review_analytics_findings\".\"security_class\" IN ('auth_access', 'injection', 'data_protection', 'request_resource_boundary', 'deserialization_object_integrity', 'dependency_supply_chain', 'memory_safety', 'availability', 'concurrency', 'security_configuration', 'other')" + }, + "code_review_analytics_findings_ordinal_check": { + "name": "code_review_analytics_findings_ordinal_check", + "value": "\"code_review_analytics_findings\".\"ordinal\" >= 0" + }, + "code_review_analytics_findings_security_class_presence_check": { + "name": "code_review_analytics_findings_security_class_presence_check", + "value": "(\n (\"code_review_analytics_findings\".\"category\" = 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NOT NULL) OR\n (\"code_review_analytics_findings\".\"category\" <> 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_analytics_results": { + "name": "code_review_analytics_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_attempt_id": { + "name": "source_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "capture_status": { + "name": "capture_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "taxonomy_version": { + "name": "taxonomy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_level": { + "name": "impact_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "complexity_level": { + "name": "complexity_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification_confidence": { + "name": "classification_confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finalized_at": { + "name": "finalized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_analytics_results_source_attempt_id": { + "name": "idx_code_review_analytics_results_source_attempt_id", + "columns": [ + { + "expression": "source_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_analytics_results_finalized_at": { + "name": "idx_code_review_analytics_results_finalized_at", + "columns": [ + { + "expression": "finalized_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "source_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_results_code_review_id": { + "name": "UQ_code_review_analytics_results_code_review_id", + "nullsNotDistinct": false, + "columns": [ + "code_review_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_results_capture_status_check": { + "name": "code_review_analytics_results_capture_status_check", + "value": "\"code_review_analytics_results\".\"capture_status\" IN ('captured', 'missing', 'invalid', 'omitted')" + }, + "code_review_analytics_results_change_type_check": { + "name": "code_review_analytics_results_change_type_check", + "value": "\"code_review_analytics_results\".\"change_type\" IN ('bug_fix', 'feature', 'refactor', 'maintenance', 'dependency', 'test', 'documentation', 'mixed', 'other')" + }, + "code_review_analytics_results_impact_level_check": { + "name": "code_review_analytics_results_impact_level_check", + "value": "\"code_review_analytics_results\".\"impact_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_complexity_level_check": { + "name": "code_review_analytics_results_complexity_level_check", + "value": "\"code_review_analytics_results\".\"complexity_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_confidence_check": { + "name": "code_review_analytics_results_classification_confidence_check", + "value": "\"code_review_analytics_results\".\"classification_confidence\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_presence_check": { + "name": "code_review_analytics_results_classification_presence_check", + "value": "(\n (\n \"code_review_analytics_results\".\"capture_status\" = 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NOT NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NOT NULL\n ) OR (\n \"code_review_analytics_results\".\"capture_status\" <> 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NULL\n )\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_feedback_events": { + "name": "code_review_feedback_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "kilo_comment_id": { + "name": "kilo_comment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_excerpt": { + "name": "reply_excerpt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_comment_excerpt": { + "name": "kilo_comment_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedupe_hash": { + "name": "dedupe_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_feedback_events_owned_by_org_id": { + "name": "idx_code_review_feedback_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_owned_by_user_id": { + "name": "idx_code_review_feedback_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_platform_repo": { + "name": "idx_code_review_feedback_events_platform_repo", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_created_at": { + "name": "idx_code_review_feedback_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_feedback_events_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_feedback_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_feedback_events_dedupe_hash": { + "name": "UQ_code_review_feedback_events_dedupe_hash", + "nullsNotDistinct": false, + "columns": [ + "dedupe_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_feedback_events_owner_check": { + "name": "code_review_feedback_events_owner_check", + "value": "(\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_memory_proposals": { + "name": "code_review_memory_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_markdown": { + "name": "proposed_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "positive_count": { + "name": "positive_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "negative_count": { + "name": "negative_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "neutral_count": { + "name": "neutral_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "change_request_url": { + "name": "change_request_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_memory_proposals_owned_by_org_id": { + "name": "idx_code_review_memory_proposals_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_owned_by_user_id": { + "name": "idx_code_review_memory_proposals_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_platform_repo_status": { + "name": "idx_code_review_memory_proposals_platform_repo_status", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_updated_at": { + "name": "idx_code_review_memory_proposals_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_org_active_scope": { + "name": "UQ_code_review_memory_proposals_org_active_scope", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_user_active_scope": { + "name": "UQ_code_review_memory_proposals_user_active_scope", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "code_review_memory_proposals_owner_check": { + "name": "code_review_memory_proposals_owner_check", + "value": "(\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_availability_intents": { + "name": "coding_plan_availability_intents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_availability_intents_user_plan": { + "name": "UQ_coding_plan_availability_intents_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_availability_intents_plan": { + "name": "IDX_coding_plan_availability_intents_plan", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_availability_intents_user_id_kilocode_users_id_fk": { + "name": "coding_plan_availability_intents_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_availability_intents", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coding_plan_key_inventory": { + "name": "coding_plan_key_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_plan_id": { + "name": "upstream_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_usage_id": { + "name": "upstream_usage_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_fingerprint": { + "name": "credential_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "assigned_to_user_id": { + "name": "assigned_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_requested_at": { + "name": "revocation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_attempt_count": { + "name": "revocation_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_revocation_error": { + "name": "last_revocation_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_key_inv_fingerprint": { + "name": "UQ_coding_plan_key_inv_fingerprint", + "columns": [ + { + "expression": "credential_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_coding_plan_key_inv_provider_usage_id": { + "name": "UQ_coding_plan_key_inv_provider_usage_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "upstream_usage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_key_inventory\".\"upstream_usage_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_plan_status": { + "name": "IDX_coding_plan_key_inv_plan_status", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_available": { + "name": "IDX_coding_plan_key_inv_available", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"coding_plan_key_inventory\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk": { + "name": "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_key_inventory", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_to_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_key_inventory_status_check": { + "name": "coding_plan_key_inventory_status_check", + "value": "\"coding_plan_key_inventory\".\"status\" IN ('available', 'assigned', 'revocation_pending', 'revoked', 'revocation_failed')" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_subscriptions": { + "name": "coding_plan_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_inventory_id": { + "name": "key_inventory_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "installed_byok_key_id": { + "name": "installed_byok_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "billing_period_days": { + "name": "billing_period_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "past_due_started_at": { + "name": "past_due_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_grace_expires_at": { + "name": "payment_grace_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_attempted_for_due": { + "name": "auto_top_up_attempted_for_due", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_sub_live_user_plan": { + "name": "UQ_coding_plan_sub_live_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_coding_plan_sub_live_user_provider": { + "name": "UQ_coding_plan_sub_live_user_provider", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_status": { + "name": "IDX_coding_plan_sub_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_renewal": { + "name": "IDX_coding_plan_sub_renewal", + "columns": [ + { + "expression": "credit_renewal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_inventory": { + "name": "IDX_coding_plan_sub_inventory", + "columns": [ + { + "expression": "key_inventory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_subscriptions_user_id_kilocode_users_id_fk": { + "name": "coding_plan_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk": { + "name": "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "coding_plan_key_inventory", + "columnsFrom": [ + "key_inventory_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk": { + "name": "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "byok_api_keys", + "columnsFrom": [ + "installed_byok_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_subscriptions_status_check": { + "name": "coding_plan_subscriptions_status_check", + "value": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due', 'canceled')" + }, + "coding_plan_subscriptions_live_access_check": { + "name": "coding_plan_subscriptions_live_access_check", + "value": "\"coding_plan_subscriptions\".\"status\" = 'canceled' OR \"coding_plan_subscriptions\".\"key_inventory_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_terms": { + "name": "coding_plan_terms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_terms_request": { + "name": "UQ_coding_plan_terms_request", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_terms_subscription": { + "name": "IDX_coding_plan_terms_subscription", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk": { + "name": "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "coding_plan_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_user_id_kilocode_users_id_fk": { + "name": "coding_plan_terms_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk": { + "name": "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_terms_kind_check": { + "name": "coding_plan_terms_kind_check", + "value": "\"coding_plan_terms\".\"kind\" IN ('activation', 'extension', 'renewal')" + } + }, + "isRLSEnabled": false + }, + "public.compute_usage_charge": { + "name": "compute_usage_charge", + "schema": "", + "columns": { + "usage_source": { + "name": "usage_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_source_id": { + "name": "usage_source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_billing_sku_id": { + "name": "cloud_billing_sku_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "settled_quantity_after": { + "name": "settled_quantity_after", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": false + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_compute_usage_charge_user_created": { + "name": "IDX_compute_usage_charge_user_created", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_compute_usage_charge_organization_created": { + "name": "IDX_compute_usage_charge_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "compute_usage_charge_user_id_kilocode_users_id_fk": { + "name": "compute_usage_charge_user_id_kilocode_users_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "compute_usage_charge_organization_id_organizations_id_fk": { + "name": "compute_usage_charge_organization_id_organizations_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "compute_usage_charge_cloud_billing_sku_id_cloud_billing_sku_id_fk": { + "name": "compute_usage_charge_cloud_billing_sku_id_cloud_billing_sku_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "cloud_billing_sku", + "columnsFrom": [ + "cloud_billing_sku_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "compute_usage_charge_usage_source_usage_source_id_created_at_pk": { + "name": "compute_usage_charge_usage_source_usage_source_id_created_at_pk", + "columns": [ + "usage_source", + "usage_source_id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "compute_usage_charge_exactly_one_payer": { + "name": "compute_usage_charge_exactly_one_payer", + "value": "(\"compute_usage_charge\".\"user_id\" IS NULL) <> (\"compute_usage_charge\".\"organization_id\" IS NULL)" + }, + "compute_usage_charge_quantity_positive": { + "name": "compute_usage_charge_quantity_positive", + "value": "\"compute_usage_charge\".\"quantity\" > 0" + }, + "compute_usage_charge_settled_quantity_positive": { + "name": "compute_usage_charge_settled_quantity_positive", + "value": "\"compute_usage_charge\".\"settled_quantity_after\" IS NULL OR \"compute_usage_charge\".\"settled_quantity_after\" > 0" + }, + "compute_usage_charge_rate_positive": { + "name": "compute_usage_charge_rate_positive", + "value": "\"compute_usage_charge\".\"rate_cents_per_unit\" > 0" + }, + "compute_usage_charge_amount_positive": { + "name": "compute_usage_charge_amount_positive", + "value": "\"compute_usage_charge\".\"amount_microdollars\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.container_usage_interval": { + "name": "container_usage_interval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_epoch_ms": { + "name": "start_epoch_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cloud_billing_sku_id": { + "name": "cloud_billing_sku_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_fingerprint": { + "name": "context_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_heartbeat_seq": { + "name": "last_heartbeat_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confirmed_seconds": { + "name": "confirmed_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_mode": { + "name": "billing_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'shadow'" + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": false + }, + "settled_billable_seconds": { + "name": "settled_billable_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_stop_seq": { + "name": "final_stop_seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_container_usage_interval_sweep": { + "name": "IDX_container_usage_interval_sweep", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_container_usage_interval_subject_started": { + "name": "IDX_container_usage_interval_subject_started", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_container_usage_interval_single_open": { + "name": "UQ_container_usage_interval_single_open", + "columns": [ + { + "expression": "service", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"container_usage_interval\".\"status\" = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_usage_interval_cloud_billing_sku_id_cloud_billing_sku_id_fk": { + "name": "container_usage_interval_cloud_billing_sku_id_cloud_billing_sku_id_fk", + "tableFrom": "container_usage_interval", + "tableTo": "cloud_billing_sku", + "columnsFrom": [ + "cloud_billing_sku_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "container_usage_interval_subject_type": { + "name": "container_usage_interval_subject_type", + "value": "\"container_usage_interval\".\"subject_type\" IN ('user', 'org')" + }, + "container_usage_interval_actor_type": { + "name": "container_usage_interval_actor_type", + "value": "\"container_usage_interval\".\"actor_type\" IN ('user', 'bot')" + }, + "container_usage_interval_context_fingerprint": { + "name": "container_usage_interval_context_fingerprint", + "value": "\"container_usage_interval\".\"context_fingerprint\" ~ '^[a-f0-9]{64}$'" + }, + "container_usage_interval_attribution": { + "name": "container_usage_interval_attribution", + "value": "\"container_usage_interval\".\"actor_type\" = 'bot' OR (\"container_usage_interval\".\"actor_type\" = 'user' AND (\"container_usage_interval\".\"subject_type\" <> 'user' OR \"container_usage_interval\".\"actor_id\" = \"container_usage_interval\".\"subject_id\"))" + }, + "container_usage_interval_status": { + "name": "container_usage_interval_status", + "value": "\"container_usage_interval\".\"status\" IN ('open', 'closed')" + }, + "container_usage_interval_billing_mode": { + "name": "container_usage_interval_billing_mode", + "value": "\"container_usage_interval\".\"billing_mode\" IN ('shadow', 'paid')" + }, + "container_usage_interval_paid_rate": { + "name": "container_usage_interval_paid_rate", + "value": "(\"container_usage_interval\".\"billing_mode\" = 'shadow' AND \"container_usage_interval\".\"rate_cents_per_unit\" IS NULL) OR (\"container_usage_interval\".\"billing_mode\" = 'paid' AND \"container_usage_interval\".\"rate_cents_per_unit\" > 0)" + }, + "container_usage_interval_open_closed_shape": { + "name": "container_usage_interval_open_closed_shape", + "value": "(\"container_usage_interval\".\"status\" = 'open' AND \"container_usage_interval\".\"stopped_at\" IS NULL AND \"container_usage_interval\".\"close_reason\" IS NULL) OR (\"container_usage_interval\".\"status\" = 'closed' AND \"container_usage_interval\".\"stopped_at\" IS NOT NULL AND \"container_usage_interval\".\"close_reason\" IS NOT NULL)" + }, + "container_usage_interval_time_order": { + "name": "container_usage_interval_time_order", + "value": "\"container_usage_interval\".\"last_seen_at\" >= \"container_usage_interval\".\"started_at\" AND (\"container_usage_interval\".\"stopped_at\" IS NULL OR (\"container_usage_interval\".\"stopped_at\" >= \"container_usage_interval\".\"started_at\" AND \"container_usage_interval\".\"stopped_at\" <= \"container_usage_interval\".\"last_seen_at\"))" + }, + "container_usage_interval_last_heartbeat_seq_nonnegative": { + "name": "container_usage_interval_last_heartbeat_seq_nonnegative", + "value": "\"container_usage_interval\".\"last_heartbeat_seq\" >= 0" + }, + "container_usage_interval_confirmed_seconds_nonnegative": { + "name": "container_usage_interval_confirmed_seconds_nonnegative", + "value": "\"container_usage_interval\".\"confirmed_seconds\" >= 0" + }, + "container_usage_interval_settled_billable_seconds_nonnegative": { + "name": "container_usage_interval_settled_billable_seconds_nonnegative", + "value": "\"container_usage_interval\".\"settled_billable_seconds\" >= 0 AND \"container_usage_interval\".\"settled_billable_seconds\" <= \"container_usage_interval\".\"confirmed_seconds\"" + }, + "container_usage_interval_final_stop_seq_positive": { + "name": "container_usage_interval_final_stop_seq_positive", + "value": "\"container_usage_interval\".\"final_stop_seq\" IS NULL OR \"container_usage_interval\".\"final_stop_seq\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.container_usage_segment": { + "name": "container_usage_segment", + "schema": "", + "columns": { + "interval_id": { + "name": "interval_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_seconds": { + "name": "reported_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "usage_seconds": { + "name": "usage_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_container_usage_segment_received": { + "name": "IDX_container_usage_segment_received", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_usage_segment_interval_id_container_usage_interval_id_fk": { + "name": "container_usage_segment_interval_id_container_usage_interval_id_fk", + "tableFrom": "container_usage_segment", + "tableTo": "container_usage_interval", + "columnsFrom": [ + "interval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "container_usage_segment_interval_id_seq_pk": { + "name": "container_usage_segment_interval_id_seq_pk", + "columns": [ + "interval_id", + "seq" + ] + } + }, + "uniqueConstraints": { + "container_usage_segment_idempotency_key_unique": { + "name": "container_usage_segment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "container_usage_segment_seq_positive": { + "name": "container_usage_segment_seq_positive", + "value": "\"container_usage_segment\".\"seq\" > 0" + }, + "container_usage_segment_reported_seconds_nonnegative": { + "name": "container_usage_segment_reported_seconds_nonnegative", + "value": "\"container_usage_segment\".\"reported_seconds\" >= 0" + }, + "container_usage_segment_usage_seconds_nonnegative": { + "name": "container_usage_segment_usage_seconds_nonnegative", + "value": "\"container_usage_segment\".\"usage_seconds\" >= 0" + }, + "container_usage_segment_usage_within_reported": { + "name": "container_usage_segment_usage_within_reported", + "value": "\"container_usage_segment\".\"usage_seconds\" <= \"container_usage_segment\".\"reported_seconds\"" + } + }, + "isRLSEnabled": false + }, + "public.content_moderation_reports": { + "name": "content_moderation_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "receipt_id": { + "name": "receipt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "triage_status": { + "name": "triage_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "appeal_status": { + "name": "appeal_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_content_moderation_reports_user_created": { + "name": "IDX_content_moderation_reports_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_content_moderation_reports_target": { + "name": "IDX_content_moderation_reports_target", + "columns": [ + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "content_moderation_reports_receipt_id_unique": { + "name": "content_moderation_reports_receipt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "receipt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_contributors": { + "name": "contributor_champion_contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_profile_url": { + "name": "github_profile_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "first_contribution_at": { + "name": "first_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_contribution_at": { + "name": "last_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "all_time_contributions": { + "name": "all_time_contributions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "manual_email": { + "name": "manual_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_contributors_last_contribution_at": { + "name": "IDX_contributor_champion_contributors_last_contribution_at", + "columns": [ + { + "expression": "last_contribution_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_contributors_manual_email": { + "name": "IDX_contributor_champion_contributors_manual_email", + "columns": [ + { + "expression": "manual_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_contributors_github_login": { + "name": "UQ_contributor_champion_contributors_github_login", + "nullsNotDistinct": false, + "columns": [ + "github_login" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_events": { + "name": "contributor_champion_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_number": { + "name": "github_pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "github_pr_url": { + "name": "github_pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_title": { + "name": "github_pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_login": { + "name": "github_author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_email": { + "name": "github_author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_events_contributor_id": { + "name": "IDX_contributor_champion_events_contributor_id", + "columns": [ + { + "expression": "contributor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_merged_at": { + "name": "IDX_contributor_champion_events_merged_at", + "columns": [ + { + "expression": "merged_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_author_email": { + "name": "IDX_contributor_champion_events_author_email", + "columns": [ + { + "expression": "github_author_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_events", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_events_repo_pr": { + "name": "UQ_contributor_champion_events_repo_pr", + "nullsNotDistinct": false, + "columns": [ + "repo_full_name", + "github_pr_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_memberships": { + "name": "contributor_champion_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selected_tier": { + "name": "selected_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_tier": { + "name": "enrolled_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_amount_microdollars": { + "name": "credit_amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credits_last_granted_at": { + "name": "credits_last_granted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "linked_kilo_user_id": { + "name": "linked_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_memberships_credits_due": { + "name": "IDX_contributor_champion_memberships_credits_due", + "columns": [ + { + "expression": "credits_last_granted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NOT NULL AND \"contributor_champion_memberships\".\"credit_amount_microdollars\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_memberships_linked_kilo_user_id": { + "name": "IDX_contributor_champion_memberships_linked_kilo_user_id", + "columns": [ + { + "expression": "linked_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk": { + "name": "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "kilocode_users", + "columnsFrom": [ + "linked_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_memberships_contributor_id": { + "name": "UQ_contributor_champion_memberships_contributor_id", + "nullsNotDistinct": false, + "columns": [ + "contributor_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contributor_champion_memberships_selected_tier_check": { + "name": "contributor_champion_memberships_selected_tier_check", + "value": "\"contributor_champion_memberships\".\"selected_tier\" IS NULL OR \"contributor_champion_memberships\".\"selected_tier\" IN ('contributor', 'ambassador', 'champion')" + }, + "contributor_champion_memberships_enrolled_tier_check": { + "name": "contributor_champion_memberships_enrolled_tier_check", + "value": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NULL OR \"contributor_champion_memberships\".\"enrolled_tier\" IN ('contributor', 'ambassador', 'champion')" + } + }, + "isRLSEnabled": false + }, + "public.contributor_champion_sync_state": { + "name": "contributor_champion_sync_state", + "schema": "", + "columns": { + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_merged_at": { + "name": "last_merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_campaigns": { + "name": "credit_campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_expiry_hours": { + "name": "credit_expiry_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_ends_at": { + "name": "campaign_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_redemptions_allowed": { + "name": "total_redemptions_allowed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_credit_campaigns_slug": { + "name": "UQ_credit_campaigns_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_credit_campaigns_credit_category": { + "name": "UQ_credit_campaigns_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credit_campaigns_slug_format_check": { + "name": "credit_campaigns_slug_format_check", + "value": "\"credit_campaigns\".\"slug\" ~ '^[a-z0-9-]{5,40}$'" + }, + "credit_campaigns_amount_positive_check": { + "name": "credit_campaigns_amount_positive_check", + "value": "\"credit_campaigns\".\"amount_microdollars\" > 0" + }, + "credit_campaigns_credit_expiry_hours_positive_check": { + "name": "credit_campaigns_credit_expiry_hours_positive_check", + "value": "\"credit_campaigns\".\"credit_expiry_hours\" IS NULL OR \"credit_campaigns\".\"credit_expiry_hours\" > 0" + }, + "credit_campaigns_total_redemptions_allowed_positive_check": { + "name": "credit_campaigns_total_redemptions_allowed_positive_check", + "value": "\"credit_campaigns\".\"total_redemptions_allowed\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.credit_transactions": { + "name": "credit_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expiration_baseline_microdollars_used": { + "name": "expiration_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "original_baseline_microdollars_used": { + "name": "original_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_transaction_id": { + "name": "original_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_id": { + "name": "stripe_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coinbase_credit_block_id": { + "name": "coinbase_credit_block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_date": { + "name": "expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "check_category_uniqueness": { + "name": "check_category_uniqueness", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_credit_transactions_created_at": { + "name": "IDX_credit_transactions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_is_free": { + "name": "IDX_credit_transactions_is_free", + "columns": [ + { + "expression": "is_free", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_kilo_user_id": { + "name": "IDX_credit_transactions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_credit_category": { + "name": "IDX_credit_transactions_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_stripe_payment_id": { + "name": "IDX_credit_transactions_stripe_payment_id", + "columns": [ + { + "expression": "stripe_payment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_original_transaction_id": { + "name": "IDX_credit_transactions_original_transaction_id", + "columns": [ + { + "expression": "original_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_coinbase_credit_block_id": { + "name": "IDX_credit_transactions_coinbase_credit_block_id", + "columns": [ + { + "expression": "coinbase_credit_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_organization_id": { + "name": "IDX_credit_transactions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_unique_category": { + "name": "IDX_credit_transactions_unique_category", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credit_transactions\".\"check_category_uniqueness\" = TRUE", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "credit_transactions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_llm2": { + "name": "custom_llm2", + "schema": "", + "columns": { + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deleted_user_email_tombstones": { + "name": "deleted_user_email_tombstones", + "schema": "", + "columns": { + "normalized_email_hash": { + "name": "normalized_email_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_builds": { + "name": "deployment_builds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_builds_deployment_id": { + "name": "idx_deployment_builds_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_builds_status": { + "name": "idx_deployment_builds_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_builds_deployment_id_deployments_id_fk": { + "name": "deployment_builds_deployment_id_deployments_id_fk", + "tableFrom": "deployment_builds", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_env_vars": { + "name": "deployment_env_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_env_vars_deployment_id": { + "name": "idx_deployment_env_vars_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_env_vars_deployment_id_deployments_id_fk": { + "name": "deployment_env_vars_deployment_id_deployments_id_fk", + "tableFrom": "deployment_env_vars", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployment_env_vars_deployment_key": { + "name": "UQ_deployment_env_vars_deployment_key", + "nullsNotDistinct": false, + "columns": [ + "deployment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_events": { + "name": "deployment_events", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'log'" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_deployment_events_build_id": { + "name": "idx_deployment_events_build_id", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_events_timestamp": { + "name": "idx_deployment_events_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_events_type": { + "name": "idx_deployment_events_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_events_build_id_deployment_builds_id_fk": { + "name": "deployment_events_build_id_deployment_builds_id_fk", + "tableFrom": "deployment_events", + "tableTo": "deployment_builds", + "columnsFrom": [ + "build_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_events_build_id_event_id_pk": { + "name": "deployment_events_build_id_event_id_pk", + "columns": [ + "build_id", + "event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_threat_detections": { + "name": "deployment_threat_detections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "build_id": { + "name": "build_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "threat_type": { + "name": "threat_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_threat_detections_deployment_id": { + "name": "idx_deployment_threat_detections_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_threat_detections_created_at": { + "name": "idx_deployment_threat_detections_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_threat_detections_deployment_id_deployments_id_fk": { + "name": "deployment_threat_detections_deployment_id_deployments_id_fk", + "tableFrom": "deployment_threat_detections", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_threat_detections_build_id_deployment_builds_id_fk": { + "name": "deployment_threat_detections_build_id_deployment_builds_id_fk", + "tableFrom": "deployment_threat_detections", + "tableTo": "deployment_builds", + "columnsFrom": [ + "build_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployments": { + "name": "deployments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deployment_slug": { + "name": "deployment_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_worker_name": { + "name": "internal_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_source": { + "name": "repository_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_url": { + "name": "deployment_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "git_auth_token": { + "name": "git_auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_deployed_at": { + "name": "last_deployed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_build_id": { + "name": "last_build_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "threat_status": { + "name": "threat_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_from": { + "name": "created_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_deployments_owned_by_user_id": { + "name": "idx_deployments_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_owned_by_organization_id": { + "name": "idx_deployments_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_platform_integration_id": { + "name": "idx_deployments_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_repository_source_branch": { + "name": "idx_deployments_repository_source_branch", + "columns": [ + { + "expression": "repository_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_threat_status_pending": { + "name": "idx_deployments_threat_status_pending", + "columns": [ + { + "expression": "threat_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"deployments\".\"threat_status\" = 'pending_scan'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_owned_by_user_id_kilocode_users_id_fk": { + "name": "deployments_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "deployments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployments_owned_by_organization_id_organizations_id_fk": { + "name": "deployments_owned_by_organization_id_organizations_id_fk", + "tableFrom": "deployments", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployments_deployment_slug": { + "name": "UQ_deployments_deployment_slug", + "nullsNotDistinct": false, + "columns": [ + "deployment_slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "deployments_owner_check": { + "name": "deployments_owner_check", + "value": "(\n (\"deployments\".\"owned_by_user_id\" IS NOT NULL AND \"deployments\".\"owned_by_organization_id\" IS NULL) OR\n (\"deployments\".\"owned_by_user_id\" IS NULL AND \"deployments\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "deployments_source_type_check": { + "name": "deployments_source_type_check", + "value": "\"deployments\".\"source_type\" IN ('github', 'git', 'app-builder')" + } + }, + "isRLSEnabled": false + }, + "public.deployments_ephemeral": { + "name": "deployments_ephemeral", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_worker_name": { + "name": "internal_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_slug": { + "name": "deployment_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_cleanup_at": { + "name": "next_cleanup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cleanup_claim_token": { + "name": "cleanup_claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cleanup_claimed_until": { + "name": "cleanup_claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployments_ephemeral_owned_by_user_id": { + "name": "idx_deployments_ephemeral_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_ephemeral_next_cleanup_at": { + "name": "idx_deployments_ephemeral_next_cleanup_at", + "columns": [ + { + "expression": "next_cleanup_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_ephemeral_owned_by_user_id_kilocode_users_id_fk": { + "name": "deployments_ephemeral_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "deployments_ephemeral", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployments_ephemeral_internal_worker_name": { + "name": "UQ_deployments_ephemeral_internal_worker_name", + "nullsNotDistinct": false, + "columns": [ + "internal_worker_name" + ] + }, + "UQ_deployments_ephemeral_deployment_slug": { + "name": "UQ_deployments_ephemeral_deployment_slug", + "nullsNotDistinct": false, + "columns": [ + "deployment_slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "deployments_ephemeral_source_type_check": { + "name": "deployments_ephemeral_source_type_check", + "value": "\"deployments_ephemeral\".\"source_type\" IN ('html')" + }, + "deployments_ephemeral_status_check": { + "name": "deployments_ephemeral_status_check", + "value": "\"deployments_ephemeral\".\"status\" IN ('pending', 'active', 'cleanup_retry')" + }, + "deployments_ephemeral_claim_fields_check": { + "name": "deployments_ephemeral_claim_fields_check", + "value": "(\"deployments_ephemeral\".\"cleanup_claim_token\" IS NULL) = (\"deployments_ephemeral\".\"cleanup_claimed_until\" IS NULL)" + }, + "deployments_ephemeral_active_fields_check": { + "name": "deployments_ephemeral_active_fields_check", + "value": "\"deployments_ephemeral\".\"status\" <> 'active' OR (\"deployments_ephemeral\".\"deployment_slug\" IS NOT NULL AND \"deployments_ephemeral\".\"expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.device_auth_requests": { + "name": "device_auth_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_device_auth_requests_code": { + "name": "UQ_device_auth_requests_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_status": { + "name": "IDX_device_auth_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_expires_at": { + "name": "IDX_device_auth_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_kilo_user_id": { + "name": "IDX_device_auth_requests_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_device_auth_requests_device_code_hash": { + "name": "UQ_device_auth_requests_device_code_hash", + "columns": [ + { + "expression": "device_code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"device_auth_requests\".\"device_code_hash\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_user_code": { + "name": "IDX_device_auth_requests_user_code", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"device_auth_requests\".\"user_code\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_auth_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "device_auth_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_auth_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_refresh_tokens": { + "name": "device_refresh_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_session_id": { + "name": "device_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_device_refresh_tokens_device_session_id": { + "name": "IDX_device_refresh_tokens_device_session_id", + "columns": [ + { + "expression": "device_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_refresh_tokens_expires_at": { + "name": "IDX_device_refresh_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_refresh_tokens_device_session_id_device_sessions_id_fk": { + "name": "device_refresh_tokens_device_session_id_device_sessions_id_fk", + "tableFrom": "device_refresh_tokens", + "tableTo": "device_sessions", + "columnsFrom": [ + "device_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_sessions": { + "name": "device_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_auth_request_id": { + "name": "device_auth_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_device_sessions_kilo_user_id": { + "name": "IDX_device_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_sessions_revoked_at": { + "name": "IDX_device_sessions_revoked_at", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "device_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord_gateway_listener": { + "name": "discord_gateway_listener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "listener_id": { + "name": "listener_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.editor_name": { + "name": "editor_name", + "schema": "", + "columns": { + "editor_name_id": { + "name": "editor_name_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_editor_name": { + "name": "UQ_editor_name", + "columns": [ + { + "expression": "editor_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.enrichment_data": { + "name": "enrichment_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_enrichment_data": { + "name": "github_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linkedin_enrichment_data": { + "name": "linkedin_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "clay_enrichment_data": { + "name": "clay_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_enrichment_data_user_id": { + "name": "IDX_enrichment_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "enrichment_data_user_id_kilocode_users_id_fk": { + "name": "enrichment_data_user_id_kilocode_users_id_fk", + "tableFrom": "enrichment_data", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_enrichment_data_user_id": { + "name": "UQ_enrichment_data_user_id", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_monthly_usage": { + "name": "exa_monthly_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "month": { + "name": "month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_charged_microdollars": { + "name": "total_charged_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "free_allowance_microdollars": { + "name": "free_allowance_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 10000000 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_monthly_usage_personal": { + "name": "idx_exa_monthly_usage_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_exa_monthly_usage_org": { + "name": "idx_exa_monthly_usage_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_usage_log": { + "name": "exa_usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "charged_to_balance": { + "name": "charged_to_balance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_usage_log_user_created": { + "name": "idx_exa_usage_log_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "exa_usage_log_id_created_at_pk": { + "name": "exa_usage_log_id_created_at_pk", + "columns": [ + "id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_side_effect_outbox": { + "name": "external_side_effect_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'send_org_invite_email'" + }, + "invitation_id": { + "name": "invitation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_external_side_effect_outbox_invitation_id": { + "name": "UQ_external_side_effect_outbox_invitation_id", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_external_side_effect_outbox_status_next_attempt_at": { + "name": "IDX_external_side_effect_outbox_status_next_attempt_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feature": { + "name": "feature", + "schema": "", + "columns": { + "feature_id": { + "name": "feature_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_feature": { + "name": "UQ_feature", + "columns": [ + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finish_reason": { + "name": "finish_reason", + "schema": "", + "columns": { + "finish_reason_id": { + "name": "finish_reason_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_finish_reason": { + "name": "UQ_finish_reason", + "columns": [ + { + "expression": "finish_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_model_usage": { + "name": "free_model_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_model_usage_ip_created_at": { + "name": "idx_free_model_usage_ip_created_at", + "columns": [ + { + "expression": "ip_address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_model_usage_created_at": { + "name": "idx_free_model_usage_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_branch_pull_requests": { + "name": "github_branch_pull_requests", + "schema": "", + "columns": { + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_state": { + "name": "pr_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_sha": { + "name": "pr_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_review_decision": { + "name": "pr_review_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_decision_pending": { + "name": "review_decision_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "review_decision_fetching_at": { + "name": "review_decision_fetching_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_last_synced_at": { + "name": "pr_last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_github_branch_prs_org": { + "name": "UQ_github_branch_prs_org", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_github_branch_prs_user": { + "name": "UQ_github_branch_prs_user", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_github_branch_prs_url_branch": { + "name": "IDX_github_branch_prs_url_branch", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk": { + "name": "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_branch_pull_requests_owner_check": { + "name": "github_branch_pull_requests_owner_check", + "value": "(\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NOT NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NULL) OR\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NOT NULL)\n )" + }, + "github_branch_pull_requests_review_decision_check": { + "name": "github_branch_pull_requests_review_decision_check", + "value": "\"github_branch_pull_requests\".\"pr_review_decision\" IS NULL OR \"github_branch_pull_requests\".\"pr_review_decision\" IN ('approved', 'changes_requested', 'review_required')" + } + }, + "isRLSEnabled": false + }, + "public.github_install_states": { + "name": "github_install_states", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_github_install_states_expires_at": { + "name": "IDX_github_install_states_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_install_states_kilo_user_id_kilocode_users_id_fk": { + "name": "github_install_states_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "github_install_states", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_install_states_owner_type_check": { + "name": "github_install_states_owner_type_check", + "value": "\"github_install_states\".\"owner_type\" IN ('org', 'user')" + } + }, + "isRLSEnabled": false + }, + "public.http_ip": { + "name": "http_ip", + "schema": "", + "columns": { + "http_ip_id": { + "name": "http_ip_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_ip": { + "name": "http_ip", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_ip": { + "name": "UQ_http_ip", + "columns": [ + { + "expression": "http_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.http_user_agent": { + "name": "http_user_agent", + "schema": "", + "columns": { + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_user_agent": { + "name": "UQ_http_user_agent", + "columns": [ + { + "expression": "http_user_agent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.impact_advocate_participants": { + "name": "impact_advocate_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_id": { + "name": "advocate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_account_id": { + "name": "advocate_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_referral_identifier": { + "name": "opaque_referral_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_state": { + "name": "registration_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_registration_attempt_at": { + "name": "last_registration_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_impact_advocate_participants_program_referral_identifier": { + "name": "UQ_impact_advocate_participants_program_referral_identifier", + "columns": [ + { + "expression": "program_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opaque_referral_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"impact_advocate_participants\".\"opaque_referral_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_participants_registration_state": { + "name": "IDX_impact_advocate_participants_registration_state", + "columns": [ + { + "expression": "registration_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_participants_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_participants_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_participants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_participants_program_user": { + "name": "UQ_impact_advocate_participants_program_user", + "nullsNotDistinct": false, + "columns": [ + "program_key", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_participants_program_key_check": { + "name": "impact_advocate_participants_program_key_check", + "value": "\"impact_advocate_participants\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_participants_registration_state_check": { + "name": "impact_advocate_participants_registration_state_check", + "value": "\"impact_advocate_participants\".\"registration_state\" IN ('pending', 'retrying', 'registered', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_registration_attempts": { + "name": "impact_advocate_registration_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_cookie_value": { + "name": "opaque_cookie_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cookie_value_length": { + "name": "cookie_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_advocate_registration_attempts_participant_id": { + "name": "IDX_impact_advocate_registration_attempts_participant_id", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_registration_attempts_delivery_state": { + "name": "IDX_impact_advocate_registration_attempts_delivery_state", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk": { + "name": "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk", + "tableFrom": "impact_advocate_registration_attempts", + "tableTo": "impact_advocate_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_registration_attempts_dedupe_key": { + "name": "UQ_impact_advocate_registration_attempts_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_registration_attempts_program_key_check": { + "name": "impact_advocate_registration_attempts_program_key_check", + "value": "\"impact_advocate_registration_attempts\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_registration_attempts_delivery_state_check": { + "name": "impact_advocate_registration_attempts_delivery_state_check", + "value": "\"impact_advocate_registration_attempts\".\"delivery_state\" IN ('queued', 'sending', 'succeeded', 'failed')" + }, + "impact_advocate_registration_attempts_cookie_value_length_non_negative_check": { + "name": "impact_advocate_registration_attempts_cookie_value_length_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"cookie_value_length\" >= 0" + }, + "impact_advocate_registration_attempts_attempt_count_non_negative_check": { + "name": "impact_advocate_registration_attempts_attempt_count_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_reward_redemptions": { + "name": "impact_advocate_reward_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "impact_reward_id": { + "name": "impact_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lookup_response_payload": { + "name": "lookup_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "redeem_response_payload": { + "name": "redeem_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_advocate_reward_redemptions_beneficiary_user_id": { + "name": "IDX_impact_advocate_reward_redemptions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_reward_redemptions_state": { + "name": "IDX_impact_advocate_reward_redemptions_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_reward_redemptions_reward_id": { + "name": "UQ_impact_advocate_reward_redemptions_reward_id", + "nullsNotDistinct": false, + "columns": [ + "reward_id" + ] + }, + "UQ_impact_advocate_reward_redemptions_dedupe_key": { + "name": "UQ_impact_advocate_reward_redemptions_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_reward_redemptions_state_check": { + "name": "impact_advocate_reward_redemptions_state_check", + "value": "\"impact_advocate_reward_redemptions\".\"state\" IN ('queued', 'retrying', 'redeemed', 'failed')" + }, + "impact_advocate_reward_redemptions_attempt_count_non_negative_check": { + "name": "impact_advocate_reward_redemptions_attempt_count_non_negative_check", + "value": "\"impact_advocate_reward_redemptions\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_attribution_touches": { + "name": "impact_attribution_touches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'kiloclaw'" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anonymous_id": { + "name": "anonymous_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touch_type": { + "name": "touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_tracking_value": { + "name": "opaque_tracking_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_value_length": { + "name": "tracking_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_tracking_value_accepted": { + "name": "is_tracking_value_accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rs_code": { + "name": "rs_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_share_medium": { + "name": "rs_share_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_engagement_medium": { + "name": "rs_engagement_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "im_ref": { + "name": "im_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_path": { + "name": "landing_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touched_at": { + "name": "touched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sale_attributed_at": { + "name": "sale_attributed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_attribution_touches_product_user_id": { + "name": "IDX_impact_attribution_touches_product_user_id", + "columns": [ + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_user_id": { + "name": "IDX_impact_attribution_touches_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_anonymous_id": { + "name": "IDX_impact_attribution_touches_anonymous_id", + "columns": [ + { + "expression": "anonymous_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_expires_at": { + "name": "IDX_impact_attribution_touches_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_sale_attributed_at": { + "name": "IDX_impact_attribution_touches_sale_attributed_at", + "columns": [ + { + "expression": "sale_attributed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_attribution_touches_user_id_kilocode_users_id_fk": { + "name": "impact_attribution_touches_user_id_kilocode_users_id_fk", + "tableFrom": "impact_attribution_touches", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_attribution_touches_dedupe_key": { + "name": "UQ_impact_attribution_touches_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_attribution_touches_product_check": { + "name": "impact_attribution_touches_product_check", + "value": "\"impact_attribution_touches\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_program_key_check": { + "name": "impact_attribution_touches_program_key_check", + "value": "\"impact_attribution_touches\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_touch_type_check": { + "name": "impact_attribution_touches_touch_type_check", + "value": "\"impact_attribution_touches\".\"touch_type\" IN ('affiliate', 'referral')" + }, + "impact_attribution_touches_provider_check": { + "name": "impact_attribution_touches_provider_check", + "value": "\"impact_attribution_touches\".\"provider\" IN ('impact_performance', 'impact_advocate')" + }, + "impact_attribution_touches_tracking_value_length_non_negative_check": { + "name": "impact_attribution_touches_tracking_value_length_non_negative_check", + "value": "\"impact_attribution_touches\".\"tracking_value_length\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_conversion_reports": { + "name": "impact_conversion_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_tracker_id": { + "name": "action_tracker_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order_id": { + "name": "order_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_conversion_reports_conversion_id": { + "name": "IDX_impact_conversion_reports_conversion_id", + "columns": [ + { + "expression": "conversion_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_conversion_reports_state": { + "name": "IDX_impact_conversion_reports_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_conversion_reports", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_conversion_reports_dedupe_key": { + "name": "UQ_impact_conversion_reports_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_conversion_reports_state_check": { + "name": "impact_conversion_reports_state_check", + "value": "\"impact_conversion_reports\".\"state\" IN ('queued', 'retrying', 'delivered', 'failed')" + }, + "impact_conversion_reports_attempt_count_non_negative_check": { + "name": "impact_conversion_reports_attempt_count_non_negative_check", + "value": "\"impact_conversion_reports\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_conversions": { + "name": "impact_referral_conversions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winning_touch_type": { + "name": "winning_touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credits'" + }, + "source_payment_id": { + "name": "source_payment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qualified": { + "name": "qualified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disqualification_reason": { + "name": "disqualification_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "converted_at": { + "name": "converted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_conversions_referee_user_id": { + "name": "IDX_impact_referral_conversions_referee_user_id", + "columns": [ + { + "expression": "referee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_conversions_referrer_user_id": { + "name": "IDX_impact_referral_conversions_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_conversions_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_conversions_product_payment_source": { + "name": "UQ_impact_referral_conversions_product_payment_source", + "nullsNotDistinct": false, + "columns": [ + "product", + "payment_provider", + "source_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_conversions_product_check": { + "name": "impact_referral_conversions_product_check", + "value": "\"impact_referral_conversions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_conversions_winning_touch_type_check": { + "name": "impact_referral_conversions_winning_touch_type_check", + "value": "\"impact_referral_conversions\".\"winning_touch_type\" IN ('referral', 'affiliate', 'none')" + }, + "impact_referral_conversions_payment_provider_check": { + "name": "impact_referral_conversions_payment_provider_check", + "value": "\"impact_referral_conversions\".\"payment_provider\" IN ('stripe', 'credits', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_applications": { + "name": "impact_referral_reward_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "previous_renewal_boundary": { + "name": "previous_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "new_renewal_boundary": { + "name": "new_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "local_operation_id": { + "name": "local_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_operation_id": { + "name": "stripe_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_idempotency_key": { + "name": "stripe_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_applications_reward_id": { + "name": "IDX_impact_referral_reward_applications_reward_id", + "columns": [ + { + "expression": "reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_reward_applications_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_applications_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_applications_product_check": { + "name": "impact_referral_reward_applications_product_check", + "value": "\"impact_referral_reward_applications\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_decisions": { + "name": "impact_referral_reward_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_decisions_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_decisions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_reward_decisions_conversion_role": { + "name": "UQ_impact_referral_reward_decisions_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_decisions_product_check": { + "name": "impact_referral_reward_decisions_product_check", + "value": "\"impact_referral_reward_decisions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_reward_decisions_beneficiary_role_check": { + "name": "impact_referral_reward_decisions_beneficiary_role_check", + "value": "\"impact_referral_reward_decisions\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_reward_decisions_outcome_check": { + "name": "impact_referral_reward_decisions_outcome_check", + "value": "\"impact_referral_reward_decisions\".\"outcome\" IN ('granted', 'cap_limited', 'disqualified')" + }, + "impact_referral_reward_decisions_reward_kind_check": { + "name": "impact_referral_reward_decisions_reward_kind_check", + "value": "\"impact_referral_reward_decisions\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_reward_decisions_months_granted_non_negative_check": { + "name": "impact_referral_reward_decisions_months_granted_non_negative_check", + "value": "\"impact_referral_reward_decisions\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_rewards": { + "name": "impact_referral_rewards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applies_to_subscription_id": { + "name": "applies_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applies_to_kilo_pass_subscription_id": { + "name": "applies_to_kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_id": { + "name": "consumed_kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_item_id": { + "name": "consumed_kilo_pass_issuance_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "earned_at": { + "name": "earned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reversed_at": { + "name": "reversed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_reason": { + "name": "review_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_rewards_beneficiary_user_id": { + "name": "IDX_impact_referral_rewards_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_rewards_status": { + "name": "IDX_impact_referral_rewards_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk": { + "name": "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_reward_decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_subscription": { + "name": "FK_impact_referral_rewards_kilo_pass_subscription", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "applies_to_kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "consumed_kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance_item": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance_item", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuance_items", + "columnsFrom": [ + "consumed_kilo_pass_issuance_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_rewards_conversion_role": { + "name": "UQ_impact_referral_rewards_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + }, + "UQ_impact_referral_rewards_decision_id": { + "name": "UQ_impact_referral_rewards_decision_id", + "nullsNotDistinct": false, + "columns": [ + "decision_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_rewards_product_check": { + "name": "impact_referral_rewards_product_check", + "value": "\"impact_referral_rewards\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_rewards_beneficiary_role_check": { + "name": "impact_referral_rewards_beneficiary_role_check", + "value": "\"impact_referral_rewards\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_rewards_reward_kind_check": { + "name": "impact_referral_rewards_reward_kind_check", + "value": "\"impact_referral_rewards\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_rewards_status_check": { + "name": "impact_referral_rewards_status_check", + "value": "\"impact_referral_rewards\".\"status\" IN ('pending', 'earned', 'applied', 'reversed', 'expired', 'canceled', 'review_required')" + }, + "impact_referral_rewards_months_granted_non_negative_check": { + "name": "impact_referral_rewards_months_granted_non_negative_check", + "value": "\"impact_referral_rewards\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referrals": { + "name": "impact_referrals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "impact_referral_id": { + "name": "impact_referral_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referrals_referrer_user_id": { + "name": "IDX_impact_referrals_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referrals_source_touch_id": { + "name": "IDX_impact_referrals_source_touch_id", + "columns": [ + { + "expression": "source_touch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referrals_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referrals_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referrals_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referrals_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referrals_product_referee_user_id": { + "name": "UQ_impact_referrals_product_referee_user_id", + "nullsNotDistinct": false, + "columns": [ + "product", + "referee_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referrals_product_check": { + "name": "impact_referrals_product_check", + "value": "\"impact_referrals\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.ja4_digest": { + "name": "ja4_digest", + "schema": "", + "columns": { + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "ja4_digest": { + "name": "ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_ja4_digest": { + "name": "UQ_ja4_digest", + "columns": [ + { + "expression": "ja4_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_audit_log": { + "name": "kilo_pass_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_credit_transaction_id": { + "name": "related_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_monthly_issuance_id": { + "name": "related_monthly_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_kilo_pass_audit_log_created_at": { + "name": "IDX_kilo_pass_audit_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_user_id": { + "name": "IDX_kilo_pass_audit_log_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_pass_subscription_id": { + "name": "IDX_kilo_pass_audit_log_kilo_pass_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_action": { + "name": "IDX_kilo_pass_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_result": { + "name": "IDX_kilo_pass_audit_log_result", + "columns": [ + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_idempotency_key": { + "name": "IDX_kilo_pass_audit_log_idempotency_key", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_event_id": { + "name": "IDX_kilo_pass_audit_log_stripe_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_invoice_id": { + "name": "IDX_kilo_pass_audit_log_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_subscription_id": { + "name": "IDX_kilo_pass_audit_log_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_credit_transaction_id": { + "name": "IDX_kilo_pass_audit_log_related_credit_transaction_id", + "columns": [ + { + "expression": "related_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_monthly_issuance_id": { + "name": "IDX_kilo_pass_audit_log_related_monthly_issuance_id", + "columns": [ + { + "expression": "related_monthly_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "credit_transactions", + "columnsFrom": [ + "related_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "related_monthly_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_audit_log_action_check": { + "name": "kilo_pass_audit_log_action_check", + "value": "\"kilo_pass_audit_log\".\"action\" IN ('stripe_webhook_received', 'kilo_pass_invoice_paid_handled', 'store_purchase_completed', 'store_notification_received', 'store_subscription_renewed', 'store_subscription_canceled', 'store_subscription_expired', 'store_subscription_refunded', 'base_credits_issued', 'bonus_credits_issued', 'bonus_credits_skipped_idempotent', 'first_month_50pct_promo_issued', 'yearly_monthly_base_cron_started', 'yearly_monthly_base_cron_completed', 'issue_yearly_remaining_credits', 'duplicate_card_subscription_canceled', 'yearly_monthly_bonus_cron_started', 'yearly_monthly_bonus_cron_completed')" + }, + "kilo_pass_audit_log_result_check": { + "name": "kilo_pass_audit_log_result_check", + "value": "\"kilo_pass_audit_log\".\"result\" IN ('success', 'skipped_idempotent', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuance_items": { + "name": "kilo_pass_issuance_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_issuance_id": { + "name": "kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "bonus_percent_applied": { + "name": "bonus_percent_applied", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_issuance_items_issuance_id": { + "name": "IDX_kilo_pass_issuance_items_issuance_id", + "columns": [ + { + "expression": "kilo_pass_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuance_items_credit_transaction_id": { + "name": "IDX_kilo_pass_issuance_items_credit_transaction_id", + "columns": [ + { + "expression": "credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_issuance_items_credit_transaction_id_unique": { + "name": "kilo_pass_issuance_items_credit_transaction_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credit_transaction_id" + ] + }, + "UQ_kilo_pass_issuance_items_issuance_kind": { + "name": "UQ_kilo_pass_issuance_items_issuance_kind", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_issuance_id", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuance_items_bonus_percent_applied_range_check": { + "name": "kilo_pass_issuance_items_bonus_percent_applied_range_check", + "value": "\"kilo_pass_issuance_items\".\"bonus_percent_applied\" IS NULL OR (\"kilo_pass_issuance_items\".\"bonus_percent_applied\" >= 0 AND \"kilo_pass_issuance_items\".\"bonus_percent_applied\" <= 1)" + }, + "kilo_pass_issuance_items_amount_usd_non_negative_check": { + "name": "kilo_pass_issuance_items_amount_usd_non_negative_check", + "value": "\"kilo_pass_issuance_items\".\"amount_usd\" >= 0" + }, + "kilo_pass_issuance_items_kind_check": { + "name": "kilo_pass_issuance_items_kind_check", + "value": "\"kilo_pass_issuance_items\".\"kind\" IN ('base', 'bonus', 'promo_first_month_50pct', 'referral_bonus')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuances": { + "name": "kilo_pass_issuances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_month": { + "name": "issue_month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initial_welcome_promo_eligibility_reason": { + "name": "initial_welcome_promo_eligibility_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_issuances_stripe_invoice_id": { + "name": "UQ_kilo_pass_issuances_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_issuances\".\"stripe_invoice_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_subscription_id": { + "name": "IDX_kilo_pass_issuances_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_issue_month": { + "name": "IDX_kilo_pass_issuances_issue_month", + "columns": [ + { + "expression": "issue_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_issuances", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_issuances_subscription_issue_month": { + "name": "UQ_kilo_pass_issuances_subscription_issue_month", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_subscription_id", + "issue_month" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuances_issue_month_day_one_check": { + "name": "kilo_pass_issuances_issue_month_day_one_check", + "value": "EXTRACT(DAY FROM \"kilo_pass_issuances\".\"issue_month\") = 1" + }, + "kilo_pass_issuances_source_check": { + "name": "kilo_pass_issuances_source_check", + "value": "\"kilo_pass_issuances\".\"source\" IN ('stripe_invoice', 'app_store_transaction', 'google_play_transaction', 'cron')" + }, + "kilo_pass_issuances_initial_welcome_promo_reason_check": { + "name": "kilo_pass_issuances_initial_welcome_promo_reason_check", + "value": "\"kilo_pass_issuances\".\"initial_welcome_promo_eligibility_reason\" IN ('first_payment_fingerprint_claim', 'fingerprint_previously_claimed', 'missing_fingerprint', 'no_supported_fingerprint', 'no_positive_settlement', 'settlement_unresolved')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_agreements": { + "name": "kilo_pass_org_agreements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "term_version_id": { + "name": "term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processing_condition": { + "name": "processing_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "purchase_channel": { + "name": "purchase_channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_pass_capacity": { + "name": "purchased_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "next_purchased_pass_capacity": { + "name": "next_purchased_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_capacity_effective_at": { + "name": "next_capacity_effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_from": { + "name": "paid_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_until": { + "name": "paid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issuance_anchor_at": { + "name": "issuance_anchor_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_seat_add_on_item_id": { + "name": "provider_seat_add_on_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activation_provider_event_id": { + "name": "activation_provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_contract_id": { + "name": "external_contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_review_required_at": { + "name": "payment_review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_effective_at": { + "name": "cancellation_effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manually_issued_through": { + "name": "manually_issued_through", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_agreements_one_non_ended_parent": { + "name": "UQ_kilo_pass_org_agreements_one_non_ended_parent", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_provider_subscription": { + "name": "UQ_kilo_pass_org_agreements_provider_subscription", + "columns": [ + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"provider_subscription_id\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_provider_seat_add_on_item": { + "name": "UQ_kilo_pass_org_agreements_provider_seat_add_on_item", + "columns": [ + { + "expression": "provider_seat_add_on_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"provider_seat_add_on_item_id\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_external_contract": { + "name": "UQ_kilo_pass_org_agreements_external_contract", + "columns": [ + { + "expression": "external_contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"external_contract_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_activation_provider_event": { + "name": "UQ_kilo_pass_org_agreements_activation_provider_event", + "columns": [ + { + "expression": "activation_provider_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"activation_provider_event_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_agreements_processing": { + "name": "IDX_kilo_pass_org_agreements_processing", + "columns": [ + { + "expression": "processing_condition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_agreements_parent_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_agreements_parent_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_agreements", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_agreements_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_agreements_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_agreements", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_agreements_purchased_capacity_non_negative_check": { + "name": "kilo_pass_org_agreements_purchased_capacity_non_negative_check", + "value": "\"kilo_pass_org_agreements\".\"purchased_pass_capacity\" >= 0" + }, + "kilo_pass_org_agreements_next_capacity_check": { + "name": "kilo_pass_org_agreements_next_capacity_check", + "value": "(\"kilo_pass_org_agreements\".\"next_purchased_pass_capacity\" IS NULL AND \"kilo_pass_org_agreements\".\"next_capacity_effective_at\" IS NULL) OR (\"kilo_pass_org_agreements\".\"next_purchased_pass_capacity\" >= 0 AND \"kilo_pass_org_agreements\".\"next_capacity_effective_at\" IS NOT NULL)" + }, + "kilo_pass_org_agreements_paid_interval_check": { + "name": "kilo_pass_org_agreements_paid_interval_check", + "value": "(\"kilo_pass_org_agreements\".\"paid_from\" IS NULL AND \"kilo_pass_org_agreements\".\"paid_until\" IS NULL) OR (\"kilo_pass_org_agreements\".\"paid_from\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"paid_until\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"paid_from\" < \"kilo_pass_org_agreements\".\"paid_until\")" + }, + "kilo_pass_org_agreements_state_check": { + "name": "kilo_pass_org_agreements_state_check", + "value": "\"kilo_pass_org_agreements\".\"state\" IN ('pending_payment', 'active', 'cancel_at_period_end', 'ended')" + }, + "kilo_pass_org_agreements_processing_condition_check": { + "name": "kilo_pass_org_agreements_processing_condition_check", + "value": "\"kilo_pass_org_agreements\".\"processing_condition\" IN ('ready', 'manual', 'blocked', 'overallocated', 'failed', 'suspended_for_review')" + }, + "kilo_pass_org_agreements_purchase_channel_check": { + "name": "kilo_pass_org_agreements_purchase_channel_check", + "value": "\"kilo_pass_org_agreements\".\"purchase_channel\" IN ('self_serve', 'manual')" + }, + "kilo_pass_org_agreements_cadence_check": { + "name": "kilo_pass_org_agreements_cadence_check", + "value": "\"kilo_pass_org_agreements\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_allocation_plan_rows": { + "name": "kilo_pass_org_allocation_plan_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "allocation_plan_id": { + "name": "allocation_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pass_capacity": { + "name": "pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_allocation_plan_rows_positive_container": { + "name": "IDX_kilo_pass_org_allocation_plan_rows_positive_container", + "columns": [ + { + "expression": "allocation_container_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kilo_pass_org_allocation_plan_rows\".\"pass_capacity\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_allocation_plan_rows_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk": { + "name": "kilo_pass_org_allocation_plan_rows_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk", + "tableFrom": "kilo_pass_org_allocation_plan_rows", + "tableTo": "kilo_pass_org_allocation_plans", + "columnsFrom": [ + "allocation_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_allocation_plan_rows_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_allocation_plan_rows_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_allocation_plan_rows", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_allocation_plan_rows_plan_container": { + "name": "UQ_kilo_pass_org_allocation_plan_rows_plan_container", + "nullsNotDistinct": false, + "columns": [ + "allocation_plan_id", + "allocation_container_organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_allocation_plan_rows_capacity_non_negative_check": { + "name": "kilo_pass_org_allocation_plan_rows_capacity_non_negative_check", + "value": "\"kilo_pass_org_allocation_plan_rows\".\"pass_capacity\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_allocation_plans": { + "name": "kilo_pass_org_allocation_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effective_window_start": { + "name": "effective_window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_allocation_plans_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_allocation_plans_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_allocation_plans", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_allocation_plans_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_allocation_plans_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_allocation_plans", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_allocation_plans_agreement_window": { + "name": "UQ_kilo_pass_org_allocation_plans_agreement_window", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "effective_window_start" + ] + }, + "UQ_kilo_pass_org_allocation_plans_agreement_version": { + "name": "UQ_kilo_pass_org_allocation_plans_agreement_version", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "version" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_allocation_plans_version_positive_check": { + "name": "kilo_pass_org_allocation_plans_version_positive_check", + "value": "\"kilo_pass_org_allocation_plans\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_audit_records": { + "name": "kilo_pass_org_audit_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_json": { + "name": "before_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_json": { + "name": "after_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_audit_records_idempotency": { + "name": "UQ_kilo_pass_org_audit_records_idempotency", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_audit_records\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_audit_records_agreement_created": { + "name": "IDX_kilo_pass_org_audit_records_agreement_created", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_audit_records_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_audit_records_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_audit_records", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_audit_records_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_audit_records_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_audit_records", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_org_issuance_snapshots": { + "name": "kilo_pass_org_issuance_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "allocation_plan_id": { + "name": "allocation_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "term_version_id": { + "name": "term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "qualifying_spend_starts_at": { + "name": "qualifying_spend_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tranche_key": { + "name": "tranche_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allocated_pass_capacity": { + "name": "allocated_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "base_credit_microdollars": { + "name": "base_credit_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_credit_microdollars": { + "name": "bonus_credit_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unlock_spend_microdollars": { + "name": "unlock_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "qualifying_spend_microdollars": { + "name": "qualifying_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bonus_mode": { + "name": "bonus_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bonus_unlocked_at": { + "name": "bonus_unlocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "repair_completed_at": { + "name": "repair_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_transaction_id": { + "name": "bonus_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "base_credit_transaction_id": { + "name": "base_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_issuance_snapshots_base_credit_transaction": { + "name": "UQ_kilo_pass_org_issuance_snapshots_base_credit_transaction", + "columns": [ + { + "expression": "base_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_issuance_snapshots\".\"base_credit_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_issuance_snapshots_bonus_credit_transaction": { + "name": "UQ_kilo_pass_org_issuance_snapshots_bonus_credit_transaction", + "columns": [ + { + "expression": "bonus_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_issuance_snapshots\".\"bonus_credit_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_issuance_snapshots_window": { + "name": "IDX_kilo_pass_org_issuance_snapshots_window", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_issuance_snapshots_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_processing_run_id_kilo_pass_org_processing_runs_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_processing_run_id_kilo_pass_org_processing_runs_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_processing_runs", + "columnsFrom": [ + "processing_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_allocation_plans", + "columnsFrom": [ + "allocation_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_bonus_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_bonus_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "credit_transactions", + "columnsFrom": [ + "bonus_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_base_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_base_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "credit_transactions", + "columnsFrom": [ + "base_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_issuance_snapshots_container_window_tranche": { + "name": "UQ_kilo_pass_org_issuance_snapshots_container_window_tranche", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "allocation_container_organization_id", + "window_start", + "tranche_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_issuance_snapshots_window_check": { + "name": "kilo_pass_org_issuance_snapshots_window_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"window_start\" < \"kilo_pass_org_issuance_snapshots\".\"window_end\"" + }, + "kilo_pass_org_issuance_snapshots_qualifying_spend_window_check": { + "name": "kilo_pass_org_issuance_snapshots_qualifying_spend_window_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"window_start\" <= \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_starts_at\" AND \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_starts_at\" < \"kilo_pass_org_issuance_snapshots\".\"window_end\"" + }, + "kilo_pass_org_issuance_snapshots_values_non_negative_check": { + "name": "kilo_pass_org_issuance_snapshots_values_non_negative_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"allocated_pass_capacity\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"base_credit_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"bonus_credit_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"unlock_spend_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_microdollars\" >= 0" + }, + "kilo_pass_org_issuance_snapshots_kind_check": { + "name": "kilo_pass_org_issuance_snapshots_kind_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"kind\" IN ('regular', 'bridge', 'supplement')" + }, + "kilo_pass_org_issuance_snapshots_bonus_mode_check": { + "name": "kilo_pass_org_issuance_snapshots_bonus_mode_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"bonus_mode\" IN ('after_base', 'upfront')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_notification_deliveries": { + "name": "kilo_pass_org_notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_kilo_user_id": { + "name": "recipient_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_notification_deliveries_status": { + "name": "IDX_kilo_pass_org_notification_deliveries_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_notification_deliveries_processing_run_id_kilo_pass_org_processing_runs_id_fk": { + "name": "kilo_pass_org_notification_deliveries_processing_run_id_kilo_pass_org_processing_runs_id_fk", + "tableFrom": "kilo_pass_org_notification_deliveries", + "tableTo": "kilo_pass_org_processing_runs", + "columnsFrom": [ + "processing_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_notification_deliveries_recipient_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_notification_deliveries_recipient_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_notification_deliveries", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_notification_deliveries_run_recipient": { + "name": "UQ_kilo_pass_org_notification_deliveries_run_recipient", + "nullsNotDistinct": false, + "columns": [ + "processing_run_id", + "recipient_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_notification_deliveries_status_check": { + "name": "kilo_pass_org_notification_deliveries_status_check", + "value": "\"kilo_pass_org_notification_deliveries\".\"status\" IN ('pending', 'sending', 'sent', 'failed')" + }, + "kilo_pass_org_notification_deliveries_attempt_count_check": { + "name": "kilo_pass_org_notification_deliveries_attempt_count_check", + "value": "\"kilo_pass_org_notification_deliveries\".\"attempt_count\" >= 0" + }, + "kilo_pass_org_notification_deliveries_sent_check": { + "name": "kilo_pass_org_notification_deliveries_sent_check", + "value": "(\"kilo_pass_org_notification_deliveries\".\"status\" = 'sent' AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NOT NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NULL) OR (\"kilo_pass_org_notification_deliveries\".\"status\" = 'sending' AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NOT NULL) OR (\"kilo_pass_org_notification_deliveries\".\"status\" IN ('pending', 'failed') AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_processing_runs": { + "name": "kilo_pass_org_processing_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_processing_runs_state_lease": { + "name": "IDX_kilo_pass_org_processing_runs_state_lease", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_processing_runs_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_processing_runs_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_processing_runs", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_processing_runs_agreement_window": { + "name": "UQ_kilo_pass_org_processing_runs_agreement_window", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "window_start" + ] + }, + "UQ_kilo_pass_org_processing_runs_idempotency": { + "name": "UQ_kilo_pass_org_processing_runs_idempotency", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_processing_runs_window_check": { + "name": "kilo_pass_org_processing_runs_window_check", + "value": "\"kilo_pass_org_processing_runs\".\"window_start\" < \"kilo_pass_org_processing_runs\".\"window_end\"" + }, + "kilo_pass_org_processing_runs_attempt_count_non_negative_check": { + "name": "kilo_pass_org_processing_runs_attempt_count_non_negative_check", + "value": "\"kilo_pass_org_processing_runs\".\"attempt_count\" >= 0" + }, + "kilo_pass_org_processing_runs_state_check": { + "name": "kilo_pass_org_processing_runs_state_check", + "value": "\"kilo_pass_org_processing_runs\".\"state\" IN ('pending', 'running', 'succeeded', 'blocked', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_qualifying_spend_events": { + "name": "kilo_pass_org_qualifying_spend_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "issuance_snapshot_id": { + "name": "issuance_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spent_microdollars": { + "name": "spent_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_qualifying_spend_events_snapshot_occurred": { + "name": "IDX_kilo_pass_org_qualifying_spend_events_snapshot_occurred", + "columns": [ + { + "expression": "issuance_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_qualifying_spend_events_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "kilo_pass_org_issuance_snapshots", + "columnsFrom": [ + "issuance_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_qualifying_spend_events_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_qualifying_spend_events_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_qualifying_spend_events_snapshot_credit_transaction": { + "name": "UQ_kilo_pass_org_qualifying_spend_events_snapshot_credit_transaction", + "nullsNotDistinct": false, + "columns": [ + "issuance_snapshot_id", + "credit_transaction_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_qualifying_spend_events_amount_positive_check": { + "name": "kilo_pass_org_qualifying_spend_events_amount_positive_check", + "value": "\"kilo_pass_org_qualifying_spend_events\".\"spent_microdollars\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_supplements": { + "name": "kilo_pass_org_supplements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "issuance_snapshot_id": { + "name": "issuance_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_invoice_line_id": { + "name": "provider_invoice_line_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remaining_service_numerator": { + "name": "remaining_service_numerator", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "remaining_service_denominator": { + "name": "remaining_service_denominator", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_supplements_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk": { + "name": "kilo_pass_org_supplements_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk", + "tableFrom": "kilo_pass_org_supplements", + "tableTo": "kilo_pass_org_issuance_snapshots", + "columnsFrom": [ + "issuance_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_supplements_provider_invoice_line": { + "name": "UQ_kilo_pass_org_supplements_provider_invoice_line", + "nullsNotDistinct": false, + "columns": [ + "provider_invoice_line_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_supplements_ratio_check": { + "name": "kilo_pass_org_supplements_ratio_check", + "value": "\"kilo_pass_org_supplements\".\"remaining_service_numerator\" > 0 AND \"kilo_pass_org_supplements\".\"remaining_service_denominator\" > 0 AND \"kilo_pass_org_supplements\".\"remaining_service_numerator\" <= \"kilo_pass_org_supplements\".\"remaining_service_denominator\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_term_transitions": { + "name": "kilo_pass_org_term_transitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_term_version_id": { + "name": "from_term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_term_version_id": { + "name": "to_term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_term_transitions_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_term_transitions_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_from_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_term_transitions_from_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "from_term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_to_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_term_transitions_to_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "to_term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_term_transitions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_term_transitions_agreement_effective": { + "name": "UQ_kilo_pass_org_term_transitions_agreement_effective", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "effective_at" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_term_transitions_changes_version_check": { + "name": "kilo_pass_org_term_transitions_changes_version_check", + "value": "\"kilo_pass_org_term_transitions\".\"from_term_version_id\" <> \"kilo_pass_org_term_transitions\".\"to_term_version_id\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_term_versions": { + "name": "kilo_pass_org_term_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "version_key": { + "name": "version_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_microdollars_per_pass": { + "name": "billing_price_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "base_credit_microdollars_per_pass": { + "name": "base_credit_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_credit_microdollars_per_pass": { + "name": "bonus_credit_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unlock_spend_microdollars_per_pass": { + "name": "unlock_spend_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_mode": { + "name": "bonus_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_term_versions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_term_versions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_term_versions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_term_versions_version_key": { + "name": "UQ_kilo_pass_org_term_versions_version_key", + "nullsNotDistinct": false, + "columns": [ + "version_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_term_versions_amounts_non_negative_check": { + "name": "kilo_pass_org_term_versions_amounts_non_negative_check", + "value": "\"kilo_pass_org_term_versions\".\"billing_price_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"base_credit_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"bonus_credit_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"unlock_spend_microdollars_per_pass\" >= 0" + }, + "kilo_pass_org_term_versions_tier_check": { + "name": "kilo_pass_org_term_versions_tier_check", + "value": "\"kilo_pass_org_term_versions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_org_term_versions_cadence_check": { + "name": "kilo_pass_org_term_versions_cadence_check", + "value": "\"kilo_pass_org_term_versions\".\"cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_org_term_versions_bonus_mode_check": { + "name": "kilo_pass_org_term_versions_bonus_mode_check", + "value": "\"kilo_pass_org_term_versions\".\"bonus_mode\" IN ('after_base', 'upfront')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_pause_events": { + "name": "kilo_pass_pause_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resumes_at": { + "name": "resumes_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resumed_at": { + "name": "resumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_pause_events_subscription_id": { + "name": "IDX_kilo_pass_pause_events_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_pause_events_one_open_per_sub": { + "name": "UQ_kilo_pass_pause_events_one_open_per_sub", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_pause_events", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_pause_events_resumed_at_after_paused_at_check": { + "name": "kilo_pass_pause_events_resumed_at_after_paused_at_check", + "value": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL OR \"kilo_pass_pause_events\".\"resumed_at\" >= \"kilo_pass_pause_events\".\"paused_at\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_scheduled_changes": { + "name": "kilo_pass_scheduled_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_tier": { + "name": "from_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_cadence": { + "name": "from_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_tier": { + "name": "to_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_cadence": { + "name": "to_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_scheduled_changes_kilo_user_id": { + "name": "IDX_kilo_pass_scheduled_changes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_status": { + "name": "IDX_kilo_pass_scheduled_changes_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_stripe_subscription_id": { + "name": "IDX_kilo_pass_scheduled_changes_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id": { + "name": "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_scheduled_changes\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_effective_at": { + "name": "IDX_kilo_pass_scheduled_changes_effective_at", + "columns": [ + { + "expression": "effective_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_deleted_at": { + "name": "IDX_kilo_pass_scheduled_changes_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk": { + "name": "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "stripe_subscription_id" + ], + "columnsTo": [ + "stripe_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_scheduled_changes_from_tier_check": { + "name": "kilo_pass_scheduled_changes_from_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_from_cadence_check": { + "name": "kilo_pass_scheduled_changes_from_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_to_tier_check": { + "name": "kilo_pass_scheduled_changes_to_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_to_cadence_check": { + "name": "kilo_pass_scheduled_changes_to_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_status_check": { + "name": "kilo_pass_scheduled_changes_status_check", + "value": "\"kilo_pass_scheduled_changes\".\"status\" IN ('not_started', 'active', 'completed', 'released', 'canceled')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_events": { + "name": "kilo_pass_store_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_store_events_provider_event": { + "name": "UQ_kilo_pass_store_events_provider_event", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_provider_subscription": { + "name": "IDX_kilo_pass_store_events_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_app_account_token": { + "name": "IDX_kilo_pass_store_events_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_events_payment_provider_check": { + "name": "kilo_pass_store_events_payment_provider_check", + "value": "\"kilo_pass_store_events\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_purchases": { + "name": "kilo_pass_store_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_original_transaction_id": { + "name": "provider_original_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purchase_token": { + "name": "purchase_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_at": { + "name": "purchased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "raw_payload_json": { + "name": "raw_payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_store_purchases_provider_transaction": { + "name": "UQ_kilo_pass_store_purchases_provider_transaction", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_subscription_id": { + "name": "IDX_kilo_pass_store_purchases_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_user_id": { + "name": "IDX_kilo_pass_store_purchases_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_app_account_token": { + "name": "IDX_kilo_pass_store_purchases_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_latest_subscription_purchase": { + "name": "IDX_kilo_pass_store_purchases_latest_subscription_purchase", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchased_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_kilo_pass_store_purchases_subscription_owner_provider": { + "name": "FK_kilo_pass_store_purchases_subscription_owner_provider", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "columnsTo": [ + "id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_purchases_store_provider_check": { + "name": "kilo_pass_store_purchases_store_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('app_store', 'google_play')" + }, + "kilo_pass_store_purchases_payment_provider_check": { + "name": "kilo_pass_store_purchases_payment_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_subscriptions": { + "name": "kilo_pass_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stripe'" + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_streak_months": { + "name": "current_streak_months", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_yearly_issue_at": { + "name": "next_yearly_issue_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_subscriptions_kilo_user_id": { + "name": "IDX_kilo_pass_subscriptions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_payment_provider": { + "name": "IDX_kilo_pass_subscriptions_payment_provider", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_status": { + "name": "IDX_kilo_pass_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_cadence": { + "name": "IDX_kilo_pass_subscriptions_cadence", + "columns": [ + { + "expression": "cadence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_provider_subscription": { + "name": "UQ_kilo_pass_subscriptions_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_store_purchase_reference": { + "name": "UQ_kilo_pass_subscriptions_store_purchase_reference", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_subscriptions_stripe_subscription_id_unique": { + "name": "kilo_pass_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_subscriptions_current_streak_months_non_negative_check": { + "name": "kilo_pass_subscriptions_current_streak_months_non_negative_check", + "value": "\"kilo_pass_subscriptions\".\"current_streak_months\" >= 0" + }, + "kilo_pass_subscriptions_provider_ids_check": { + "name": "kilo_pass_subscriptions_provider_ids_check", + "value": "(\n \"kilo_pass_subscriptions\".\"payment_provider\" = 'stripe'\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" = \"kilo_pass_subscriptions\".\"stripe_subscription_id\"\n ) OR (\n \"kilo_pass_subscriptions\".\"payment_provider\" IN ('app_store', 'google_play')\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NULL\n )" + }, + "kilo_pass_subscriptions_payment_provider_check": { + "name": "kilo_pass_subscriptions_payment_provider_check", + "value": "\"kilo_pass_subscriptions\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + }, + "kilo_pass_subscriptions_tier_check": { + "name": "kilo_pass_subscriptions_tier_check", + "value": "\"kilo_pass_subscriptions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_subscriptions_cadence_check": { + "name": "kilo_pass_subscriptions_cadence_check", + "value": "\"kilo_pass_subscriptions\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_welcome_promo_payment_fingerprint_claims": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims", + "schema": "", + "columns": { + "stripe_payment_method_type": { + "name": "stripe_payment_method_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_stripe_invoice_id": { + "name": "source_stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk", + "columns": [ + "stripe_payment_method_type", + "stripe_fingerprint" + ] + } + }, + "uniqueConstraints": { + "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id": { + "name": "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id", + "nullsNotDistinct": false, + "columns": [ + "source_stripe_invoice_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check", + "value": "\"kilo_pass_welcome_promo_payment_fingerprint_claims\".\"stripe_payment_method_type\" IN ('card', 'sepa_debit', 'us_bank_account', 'bacs_debit', 'au_becs_debit')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_access_codes": { + "name": "kiloclaw_access_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_access_codes_code": { + "name": "UQ_kiloclaw_access_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_access_codes_user_status": { + "name": "IDX_kiloclaw_access_codes_user_status", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_access_codes_one_active_per_user": { + "name": "UQ_kiloclaw_access_codes_one_active_per_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_access_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_admin_audit_logs": { + "name": "kiloclaw_admin_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_admin_audit_logs_target_user_id": { + "name": "IDX_kiloclaw_admin_audit_logs_target_user_id", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_action": { + "name": "IDX_kiloclaw_admin_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_created_at": { + "name": "IDX_kiloclaw_admin_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_cli_runs": { + "name": "kiloclaw_cli_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "initiated_by_admin_id": { + "name": "initiated_by_admin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_cli_runs_user_id": { + "name": "IDX_kiloclaw_cli_runs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_started_at": { + "name": "IDX_kiloclaw_cli_runs_started_at", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_instance_id": { + "name": "IDX_kiloclaw_cli_runs_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_cli_runs_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "initiated_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_earlybird_purchases": { + "name": "kiloclaw_earlybird_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manual_payment_id": { + "name": "manual_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_earlybird_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_earlybird_purchases_user_id_unique": { + "name": "kiloclaw_earlybird_purchases_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "kiloclaw_earlybird_purchases_stripe_charge_id_unique": { + "name": "kiloclaw_earlybird_purchases_stripe_charge_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_charge_id" + ] + }, + "kiloclaw_earlybird_purchases_manual_payment_id_unique": { + "name": "kiloclaw_earlybird_purchases_manual_payment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "manual_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_email_log": { + "name": "kiloclaw_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "'epoch'" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_email_log_user_type_global": { + "name": "UQ_kiloclaw_email_log_user_type_global", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_email_log_user_instance_type_period": { + "name": "UQ_kiloclaw_email_log_user_instance_type_period", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_email_log_type_sent_instance": { + "name": "IDX_kiloclaw_email_log_type_sent_instance", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_email_log_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_google_oauth_connections": { + "name": "kiloclaw_google_oauth_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'google'" + }, + "account_email": { + "name": "account_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_subject": { + "name": "account_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_profile": { + "name": "credential_profile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kilo_owned'" + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "grants_by_source": { + "name": "grants_by_source", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_google_oauth_connections_instance": { + "name": "UQ_kiloclaw_google_oauth_connections_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_status": { + "name": "IDX_kiloclaw_google_oauth_connections_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_provider": { + "name": "IDX_kiloclaw_google_oauth_connections_provider", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_google_oauth_connections", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_google_oauth_connections_status_check": { + "name": "kiloclaw_google_oauth_connections_status_check", + "value": "\"kiloclaw_google_oauth_connections\".\"status\" IN ('active', 'action_required', 'disconnected')" + }, + "kiloclaw_google_oauth_connections_credential_profile_check": { + "name": "kiloclaw_google_oauth_connections_credential_profile_check", + "value": "\"kiloclaw_google_oauth_connections\".\"credential_profile\" IN ('legacy', 'kilo_owned')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_image_catalog": { + "name": "kiloclaw_image_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_digest": { + "name": "image_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rollout_percent": { + "name": "rollout_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_latest": { + "name": "is_latest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kiloclaw_image_catalog_status": { + "name": "IDX_kiloclaw_image_catalog_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_image_catalog_variant": { + "name": "IDX_kiloclaw_image_catalog_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_latest_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_latest_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_candidate_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_candidate_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = false AND \"kiloclaw_image_catalog\".\"rollout_percent\" > 0 AND \"kiloclaw_image_catalog\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_image_catalog_image_tag_unique": { + "name": "kiloclaw_image_catalog_image_tag_unique", + "nullsNotDistinct": false, + "columns": [ + "image_tag" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_aliases": { + "name": "kiloclaw_inbound_email_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_inbound_email_aliases_instance_id": { + "name": "IDX_kiloclaw_inbound_email_aliases_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_inbound_email_aliases_active_instance": { + "name": "UQ_kiloclaw_inbound_email_aliases_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_inbound_email_aliases\".\"retired_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_inbound_email_aliases", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_reserved_aliases": { + "name": "kiloclaw_inbound_email_reserved_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_instances": { + "name": "kiloclaw_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fly'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbound_email_enabled": { + "name": "inbound_email_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inactive_trial_stopped_at": { + "name": "inactive_trial_stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracked_image_tag": { + "name": "tracked_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instance_type": { + "name": "instance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_size_override": { + "name": "admin_size_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_instances_active": { + "name": "UQ_kiloclaw_instances_active", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_personal_by_user": { + "name": "IDX_kiloclaw_instances_active_personal_by_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_user_org": { + "name": "IDX_kiloclaw_instances_active_org_by_user_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_org_created": { + "name": "IDX_kiloclaw_instances_active_org_by_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_user_id_created_at": { + "name": "IDX_kiloclaw_instances_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_tracked_image_tag": { + "name": "IDX_kiloclaw_instances_tracked_image_tag", + "columns": [ + { + "expression": "tracked_image_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_instance_type": { + "name": "IDX_kiloclaw_instances_instance_type", + "columns": [ + { + "expression": "instance_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_admin_size_override": { + "name": "IDX_kiloclaw_instances_admin_size_override", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"admin_size_override\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_instances_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_instances_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_instances_organization_id_organizations_id_fk": { + "name": "kiloclaw_instances_organization_id_organizations_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_kiloclaw_instances_instance_type": { + "name": "CHK_kiloclaw_instances_instance_type", + "value": "\"kiloclaw_instances\".\"instance_type\" IS NULL OR \"kiloclaw_instances\".\"instance_type\" IN ('perf-1-3', 'perf-4-8', 'perf-4-16', 'shared-2-3', 'shared-2-4', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_morning_briefing_configs": { + "name": "kiloclaw_morning_briefing_configs", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0 7 * * *'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "interest_topics": { + "name": "interest_topics", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_morning_briefing_configs_enabled": { + "name": "IDX_kiloclaw_morning_briefing_configs_enabled", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_morning_briefing_configs\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_morning_briefing_configs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_notifications": { + "name": "kiloclaw_scheduled_action_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'notice'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel": { + "name": "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_notifications_pending": { + "name": "IDX_kiloclaw_scheduled_action_notifications_pending", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk": { + "name": "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk", + "tableFrom": "kiloclaw_scheduled_action_notifications", + "tableTo": "kiloclaw_scheduled_action_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_stages": { + "name": "kiloclaw_scheduled_action_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_index": { + "name": "stage_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "notice_sent_at": { + "name": "notice_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_stages_parent_index": { + "name": "UQ_kiloclaw_scheduled_action_stages_parent_index", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_stages_notice_due": { + "name": "IDX_kiloclaw_scheduled_action_stages_notice_due", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_stages\".\"notice_sent_at\" IS NULL AND \"kiloclaw_scheduled_action_stages\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_stages", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_targets": { + "name": "kiloclaw_scheduled_action_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_image_tag": { + "name": "source_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_targets_parent_instance": { + "name": "UQ_kiloclaw_scheduled_action_targets_parent_instance", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_stage": { + "name": "IDX_kiloclaw_scheduled_action_targets_stage", + "columns": [ + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_pending_by_instance": { + "name": "IDX_kiloclaw_scheduled_action_targets_pending_by_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_targets\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk": { + "name": "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_action_stages", + "columnsFrom": [ + "stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_actions": { + "name": "kiloclaw_scheduled_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_pins": { + "name": "override_pins", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notice_lead_hours": { + "name": "notice_lead_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "notice_subject": { + "name": "notice_subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "notice_body": { + "name": "notice_body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_count": { + "name": "total_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "IDX_kiloclaw_scheduled_actions_status": { + "name": "IDX_kiloclaw_scheduled_actions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_action_type": { + "name": "IDX_kiloclaw_scheduled_actions_action_type", + "columns": [ + { + "expression": "action_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_created_by": { + "name": "IDX_kiloclaw_scheduled_actions_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "target_image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_subscription_change_log": { + "name": "kiloclaw_subscription_change_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_subscription_change_log_subscription_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_subscription_created_at", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscription_change_log_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscription_change_log", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscription_change_log_actor_type_check": { + "name": "kiloclaw_subscription_change_log_actor_type_check", + "value": "\"kiloclaw_subscription_change_log\".\"actor_type\" IN ('user', 'system')" + }, + "kiloclaw_subscription_change_log_action_check": { + "name": "kiloclaw_subscription_change_log_action_check", + "value": "\"kiloclaw_subscription_change_log\".\"action\" IN ('created', 'status_changed', 'plan_switched', 'period_advanced', 'canceled', 'reactivated', 'suspended', 'destruction_scheduled', 'reassigned', 'backfilled', 'payment_source_changed', 'schedule_changed', 'admin_override')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_subscriptions": { + "name": "kiloclaw_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transferred_to_subscription_id": { + "name": "transferred_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "access_origin": { + "name": "access_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_source": { + "name": "payment_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kiloclaw_price_version": { + "name": "kiloclaw_price_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_plan": { + "name": "scheduled_plan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_by": { + "name": "scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pending_conversion": { + "name": "pending_conversion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trial_started_at": { + "name": "trial_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "commit_ends_at": { + "name": "commit_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "destruction_deadline": { + "name": "destruction_deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_requested_at": { + "name": "auto_resume_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_retry_after": { + "name": "auto_resume_retry_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_attempt_count": { + "name": "auto_resume_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_top_up_triggered_for_period": { + "name": "auto_top_up_triggered_for_period", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_subscriptions_status": { + "name": "IDX_kiloclaw_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_id": { + "name": "IDX_kiloclaw_subscriptions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_status": { + "name": "IDX_kiloclaw_subscriptions_user_status", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_price_version": { + "name": "IDX_kiloclaw_subscriptions_price_version", + "columns": [ + { + "expression": "kiloclaw_price_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_transferred_to": { + "name": "IDX_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_stripe_schedule_id": { + "name": "IDX_kiloclaw_subscriptions_stripe_schedule_id", + "columns": [ + { + "expression": "stripe_schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_auto_resume_retry_after": { + "name": "IDX_kiloclaw_subscriptions_auto_resume_retry_after", + "columns": [ + { + "expression": "auto_resume_retry_after", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_instance": { + "name": "UQ_kiloclaw_subscriptions_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_transferred_to": { + "name": "UQ_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"transferred_to_subscription_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_earlybird_origin": { + "name": "IDX_kiloclaw_subscriptions_earlybird_origin", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "access_origin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_subscriptions\".\"access_origin\" = 'earlybird'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscriptions_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "transferred_to_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_subscriptions_stripe_subscription_id_unique": { + "name": "kiloclaw_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscriptions_price_version_check": { + "name": "kiloclaw_subscriptions_price_version_check", + "value": "\"kiloclaw_subscriptions\".\"kiloclaw_price_version\" IN ('2026-03-19', '2026-05-10')" + }, + "kiloclaw_subscriptions_plan_check": { + "name": "kiloclaw_subscriptions_plan_check", + "value": "\"kiloclaw_subscriptions\".\"plan\" IN ('trial', 'commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_plan_check": { + "name": "kiloclaw_subscriptions_scheduled_plan_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_plan\" IN ('commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_by_check": { + "name": "kiloclaw_subscriptions_scheduled_by_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_by\" IN ('auto', 'user')" + }, + "kiloclaw_subscriptions_status_check": { + "name": "kiloclaw_subscriptions_status_check", + "value": "\"kiloclaw_subscriptions\".\"status\" IN ('trialing', 'active', 'past_due', 'canceled', 'unpaid')" + }, + "kiloclaw_subscriptions_access_origin_check": { + "name": "kiloclaw_subscriptions_access_origin_check", + "value": "\"kiloclaw_subscriptions\".\"access_origin\" IN ('earlybird')" + }, + "kiloclaw_subscriptions_payment_source_check": { + "name": "kiloclaw_subscriptions_payment_source_check", + "value": "\"kiloclaw_subscriptions\".\"payment_source\" IN ('stripe', 'credits')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_terminal_renewal_failures": { + "name": "kiloclaw_terminal_renewal_failures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "renewal_boundary": { + "name": "renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unresolved'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_failure_at": { + "name": "first_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_failure_message": { + "name": "last_failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_type": { + "name": "resolution_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_id": { + "name": "resolution_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_at": { + "name": "resolution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary": { + "name": "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_unresolved": { + "name": "IDX_kiloclaw_terminal_renewal_failures_unresolved", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_terminal_renewal_failures\".\"status\" = 'unresolved'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at": { + "name": "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_terminal_renewal_failures", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_terminal_renewal_failures_status_check": { + "name": "kiloclaw_terminal_renewal_failures_status_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"status\" IN ('unresolved', 'resolved', 'waived', 'superseded')" + }, + "kiloclaw_terminal_renewal_failures_last_failure_code_check": { + "name": "kiloclaw_terminal_renewal_failures_last_failure_code_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"last_failure_code\" IN ('credit_balance_read_failed', 'renewal_transaction_failed', 'auto_top_up_marker_write_failed', 'worker_timeout', 'poison_payload', 'queue_delivery_exhausted')" + }, + "kiloclaw_terminal_renewal_failures_resolution_actor_type_check": { + "name": "kiloclaw_terminal_renewal_failures_resolution_actor_type_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"resolution_actor_type\" IN ('operator', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_version_pins": { + "name": "kiloclaw_version_pins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_by": { + "name": "pinned_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk": { + "name": "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kilocode_users", + "columnsFrom": [ + "pinned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_version_pins_instance_id_unique": { + "name": "kiloclaw_version_pins_instance_id_unique", + "nullsNotDistinct": false, + "columns": [ + "instance_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilocode_users": { + "name": "kilocode_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "google_user_email": { + "name": "google_user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_name": { + "name": "google_user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_image_url": { + "name": "google_user_image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "kilo_pass_threshold": { + "name": "kilo_pass_threshold", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_store_account_token": { + "name": "app_store_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_super_admin": { + "name": "is_super_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_view_sessions": { + "name": "can_view_sessions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_manage_credits": { + "name": "can_manage_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "has_validation_stytch": { + "name": "has_validation_stytch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_validation_novel_card_with_hold": { + "name": "has_validation_novel_card_with_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_at": { + "name": "blocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_by_kilo_user_id": { + "name": "blocked_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_token_pepper": { + "name": "api_token_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_session_pepper": { + "name": "web_session_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "kiloclaw_early_access": { + "name": "kiloclaw_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cohorts": { + "name": "cohorts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_welcome_form": { + "name": "completed_welcome_form", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "linkedin_url": { + "name": "linkedin_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discord_server_membership_verified_at": { + "name": "discord_server_membership_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "openrouter_upstream_safety_identifier": { + "name": "openrouter_upstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openrouter_downstream_safety_identifier": { + "name": "openrouter_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_downstream_safety_identifier": { + "name": "vercel_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_source": { + "name": "customer_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signup_ip": { + "name": "signup_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_deletion_requested_at": { + "name": "account_deletion_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_domain": { + "name": "email_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "personal_account_disabled": { + "name": "personal_account_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kilocode_users_signup_ip_created_at": { + "name": "IDX_kilocode_users_signup_ip_created_at", + "columns": [ + { + "expression": "signup_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_at": { + "name": "IDX_kilocode_users_blocked_at", + "columns": [ + { + "expression": "blocked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_by_kilo_user_id": { + "name": "IDX_kilocode_users_blocked_by_kilo_user_id", + "columns": [ + { + "expression": "blocked_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_upstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_upstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_upstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_upstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_downstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_downstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_downstream_safety_identifier\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_vercel_downstream_safety_identifier": { + "name": "UQ_kilocode_users_vercel_downstream_safety_identifier", + "columns": [ + { + "expression": "vercel_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"vercel_downstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_normalized_email": { + "name": "IDX_kilocode_users_normalized_email", + "columns": [ + { + "expression": "normalized_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_email_domain": { + "name": "IDX_kilocode_users_email_domain", + "columns": [ + { + "expression": "email_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilocode_users_app_store_account_token_unique": { + "name": "kilocode_users_app_store_account_token_unique", + "nullsNotDistinct": false, + "columns": [ + "app_store_account_token" + ] + }, + "UQ_b1afacbcf43f2c7c4cb9f7e7faa": { + "name": "UQ_b1afacbcf43f2c7c4cb9f7e7faa", + "nullsNotDistinct": false, + "columns": [ + "google_user_email" + ] + } + }, + "policies": {}, + "checkConstraints": { + "blocked_reason_not_empty": { + "name": "blocked_reason_not_empty", + "value": "length(blocked_reason) > 0" + }, + "kilocode_users_is_super_admin_requires_admin_check": { + "name": "kilocode_users_is_super_admin_requires_admin_check", + "value": "NOT \"kilocode_users\".\"is_super_admin\" OR \"kilocode_users\".\"is_admin\"" + }, + "kilocode_users_can_view_sessions_requires_admin_check": { + "name": "kilocode_users_can_view_sessions_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_view_sessions\" OR \"kilocode_users\".\"is_admin\"" + }, + "kilocode_users_can_manage_credits_requires_admin_check": { + "name": "kilocode_users_can_manage_credits_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_manage_credits\" OR \"kilocode_users\".\"is_admin\"" + } + }, + "isRLSEnabled": false + }, + "public.magic_link_tokens": { + "name": "magic_link_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reserved_until": { + "name": "reserved_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'magic_link'" + }, + "challenge_id": { + "name": "challenge_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_magic_link_tokens_email": { + "name": "idx_magic_link_tokens_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_magic_link_tokens_expires_at": { + "name": "idx_magic_link_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_magic_link_tokens_challenge_id": { + "name": "UQ_magic_link_tokens_challenge_id", + "columns": [ + { + "expression": "challenge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"magic_link_tokens\".\"challenge_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_expires_at_future": { + "name": "check_expires_at_future", + "value": "\"magic_link_tokens\".\"expires_at\" > \"magic_link_tokens\".\"created_at\"" + }, + "check_magic_link_tokens_purpose": { + "name": "check_magic_link_tokens_purpose", + "value": "\"magic_link_tokens\".\"purpose\" IN ('magic_link', 'sign_in_code', 'data_export_download')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_assignments": { + "name": "mcp_gateway_assignments", + "schema": "", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_kilo_user_id": { + "name": "assigned_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "single_user_slot": { + "name": "single_user_slot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_assignments_active": { + "name": "UQ_mcp_gateway_assignments_active", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_assignments\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_assignments_single_user_slot": { + "name": "UQ_mcp_gateway_assignments_single_user_slot", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "single_user_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_assignments\".\"revoked_at\" is null and \"mcp_gateway_assignments\".\"single_user_slot\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_assignments_config": { + "name": "IDX_mcp_gateway_assignments_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_assignments_user": { + "name": "IDX_mcp_gateway_assignments_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_assignments_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_assignments_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_assignments_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_assignments_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_assignments_assigned_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_assignments_assigned_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_gateway_audit_events": { + "name": "mcp_gateway_audit_events", + "schema": "", + "columns": { + "audit_event_id": { + "name": "audit_event_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_metadata": { + "name": "correlation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_audit_events_config": { + "name": "IDX_mcp_gateway_audit_events_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_grant": { + "name": "IDX_mcp_gateway_audit_events_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_audit_events\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_owner": { + "name": "IDX_mcp_gateway_audit_events_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_created_at": { + "name": "IDX_mcp_gateway_audit_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_audit_events_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_audit_events_owner_scope": { + "name": "mcp_gateway_audit_events_owner_scope", + "value": "\"mcp_gateway_audit_events\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_audit_events_outcome": { + "name": "mcp_gateway_audit_events_outcome", + "value": "\"mcp_gateway_audit_events\".\"outcome\" IN ('success', 'failure', 'blocked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_codes": { + "name": "mcp_gateway_authorization_codes", + "schema": "", + "columns": { + "authorization_code_id": { + "name": "authorization_code_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_codes_code_hash": { + "name": "UQ_mcp_gateway_authorization_codes_code_hash", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_expires_at": { + "name": "IDX_mcp_gateway_authorization_codes_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_client": { + "name": "IDX_mcp_gateway_authorization_codes_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_grant": { + "name": "IDX_mcp_gateway_authorization_codes_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_authorization_codes\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_codes_owner_scope": { + "name": "mcp_gateway_authorization_codes_owner_scope", + "value": "\"mcp_gateway_authorization_codes\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_requests": { + "name": "mcp_gateway_authorization_requests", + "schema": "", + "columns": { + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_state_hash": { + "name": "request_state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_scopes": { + "name": "requested_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "oauth_state": { + "name": "oauth_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_status": { + "name": "request_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_requests_state_hash": { + "name": "UQ_mcp_gateway_authorization_requests_state_hash", + "columns": [ + { + "expression": "request_state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_config": { + "name": "IDX_mcp_gateway_authorization_requests_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_grant": { + "name": "IDX_mcp_gateway_authorization_requests_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_authorization_requests\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_user": { + "name": "IDX_mcp_gateway_authorization_requests_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_expires_at": { + "name": "IDX_mcp_gateway_authorization_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_requests_owner_scope": { + "name": "mcp_gateway_authorization_requests_owner_scope", + "value": "\"mcp_gateway_authorization_requests\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_authorization_requests_status": { + "name": "mcp_gateway_authorization_requests_status", + "value": "\"mcp_gateway_authorization_requests\".\"request_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_config_secrets": { + "name": "mcp_gateway_config_secrets", + "schema": "", + "columns": { + "config_secret_id": { + "name": "config_secret_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_kind": { + "name": "secret_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_config_secrets_active_kind": { + "name": "UQ_mcp_gateway_config_secrets_active_kind", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_config_secrets\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_config_secrets_config": { + "name": "IDX_mcp_gateway_config_secrets_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_config_secrets", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_config_secrets_version_positive": { + "name": "mcp_gateway_config_secrets_version_positive", + "value": "\"mcp_gateway_config_secrets\".\"secret_version\" > 0" + }, + "mcp_gateway_config_secrets_kind": { + "name": "mcp_gateway_config_secrets_kind", + "value": "\"mcp_gateway_config_secrets\".\"secret_kind\" IN ('static_provider_credentials', 'dynamic_registration', 'static_headers')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_configs": { + "name": "mcp_gateway_configs", + "schema": "", + "columns": { + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharing_mode": { + "name": "sharing_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_scope_source": { + "name": "provider_scope_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "provider_resource": { + "name": "provider_resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "path_passthrough": { + "name": "path_passthrough", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "discovered_provider_metadata": { + "name": "discovered_provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "registry_metadata": { + "name": "registry_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "auxiliary_headers": { + "name": "auxiliary_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_configs_owner": { + "name": "IDX_mcp_gateway_configs_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_enabled": { + "name": "IDX_mcp_gateway_configs_enabled", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_remote_url": { + "name": "IDX_mcp_gateway_configs_remote_url", + "columns": [ + { + "expression": "remote_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_configs_name_not_empty": { + "name": "mcp_gateway_configs_name_not_empty", + "value": "length(trim(\"mcp_gateway_configs\".\"name\")) > 0" + }, + "mcp_gateway_configs_config_version_positive": { + "name": "mcp_gateway_configs_config_version_positive", + "value": "\"mcp_gateway_configs\".\"config_version\" > 0" + }, + "mcp_gateway_configs_personal_single_user": { + "name": "mcp_gateway_configs_personal_single_user", + "value": "\"mcp_gateway_configs\".\"owner_scope\" <> 'personal' OR \"mcp_gateway_configs\".\"sharing_mode\" = 'single_user'" + }, + "mcp_gateway_configs_owner_scope": { + "name": "mcp_gateway_configs_owner_scope", + "value": "\"mcp_gateway_configs\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_configs_auth_mode": { + "name": "mcp_gateway_configs_auth_mode", + "value": "\"mcp_gateway_configs\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_configs_sharing_mode": { + "name": "mcp_gateway_configs_sharing_mode", + "value": "\"mcp_gateway_configs\".\"sharing_mode\" IN ('single_user', 'multi_user')" + }, + "mcp_gateway_configs_provider_scope_source": { + "name": "mcp_gateway_configs_provider_scope_source", + "value": "\"mcp_gateway_configs\".\"provider_scope_source\" IN ('none', 'discovered', 'override')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connect_resources": { + "name": "mcp_gateway_connect_resources", + "schema": "", + "columns": { + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_status": { + "name": "route_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "route_version": { + "name": "route_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connect_resources_route_key": { + "name": "UQ_mcp_gateway_connect_resources_route_key", + "columns": [ + { + "expression": "route_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_connect_resources_active_config": { + "name": "UQ_mcp_gateway_connect_resources_active_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connect_resources\".\"route_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_config": { + "name": "IDX_mcp_gateway_connect_resources_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_canonical_url": { + "name": "IDX_mcp_gateway_connect_resources_canonical_url", + "columns": [ + { + "expression": "canonical_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connect_resources", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connect_resources_route_key_format": { + "name": "mcp_gateway_connect_resources_route_key_format", + "value": "\"mcp_gateway_connect_resources\".\"route_key\" ~ '^[A-Za-z0-9_-]{32,}$'" + }, + "mcp_gateway_connect_resources_route_version_positive": { + "name": "mcp_gateway_connect_resources_route_version_positive", + "value": "\"mcp_gateway_connect_resources\".\"route_version\" > 0" + }, + "mcp_gateway_connect_resources_owner_scope": { + "name": "mcp_gateway_connect_resources_owner_scope", + "value": "\"mcp_gateway_connect_resources\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connect_resources_route_status": { + "name": "mcp_gateway_connect_resources_route_status", + "value": "\"mcp_gateway_connect_resources\".\"route_status\" IN ('active', 'rotated', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connection_instances": { + "name": "mcp_gateway_connection_instances", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_status": { + "name": "instance_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "instance_version": { + "name": "instance_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connection_instances_non_terminal": { + "name": "UQ_mcp_gateway_connection_instances_non_terminal", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_config": { + "name": "IDX_mcp_gateway_connection_instances_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_user": { + "name": "IDX_mcp_gateway_connection_instances_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connection_instances_version_positive": { + "name": "mcp_gateway_connection_instances_version_positive", + "value": "\"mcp_gateway_connection_instances\".\"instance_version\" > 0" + }, + "mcp_gateway_connection_instances_owner_scope": { + "name": "mcp_gateway_connection_instances_owner_scope", + "value": "\"mcp_gateway_connection_instances\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connection_instances_status": { + "name": "mcp_gateway_connection_instances_status", + "value": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth', 'revoked', 'removed')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_clients": { + "name": "mcp_gateway_oauth_clients", + "schema": "", + "columns": { + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_token_hash": { + "name": "registration_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "declared_scopes": { + "name": "declared_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "registration_access_token_expires_at": { + "name": "registration_access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_clients_client_id": { + "name": "UQ_mcp_gateway_oauth_clients_client_id", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_oauth_clients_registration_token_hash": { + "name": "UQ_mcp_gateway_oauth_clients_registration_token_hash", + "columns": [ + { + "expression": "registration_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_clients_deleted_at": { + "name": "IDX_mcp_gateway_oauth_clients_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_clients_client_id_format": { + "name": "mcp_gateway_oauth_clients_client_id_format", + "value": "\"mcp_gateway_oauth_clients\".\"client_id\" ~ '^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+$'" + }, + "mcp_gateway_oauth_clients_auth_method": { + "name": "mcp_gateway_oauth_clients_auth_method", + "value": "\"mcp_gateway_oauth_clients\".\"token_endpoint_auth_method\" IN ('none', 'client_secret_post', 'client_secret_basic')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_grants": { + "name": "mcp_gateway_oauth_grants", + "schema": "", + "columns": { + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_grants_active_binding": { + "name": "UQ_mcp_gateway_oauth_grants_active_binding", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connect_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "redirect_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_oauth_grants\".\"revoked_at\" is null and \"mcp_gateway_oauth_grants\".\"grant_status\" in ('pending', 'active')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_client": { + "name": "IDX_mcp_gateway_oauth_grants_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_user": { + "name": "IDX_mcp_gateway_oauth_grants_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_config": { + "name": "IDX_mcp_gateway_oauth_grants_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_owner": { + "name": "IDX_mcp_gateway_oauth_grants_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_resource": { + "name": "IDX_mcp_gateway_oauth_grants_resource", + "columns": [ + { + "expression": "connect_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_instance": { + "name": "IDX_mcp_gateway_oauth_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_revoked_at": { + "name": "IDX_mcp_gateway_oauth_grants_revoked_at", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_oauth_grants_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_oauth_grants_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_oauth_grants_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_oauth_grants_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_oauth_grants_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_oauth_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_grants_config_version_positive": { + "name": "mcp_gateway_oauth_grants_config_version_positive", + "value": "\"mcp_gateway_oauth_grants\".\"config_version\" > 0" + }, + "mcp_gateway_oauth_grants_owner_scope": { + "name": "mcp_gateway_oauth_grants_owner_scope", + "value": "\"mcp_gateway_oauth_grants\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_oauth_grants_status": { + "name": "mcp_gateway_oauth_grants_status", + "value": "\"mcp_gateway_oauth_grants\".\"grant_status\" IN ('pending', 'active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_pending_provider_authorizations": { + "name": "mcp_gateway_pending_provider_authorizations", + "schema": "", + "columns": { + "pending_provider_authorization_id": { + "name": "pending_provider_authorization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_authorization_endpoint": { + "name": "provider_authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_token_endpoint": { + "name": "provider_token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pending_status": { + "name": "pending_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_pending_provider_authorizations_state_hash": { + "name": "UQ_mcp_gateway_pending_provider_authorizations_state_hash", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_config": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_grant": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_pending_provider_authorizations\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_expires_at": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_pending_provider_authorizations_config_version_positive": { + "name": "mcp_gateway_pending_provider_authorizations_config_version_positive", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"config_version\" > 0" + }, + "mcp_gateway_pending_provider_authorizations_owner_scope": { + "name": "mcp_gateway_pending_provider_authorizations_owner_scope", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_pending_provider_authorizations_auth_mode": { + "name": "mcp_gateway_pending_provider_authorizations_auth_mode", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_pending_provider_authorizations_status": { + "name": "mcp_gateway_pending_provider_authorizations_status", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"pending_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_provider_grants": { + "name": "mcp_gateway_provider_grants", + "schema": "", + "columns": { + "provider_grant_id": { + "name": "provider_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "encrypted_grant": { + "name": "encrypted_grant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject": { + "name": "provider_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_scope": { + "name": "grant_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_provider_grants_active_instance": { + "name": "UQ_mcp_gateway_provider_grants_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_provider_grants\".\"grant_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_provider_grants_instance": { + "name": "IDX_mcp_gateway_provider_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_provider_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_provider_grants_version_positive": { + "name": "mcp_gateway_provider_grants_version_positive", + "value": "\"mcp_gateway_provider_grants\".\"grant_version\" > 0" + }, + "mcp_gateway_provider_grants_status": { + "name": "mcp_gateway_provider_grants_status", + "value": "\"mcp_gateway_provider_grants\".\"grant_status\" IN ('active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_rate_limit_windows": { + "name": "mcp_gateway_rate_limit_windows", + "schema": "", + "columns": { + "rate_limit_window_id": { + "name": "rate_limit_window_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_started_at": { + "name": "window_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_rate_limit_windows_ip_window": { + "name": "UQ_mcp_gateway_rate_limit_windows_ip_window", + "columns": [ + { + "expression": "ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_rate_limit_windows_window": { + "name": "IDX_mcp_gateway_rate_limit_windows_window", + "columns": [ + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_rate_limit_windows_attempt_count_non_negative": { + "name": "mcp_gateway_rate_limit_windows_attempt_count_non_negative", + "value": "\"mcp_gateway_rate_limit_windows\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_refresh_tokens": { + "name": "mcp_gateway_refresh_tokens", + "schema": "", + "columns": { + "refresh_token_id": { + "name": "refresh_token_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotated_from_refresh_token_id": { + "name": "rotated_from_refresh_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_refresh_tokens_token_hash": { + "name": "UQ_mcp_gateway_refresh_tokens_token_hash", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_user": { + "name": "IDX_mcp_gateway_refresh_tokens_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_grant": { + "name": "IDX_mcp_gateway_refresh_tokens_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_refresh_tokens\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_config": { + "name": "IDX_mcp_gateway_refresh_tokens_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_consumed_at": { + "name": "IDX_mcp_gateway_refresh_tokens_consumed_at", + "columns": [ + { + "expression": "consumed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_refresh_tokens_owner_scope": { + "name": "mcp_gateway_refresh_tokens_owner_scope", + "value": "\"mcp_gateway_refresh_tokens\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage": { + "name": "microdollar_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_abuse_classification": { + "name": "idx_abuse_classification", + "columns": [ + { + "expression": "abuse_classification", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id_created_at2": { + "name": "idx_kilo_user_id_created_at2", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_organization_id": { + "name": "idx_microdollar_usage_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily": { + "name": "microdollar_usage_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_microdollar_usage_daily_personal": { + "name": "idx_microdollar_usage_daily_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_daily_org": { + "name": "idx_microdollar_usage_daily_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily_repairs": { + "name": "microdollar_usage_daily_repairs", + "schema": "", + "columns": { + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_microdollar_usage_daily_repairs_claim": { + "name": "IDX_microdollar_usage_daily_repairs_claim", + "columns": [ + { + "expression": "attempt_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_daily_repairs_usage_id_microdollar_usage_id_fk": { + "name": "microdollar_usage_daily_repairs_usage_id_microdollar_usage_id_fk", + "tableFrom": "microdollar_usage_daily_repairs", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "microdollar_usage_daily_repairs_attempt_count_check": { + "name": "microdollar_usage_daily_repairs_attempt_count_check", + "value": "\"microdollar_usage_daily_repairs\".\"attempt_count\" >= 0" + }, + "microdollar_usage_daily_repairs_claim_token_check": { + "name": "microdollar_usage_daily_repairs_claim_token_check", + "value": "(\"microdollar_usage_daily_repairs\".\"claimed_at\" IS NULL AND \"microdollar_usage_daily_repairs\".\"claim_token\" IS NULL) OR (\"microdollar_usage_daily_repairs\".\"claimed_at\" IS NOT NULL AND \"microdollar_usage_daily_repairs\".\"claim_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage_metadata": { + "name": "microdollar_usage_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_ip_id": { + "name": "http_ip_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_latitude": { + "name": "vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_longitude": { + "name": "vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason_id": { + "name": "finish_reason_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name_id": { + "name": "editor_name_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_kind_id": { + "name": "api_kind_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode_id": { + "name": "mode_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_model_id": { + "name": "auto_model_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_microdollar_usage_metadata_created_at": { + "name": "idx_microdollar_usage_metadata_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_metadata_session_id": { + "name": "idx_microdollar_usage_metadata_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage_metadata\".\"session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk": { + "name": "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_user_agent", + "columnsFrom": [ + "http_user_agent_id" + ], + "columnsTo": [ + "http_user_agent_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk": { + "name": "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_ip", + "columnsFrom": [ + "http_ip_id" + ], + "columnsTo": [ + "http_ip_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_city", + "columnsFrom": [ + "vercel_ip_city_id" + ], + "columnsTo": [ + "vercel_ip_city_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_country", + "columnsFrom": [ + "vercel_ip_country_id" + ], + "columnsTo": [ + "vercel_ip_country_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk": { + "name": "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "ja4_digest", + "columnsFrom": [ + "ja4_digest_id" + ], + "columnsTo": [ + "ja4_digest_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk": { + "name": "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "system_prompt_prefix", + "columnsFrom": [ + "system_prompt_prefix_id" + ], + "columnsTo": [ + "system_prompt_prefix_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mode": { + "name": "mode", + "schema": "", + "columns": { + "mode_id": { + "name": "mode_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_mode": { + "name": "UQ_mode", + "columns": [ + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_stats": { + "name": "model_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_stealth": { + "name": "is_stealth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_recommended": { + "name": "is_recommended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "openrouter_id": { + "name": "openrouter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aa_slug": { + "name": "aa_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_creator": { + "name": "model_creator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_slug": { + "name": "creator_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_date": { + "name": "release_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "price_input": { + "name": "price_input", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "price_output": { + "name": "price_output", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "coding_index": { + "name": "coding_index", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "speed_tokens_per_sec": { + "name": "speed_tokens_per_sec", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "openrouter_data": { + "name": "openrouter_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "benchmarks": { + "name": "benchmarks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chart_data": { + "name": "chart_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_stats_openrouter_id": { + "name": "IDX_model_stats_openrouter_id", + "columns": [ + { + "expression": "openrouter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_slug": { + "name": "IDX_model_stats_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_is_active": { + "name": "IDX_model_stats_is_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_creator_slug": { + "name": "IDX_model_stats_creator_slug", + "columns": [ + { + "expression": "creator_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_price_input": { + "name": "IDX_model_stats_price_input", + "columns": [ + { + "expression": "price_input", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_coding_index": { + "name": "IDX_model_stats_coding_index", + "columns": [ + { + "expression": "coding_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_context_length": { + "name": "IDX_model_stats_context_length", + "columns": [ + { + "expression": "context_length", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_stats_openrouter_id_unique": { + "name": "model_stats_openrouter_id_unique", + "nullsNotDistinct": false, + "columns": [ + "openrouter_id" + ] + }, + "model_stats_slug_unique": { + "name": "model_stats_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_eval_ingestions": { + "name": "model_eval_ingestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bench_eval_name": { + "name": "bench_eval_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bench_eval_url": { + "name": "bench_eval_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_stats_id": { + "name": "model_stats_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "n_total_trials": { + "name": "n_total_trials", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "n_attempts": { + "name": "n_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_score": { + "name": "total_score", + "type": "numeric(14, 6)", + "primaryKey": false, + "notNull": true + }, + "overall_score": { + "name": "overall_score", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": true + }, + "n_errored": { + "name": "n_errored", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "avg_cost_microdollars": { + "name": "avg_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_input_tokens": { + "name": "avg_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_output_tokens": { + "name": "avg_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_cache_read_tokens": { + "name": "avg_cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cache_read_tokens": { + "name": "total_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_execution_ms": { + "name": "avg_execution_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promoted_at": { + "name": "promoted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "promoted_by_email": { + "name": "promoted_by_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "promotion_note": { + "name": "promotion_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_eval_ingestions_lookup": { + "name": "IDX_model_eval_ingestions_lookup", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "promoted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_model_stats": { + "name": "IDX_model_eval_ingestions_model_stats", + "columns": [ + { + "expression": "model_stats_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_promoted_by_email_lower": { + "name": "IDX_model_eval_ingestions_promoted_by_email_lower", + "columns": [ + { + "expression": "LOWER(\"promoted_by_email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_eval_ingestions_model_stats_id_model_stats_id_fk": { + "name": "model_eval_ingestions_model_stats_id_model_stats_id_fk", + "tableFrom": "model_eval_ingestions", + "tableTo": "model_stats", + "columnsFrom": [ + "model_stats_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_eval_ingestions_bench_eval_name_unique": { + "name": "model_eval_ingestions_bench_eval_name_unique", + "nullsNotDistinct": false, + "columns": [ + "bench_eval_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_experiment": { + "name": "model_experiment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "public_model_id": { + "name": "public_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_model_experiment_public_model_id_routing": { + "name": "UQ_model_experiment_public_model_id_routing", + "columns": [ + { + "expression": "public_model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"model_experiment\".\"status\" IN ('active', 'paused')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_experiment_status": { + "name": "IDX_model_experiment_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_created_by_user_id_kilocode_users_id_fk": { + "name": "model_experiment_created_by_user_id_kilocode_users_id_fk", + "tableFrom": "model_experiment", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_experiment_status_valid": { + "name": "model_experiment_status_valid", + "value": "\"model_experiment\".\"status\" IN ('draft', 'active', 'paused', 'completed')" + }, + "model_experiment_active_not_archived": { + "name": "model_experiment_active_not_archived", + "value": "\"model_experiment\".\"status\" <> 'active' OR \"model_experiment\".\"is_archived\" = false" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_request": { + "name": "model_experiment_request", + "schema": "", + "columns": { + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "variant_version_id": { + "name": "variant_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_subject": { + "name": "allocation_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_kind": { + "name": "request_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_body_sha256": { + "name": "request_body_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "was_truncated": { + "name": "was_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_request_variant_version_created_at": { + "name": "IDX_model_experiment_request_variant_version_created_at", + "columns": [ + { + "expression": "variant_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_experiment_request_client_request_id": { + "name": "IDX_model_experiment_request_client_request_id", + "columns": [ + { + "expression": "client_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"model_experiment_request\".\"client_request_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_request_usage_id_microdollar_usage_id_fk": { + "name": "model_experiment_request_usage_id_microdollar_usage_id_fk", + "tableFrom": "model_experiment_request", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_experiment_request_variant_version_id_model_experiment_variant_version_id_fk": { + "name": "model_experiment_request_variant_version_id_model_experiment_variant_version_id_fk", + "tableFrom": "model_experiment_request", + "tableTo": "model_experiment_variant_version", + "columnsFrom": [ + "variant_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "model_experiment_request_usage_id_created_at_pk": { + "name": "model_experiment_request_usage_id_created_at_pk", + "columns": [ + "usage_id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_experiment_request_allocation_subject_valid": { + "name": "model_experiment_request_allocation_subject_valid", + "value": "\"model_experiment_request\".\"allocation_subject\" IN ('user', 'machine', 'ip')" + }, + "model_experiment_request_request_kind_valid": { + "name": "model_experiment_request_request_kind_valid", + "value": "\"model_experiment_request\".\"request_kind\" IN ('chat_completions', 'messages', 'responses')" + }, + "model_experiment_request_request_body_sha256_format": { + "name": "model_experiment_request_request_body_sha256_format", + "value": "\"model_experiment_request\".\"request_body_sha256\" ~ '^[0-9a-f]{64}$' OR \"model_experiment_request\".\"request_body_sha256\" IN ('__failed__', '__deleted__')" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_variant": { + "name": "model_experiment_variant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "experiment_id": { + "name": "experiment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_variant_experiment_id": { + "name": "IDX_model_experiment_variant_experiment_id", + "columns": [ + { + "expression": "experiment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_variant_experiment_id_model_experiment_id_fk": { + "name": "model_experiment_variant_experiment_id_model_experiment_id_fk", + "tableFrom": "model_experiment_variant", + "tableTo": "model_experiment", + "columnsFrom": [ + "experiment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_model_experiment_variant_experiment_label": { + "name": "UQ_model_experiment_variant_experiment_label", + "nullsNotDistinct": false, + "columns": [ + "experiment_id", + "label" + ] + } + }, + "policies": {}, + "checkConstraints": { + "model_experiment_variant_weight_positive": { + "name": "model_experiment_variant_weight_positive", + "value": "\"model_experiment_variant\".\"weight\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_variant_version": { + "name": "model_experiment_variant_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "variant_id": { + "name": "variant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_variant_version_variant_effective": { + "name": "IDX_model_experiment_variant_version_variant_effective", + "columns": [ + { + "expression": "variant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_variant_version_variant_id_model_experiment_variant_id_fk": { + "name": "model_experiment_variant_version_variant_id_model_experiment_variant_id_fk", + "tableFrom": "model_experiment_variant_version", + "tableTo": "model_experiment_variant", + "columnsFrom": [ + "variant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_experiment_variant_version_created_by_kilocode_users_id_fk": { + "name": "model_experiment_variant_version_created_by_kilocode_users_id_fk", + "tableFrom": "model_experiment_variant_version", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models_by_provider": { + "name": "models_by_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "openrouter": { + "name": "openrouter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "vercel": { + "name": "vercel", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_admission_challenges": { + "name": "native_admission_challenges", + "schema": "", + "columns": { + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_native_admission_challenges_expires_at": { + "name": "IDX_native_admission_challenges_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_attested_keys": { + "name": "native_attested_keys", + "schema": "", + "columns": { + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sign_count": { + "name": "sign_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attested_at": { + "name": "attested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_native_attested_keys_kilo_user_id": { + "name": "IDX_native_attested_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "native_attested_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "native_attested_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "native_attested_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "native_attested_keys_platform_check": { + "name": "native_attested_keys_platform_check", + "value": "\"native_attested_keys\".\"platform\" IN ('ios', 'android')" + } + }, + "isRLSEnabled": false + }, + "public.operation_ledgers": { + "name": "operation_ledgers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "operation_key": { + "name": "operation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "taxonomy": { + "name": "taxonomy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admitted'" + }, + "outcome_code": { + "name": "outcome_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_result": { + "name": "canonical_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_operation_ledgers_kilo_user_id_domain_operation_key": { + "name": "UQ_operation_ledgers_kilo_user_id_domain_operation_key", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_operation_ledgers_status_expires_at": { + "name": "IDX_operation_ledgers_status_expires_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_operation_ledgers_provider_ref": { + "name": "IDX_operation_ledgers_provider_ref", + "columns": [ + { + "expression": "provider_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"operation_ledgers\".\"provider_ref\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_audit_logs": { + "name": "organization_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_audit_logs_organization_id": { + "name": "IDX_organization_audit_logs_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_action": { + "name": "IDX_organization_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_actor_id": { + "name": "IDX_organization_audit_logs_actor_id", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_created_at": { + "name": "IDX_organization_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_domain_claims": { + "name": "organization_domain_claims", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "workos_organization_id": { + "name": "workos_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workos_domain_id": { + "name": "workos_domain_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_domain_claims_verified_domain": { + "name": "UQ_organization_domain_claims_verified_domain", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_domain_claims\".\"status\" = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_organization_domain_claims_workos_domain_id": { + "name": "UQ_organization_domain_claims_workos_domain_id", + "columns": [ + { + "expression": "workos_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_domain_claims\".\"workos_domain_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_domain_claims_organization_id": { + "name": "IDX_organization_domain_claims_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_domain_claims_organization_id_organizations_id_fk": { + "name": "organization_domain_claims_organization_id_organizations_id_fk", + "tableFrom": "organization_domain_claims", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_domain_claims_organization_domain": { + "name": "UQ_organization_domain_claims_organization_domain", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_domain_claims_canonical_domain_check": { + "name": "organization_domain_claims_canonical_domain_check", + "value": "length(\"organization_domain_claims\".\"domain\") BETWEEN 1 AND 253 AND \"organization_domain_claims\".\"domain\" = lower(btrim(\"organization_domain_claims\".\"domain\"))" + }, + "organization_domain_claims_status_check": { + "name": "organization_domain_claims_status_check", + "value": "\"organization_domain_claims\".\"status\" IN ('pending', 'verified')" + }, + "organization_domain_claims_verification_shape_check": { + "name": "organization_domain_claims_verification_shape_check", + "value": "(\"organization_domain_claims\".\"status\" = 'pending' AND \"organization_domain_claims\".\"verified_at\" IS NULL)\n OR (\"organization_domain_claims\".\"status\" = 'verified' AND \"organization_domain_claims\".\"verified_at\" IS NOT NULL AND \"organization_domain_claims\".\"workos_organization_id\" IS NOT NULL AND \"organization_domain_claims\".\"workos_domain_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.organization_group_memberships": { + "name": "organization_group_memberships", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_kilo_user_id": { + "name": "assigned_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_group_memberships_organization_user": { + "name": "IDX_organization_group_memberships_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "FK_organization_group_memberships_group": { + "name": "FK_organization_group_memberships_group", + "tableFrom": "organization_group_memberships", + "tableTo": "organization_groups", + "columnsFrom": [ + "organization_id", + "group_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_organization_group_memberships_member": { + "name": "FK_organization_group_memberships_member", + "tableFrom": "organization_group_memberships", + "tableTo": "organization_memberships", + "columnsFrom": [ + "organization_id", + "kilo_user_id" + ], + "columnsTo": [ + "organization_id", + "kilo_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "PK_organization_group_memberships": { + "name": "PK_organization_group_memberships", + "columns": [ + "organization_id", + "group_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_group_policy_settings": { + "name": "organization_group_policy_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "default_policies": { + "name": "default_policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[{\"type\":\"model_access\",\"data\":{\"mode\":\"all\"}}]'::jsonb" + }, + "policy_revision": { + "name": "policy_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated_by_kilo_user_id": { + "name": "updated_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_group_policy_settings_organization_id_organizations_id_fk": { + "name": "organization_group_policy_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_group_policy_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_group_policy_settings_revision_check": { + "name": "organization_group_policy_settings_revision_check", + "value": "\"organization_group_policy_settings\".\"policy_revision\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.organization_groups": { + "name": "organization_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policies": { + "name": "policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_groups_organization_id_canonical_name": { + "name": "UQ_organization_groups_organization_id_canonical_name", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(btrim(\"name\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_groups_organization_id": { + "name": "IDX_organization_groups_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_groups_organization_id_organizations_id_fk": { + "name": "organization_groups_organization_id_organizations_id_fk", + "tableFrom": "organization_groups", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_groups_organization_id_id": { + "name": "UQ_organization_groups_organization_id_id", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_groups_name_check": { + "name": "organization_groups_name_check", + "value": "char_length(btrim(\"organization_groups\".\"name\")) BETWEEN 1 AND 80" + }, + "organization_groups_description_check": { + "name": "organization_groups_description_check", + "value": "\"organization_groups\".\"description\" IS NULL OR char_length(\"organization_groups\".\"description\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "authentication_requirement": { + "name": "authentication_requirement", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "sso_source_organization_id": { + "name": "sso_source_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_invitations_token": { + "name": "UQ_organization_invitations_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_org_id": { + "name": "IDX_organization_invitations_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_email": { + "name": "IDX_organization_invitations_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_expires_at": { + "name": "IDX_organization_invitations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_sso_source_organization_id_organizations_id_fk": { + "name": "organization_invitations_sso_source_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "sso_source_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_membership_removals": { + "name": "organization_membership_removals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_by": { + "name": "removed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_role": { + "name": "previous_role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_org_membership_removals_org_id": { + "name": "IDX_org_membership_removals_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_org_membership_removals_user_id": { + "name": "IDX_org_membership_removals_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_membership_removals_org_user": { + "name": "UQ_org_membership_removals_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_memberships": { + "name": "organization_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_memberships_org_id": { + "name": "IDX_organization_memberships_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_memberships_user_id": { + "name": "IDX_organization_memberships_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_memberships_org_user": { + "name": "UQ_organization_memberships_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_recommendation_dismissals": { + "name": "organization_recommendation_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recommendation_key": { + "name": "recommendation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_by_user_id": { + "name": "dismissed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk": { + "name": "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk": { + "name": "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "dismissed_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_recommendation_dismissals_org_key": { + "name": "UQ_org_recommendation_dismissals_org_key", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "recommendation_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_seats_purchases": { + "name": "organization_seats_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subscription_stripe_id": { + "name": "subscription_stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "subscription_status": { + "name": "subscription_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_cycle": { + "name": "billing_cycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + } + }, + "indexes": { + "IDX_organization_seats_org_id": { + "name": "IDX_organization_seats_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_expires_at": { + "name": "IDX_organization_seats_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_created_at": { + "name": "IDX_organization_seats_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_updated_at": { + "name": "IDX_organization_seats_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_starts_at": { + "name": "IDX_organization_seats_starts_at", + "columns": [ + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_seats_idempotency_key": { + "name": "UQ_organization_seats_idempotency_key", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_limits": { + "name": "organization_user_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_limit": { + "name": "microdollar_limit", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_user_limits_org_id": { + "name": "IDX_organization_user_limits_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_limits_user_id": { + "name": "IDX_organization_user_limits_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_limits_org_user": { + "name": "UQ_organization_user_limits_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_usage": { + "name": "organization_user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_usage": { + "name": "microdollar_usage", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_user_daily_usage_org_id": { + "name": "IDX_organization_user_daily_usage_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_daily_usage_user_id": { + "name": "IDX_organization_user_daily_usage_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_daily_usage_org_user_date": { + "name": "UQ_organization_user_daily_usage_org_user_date", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type", + "usage_date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "microdollars_balance": { + "name": "microdollars_balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_seats": { + "name": "require_seats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sso_domain": { + "name": "sso_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'teams'" + }, + "free_trial_end_at": { + "name": "free_trial_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "company_domain": { + "name": "company_domain", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_organizations_sso_domain": { + "name": "IDX_organizations_sso_domain", + "columns": [ + { + "expression": "sso_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organizations_parent_organization_id": { + "name": "IDX_organizations_parent_organization_id", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_organizations_live_sales_demo_per_owner": { + "name": "UQ_organizations_live_sales_demo_per_owner", + "columns": [ + { + "expression": "created_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "(\"organizations\".\"settings\"->>'is_sales_demo')::boolean = true AND \"organizations\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_parent_organization_id_organizations_id_fk": { + "name": "organizations_parent_organization_id_organizations_id_fk", + "tableFrom": "organizations", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organizations_name_not_empty_check": { + "name": "organizations_name_not_empty_check", + "value": "length(trim(\"organizations\".\"name\")) > 0" + }, + "organizations_not_parented_by_self_check": { + "name": "organizations_not_parented_by_self_check", + "value": "\"organizations\".\"parent_organization_id\" IS NULL OR \"organizations\".\"parent_organization_id\" <> \"organizations\".\"id\"" + } + }, + "isRLSEnabled": false + }, + "public.organization_modes": { + "name": "organization_modes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_organization_modes_organization_id": { + "name": "IDX_organization_modes_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_modes_org_id_slug": { + "name": "UQ_organization_modes_org_id_slug", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_methods": { + "name": "payment_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last4": { + "name": "last4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1": { + "name": "address_line1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line2": { + "name": "address_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_state": { + "name": "address_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "three_d_secure_supported": { + "name": "three_d_secure_supported", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regulated_status": { + "name": "regulated_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1_check_status": { + "name": "address_line1_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code_check_status": { + "name": "postal_code_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eligible_for_free_credits": { + "name": "eligible_for_free_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_data": { + "name": "stripe_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_d7d7fb15569674aaadcfbc0428": { + "name": "IDX_d7d7fb15569674aaadcfbc0428", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_e1feb919d0ab8a36381d5d5138": { + "name": "IDX_e1feb919d0ab8a36381d5d5138", + "columns": [ + { + "expression": "stripe_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_payment_methods_organization_id": { + "name": "IDX_payment_methods_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_29df1b0403df5792c96bbbfdbe6": { + "name": "UQ_29df1b0403df5792c96bbbfdbe6", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_impact_sale_reversals": { + "name": "pending_impact_sale_reversals", + "schema": "", + "columns": { + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "dispute_id": { + "name": "dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_impact_sale_reversals_attempt_count_non_negative_check": { + "name": "pending_impact_sale_reversals_attempt_count_non_negative_check", + "value": "\"pending_impact_sale_reversals\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.platform_access_token_credentials": { + "name": "platform_access_token_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_encrypted": { + "name": "token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_credential_type": { + "name": "provider_credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_resource_id": { + "name": "provider_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_verified_at": { + "name": "provider_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_validated_at": { + "name": "last_validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_access_token_credentials_integration_level": { + "name": "UQ_platform_access_token_credentials_integration_level", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_access_token_credentials_resource": { + "name": "UQ_platform_access_token_credentials_resource", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_credential_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_access_token_credentials_authorized_by_user_id": { + "name": "IDX_platform_access_token_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_access_token_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_platform_access_token_credentials_parent": { + "name": "FK_platform_access_token_credentials_parent", + "tableFrom": "platform_access_token_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_access_token_credentials_credential_version_check": { + "name": "platform_access_token_credentials_credential_version_check", + "value": "\"platform_access_token_credentials\".\"credential_version\" > 0" + }, + "platform_access_token_credentials_resource_id_check": { + "name": "platform_access_token_credentials_resource_id_check", + "value": "\"platform_access_token_credentials\".\"provider_resource_id\" IS NULL OR \"platform_access_token_credentials\".\"provider_resource_id\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.platform_integrations": { + "name": "platform_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_installation_id": { + "name": "platform_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_id": { + "name": "platform_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_login": { + "name": "platform_account_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "repository_access": { + "name": "repository_access", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repositories": { + "name": "repositories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repositories_synced_at": { + "name": "repositories_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_at": { + "name": "auth_invalid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_reason": { + "name": "auth_invalid_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "kilo_requester_user_id": { + "name": "kilo_requester_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_requester_account_id": { + "name": "platform_requester_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_status": { + "name": "integration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_by": { + "name": "suspended_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_integrations_owned_by_org_platform_inst": { + "name": "UQ_platform_integrations_owned_by_org_platform_inst", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_owned_by_user_platform_inst": { + "name": "UQ_platform_integrations_owned_by_user_platform_inst", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_slack_platform_inst": { + "name": "UQ_platform_integrations_slack_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'slack' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_linear_platform_inst": { + "name": "UQ_platform_integrations_linear_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'linear' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_platform_inst": { + "name": "UQ_platform_integrations_github_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_pending_target": { + "name": "UQ_platform_integrations_github_pending_target", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"integration_status\" = 'pending' AND \"platform_integrations\".\"platform_installation_id\" IS NULL AND \"platform_integrations\".\"platform_account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_user_bitbucket": { + "name": "UQ_platform_integrations_user_bitbucket", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_org_bitbucket": { + "name": "UQ_platform_integrations_org_bitbucket", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_id": { + "name": "IDX_platform_integrations_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_id": { + "name": "IDX_platform_integrations_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_inst_id": { + "name": "IDX_platform_integrations_platform_inst_id", + "columns": [ + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform": { + "name": "IDX_platform_integrations_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_platform": { + "name": "IDX_platform_integrations_owned_by_org_platform", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_platform": { + "name": "IDX_platform_integrations_owned_by_user_platform", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_integration_status": { + "name": "IDX_platform_integrations_integration_status", + "columns": [ + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_kilo_requester": { + "name": "IDX_platform_integrations_kilo_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_requester_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_requester": { + "name": "IDX_platform_integrations_platform_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_requester_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_integrations_owned_by_organization_id_organizations_id_fk": { + "name": "platform_integrations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_integrations_owned_by_user_id_kilocode_users_id_fk": { + "name": "platform_integrations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_integrations_owner_check": { + "name": "platform_integrations_owner_check", + "value": "(\n (\"platform_integrations\".\"owned_by_user_id\" IS NOT NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NULL) OR\n (\"platform_integrations\".\"owned_by_user_id\" IS NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.platform_oauth_credentials": { + "name": "platform_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject_login": { + "name": "provider_subject_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_oauth_credentials_platform_integration_id": { + "name": "UQ_platform_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_oauth_credentials_authorized_by_user_id": { + "name": "IDX_platform_oauth_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_oauth_credentials_credential_version_check": { + "name": "platform_oauth_credentials_credential_version_check", + "value": "\"platform_oauth_credentials\".\"credential_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.referral_code_usages": { + "name": "referral_code_usages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "referring_kilo_user_id": { + "name": "referring_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeeming_kilo_user_id": { + "name": "redeeming_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_referral_code_usages_redeeming_kilo_user_id": { + "name": "IDX_referral_code_usages_redeeming_kilo_user_id", + "columns": [ + { + "expression": "redeeming_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_referral_code_usages_redeeming_user_id_code": { + "name": "UQ_referral_code_usages_redeeming_user_id_code", + "nullsNotDistinct": false, + "columns": [ + "redeeming_kilo_user_id", + "referring_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_referral_codes_kilo_user_id": { + "name": "UQ_referral_codes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_referral_codes_code": { + "name": "IDX_referral_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sales_demo_spend_ledger": { + "name": "sales_demo_spend_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_kilo_user_id": { + "name": "owner_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sales_demo_spend_ledger_organization_id_organizations_id_fk": { + "name": "sales_demo_spend_ledger_organization_id_organizations_id_fk", + "tableFrom": "sales_demo_spend_ledger", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sales_demo_spend_ledger_spend_positive": { + "name": "sales_demo_spend_ledger_spend_positive", + "value": "\"sales_demo_spend_ledger\".\"microdollars_used\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_check_catalog": { + "name": "security_advisor_check_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_check_catalog_check_id_unique": { + "name": "security_advisor_check_catalog_check_id_unique", + "nullsNotDistinct": false, + "columns": [ + "check_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "security_advisor_check_catalog_severity_check": { + "name": "security_advisor_check_catalog_severity_check", + "value": "\"security_advisor_check_catalog\".\"severity\" in ('critical', 'warn', 'info')" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_content": { + "name": "security_advisor_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_content_key_unique": { + "name": "security_advisor_content_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_kiloclaw_coverage": { + "name": "security_advisor_kiloclaw_coverage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_check_ids": { + "name": "match_check_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_kiloclaw_coverage_area_unique": { + "name": "security_advisor_kiloclaw_coverage_area_unique", + "nullsNotDistinct": false, + "columns": [ + "area" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_scans": { + "name": "security_advisor_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_platform": { + "name": "source_platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_method": { + "name": "source_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_ip": { + "name": "public_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "findings_critical": { + "name": "findings_critical", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_warn": { + "name": "findings_warn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_info": { + "name": "findings_info", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_advisor_scans_user_created_at": { + "name": "idx_security_advisor_scans_user_created_at", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_created_at": { + "name": "idx_security_advisor_scans_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_platform": { + "name": "idx_security_advisor_scans_platform", + "columns": [ + { + "expression": "source_platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_agent_commands": { + "name": "security_agent_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "operation_key": { + "name": "operation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'accepted'" + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_agent_commands_org_created": { + "name": "idx_security_agent_commands_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_user_created": { + "name": "idx_security_agent_commands_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_status_updated": { + "name": "idx_security_agent_commands_status_updated", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_finding_created": { + "name": "idx_security_agent_commands_finding_created", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_commands_org_operation_key": { + "name": "UQ_security_agent_commands_org_operation_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL AND \"security_agent_commands\".\"operation_key\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_security_agent_commands_user_operation_key": { + "name": "UQ_security_agent_commands_user_operation_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"operation_key\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_commands_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_commands_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_commands_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_finding_id_security_findings_id_fk": { + "name": "security_agent_commands_finding_id_security_findings_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_commands_owner_check": { + "name": "security_agent_commands_owner_check", + "value": "(\n (\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_commands\".\"owned_by_user_id\" IS NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_agent_commands_type_check": { + "name": "security_agent_commands_type_check", + "value": "\"security_agent_commands\".\"command_type\" IN ('sync', 'dismiss_finding', 'start_analysis', 'apply_auto_remediation')" + }, + "security_agent_commands_origin_check": { + "name": "security_agent_commands_origin_check", + "value": "\"security_agent_commands\".\"origin\" IN ('manual', 'dashboard_refresh', 'enable_initial_sync', 'settings_include_existing')" + }, + "security_agent_commands_status_check": { + "name": "security_agent_commands_status_check", + "value": "\"security_agent_commands\".\"status\" IN ('accepted', 'running', 'succeeded', 'failed', 'no_op')" + } + }, + "isRLSEnabled": false + }, + "public.security_agent_repository_sync_state": { + "name": "security_agent_repository_sync_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_succeeded_at": { + "name": "last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_agent_repository_sync_state_org_repo": { + "name": "UQ_security_agent_repository_sync_state_org_repo", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_repository_sync_state_user_repo": { + "name": "UQ_security_agent_repository_sync_state_user_repo", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_repository_sync_state_owner_check": { + "name": "security_agent_repository_sync_state_owner_check", + "value": "(\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_owner_state": { + "name": "security_analysis_owner_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_analysis_enabled_at": { + "name": "auto_analysis_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "block_reason": { + "name": "block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_actor_resolution_failures": { + "name": "consecutive_actor_resolution_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_actor_resolution_failure_at": { + "name": "last_actor_resolution_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_owner_state_org_owner": { + "name": "UQ_security_analysis_owner_state_org_owner", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_analysis_owner_state_user_owner": { + "name": "UQ_security_analysis_owner_state_user_owner", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_owner_state_owner_check": { + "name": "security_analysis_owner_state_owner_check", + "value": "(\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_owner_state_block_reason_check": { + "name": "security_analysis_owner_state_block_reason_check", + "value": "\"security_analysis_owner_state\".\"block_reason\" IS NULL OR \"security_analysis_owner_state\".\"block_reason\" IN ('INSUFFICIENT_CREDITS', 'ACTOR_RESOLUTION_FAILED', 'OPERATOR_PAUSE')" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_queue": { + "name": "security_analysis_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queue_status": { + "name": "queue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity_rank": { + "name": "severity_rank", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "admitted_config_revision": { + "name": "admitted_config_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reopen_requeue_count": { + "name": "reopen_requeue_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_queue_finding_id": { + "name": "UQ_security_analysis_queue_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_org": { + "name": "idx_security_analysis_queue_claim_path_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_user": { + "name": "idx_security_analysis_queue_claim_path_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_org": { + "name": "idx_security_analysis_queue_in_flight_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_user": { + "name": "idx_security_analysis_queue_in_flight_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_lag_dashboards": { + "name": "idx_security_analysis_queue_lag_dashboards", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_pending_reconciliation": { + "name": "idx_security_analysis_queue_pending_reconciliation", + "columns": [ + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_running_reconciliation": { + "name": "idx_security_analysis_queue_running_reconciliation", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_failure_trend": { + "name": "idx_security_analysis_queue_failure_trend", + "columns": [ + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"failure_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_queue_finding_id_security_findings_id_fk": { + "name": "security_analysis_queue_finding_id_security_findings_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_queue_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_queue_owner_check": { + "name": "security_analysis_queue_owner_check", + "value": "(\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_queue_status_check": { + "name": "security_analysis_queue_status_check", + "value": "\"security_analysis_queue\".\"queue_status\" IN ('queued', 'pending', 'running', 'failed', 'completed')" + }, + "security_analysis_queue_claim_token_required_check": { + "name": "security_analysis_queue_claim_token_required_check", + "value": "\"security_analysis_queue\".\"queue_status\" NOT IN ('pending', 'running') OR \"security_analysis_queue\".\"claim_token\" IS NOT NULL" + }, + "security_analysis_queue_attempt_count_non_negative_check": { + "name": "security_analysis_queue_attempt_count_non_negative_check", + "value": "\"security_analysis_queue\".\"attempt_count\" >= 0" + }, + "security_analysis_queue_reopen_requeue_count_non_negative_check": { + "name": "security_analysis_queue_reopen_requeue_count_non_negative_check", + "value": "\"security_analysis_queue\".\"reopen_requeue_count\" >= 0" + }, + "security_analysis_queue_severity_rank_check": { + "name": "security_analysis_queue_severity_rank_check", + "value": "\"security_analysis_queue\".\"severity_rank\" IN (0, 1, 2, 3)" + }, + "security_analysis_queue_failure_code_check": { + "name": "security_analysis_queue_failure_code_check", + "value": "\"security_analysis_queue\".\"failure_code\" IS NULL OR \"security_analysis_queue\".\"failure_code\" IN (\n 'NETWORK_TIMEOUT',\n 'UPSTREAM_5XX',\n 'TEMP_TOKEN_FAILURE',\n 'START_CALL_AMBIGUOUS',\n 'REQUEUE_TEMPORARY_PRECONDITION',\n 'ACTOR_RESOLUTION_FAILED',\n 'GITHUB_TOKEN_UNAVAILABLE',\n 'INVALID_CONFIG',\n 'MISSING_OWNERSHIP',\n 'PERMISSION_DENIED_PERMANENT',\n 'UNSUPPORTED_SEVERITY',\n 'INSUFFICIENT_CREDITS',\n 'STATE_GUARD_REJECTED',\n 'SKIPPED_ALREADY_IN_PROGRESS',\n 'SKIPPED_NO_LONGER_ELIGIBLE',\n 'REOPEN_LOOP_GUARD',\n 'RUN_LOST'\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_audit_log": { + "name": "security_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source_occurred_at": { + "name": "source_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "finding_snapshot": { + "name": "finding_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_context": { + "name": "source_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_security_audit_log_org_created": { + "name": "IDX_security_audit_log_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_created": { + "name": "IDX_security_audit_log_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_resource": { + "name": "IDX_security_audit_log_resource", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_actor": { + "name": "IDX_security_audit_log_actor", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_action": { + "name": "IDX_security_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_org_event_key": { + "name": "UQ_security_audit_log_org_event_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_user_event_key": { + "name": "UQ_security_audit_log_user_event_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_org_occurred": { + "name": "IDX_security_audit_log_org_occurred", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_occurred": { + "name": "IDX_security_audit_log_user_occurred", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_audit_log_owned_by_organization_id_organizations_id_fk": { + "name": "security_audit_log_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_audit_log_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_audit_log_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_audit_log_owner_check": { + "name": "security_audit_log_owner_check", + "value": "(\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NULL) OR (\"security_audit_log\".\"owned_by_user_id\" IS NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "security_audit_log_action_check": { + "name": "security_audit_log_action_check", + "value": "\"security_audit_log\".\"action\" IN ('security.finding.created', 'security.finding.severity_changed', 'security.finding.status_change', 'security.finding.dismissed', 'security.finding.auto_dismissed', 'security.finding.superseded', 'security.finding.analysis_started', 'security.finding.analysis_completed', 'security.finding.analysis_failed', 'security.remediation.queued', 'security.remediation.started', 'security.remediation.pr_opened', 'security.remediation.failed', 'security.remediation.blocked', 'security.remediation.no_changes_needed', 'security.remediation.cancelled', 'security.remediation.retried', 'security.finding.deleted', 'security.config.enabled', 'security.config.disabled', 'security.config.updated', 'security.sync.triggered', 'security.sync.completed', 'security.audit_log.exported', 'security.audit_report.generated')" + }, + "security_audit_log_actor_type_check": { + "name": "security_audit_log_actor_type_check", + "value": "\"security_audit_log\".\"actor_type\" IN ('customer_user', 'kilo_admin', 'system')" + }, + "security_audit_log_source_context_check": { + "name": "security_audit_log_source_context_check", + "value": "\"security_audit_log\".\"source_context\" IN ('security_sync', 'web', 'analysis_worker', 'remediation_callback', 'rollout_baseline')" + } + }, + "isRLSEnabled": false + }, + "public.security_finding_notifications": { + "name": "security_finding_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'staged'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_finding_notifications_finding_recipient_kind": { + "name": "uq_security_finding_notifications_finding_recipient_kind", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_pending": { + "name": "idx_security_finding_notifications_pending", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_staged": { + "name": "idx_security_finding_notifications_staged", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'staged'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_finding_id": { + "name": "idx_security_finding_notifications_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_recipient_user_id": { + "name": "idx_security_finding_notifications_recipient_user_id", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_finding_notifications_finding_fk": { + "name": "security_finding_notifications_finding_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_finding_notifications_recipient_fk": { + "name": "security_finding_notifications_recipient_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_finding_notifications_kind_check": { + "name": "security_finding_notifications_kind_check", + "value": "\"security_finding_notifications\".\"kind\" IN ('new_finding', 'sla_warning', 'sla_breach')" + }, + "security_finding_notifications_status_check": { + "name": "security_finding_notifications_status_check", + "value": "\"security_finding_notifications\".\"status\" IN ('staged', 'pending', 'sending', 'sent', 'failed', 'cancelled')" + }, + "security_finding_notifications_attempt_count_check": { + "name": "security_finding_notifications_attempt_count_check", + "value": "\"security_finding_notifications\".\"attempt_count\" >= 0" + }, + "security_finding_notifications_claimed_at_check": { + "name": "security_finding_notifications_claimed_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NULL)\n )" + }, + "security_finding_notifications_sent_at_check": { + "name": "security_finding_notifications_sent_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NULL)\n )" + }, + "security_finding_notifications_error_message_length_check": { + "name": "security_finding_notifications_error_message_length_check", + "value": "\"security_finding_notifications\".\"error_message\" IS NULL OR length(\"security_finding_notifications\".\"error_message\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.security_findings": { + "name": "security_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ghsa_id": { + "name": "ghsa_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cve_id": { + "name": "cve_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_ecosystem": { + "name": "package_ecosystem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vulnerable_version_range": { + "name": "vulnerable_version_range", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patched_version": { + "name": "patched_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest_path": { + "name": "manifest_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "ignored_reason": { + "name": "ignored_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignored_by": { + "name": "ignored_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fixed_at": { + "name": "fixed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sla_due_at": { + "name": "sla_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dependabot_html_url": { + "name": "dependabot_html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwe_ids": { + "name": "cwe_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cvss_score": { + "name": "cvss_score", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": false + }, + "dependency_scope": { + "name": "dependency_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_status": { + "name": "analysis_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_started_at": { + "name": "analysis_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_error": { + "name": "analysis_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis": { + "name": "analysis", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_detected_at": { + "name": "first_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_findings_user_source": { + "name": "uq_security_findings_user_source", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_security_findings_org_source": { + "name": "uq_security_findings_org_source", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_id": { + "name": "idx_security_findings_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_id": { + "name": "idx_security_findings_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_repo": { + "name": "idx_security_findings_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_severity": { + "name": "idx_security_findings_severity", + "columns": [ + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_status": { + "name": "idx_security_findings_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_package": { + "name": "idx_security_findings_package", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_sla_due_at": { + "name": "idx_security_findings_sla_due_at", + "columns": [ + { + "expression": "sla_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_session_id": { + "name": "idx_security_findings_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_cli_session_id": { + "name": "idx_security_findings_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_analysis_status": { + "name": "idx_security_findings_analysis_status", + "columns": [ + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_analysis_in_flight": { + "name": "idx_security_findings_org_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_analysis_in_flight": { + "name": "idx_security_findings_user_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_findings_owned_by_organization_id_organizations_id_fk": { + "name": "security_findings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_findings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_findings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_findings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_platform_integration_id_platform_integrations_id_fk": { + "name": "security_findings_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "security_findings", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_findings_owner_check": { + "name": "security_findings_owner_check", + "value": "(\n (\"security_findings\".\"owned_by_user_id\" IS NOT NULL AND \"security_findings\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_findings\".\"owned_by_user_id\" IS NULL AND \"security_findings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_remediation_attempts": { + "name": "security_remediation_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "remediation_id": { + "name": "remediation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_fingerprint": { + "name": "analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "remediation_model_slug": { + "name": "remediation_model_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_attempt_count": { + "name": "launch_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "callback_attempt_token_hash": { + "name": "callback_attempt_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_result": { + "name": "structured_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "final_assistant_message": { + "name": "final_assistant_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "validation_evidence": { + "name": "validation_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "risk_notes": { + "name": "risk_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_reason": { + "name": "draft_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_by_user_id": { + "name": "cancellation_requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "launched_at": { + "name": "launched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediation_attempts_number": { + "name": "UQ_security_remediation_attempts_number", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_finding": { + "name": "UQ_security_remediation_attempts_active_finding", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_remediation": { + "name": "UQ_security_remediation_attempts_active_remediation", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_finding_fingerprint_terminal": { + "name": "UQ_security_remediation_attempts_finding_fingerprint_terminal", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_claim": { + "name": "idx_security_remediation_attempts_org_claim", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_claim": { + "name": "idx_security_remediation_attempts_user_claim", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_claim": { + "name": "idx_security_remediation_attempts_repo_claim", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_inflight": { + "name": "idx_security_remediation_attempts_org_inflight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_inflight": { + "name": "idx_security_remediation_attempts_user_inflight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_inflight": { + "name": "idx_security_remediation_attempts_repo_inflight", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_cloud_agent_session": { + "name": "idx_security_remediation_attempts_cloud_agent_session", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_finding_fingerprint": { + "name": "idx_security_remediation_attempts_finding_fingerprint", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediation_attempts_remediation_id_security_remediations_id_fk": { + "name": "security_remediation_attempts_remediation_id_security_remediations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_remediations", + "columnsFrom": [ + "remediation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_finding_id_security_findings_id_fk": { + "name": "security_remediation_attempts_finding_id_security_findings_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediation_attempts_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "cancellation_requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediation_attempts_owner_check": { + "name": "security_remediation_attempts_owner_check", + "value": "(\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediation_attempts_status_check": { + "name": "security_remediation_attempts_status_check", + "value": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + }, + "security_remediation_attempts_origin_check": { + "name": "security_remediation_attempts_origin_check", + "value": "\"security_remediation_attempts\".\"origin\" IN ('auto_policy', 'bulk_existing', 'manual')" + }, + "security_remediation_attempts_attempt_number_check": { + "name": "security_remediation_attempts_attempt_number_check", + "value": "\"security_remediation_attempts\".\"attempt_number\" >= 1" + }, + "security_remediation_attempts_launch_attempt_count_check": { + "name": "security_remediation_attempts_launch_attempt_count_check", + "value": "\"security_remediation_attempts\".\"launch_attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.security_remediations": { + "name": "security_remediations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "latest_attempt_id": { + "name": "latest_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_fingerprint": { + "name": "latest_analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_completed_at": { + "name": "latest_analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_summary": { + "name": "outcome_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediations_finding_id": { + "name": "UQ_security_remediations_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_org_status": { + "name": "idx_security_remediations_org_status", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_user_status": { + "name": "idx_security_remediations_user_status", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_repo_status": { + "name": "idx_security_remediations_repo_status", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_latest_attempt": { + "name": "idx_security_remediations_latest_attempt", + "columns": [ + { + "expression": "latest_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediations_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_finding_id_security_findings_id_fk": { + "name": "security_remediations_finding_id_security_findings_id_fk", + "tableFrom": "security_remediations", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediations_owner_check": { + "name": "security_remediations_owner_check", + "value": "(\n (\"security_remediations\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediations\".\"owned_by_user_id\" IS NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediations_status_check": { + "name": "security_remediations_status_check", + "value": "\"security_remediations\".\"status\" IN ('queued', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.shared_cli_sessions": { + "name": "shared_cli_sessions", + "schema": "", + "columns": { + "share_id": { + "name": "share_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_state": { + "name": "shared_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_shared_cli_sessions_session_id": { + "name": "IDX_shared_cli_sessions_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_shared_cli_sessions_created_at": { + "name": "IDX_shared_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_cli_sessions_session_id_cli_sessions_session_id_fk": { + "name": "shared_cli_sessions_session_id_cli_sessions_session_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_cli_sessions_shared_state_check": { + "name": "shared_cli_sessions_shared_state_check", + "value": "\"shared_cli_sessions\".\"shared_state\" IN ('public', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.slack_bot_requests": { + "name": "slack_bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_name": { + "name": "slack_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message_truncated": { + "name": "user_message_truncated", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_calls_made": { + "name": "tool_calls_made", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_slack_bot_requests_created_at": { + "name": "idx_slack_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_slack_team_id": { + "name": "idx_slack_bot_requests_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_org_id": { + "name": "idx_slack_bot_requests_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_user_id": { + "name": "idx_slack_bot_requests_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_status": { + "name": "idx_slack_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_event_type": { + "name": "idx_slack_bot_requests_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_team_created": { + "name": "idx_slack_bot_requests_team_created", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_bot_requests_owned_by_organization_id_organizations_id_fk": { + "name": "slack_bot_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_bot_requests_owner_check": { + "name": "slack_bot_requests_owner_check", + "value": "(\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NOT NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NOT NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.slack_oauth_credentials": { + "name": "slack_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_enterprise_id": { + "name": "slack_enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enterprise_install": { + "name": "is_enterprise_install", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "refresh_claimed_at": { + "name": "refresh_claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_attempt_count": { + "name": "refresh_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_refresh_attempt_at": { + "name": "next_refresh_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_slack_oauth_credentials_platform_integration_id": { + "name": "UQ_slack_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_slack_oauth_credentials_slack_team_id": { + "name": "IDX_slack_oauth_credentials_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_slack_oauth_credentials_refresh_due": { + "name": "IDX_slack_oauth_credentials_refresh_due", + "columns": [ + { + "expression": "access_token_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"slack_oauth_credentials\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_oauth_credentials_credential_version_check": { + "name": "slack_oauth_credentials_credential_version_check", + "value": "\"slack_oauth_credentials\".\"credential_version\" > 0" + }, + "slack_oauth_credentials_refresh_attempt_count_check": { + "name": "slack_oauth_credentials_refresh_attempt_count_check", + "value": "\"slack_oauth_credentials\".\"refresh_attempt_count\" >= 0" + }, + "slack_oauth_credentials_slack_team_id_check": { + "name": "slack_oauth_credentials_slack_team_id_check", + "value": "\"slack_oauth_credentials\".\"slack_team_id\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.source_embeddings": { + "name": "source_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_base_branch": { + "name": "is_base_branch", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_source_embeddings_organization_id": { + "name": "IDX_source_embeddings_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_kilo_user_id": { + "name": "IDX_source_embeddings_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_project_id": { + "name": "IDX_source_embeddings_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_created_at": { + "name": "IDX_source_embeddings_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_updated_at": { + "name": "IDX_source_embeddings_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_file_path_lower": { + "name": "IDX_source_embeddings_file_path_lower", + "columns": [ + { + "expression": "LOWER(\"file_path\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_git_branch": { + "name": "IDX_source_embeddings_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_org_project_branch": { + "name": "IDX_source_embeddings_org_project_branch", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "source_embeddings_organization_id_organizations_id_fk": { + "name": "source_embeddings_organization_id_organizations_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "source_embeddings_kilo_user_id_kilocode_users_id_fk": { + "name": "source_embeddings_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_source_embeddings_org_project_branch_file_lines": { + "name": "UQ_source_embeddings_org_project_branch_file_lines", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "project_id", + "git_branch", + "file_path", + "start_line", + "end_line" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stripe_dispute_actions": { + "name": "stripe_dispute_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_actions_case_id": { + "name": "IDX_stripe_dispute_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_actions_claim_path": { + "name": "IDX_stripe_dispute_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk": { + "name": "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk", + "tableFrom": "stripe_dispute_actions", + "tableTo": "stripe_dispute_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_actions_case_type_target": { + "name": "UQ_stripe_dispute_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_actions_action_type_check": { + "name": "stripe_dispute_actions_action_type_check", + "value": "\"stripe_dispute_actions\".\"action_type\" IN ('stripe_acceptance', 'user_block', 'auto_top_up_disable', 'credit_balance_reset', 'subscription_cancellation', 'access_termination', 'kiloclaw_suspension')" + }, + "stripe_dispute_actions_status_check": { + "name": "stripe_dispute_actions_status_check", + "value": "\"stripe_dispute_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'skipped')" + }, + "stripe_dispute_actions_attempt_count_non_negative_check": { + "name": "stripe_dispute_actions_attempt_count_non_negative_check", + "value": "\"stripe_dispute_actions\".\"attempt_count\" >= 0" + }, + "stripe_dispute_actions_target_key_not_empty_check": { + "name": "stripe_dispute_actions_target_key_not_empty_check", + "value": "length(\"stripe_dispute_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_dispute_cases": { + "name": "stripe_dispute_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_dispute_id": { + "name": "stripe_dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_created_at": { + "name": "stripe_event_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispute_reason": { + "name": "dispute_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_status": { + "name": "stripe_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'needs_action'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_created_at": { + "name": "stripe_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evidence_due_by": { + "name": "evidence_due_by", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by_kilo_user_id": { + "name": "accepted_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance_started_at": { + "name": "acceptance_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enforcement_completed_at": { + "name": "enforcement_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_cases_event_id": { + "name": "IDX_stripe_dispute_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_charge_id": { + "name": "IDX_stripe_dispute_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_payment_intent_id": { + "name": "IDX_stripe_dispute_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_customer_id": { + "name": "IDX_stripe_dispute_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_kilo_user_id": { + "name": "IDX_stripe_dispute_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_organization_id": { + "name": "IDX_stripe_dispute_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_status_due_by": { + "name": "IDX_stripe_dispute_cases_status_due_by", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "evidence_due_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_organization_id_organizations_id_fk": { + "name": "stripe_dispute_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "accepted_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_cases_dispute_id": { + "name": "UQ_stripe_dispute_cases_dispute_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_dispute_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_cases_owner_classification_check": { + "name": "stripe_dispute_cases_owner_classification_check", + "value": "\"stripe_dispute_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_dispute_cases_status_check": { + "name": "stripe_dispute_cases_status_check", + "value": "\"stripe_dispute_cases\".\"status\" IN ('needs_action', 'processing', 'accepted', 'acceptance_failed', 'enforcement_failed', 'review_required', 'closed')" + }, + "stripe_dispute_cases_amount_minor_units_non_negative_check": { + "name": "stripe_dispute_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_dispute_cases\".\"amount_minor_units\" IS NULL OR \"stripe_dispute_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_actions": { + "name": "stripe_early_fraud_warning_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_actions_case_id": { + "name": "IDX_stripe_early_fraud_warning_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_actions_claim_path": { + "name": "IDX_stripe_early_fraud_warning_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk": { + "name": "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk", + "tableFrom": "stripe_early_fraud_warning_actions", + "tableTo": "stripe_early_fraud_warning_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_actions_case_type_target": { + "name": "UQ_stripe_early_fraud_warning_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_actions_action_type_check": { + "name": "stripe_early_fraud_warning_actions_action_type_check", + "value": "\"stripe_early_fraud_warning_actions\".\"action_type\" IN ('containment', 'refund', 'payment_value_clawback', 'subscription_termination', 'access_termination', 'kiloclaw_suspension', 'affiliate_payout_reversal', 'referral_reward_reversal', 'user_notice')" + }, + "stripe_early_fraud_warning_actions_status_check": { + "name": "stripe_early_fraud_warning_actions_status_check", + "value": "\"stripe_early_fraud_warning_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'review_required', 'dismissed')" + }, + "stripe_early_fraud_warning_actions_attempt_count_non_negative_check": { + "name": "stripe_early_fraud_warning_actions_attempt_count_non_negative_check", + "value": "\"stripe_early_fraud_warning_actions\".\"attempt_count\" >= 0" + }, + "stripe_early_fraud_warning_actions_target_key_not_empty_check": { + "name": "stripe_early_fraud_warning_actions_target_key_not_empty_check", + "value": "length(\"stripe_early_fraud_warning_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_cases": { + "name": "stripe_early_fraud_warning_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_early_fraud_warning_id": { + "name": "stripe_early_fraud_warning_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warning_created_at": { + "name": "warning_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "contained_at": { + "name": "contained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remediated_at": { + "name": "remediated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_cases_event_id": { + "name": "IDX_stripe_early_fraud_warning_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_charge_id": { + "name": "IDX_stripe_early_fraud_warning_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_payment_intent_id": { + "name": "IDX_stripe_early_fraud_warning_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_customer_id": { + "name": "IDX_stripe_early_fraud_warning_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_kilo_user_id": { + "name": "IDX_stripe_early_fraud_warning_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_organization_id": { + "name": "IDX_stripe_early_fraud_warning_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_status_created_at": { + "name": "IDX_stripe_early_fraud_warning_cases_status_created_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk": { + "name": "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_cases_warning_id": { + "name": "UQ_stripe_early_fraud_warning_cases_warning_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_early_fraud_warning_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_cases_owner_classification_check": { + "name": "stripe_early_fraud_warning_cases_owner_classification_check", + "value": "\"stripe_early_fraud_warning_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_early_fraud_warning_cases_status_check": { + "name": "stripe_early_fraud_warning_cases_status_check", + "value": "\"stripe_early_fraud_warning_cases\".\"status\" IN ('queued', 'contained', 'processing', 'completed', 'review_required', 'failed', 'remediated', 'dismissed')" + }, + "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check": { + "name": "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_early_fraud_warning_cases\".\"amount_minor_units\" IS NULL OR \"stripe_early_fraud_warning_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stytch_fingerprints": { + "name": "stytch_fingerprints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_fingerprint": { + "name": "visitor_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_fingerprint": { + "name": "browser_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_id": { + "name": "browser_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hardware_fingerprint": { + "name": "hardware_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network_fingerprint": { + "name": "network_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_id": { + "name": "visitor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verdict_action": { + "name": "verdict_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_device_type": { + "name": "detected_device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_authentic_device": { + "name": "is_authentic_device", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"\"}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fingerprint_data": { + "name": "fingerprint_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_free_tier_allowed": { + "name": "kilo_free_tier_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_hardware_fingerprint": { + "name": "idx_hardware_fingerprint", + "columns": [ + { + "expression": "hardware_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id": { + "name": "idx_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_stytch_fingerprints_reasons_gin": { + "name": "idx_stytch_fingerprints_reasons_gin", + "columns": [ + { + "expression": "reasons", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_verdict_action": { + "name": "idx_verdict_action", + "columns": [ + { + "expression": "verdict_action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_visitor_fingerprint": { + "name": "idx_visitor_fingerprint", + "columns": [ + { + "expression": "visitor_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_prompt_prefix": { + "name": "system_prompt_prefix", + "schema": "", + "columns": { + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_system_prompt_prefix": { + "name": "UQ_system_prompt_prefix", + "columns": [ + { + "expression": "system_prompt_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactional_email_log": { + "name": "transactional_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_transactional_email_log_type_idempotency_key": { + "name": "UQ_transactional_email_log_type_idempotency_key", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_user_id": { + "name": "IDX_transactional_email_log_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_organization_id": { + "name": "IDX_transactional_email_log_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactional_email_log_user_id_kilocode_users_id_fk": { + "name": "transactional_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactional_email_log_organization_id_organizations_id_fk": { + "name": "transactional_email_log_organization_id_organizations_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_transactional_email_log_owner": { + "name": "CHK_transactional_email_log_owner", + "value": "\"transactional_email_log\".\"user_id\" IS NOT NULL OR \"transactional_email_log\".\"organization_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.user_activity_tokens": { + "name": "user_activity_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_activity_tokens_token": { + "name": "UQ_user_activity_tokens_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_activity_tokens_user_org": { + "name": "IDX_user_activity_tokens_user_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_activity_tokens_user_id_kilocode_users_id_fk": { + "name": "user_activity_tokens_user_id_kilocode_users_id_fk", + "tableFrom": "user_activity_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_admin_notes": { + "name": "user_admin_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note_content": { + "name": "note_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_kilo_user_id": { + "name": "admin_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_34517df0b385234babc38fe81b": { + "name": "IDX_34517df0b385234babc38fe81b", + "columns": [ + { + "expression": "admin_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_ccbde98c4c14046daa5682ec4f": { + "name": "IDX_ccbde98c4c14046daa5682ec4f", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_d0270eb24ef6442d65a0b7853c": { + "name": "IDX_d0270eb24ef6442d65a0b7853c", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_affiliate_attributions": { + "name": "user_affiliate_attributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_id": { + "name": "tracking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_attributions_user_id": { + "name": "IDX_user_affiliate_attributions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_attributions_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_attributions_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_attributions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_attributions_user_provider": { + "name": "UQ_user_affiliate_attributions_user_provider", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_attributions_provider_check": { + "name": "user_affiliate_attributions_provider_check", + "value": "\"user_affiliate_attributions\".\"provider\" IN ('impact')" + } + }, + "isRLSEnabled": false + }, + "public.user_affiliate_events": { + "name": "user_affiliate_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_event_id": { + "name": "parent_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_action_id": { + "name": "impact_action_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_submission_uri": { + "name": "impact_submission_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_events_claim_path": { + "name": "IDX_user_affiliate_events_claim_path", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_parent_event_id": { + "name": "IDX_user_affiliate_events_parent_event_id", + "columns": [ + { + "expression": "parent_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_provider_event_type_charge": { + "name": "IDX_user_affiliate_events_provider_event_type_charge", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_events_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_events_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_affiliate_events_parent_event_id_fk": { + "name": "user_affiliate_events_parent_event_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "user_affiliate_events", + "columnsFrom": [ + "parent_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_events_dedupe_key": { + "name": "UQ_user_affiliate_events_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_events_provider_check": { + "name": "user_affiliate_events_provider_check", + "value": "\"user_affiliate_events\".\"provider\" IN ('impact')" + }, + "user_affiliate_events_event_type_check": { + "name": "user_affiliate_events_event_type_check", + "value": "\"user_affiliate_events\".\"event_type\" IN ('signup', 'trial_start', 'trial_end', 'sale', 'sale_reversal')" + }, + "user_affiliate_events_delivery_state_check": { + "name": "user_affiliate_events_delivery_state_check", + "value": "\"user_affiliate_events\".\"delivery_state\" IN ('queued', 'blocked', 'sending', 'delivered', 'failed')" + }, + "user_affiliate_events_attempt_count_non_negative_check": { + "name": "user_affiliate_events_attempt_count_non_negative_check", + "value": "\"user_affiliate_events\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_auth_provider": { + "name": "user_auth_provider", + "schema": "", + "columns": { + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_auth_provider_kilo_user_id": { + "name": "IDX_user_auth_provider_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_hosted_domain": { + "name": "IDX_user_auth_provider_hosted_domain", + "columns": [ + { + "expression": "hosted_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_lower_email": { + "name": "IDX_user_auth_provider_lower_email", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_auth_provider_provider_provider_account_id_pk": { + "name": "user_auth_provider_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_data_export_object_deletions": { + "name": "user_data_export_object_deletions", + "schema": "", + "columns": { + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'account_deletion'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_data_export_object_deletions_ready": { + "name": "IDX_user_data_export_object_deletions_ready", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_export_object_deletions_reason_check": { + "name": "user_data_export_object_deletions_reason_check", + "value": "\"user_data_export_object_deletions\".\"reason\" IN ('account_deletion', 'admin_cancel', 'admin_replace')" + }, + "user_data_export_object_deletions_attempt_count_nonnegative": { + "name": "user_data_export_object_deletions_attempt_count_nonnegative", + "value": "\"user_data_export_object_deletions\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_export_outbox": { + "name": "user_data_export_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "export_id": { + "name": "export_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'generate'" + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_data_export_outbox_pending": { + "name": "IDX_user_data_export_outbox_pending", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_export_outbox\".\"sent_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_data_export_outbox_export_id_user_data_exports_id_fk": { + "name": "user_data_export_outbox_export_id_user_data_exports_id_fk", + "tableFrom": "user_data_export_outbox", + "tableTo": "user_data_exports", + "columnsFrom": [ + "export_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_data_export_outbox_generation_operation": { + "name": "UQ_user_data_export_outbox_generation_operation", + "nullsNotDistinct": false, + "columns": [ + "export_id", + "generation", + "operation" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_data_export_outbox_operation_check": { + "name": "user_data_export_outbox_operation_check", + "value": "\"user_data_export_outbox\".\"operation\" = 'generate'" + }, + "user_data_export_outbox_generation_nonnegative": { + "name": "user_data_export_outbox_generation_nonnegative", + "value": "\"user_data_export_outbox\".\"generation\" >= 0" + }, + "user_data_export_outbox_attempt_count_nonnegative": { + "name": "user_data_export_outbox_attempt_count_nonnegative", + "value": "\"user_data_export_outbox\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_export_parts": { + "name": "user_data_export_parts", + "schema": "", + "columns": { + "export_id": { + "name": "export_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "part_number": { + "name": "part_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_data_export_parts_export_id_user_data_exports_id_fk": { + "name": "user_data_export_parts_export_id_user_data_exports_id_fk", + "tableFrom": "user_data_export_parts", + "tableTo": "user_data_exports", + "columnsFrom": [ + "export_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_data_export_parts_export_id_part_number_pk": { + "name": "user_data_export_parts_export_id_part_number_pk", + "columns": [ + "export_id", + "part_number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_export_parts_part_number_positive": { + "name": "user_data_export_parts_part_number_positive", + "value": "\"user_data_export_parts\".\"part_number\" > 0" + }, + "user_data_export_parts_size_bytes_nonnegative": { + "name": "user_data_export_parts_size_bytes_nonnegative", + "value": "\"user_data_export_parts\".\"size_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_exports": { + "name": "user_data_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "snapshot_at": { + "name": "snapshot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "current_source": { + "name": "current_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_cursor": { + "name": "source_cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_part_number": { + "name": "next_part_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "dispatch_generation": { + "name": "dispatch_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "row_count": { + "name": "row_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "r2_object_key": { + "name": "r2_object_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "r2_etag": { + "name": "r2_etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_status": { + "name": "email_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "email_attempt_count": { + "name": "email_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "email_lease_token": { + "name": "email_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_lease_expires_at": { + "name": "email_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_sent_at": { + "name": "email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_data_exports_single_active": { + "name": "UQ_user_data_exports_single_active", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing') AND \"user_data_exports\".\"subject_type\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_data_exports_single_active_org": { + "name": "UQ_user_data_exports_single_active_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing') AND \"user_data_exports\".\"subject_type\" = 'organization'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_user_created": { + "name": "IDX_user_data_exports_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_org_created": { + "name": "IDX_user_data_exports_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_lease_expiry": { + "name": "IDX_user_data_exports_lease_expiry", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" IN ('processing', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_ready_expiry": { + "name": "IDX_user_data_exports_ready_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" = 'ready'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_failed_multipart": { + "name": "IDX_user_data_exports_failed_multipart", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" = 'failed' AND \"user_data_exports\".\"multipart_upload_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_email_lease_expiry": { + "name": "IDX_user_data_exports_email_lease_expiry", + "columns": [ + { + "expression": "email_lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"email_status\" = 'sending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_data_exports_kilo_user_id_kilocode_users_id_fk": { + "name": "user_data_exports_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_data_exports", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "user_data_exports_organization_id_organizations_id_fk": { + "name": "user_data_exports_organization_id_organizations_id_fk", + "tableFrom": "user_data_exports", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_exports_status_check": { + "name": "user_data_exports_status_check", + "value": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing', 'ready', 'failed', 'expired')" + }, + "user_data_exports_subject_type_check": { + "name": "user_data_exports_subject_type_check", + "value": "\"user_data_exports\".\"subject_type\" IN ('user', 'organization')" + }, + "user_data_exports_subject_shape": { + "name": "user_data_exports_subject_shape", + "value": "(\"user_data_exports\".\"subject_type\" = 'user' AND \"user_data_exports\".\"organization_id\" IS NULL)\n OR (\"user_data_exports\".\"subject_type\" = 'organization' AND \"user_data_exports\".\"organization_id\" IS NOT NULL)" + }, + "user_data_exports_schema_version_positive": { + "name": "user_data_exports_schema_version_positive", + "value": "\"user_data_exports\".\"schema_version\" > 0" + }, + "user_data_exports_next_part_number_positive": { + "name": "user_data_exports_next_part_number_positive", + "value": "\"user_data_exports\".\"next_part_number\" > 0" + }, + "user_data_exports_dispatch_generation_nonnegative": { + "name": "user_data_exports_dispatch_generation_nonnegative", + "value": "\"user_data_exports\".\"dispatch_generation\" >= 0" + }, + "user_data_exports_attempt_count_nonnegative": { + "name": "user_data_exports_attempt_count_nonnegative", + "value": "\"user_data_exports\".\"attempt_count\" >= 0" + }, + "user_data_exports_row_count_nonnegative": { + "name": "user_data_exports_row_count_nonnegative", + "value": "\"user_data_exports\".\"row_count\" >= 0" + }, + "user_data_exports_size_bytes_nonnegative": { + "name": "user_data_exports_size_bytes_nonnegative", + "value": "\"user_data_exports\".\"size_bytes\" IS NULL OR \"user_data_exports\".\"size_bytes\" >= 0" + }, + "user_data_exports_lease_shape": { + "name": "user_data_exports_lease_shape", + "value": "(\"user_data_exports\".\"lease_token\" IS NULL) = (\"user_data_exports\".\"lease_expires_at\" IS NULL)" + }, + "user_data_exports_ready_shape": { + "name": "user_data_exports_ready_shape", + "value": "\"user_data_exports\".\"status\" <> 'ready' OR (\"user_data_exports\".\"r2_object_key\" IS NOT NULL AND \"user_data_exports\".\"size_bytes\" IS NOT NULL AND \"user_data_exports\".\"completed_at\" IS NOT NULL AND \"user_data_exports\".\"expires_at\" IS NOT NULL)" + }, + "user_data_exports_last_error_redacted_length": { + "name": "user_data_exports_last_error_redacted_length", + "value": "\"user_data_exports\".\"last_error_redacted\" IS NULL OR length(\"user_data_exports\".\"last_error_redacted\") <= 500" + }, + "user_data_exports_email_attempt_count_nonnegative": { + "name": "user_data_exports_email_attempt_count_nonnegative", + "value": "\"user_data_exports\".\"email_attempt_count\" >= 0" + }, + "user_data_exports_email_status_check": { + "name": "user_data_exports_email_status_check", + "value": "\"user_data_exports\".\"email_status\" IN ('pending', 'sending', 'sent', 'failed')" + }, + "user_data_exports_email_lease_shape": { + "name": "user_data_exports_email_lease_shape", + "value": "(\"user_data_exports\".\"email_status\" = 'sending') = (\"user_data_exports\".\"email_lease_token\" IS NOT NULL AND \"user_data_exports\".\"email_lease_expires_at\" IS NOT NULL)" + }, + "user_data_exports_email_sent_shape": { + "name": "user_data_exports_email_sent_shape", + "value": "(\"user_data_exports\".\"email_status\" = 'sent') = (\"user_data_exports\".\"email_sent_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_activity": { + "name": "user_deletion_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_key": { + "name": "step_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details_json": { + "name": "details_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_deletion_activity_request_created": { + "name": "IDX_user_deletion_activity_request_created", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_activity_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_activity_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_activity", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_deletion_audit_events": { + "name": "user_deletion_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email_hmac": { + "name": "target_email_hmac", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_key": { + "name": "subject_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details_json": { + "name": "details_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_deletion_audit_events_idempotent": { + "name": "UQ_user_deletion_audit_events_idempotent", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_audit_events\".\"request_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_audit_events_request_id": { + "name": "IDX_user_deletion_audit_events_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_audit_events_hmac": { + "name": "IDX_user_deletion_audit_events_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_audit_events_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_audit_events_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_audit_events", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_audit_events_event_type_check": { + "name": "user_deletion_audit_events_event_type_check", + "value": "\"user_deletion_audit_events\".\"event_type\" IN ('request_created', 'intake_refused', 'access_disabled', 'access_absent', 'preflight_disposition', 'task_disposition', 'manual_retry', 'manual_action', 'anonymized', 'deletion_ready_for_customer_reply', 'cancelled', 'completed')" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_provider_credentials": { + "name": "user_deletion_provider_credentials", + "schema": "", + "columns": { + "provider_scope": { + "name": "provider_scope", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "encrypted_material": { + "name": "encrypted_material", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_kilo_user_id": { + "name": "updated_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_deletion_provider_credentials_updated_by_kilo_user_id_kilocode_users_id_fk": { + "name": "user_deletion_provider_credentials_updated_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_provider_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "updated_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_provider_credentials_scope_check": { + "name": "user_deletion_provider_credentials_scope_check", + "value": "\"user_deletion_provider_credentials\".\"provider_scope\" IN ('kiloclaw', 'customerio', 'cloud_storage', 'session_ingest', 'posthog', 'substack', 'pylon', 'csa')" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_requests": { + "name": "user_deletion_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "catalog_version": { + "name": "catalog_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requested_by_kilo_user_id": { + "name": "requested_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_email": { + "name": "requested_by_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email_hmac": { + "name": "target_email_hmac", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pylon_ticket_ref": { + "name": "pylon_ticket_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_subject_resolution": { + "name": "cloud_subject_resolution", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_subject_proof_ref": { + "name": "cloud_subject_proof_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preflight_attention_code": { + "name": "preflight_attention_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_progress_at": { + "name": "last_progress_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "anonymized_at": { + "name": "anonymized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_user_deletion_requests_active_email_hmac": { + "name": "UQ_user_deletion_requests_active_email_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"target_email_hmac\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_deletion_requests_active_user_id": { + "name": "UQ_user_deletion_requests_active_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"user_id\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_deletion_requests_active_pylon_ticket": { + "name": "UQ_user_deletion_requests_active_pylon_ticket", + "columns": [ + { + "expression": "regexp_replace(\"pylon_ticket_ref\", '^#', '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"pylon_ticket_ref\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_fairness": { + "name": "IDX_user_deletion_requests_fairness", + "columns": [ + { + "expression": "last_progress_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_email_hmac": { + "name": "IDX_user_deletion_requests_email_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_user_id": { + "name": "IDX_user_deletion_requests_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_requests_user_id_kilocode_users_id_fk": { + "name": "user_deletion_requests_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_deletion_requests_requested_by_kilo_user_id_kilocode_users_id_fk": { + "name": "user_deletion_requests_requested_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_requests_status_check": { + "name": "user_deletion_requests_status_check", + "value": "\"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing', 'completed', 'cancelled')" + }, + "user_deletion_requests_cloud_subject_resolution_check": { + "name": "user_deletion_requests_cloud_subject_resolution_check", + "value": "\"user_deletion_requests\".\"cloud_subject_resolution\" IN ('current_user', 'authoritative_absence', 'prior_queue_cleanup', 'legacy_identity_unresolved', 'unresolved')" + }, + "user_deletion_requests_catalog_version_positive": { + "name": "user_deletion_requests_catalog_version_positive", + "value": "\"user_deletion_requests\".\"catalog_version\" >= 1" + }, + "user_deletion_requests_completed_at_check": { + "name": "user_deletion_requests_completed_at_check", + "value": "(\"user_deletion_requests\".\"status\" = 'completed') = (\"user_deletion_requests\".\"completed_at\" IS NOT NULL)" + }, + "user_deletion_requests_cancelled_at_check": { + "name": "user_deletion_requests_cancelled_at_check", + "value": "(\"user_deletion_requests\".\"status\" = 'cancelled') = (\"user_deletion_requests\".\"cancelled_at\" IS NOT NULL)" + }, + "user_deletion_requests_active_email_check": { + "name": "user_deletion_requests_active_email_check", + "value": "(\"user_deletion_requests\".\"status\" NOT IN ('in_progress', 'finalizing') OR \"user_deletion_requests\".\"target_email\" IS NOT NULL) AND (\"user_deletion_requests\".\"status\" NOT IN ('completed', 'cancelled') OR \"user_deletion_requests\".\"target_email\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_steps": { + "name": "user_deletion_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_key": { + "name": "step_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "claimed_until": { + "name": "claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "window_attempt_count": { + "name": "window_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lifetime_attempt_count": { + "name": "lifetime_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "progress_json": { + "name": "progress_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rate_limited_since": { + "name": "rate_limited_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manual_evidence_json": { + "name": "manual_evidence_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_deletion_steps_due": { + "name": "IDX_user_deletion_steps_due", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_deletion_steps\".\"status\" IN ('pending', 'retry_wait', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_steps_request_id": { + "name": "IDX_user_deletion_steps_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_steps_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_steps_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_steps", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_deletion_steps_request_step": { + "name": "UQ_user_deletion_steps_request_step", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "step_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_deletion_steps_step_key_check": { + "name": "user_deletion_steps_step_key_check", + "value": "\"user_deletion_steps\".\"step_key\" IN ('kiloclaw_destroy', 'customerio', 'cli_v1_blobs', 'cli_v2_sessions', 'usage_prompt_prefixes', 'posthog', 'substack', 'anonymize', 'pylon_reply', 'pylon_finalize', 'completion_email', 'pylon_contact', 'csa_support_db')" + }, + "user_deletion_steps_status_check": { + "name": "user_deletion_steps_status_check", + "value": "\"user_deletion_steps\".\"status\" IN ('pending', 'running', 'retry_wait', 'needs_attention', 'manual_action_required', 'succeeded', 'not_applicable', 'manually_verified')" + }, + "user_deletion_steps_window_attempt_count_nonnegative": { + "name": "user_deletion_steps_window_attempt_count_nonnegative", + "value": "\"user_deletion_steps\".\"window_attempt_count\" >= 0" + }, + "user_deletion_steps_lifetime_attempt_count_nonnegative": { + "name": "user_deletion_steps_lifetime_attempt_count_nonnegative", + "value": "\"user_deletion_steps\".\"lifetime_attempt_count\" >= 0" + }, + "user_deletion_steps_claim_fields_check": { + "name": "user_deletion_steps_claim_fields_check", + "value": "(\"user_deletion_steps\".\"claim_token\" IS NULL) = (\"user_deletion_steps\".\"claimed_until\" IS NULL)" + }, + "user_deletion_steps_manual_evidence_check": { + "name": "user_deletion_steps_manual_evidence_check", + "value": "(\"user_deletion_steps\".\"status\" = 'manually_verified') = (\"user_deletion_steps\".\"manual_evidence_json\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_feedback": { + "name": "user_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feedback_for": { + "name": "feedback_for", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "feedback_batch": { + "name": "feedback_batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_feedback_created_at": { + "name": "IDX_user_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_kilo_user_id": { + "name": "IDX_user_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_for": { + "name": "IDX_user_feedback_feedback_for", + "columns": [ + { + "expression": "feedback_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_batch": { + "name": "IDX_user_feedback_feedback_batch", + "columns": [ + { + "expression": "feedback_batch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_source": { + "name": "IDX_user_feedback_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "user_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_github_app_tokens": { + "name": "user_github_app_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_github_app_tokens_user_app": { + "name": "UQ_user_github_app_tokens_user_app", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_github_app_tokens_github_user_app": { + "name": "UQ_user_github_app_tokens_github_user_app", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_github_app_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_github_app_tokens_app_type_check": { + "name": "user_github_app_tokens_app_type_check", + "value": "\"user_github_app_tokens\".\"github_app_type\" IN ('standard', 'lite')" + } + }, + "isRLSEnabled": false + }, + "public.user_model_preferences": { + "name": "user_model_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favorites": { + "name": "favorites", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_selected": { + "name": "last_selected", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_model_preferences_user_id": { + "name": "UQ_user_model_preferences_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_model_preferences_user_id_kilocode_users_id_fk": { + "name": "user_model_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_model_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_moderation_blocks": { + "name": "user_moderation_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "blocker_user_id": { + "name": "blocker_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blocked_github_login": { + "name": "blocked_github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_moderation_blocks_blocker_login": { + "name": "UQ_user_moderation_blocks_blocker_login", + "columns": [ + { + "expression": "blocker_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blocked_github_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_moderation_mutes": { + "name": "user_moderation_mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "blocker_user_id": { + "name": "blocker_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "muted_github_login": { + "name": "muted_github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_moderation_mutes_blocker_login": { + "name": "UQ_user_moderation_mutes_blocker_login", + "columns": [ + { + "expression": "blocker_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "muted_github_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_notification_preferences": { + "name": "user_notification_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_push_enabled": { + "name": "agent_push_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "chat_messages_enabled": { + "name": "chat_messages_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "agent_attention_enabled": { + "name": "agent_attention_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "session_status_enabled": { + "name": "session_status_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kiloclaw_activity_enabled": { + "name": "kiloclaw_activity_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "balance_alerts_enabled": { + "name": "balance_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "security_findings_enabled": { + "name": "security_findings_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notification_previews": { + "name": "notification_previews", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'generic'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notification_preferences_user_id_kilocode_users_id_fk": { + "name": "user_notification_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_notification_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_period_cache": { + "name": "user_period_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cache_type": { + "name": "cache_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_type": { + "name": "period_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_key": { + "name": "period_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "shared_url_token": { + "name": "shared_url_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_user_period_cache_kilo_user_id": { + "name": "IDX_user_period_cache_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_period_cache": { + "name": "UQ_user_period_cache", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cache_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_period_cache_lookup": { + "name": "IDX_user_period_cache_lookup", + "columns": [ + { + "expression": "cache_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_period_cache_share_token": { + "name": "UQ_user_period_cache_share_token", + "columns": [ + { + "expression": "shared_url_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_period_cache\".\"shared_url_token\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_period_cache_kilo_user_id_kilocode_users_id_fk": { + "name": "user_period_cache_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_period_cache", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_period_cache_period_type_check": { + "name": "user_period_cache_period_type_check", + "value": "\"user_period_cache\".\"period_type\" IN ('year', 'quarter', 'month', 'week', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.user_push_tokens": { + "name": "user_push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_version": { + "name": "app_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_push_tokens_token": { + "name": "UQ_user_push_tokens_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_push_tokens_user_id": { + "name": "IDX_user_push_tokens_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_push_tokens_user_id_kilocode_users_id_fk": { + "name": "user_push_tokens_user_id_kilocode_users_id_fk", + "tableFrom": "user_push_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_terms_acceptances": { + "name": "user_terms_acceptances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "terms_version": { + "name": "terms_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "age_posture": { + "name": "age_posture", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'13_plus'" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_terms_acceptances_user_version": { + "name": "UQ_user_terms_acceptances_user_version", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terms_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_city": { + "name": "vercel_ip_city", + "schema": "", + "columns": { + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_city": { + "name": "vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_city": { + "name": "UQ_vercel_ip_city", + "columns": [ + { + "expression": "vercel_ip_city", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_country": { + "name": "vercel_ip_country", + "schema": "", + "columns": { + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_country": { + "name": "vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_country": { + "name": "UQ_vercel_ip_country", + "columns": [ + { + "expression": "vercel_ip_country", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_events": { + "name": "webhook_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_action": { + "name": "event_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "handlers_triggered": { + "name": "handlers_triggered", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "event_signature": { + "name": "event_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_webhook_events_owned_by_org_id": { + "name": "IDX_webhook_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_owned_by_user_id": { + "name": "IDX_webhook_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_platform": { + "name": "IDX_webhook_events_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_event_type": { + "name": "IDX_webhook_events_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_created_at": { + "name": "IDX_webhook_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_events_owned_by_organization_id_organizations_id_fk": { + "name": "webhook_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "webhook_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "webhook_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "webhook_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_webhook_events_signature": { + "name": "UQ_webhook_events_signature", + "nullsNotDistinct": false, + "columns": [ + "event_signature" + ] + } + }, + "policies": {}, + "checkConstraints": { + "webhook_events_owner_check": { + "name": "webhook_events_owner_check", + "value": "(\n (\"webhook_events\".\"owned_by_user_id\" IS NOT NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"webhook_events\".\"owned_by_user_id\" IS NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.microdollar_usage_view": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n mu.id,\n mu.kilo_user_id,\n meta.message_id,\n mu.cost,\n mu.input_tokens,\n mu.output_tokens,\n mu.cache_write_tokens,\n mu.cache_hit_tokens,\n mu.created_at,\n ip.http_ip AS http_x_forwarded_for,\n city.vercel_ip_city AS http_x_vercel_ip_city,\n country.vercel_ip_country AS http_x_vercel_ip_country,\n meta.vercel_ip_latitude AS http_x_vercel_ip_latitude,\n meta.vercel_ip_longitude AS http_x_vercel_ip_longitude,\n ja4.ja4_digest AS http_x_vercel_ja4_digest,\n mu.provider,\n mu.model,\n mu.requested_model,\n meta.user_prompt_prefix,\n spp.system_prompt_prefix,\n meta.system_prompt_length,\n ua.http_user_agent,\n mu.cache_discount,\n meta.max_tokens,\n meta.has_middle_out_transform,\n mu.has_error,\n mu.abuse_classification,\n mu.organization_id,\n mu.inference_provider,\n mu.project_id,\n meta.status_code,\n meta.upstream_id,\n frfr.finish_reason,\n meta.latency,\n meta.moderation_latency,\n meta.generation_time,\n meta.is_byok,\n meta.is_user_byok,\n meta.streamed,\n meta.cancelled,\n edit.editor_name,\n ak.api_kind,\n meta.has_tools,\n meta.machine_id,\n feat.feature,\n meta.session_id,\n md.mode,\n am.auto_model,\n meta.market_cost,\n meta.is_free,\n meta.abuse_delay,\n meta.abuse_downgraded_from\n FROM \"microdollar_usage\" mu\n LEFT JOIN \"microdollar_usage_metadata\" meta ON mu.id = meta.id\n LEFT JOIN \"http_ip\" ip ON meta.http_ip_id = ip.http_ip_id\n LEFT JOIN \"vercel_ip_city\" city ON meta.vercel_ip_city_id = city.vercel_ip_city_id\n LEFT JOIN \"vercel_ip_country\" country ON meta.vercel_ip_country_id = country.vercel_ip_country_id\n LEFT JOIN \"ja4_digest\" ja4 ON meta.ja4_digest_id = ja4.ja4_digest_id\n LEFT JOIN \"system_prompt_prefix\" spp ON meta.system_prompt_prefix_id = spp.system_prompt_prefix_id\n LEFT JOIN \"http_user_agent\" ua ON meta.http_user_agent_id = ua.http_user_agent_id\n LEFT JOIN \"finish_reason\" frfr ON meta.finish_reason_id = frfr.finish_reason_id\n LEFT JOIN \"editor_name\" edit ON meta.editor_name_id = edit.editor_name_id\n LEFT JOIN \"api_kind\" ak ON meta.api_kind_id = ak.api_kind_id\n LEFT JOIN \"feature\" feat ON meta.feature_id = feat.feature_id\n LEFT JOIN \"mode\" md ON meta.mode_id = md.mode_id\n LEFT JOIN \"auto_model\" am ON meta.auto_model_id = am.auto_model_id\n", + "name": "microdollar_usage_view", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index d37e2e1171..9c743d8d05 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1632,6 +1632,13 @@ "when": 1787769039561, "tag": "0232_sweet_diamondback", "breakpoints": true + }, + { + "idx": 233, + "version": "7", + "when": 1787832203585, + "tag": "0233_square_daimon_hellstrom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 02a0edeb39..8c0e25abda 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -9410,6 +9410,46 @@ export const user_push_tokens = pgTable( export type UserPushToken = typeof user_push_tokens.$inferSelect; export type NewUserPushToken = typeof user_push_tokens.$inferInsert; +// ─── Activity Tokens (Live Activity / push-to-start / Android ongoing) ── +// +// Tokens for the glanceable surfaces (iOS Live Activity + push-to-start, +// Android ongoing notification). These are NOT Expo push tokens and never +// share a table with `user_push_tokens`. `organization_id` is null for the +// personal surface and is a server-only lookup key — it never enters a +// glanceable payload. Old clients never insert rows; drop this table when +// every client is past this release and no tokens remain. + +export const user_activity_tokens = pgTable( + 'user_activity_tokens', + { + id: uuid() + .default(sql`gen_random_uuid()`) + .primaryKey() + .notNull(), + user_id: text() + .notNull() + .references(() => kilocode_users.id, { onDelete: 'cascade' }), + token: text().notNull(), + kind: text().$type<'ios_push_to_start' | 'ios_activity' | 'android_ongoing'>().notNull(), + platform: text().$type<'ios' | 'android'>().notNull(), + // Null means the personal surface. Server-only lookup key; never sent in a + // glanceable payload. + organization_id: text(), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + uniqueIndex('UQ_user_activity_tokens_token').on(table.token), + index('IDX_user_activity_tokens_user_org').on(table.user_id, table.organization_id), + ] +); + +export type UserActivityToken = typeof user_activity_tokens.$inferSelect; +export type NewUserActivityToken = typeof user_activity_tokens.$inferInsert; + // ─── Notification Preferences ───────────────────────────────────────── export const user_notification_preferences = pgTable('user_notification_preferences', { diff --git a/packages/notifications/src/locales/en.json b/packages/notifications/src/locales/en.json index 2889dcba0f..30e050583a 100644 --- a/packages/notifications/src/locales/en.json +++ b/packages/notifications/src/locales/en.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Your instance has an update", "scheduledAction": "A scheduled action has an update", "lowBalance": "Your balance needs attention", - "securityFinding": "A security finding needs attention" + "securityFinding": "A security finding needs attention", + "activeAgentsGlanceable": "Active agents have an update" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/push-data.ts b/packages/notifications/src/push-data.ts index 29e0531f29..5f901c5cb4 100644 --- a/packages/notifications/src/push-data.ts +++ b/packages/notifications/src/push-data.ts @@ -76,6 +76,26 @@ export const pushDataSchema = z.discriminatedUnion('type', [ remediationId: nonEmptyStringSchema.optional(), prUrl: nonEmptyStringSchema.optional(), }), + // Aggregate glanceable snapshot for the Active Agents Live Activity / widget + // / Android ongoing. Carries generic status, counts, safe timestamps, and an + // opaque scope key only — no titles, ids, or accountEpoch (the client sets + // its local epoch). `status` mirrors the shared glanceable status enum. + // Old clients omit this type; remove the send gate when every client is past + // this release. + z.object({ + type: z.literal('active_agents_glanceable'), + schemaVersion: z.literal(1), + revision: z.number().int().min(1), + scopeKey: nonEmptyStringSchema, + organizationBound: z.boolean(), + status: z.enum(['waiting', 'empty', 'happy', 'stale', 'expired', 'signed_out', 'privacy']), + running: z.number().int().min(0), + needsInput: z.number().int().min(0), + reconnecting: z.number().int().min(0), + updatedAt: z.string(), + expiresAt: z.string(), + eligibleStartedAt: z.string().nullable(), + }), ]); export type PushData = z.infer; diff --git a/packages/notifications/src/push-presentation.ts b/packages/notifications/src/push-presentation.ts index 223811fbe6..c8b934942d 100644 --- a/packages/notifications/src/push-presentation.ts +++ b/packages/notifications/src/push-presentation.ts @@ -12,6 +12,7 @@ export const ANDROID_NOTIFICATION_CHANNELS = [ { id: 'kiloclaw', name: 'KiloClaw activity', importance: 'default' }, { id: 'balance', name: 'Balance alerts', importance: 'default' }, { id: 'security', name: 'Security findings', importance: 'high' }, + { id: 'active-agents', name: 'Active agents', importance: 'default' }, ] as const; export type AndroidNotificationChannelId = (typeof ANDROID_NOTIFICATION_CHANNELS)[number]['id']; @@ -31,6 +32,8 @@ export function androidChannelIdForPushData(data: PushData): AndroidNotification case 'security_finding': case 'security_lifecycle': return 'security'; + case 'active_agents_glanceable': + return 'active-agents'; default: { // Exhaustiveness: new PushData variants must be handled above. const _exhaustive: never = data; @@ -111,6 +114,18 @@ export function genericPushContentForPushData( 'A security finding needs attention' ), }; + case 'active_agents_glanceable': + // Generic, count-free lock-screen banner copy: the ongoing notification + // never leaks how many agents are running or which sessions they are. + return { + title: translatePush(locale, 'generic.title', undefined, 'Kilo'), + body: translatePush( + locale, + 'generic.body.activeAgentsGlanceable', + undefined, + 'Active agents have an update' + ), + }; default: { // Exhaustiveness: new PushData variants must be handled above. const _exhaustive: never = data; diff --git a/services/notifications/src/bindings.d.ts b/services/notifications/src/bindings.d.ts index 329b6952de..619bd2d556 100644 --- a/services/notifications/src/bindings.d.ts +++ b/services/notifications/src/bindings.d.ts @@ -12,6 +12,18 @@ declare global { // is single-config production); supplied via `.dev.vars` by // `pnpm dev:env`. The runtime check is string equality on `'log'`. PUSH_SINK_MODE?: string; + // Base origin of the web app, used to reach the internal + // glanceable-agents-snapshot route (see ENVIRONMENT.md). Optional so a + // missing value degrades to "skip aggregate delivery". + KILO_WEB_API_BASE_URL?: string; + // APNs token-based credentials for Live Activity delivery. Optional so a + // missing credential degrades to "skip the iOS send" with a warning. See + // ENVIRONMENT.md for the meaning of each name; never log the private key + // or a device token. + APNS_TEAM_ID?: string; + APNS_KEY_ID?: string; + APNS_PRIVATE_KEY?: SecretsStoreSecret; + APNS_TOPIC?: string; } } diff --git a/services/notifications/src/index.ts b/services/notifications/src/index.ts index 37e90cccad..058dce76ec 100644 --- a/services/notifications/src/index.ts +++ b/services/notifications/src/index.ts @@ -4,10 +4,11 @@ import { getWorkerDb } from '@kilocode/db/client'; import { cli_sessions_v2, organization_memberships, + user_activity_tokens, user_notification_preferences, user_push_tokens, } from '@kilocode/db/schema'; -import { and, eq, inArray } from 'drizzle-orm'; +import { and, eq, inArray, isNotNull, isNull } from 'drizzle-orm'; import { Hono } from 'hono'; import type { MiddlewareHandler } from 'hono'; import { cors } from 'hono/cors'; @@ -18,6 +19,7 @@ import { badgeBucketForConversation, internalDispatchRequestSchema, markBadgeReadInputSchema, + pushDataSchema, type ClearBadgeBucketForUserInput, type ClearBadgeBucketForUserOutput, type DispatchPushInput, @@ -42,6 +44,7 @@ import { dispatchAgentSessionNotificationPush, type DispatchAgentSessionNotificationPushDeps, } from './lib/agent-session-notification-push'; +import { sendLiveActivityApns, type ApnsCredentials } from './lib/apns-live-activity'; import { dispatchCloudAgentSessionPush, dispatchSessionReadyPush, @@ -50,6 +53,11 @@ import { } from './lib/cloud-agent-session-push'; import type { TicketTokenPair } from './lib/expo-push'; import { sendPushNotifications } from './lib/expo-push'; +import { + deliverGlanceableSnapshot, + type GlanceableDeliveryDeps, + type IosActivityToken, +} from './lib/glanceable-delivery'; import { dispatchInstanceLifecyclePush } from './lib/instance-lifecycle-push'; import { dispatchInternalPushCore } from './lib/internal-dispatch-push'; import { @@ -328,7 +336,181 @@ export class NotificationsService extends WorkerEntrypoint { async sendCloudAgentSessionNotification( params: SendCloudAgentSessionNotificationParams ): Promise { - return dispatchCloudAgentSessionPush(params, this.cloudAgentSessionPushDeps()); + const deps = this.cloudAgentSessionPushDeps(); + const result = await dispatchCloudAgentSessionPush(params, deps); + // Best-effort aggregate glanceable delivery (§psh). Runs after the push + // result is terminal so a failure here never changes the RPC outcome. + this.ctx.waitUntil( + this.deliverGlanceableAfterSessionPush(params.userId, params.cliSessionId, deps.getSession) + ); + return result; + } + + /** + * Resolve the session's organization (null means personal) and deliver the + * fresh glanceable snapshot to iOS activity tokens and Android Expo tokens. + * A session whose row cannot be resolved skips delivery (there is nothing to + * scope the snapshot to). All failures are best-effort and logged without + * device tokens or private content. + */ + private async deliverGlanceableAfterSessionPush( + userId: string, + cliSessionId: string, + getSession: DispatchCloudAgentSessionPushDeps['getSession'] + ): Promise { + try { + const session = await getSession(userId, cliSessionId); + if (!session) { + return; + } + await deliverGlanceableSnapshot( + { userId, organizationId: session.organizationId }, + this.glanceableDeliveryDeps() + ); + } catch (error) { + console.warn('Glanceable aggregate delivery failed', { + error: error instanceof Error ? error.message : String(error), + }); + } + } + + private glanceableDeliveryDeps(): GlanceableDeliveryDeps { + let db: ReturnType | undefined; + const getDbForCall = () => (db ??= getWorkerDb(this.env.HYPERDRIVE.connectionString)); + + return { + buildSnapshot: async (userId, organizationId) => { + const baseUrl = this.env.KILO_WEB_API_BASE_URL; + if (!baseUrl) { + console.warn('KILO_WEB_API_BASE_URL missing; skipping glanceable aggregate delivery'); + return null; + } + let internalApiSecret: string | undefined; + try { + internalApiSecret = await this.env.INTERNAL_API_SECRET.get(); + } catch { + internalApiSecret = undefined; + } + if (!internalApiSecret) { + console.warn('INTERNAL_API_SECRET missing; skipping glanceable aggregate delivery'); + return null; + } + + const response = await fetch(`${baseUrl}/api/internal/glanceable-agents-snapshot`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-secret': internalApiSecret, + }, + body: JSON.stringify({ userId, organizationId }), + }); + if (!response.ok) { + console.warn('Glanceable snapshot route failed', { status: response.status }); + return null; + } + const raw: unknown = await response.json().catch(() => null); + const candidate = { + type: 'active_agents_glanceable', + ...(typeof raw === 'object' && raw !== null ? raw : {}), + }; + const parsed = pushDataSchema.safeParse(candidate); + if (!parsed.success || parsed.data.type !== 'active_agents_glanceable') { + console.warn('Glanceable snapshot route returned an invalid snapshot'); + return null; + } + return parsed.data; + }, + listIosActivityTokens: async (userId, organizationId) => { + const orgPredicate = + organizationId === null + ? isNull(user_activity_tokens.organization_id) + : eq(user_activity_tokens.organization_id, organizationId); + const rows = await getDbForCall() + .select({ token: user_activity_tokens.token, kind: user_activity_tokens.kind }) + .from(user_activity_tokens) + .where( + and( + eq(user_activity_tokens.user_id, userId), + orgPredicate, + inArray(user_activity_tokens.kind, ['ios_activity', 'ios_push_to_start']) + ) + ); + return rows.map(row => ({ + token: row.token, + kind: row.kind as IosActivityToken['kind'], + })); + }, + sendIosLiveActivity: async (tokens, contentState) => { + const credentials = await this.readApnsCredentials(); + if (credentials === null) { + return; + } + const result = await sendLiveActivityApns({ + credentials, + tokens, + contentState, + nowSeconds: Math.floor(Date.now() / 1000), + }); + if (result.failed > 0) { + console.warn('Some Live Activity APNs sends failed', { + attempted: result.attempted, + failed: result.failed, + }); + } + }, + listAndroidExpoTokens: async userId => { + const rows = await getDbForCall() + .select({ token: user_push_tokens.token, locale: user_push_tokens.locale }) + .from(user_push_tokens) + .where( + and(eq(user_push_tokens.user_id, userId), isNotNull(user_push_tokens.app_version)) + ); + return rows.map(row => ({ token: row.token, locale: row.locale })); + }, + hasAndroidOngoingToken: async (userId, organizationId) => { + const orgPredicate = + organizationId === null + ? isNull(user_activity_tokens.organization_id) + : eq(user_activity_tokens.organization_id, organizationId); + const [row] = await getDbForCall() + .select({ id: user_activity_tokens.id }) + .from(user_activity_tokens) + .where( + and( + eq(user_activity_tokens.user_id, userId), + orgPredicate, + eq(user_activity_tokens.kind, 'android_ongoing') + ) + ) + .limit(1); + return row !== undefined; + }, + sendAndroidPush: async messages => { + const accessToken = await this.env.EXPO_ACCESS_TOKEN.get(); + await sendPushNotifications(messages, accessToken); + }, + }; + } + + private async readApnsCredentials(): Promise { + const { APNS_TEAM_ID: teamId, APNS_KEY_ID: keyId, APNS_TOPIC: topic } = this.env; + const privateKeyBinding = this.env.APNS_PRIVATE_KEY; + if (!teamId || !keyId || !topic || !privateKeyBinding) { + console.warn('APNs Live Activity credentials missing; skipping Live Activity delivery'); + return null; + } + let privateKeyPem: string; + try { + privateKeyPem = await privateKeyBinding.get(); + } catch { + console.warn('APNs Live Activity private key read failed; skipping Live Activity delivery'); + return null; + } + if (!privateKeyPem) { + console.warn('APNs Live Activity private key empty; skipping Live Activity delivery'); + return null; + } + return { teamId, keyId, topic, privateKeyPem }; } /** diff --git a/services/notifications/src/lib/apns-live-activity.test.ts b/services/notifications/src/lib/apns-live-activity.test.ts new file mode 100644 index 0000000000..b0488d5a9a --- /dev/null +++ b/services/notifications/src/lib/apns-live-activity.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + buildLiveActivityApnsRequest, + sendLiveActivityApns, + signApnsJwt, + type ApnsCredentials, +} from './apns-live-activity'; + +const TEAM_ID = 'TEAM123456'; +const KEY_ID = 'KEY123456'; +const TOPIC = 'com.kilocode.kiloapp'; + +async function generateTestPrivateKeyPem(): Promise { + const keyPair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [ + 'sign', + 'verify', + ])) as CryptoKeyPair; + const der = (await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)) as ArrayBuffer; + const bytes = new Uint8Array(der); + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + const b64 = btoa(binary); + return `-----BEGIN PRIVATE KEY-----\n${b64}\n-----END PRIVATE KEY-----`; +} + +describe('buildLiveActivityApnsRequest', () => { + it('builds the Live Activity push URL, headers, and aps payload', () => { + const request = buildLiveActivityApnsRequest({ + token: 'device-token-1', + event: 'update', + contentState: { revision: 7, running: 1 }, + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem: 'pem', topic: TOPIC }, + authorizationJwt: 'header.payload.sig', + timestampSeconds: 1_750_000_000, + }); + + expect(request.url).toBe('https://api.push.apple.com/3/device/device-token-1'); + expect(request.headers).toMatchObject({ + authorization: 'bearer header.payload.sig', + 'apns-topic': 'com.kilocode.kiloapp.push-type.liveactivity', + 'apns-push-type': 'liveactivity', + 'apns-priority': '10', + 'apns-expiration': '0', + 'content-type': 'application/json', + }); + + const body = JSON.parse(request.body) as { + aps: { timestamp: number; event: string; 'content-state': Record }; + }; + expect(body.aps.timestamp).toBe(1_750_000_000); + expect(body.aps.event).toBe('update'); + expect(body.aps['content-state']).toEqual({ revision: 7, running: 1 }); + }); +}); + +describe('signApnsJwt', () => { + it('signs a JWT whose header carries alg/kid and claims carry iss/iat', async () => { + const privateKeyPem = await generateTestPrivateKeyPem(); + const credentials: ApnsCredentials = { + teamId: TEAM_ID, + keyId: KEY_ID, + privateKeyPem, + topic: TOPIC, + }; + + const jwt = await signApnsJwt(credentials, 1_750_000_000); + + const [headerPart, claimsPart, signaturePart] = jwt.split('.'); + expect(headerPart).toBeDefined(); + expect(claimsPart).toBeDefined(); + expect(signaturePart).toBeDefined(); + expect(signaturePart).not.toBe(''); + + const decode = (part: string): Record => + JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/'))); + + expect(decode(headerPart)).toEqual({ alg: 'ES256', kid: KEY_ID }); + expect(decode(claimsPart)).toEqual({ iss: TEAM_ID, iat: 1_750_000_000 }); + }); +}); + +describe('sendLiveActivityApns', () => { + it('POSTs one Live Activity push per token and counts successes', async () => { + const privateKeyPem = await generateTestPrivateKeyPem(); + const fetchFn = vi.fn(async () => new Response('', { status: 200 })); + + const result = await sendLiveActivityApns({ + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem, topic: TOPIC }, + tokens: [ + { token: 'token-a', event: 'start' }, + { token: 'token-b', event: 'update' }, + ], + contentState: { revision: 1, running: 1 }, + nowSeconds: 1_750_000_000, + fetchFn, + }); + + expect(result).toEqual({ attempted: 2, ok: 2, failed: 0 }); + expect(fetchFn).toHaveBeenCalledTimes(2); + + const firstUrl = fetchFn.mock.calls[0]?.[0] as string; + const firstInit = fetchFn.mock.calls[0]?.[1] as { + method: string; + headers: Record; + body: string; + }; + expect(firstUrl).toBe('https://api.push.apple.com/3/device/token-a'); + expect(firstInit.method).toBe('POST'); + expect(firstInit.headers['apns-push-type']).toBe('liveactivity'); + expect(firstInit.headers.authorization).toMatch(/^bearer /); + const body = JSON.parse(firstInit.body) as { + aps: { event: string; 'content-state': Record }; + }; + expect(body.aps.event).toBe('start'); + expect(body.aps['content-state']).toEqual({ revision: 1, running: 1 }); + }); + + it('counts rejected pushes as failures', async () => { + const privateKeyPem = await generateTestPrivateKeyPem(); + const fetchFn = vi + .fn() + .mockResolvedValueOnce(new Response('', { status: 200 })) + .mockResolvedValueOnce(new Response('', { status: 400 })); + + const result = await sendLiveActivityApns({ + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem, topic: TOPIC }, + tokens: [ + { token: 'token-ok', event: 'start' }, + { token: 'token-bad', event: 'update' }, + ], + contentState: { revision: 2, running: 0 }, + nowSeconds: 1_750_000_000, + fetchFn, + }); + + expect(result).toEqual({ attempted: 2, ok: 1, failed: 1 }); + }); + + it('returns a zero result without signing or fetching when there are no tokens', async () => { + const fetchFn = vi.fn(); + const result = await sendLiveActivityApns({ + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem: 'not-a-key', topic: TOPIC }, + tokens: [], + contentState: { revision: 3, running: 0 }, + nowSeconds: 1_750_000_000, + fetchFn, + }); + + expect(result).toEqual({ attempted: 0, ok: 0, failed: 0 }); + expect(fetchFn).not.toHaveBeenCalled(); + }); +}); diff --git a/services/notifications/src/lib/apns-live-activity.ts b/services/notifications/src/lib/apns-live-activity.ts new file mode 100644 index 0000000000..0e44af929e --- /dev/null +++ b/services/notifications/src/lib/apns-live-activity.ts @@ -0,0 +1,148 @@ +/** + * Token-based APNs client for Live Activity pushes (push-to-start and update). + * Pure: every network hop goes through the injected `fetchFn` so unit tests + * substitute a fake. Never logs a device token or the private key. + */ + +export type ApnsCredentials = { + teamId: string; + keyId: string; + /** PKCS#8 ES256 `.p8` contents, PEM-armoured. */ + privateKeyPem: string; + /** iOS app bundle id (e.g. `com.kilocode.kiloapp`). */ + topic: string; +}; + +export type LiveActivityEvent = 'start' | 'update'; + +const APNS_BASE_URL = 'https://api.push.apple.com'; +const APNS_KEY_PREFIX = '-----BEGIN PRIVATE KEY-----'; +const APNS_KEY_SUFFIX = '-----END PRIVATE KEY-----'; + +const encoder = new TextEncoder(); + +function base64Url(bytes: Uint8Array): string { + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function pemToDer(pem: string): Uint8Array { + const body = pem.replace(APNS_KEY_PREFIX, '').replace(APNS_KEY_SUFFIX, '').replace(/\s/g, ''); + const binary = atob(body); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i); + return bytes; +} + +/** Sign a short-lived ES256 APNs provider token (JWT). */ +export async function signApnsJwt( + credentials: ApnsCredentials, + nowSeconds: number +): Promise { + const header = base64Url( + encoder.encode(JSON.stringify({ alg: 'ES256', kid: credentials.keyId })) + ); + const claims = base64Url( + encoder.encode(JSON.stringify({ iss: credentials.teamId, iat: nowSeconds })) + ); + const signingInput = `${header}.${claims}`; + + const key = await crypto.subtle.importKey( + 'pkcs8', + pemToDer(credentials.privateKeyPem), + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['sign'] + ); + const signature = await crypto.subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, + key, + encoder.encode(signingInput) + ); + return `${signingInput}.${base64Url(new Uint8Array(signature))}`; +} + +/** + * Build the HTTP request shape for one Live Activity APNs push. Live Activity + * pushes use `apns-push-type: liveactivity`, `apns-priority: 10`, and the + * `.push-type.liveactivity` topic suffix. The `timestamp` (Unix seconds) is + * what lets iOS discard an older revision that arrives late. + */ +export function buildLiveActivityApnsRequest(params: { + token: string; + event: LiveActivityEvent; + contentState: Record; + credentials: ApnsCredentials; + authorizationJwt: string; + timestampSeconds: number; +}): { url: string; headers: Record; body: string } { + return { + url: `${APNS_BASE_URL}/3/device/${params.token}`, + headers: { + authorization: `bearer ${params.authorizationJwt}`, + 'apns-topic': `${params.credentials.topic}.push-type.liveactivity`, + 'apns-push-type': 'liveactivity', + 'apns-priority': '10', + 'apns-expiration': '0', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + aps: { + timestamp: params.timestampSeconds, + event: params.event, + 'content-state': params.contentState, + }, + }), + }; +} + +export type LiveActivityApnsSendResult = { + attempted: number; + ok: number; + failed: number; +}; + +/** Send one Live Activity push per token in parallel. */ +export async function sendLiveActivityApns(params: { + credentials: ApnsCredentials; + tokens: readonly { token: string; event: LiveActivityEvent }[]; + contentState: Record; + nowSeconds: number; + fetchFn?: typeof fetch; +}): Promise { + if (params.tokens.length === 0) { + return { attempted: 0, ok: 0, failed: 0 }; + } + + const authorizationJwt = await signApnsJwt(params.credentials, params.nowSeconds); + const fetchFn = params.fetchFn ?? fetch; + + const results = await Promise.allSettled( + params.tokens.map(async ({ token, event }) => { + const request = buildLiveActivityApnsRequest({ + token, + event, + contentState: params.contentState, + credentials: params.credentials, + authorizationJwt, + timestampSeconds: params.nowSeconds, + }); + const response = await fetchFn(request.url, { + method: 'POST', + headers: request.headers, + body: request.body, + }); + if (!response.ok) { + throw new Error(`APNs rejected the push with status ${response.status}`); + } + }) + ); + + const ok = results.filter(result => result.status === 'fulfilled').length; + return { + attempted: params.tokens.length, + ok, + failed: params.tokens.length - ok, + }; +} diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts new file mode 100644 index 0000000000..ebec737f4e --- /dev/null +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -0,0 +1,181 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { ExpoPushMessage } from './expo-push'; +import { + apnsEventForTokenKind, + buildAndroidGlanceableMessages, + deliverGlanceableSnapshot, + toGlanceableContentState, + type ActiveAgentsGlanceable, + type GlanceableDeliveryDeps, + type IosActivityToken, +} from './glanceable-delivery'; + +const snapshot: ActiveAgentsGlanceable = { + type: 'active_agents_glanceable', + schemaVersion: 1, + revision: 3, + scopeKey: 'deadbeef', + organizationBound: false, + status: 'happy', + running: 2, + needsInput: 1, + reconnecting: 0, + updatedAt: '2026-08-27T10:00:00.000Z', + expiresAt: '2026-08-27T18:00:00.000Z', + eligibleStartedAt: '2026-08-27T09:00:00.000Z', +}; + +function fakeDeps(overrides: Partial = {}): { + deps: GlanceableDeliveryDeps; + calls: { iosSends: unknown[][]; androidSends: ExpoPushMessage[][] }; +} { + const calls = { iosSends: [] as unknown[][], androidSends: [] as ExpoPushMessage[][] }; + + const deps: GlanceableDeliveryDeps = { + buildSnapshot: vi.fn(async () => snapshot), + listIosActivityTokens: vi.fn(async () => [] as IosActivityToken[]), + sendIosLiveActivity: vi.fn(async (_tokens, _contentState) => { + calls.iosSends.push([_tokens, _contentState]); + }), + listAndroidExpoTokens: vi.fn(async () => []), + hasAndroidOngoingToken: vi.fn(async () => false), + sendAndroidPush: vi.fn(async messages => { + calls.androidSends.push(messages); + }), + ...overrides, + }; + + return { deps, calls }; +} + +describe('apnsEventForTokenKind', () => { + it('maps the push-to-start token to the start event', () => { + expect(apnsEventForTokenKind('ios_push_to_start')).toBe('start'); + }); + + it('maps the activity token to the update event', () => { + expect(apnsEventForTokenKind('ios_activity')).toBe('update'); + }); +}); + +describe('toGlanceableContentState', () => { + it('strips the type discriminator and keeps every content-state field', () => { + const contentState = toGlanceableContentState(snapshot); + expect(contentState).not.toHaveProperty('type'); + expect(contentState).toEqual({ + schemaVersion: 1, + revision: 3, + scopeKey: 'deadbeef', + organizationBound: false, + status: 'happy', + running: 2, + needsInput: 1, + reconnecting: 0, + updatedAt: '2026-08-27T10:00:00.000Z', + expiresAt: '2026-08-27T18:00:00.000Z', + eligibleStartedAt: '2026-08-27T09:00:00.000Z', + }); + }); +}); + +describe('buildAndroidGlanceableMessages', () => { + it('emits one low-interruption, tag-collapsed message per Expo token', () => { + const messages = buildAndroidGlanceableMessages( + [ + { token: 'ExponentPushToken[aaa]', locale: null }, + { token: 'ExponentPushToken[bbb]', locale: 'es' }, + ], + snapshot + ); + + expect(messages).toHaveLength(2); + for (const message of messages) { + expect(message.data).toEqual(snapshot); + expect(message.sound).toBeNull(); + expect(message.priority).toBe('default'); + expect(message.channelId).toBe('active-agents'); + expect(message.tag).toBe('deadbeef'); + expect(typeof message.title).toBe('string'); + expect(typeof message.body).toBe('string'); + } + expect(messages.map(m => m.to)).toEqual(['ExponentPushToken[aaa]', 'ExponentPushToken[bbb]']); + }); +}); + +describe('deliverGlanceableSnapshot', () => { + it('skips all delivery when the snapshot cannot be built', async () => { + const { deps, calls } = fakeDeps({ buildSnapshot: vi.fn(async () => null) }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); + + expect(deps.listIosActivityTokens).not.toHaveBeenCalled(); + expect(deps.hasAndroidOngoingToken).not.toHaveBeenCalled(); + expect(calls.iosSends).toHaveLength(0); + expect(calls.androidSends).toHaveLength(0); + }); + + it('delivers the content-state to iOS tokens with the right start/update events', async () => { + const iosTokens: IosActivityToken[] = [ + { token: 'ptt-token', kind: 'ios_push_to_start' }, + { token: 'activity-token', kind: 'ios_activity' }, + ]; + const { deps, calls } = fakeDeps({ + listIosActivityTokens: vi.fn(async () => iosTokens), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: 'org-1' }, deps); + + expect(calls.iosSends).toHaveLength(1); + const [tokens, contentState] = calls.iosSends[0] as [ + { token: string; event: string }[], + Record, + ]; + expect(tokens).toEqual([ + { token: 'ptt-token', event: 'start' }, + { token: 'activity-token', event: 'update' }, + ]); + expect(contentState).not.toHaveProperty('type'); + expect(contentState).not.toHaveProperty('accountEpoch'); + expect(contentState.revision).toBe(3); + expect(calls.androidSends).toHaveLength(0); + }); + + it('skips Android when no android_ongoing activity token exists', async () => { + const { deps, calls } = fakeDeps({ + hasAndroidOngoingToken: vi.fn(async () => false), + listAndroidExpoTokens: vi.fn(async () => [{ token: 'ExponentPushToken[aaa]', locale: null }]), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); + + expect(deps.listAndroidExpoTokens).not.toHaveBeenCalled(); + expect(calls.androidSends).toHaveLength(0); + }); + + it('sends the Android Expo push only when an ongoing token and Expo tokens both exist', async () => { + const { deps, calls } = fakeDeps({ + hasAndroidOngoingToken: vi.fn(async () => true), + listAndroidExpoTokens: vi.fn(async () => [{ token: 'ExponentPushToken[aaa]', locale: null }]), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); + + expect(calls.androidSends).toHaveLength(1); + expect(calls.androidSends[0]).toHaveLength(1); + expect(calls.androidSends[0][0].to).toBe('ExponentPushToken[aaa]'); + expect(calls.androidSends[0][0].tag).toBe('deadbeef'); + }); + + it('sends nothing on Android when the user has no Expo tokens even with an ongoing token', async () => { + const { deps, calls } = fakeDeps({ + hasAndroidOngoingToken: vi.fn(async () => true), + listAndroidExpoTokens: vi.fn(async () => []), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); + + expect(deps.sendAndroidPush).not.toHaveBeenCalled(); + expect(calls.androidSends).toHaveLength(0); + }); +}); diff --git a/services/notifications/src/lib/glanceable-delivery.ts b/services/notifications/src/lib/glanceable-delivery.ts new file mode 100644 index 0000000000..bd7ab2f1f3 --- /dev/null +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -0,0 +1,108 @@ +/** + * Aggregate glanceable snapshot delivery for the Active Agents Live Activity, + * widgets, and Android ongoing notification. Runs after a cloud-agent session + * notification send: it fetches the fresh snapshot from the web internal route, + * then pushes it to the registered iOS activity tokens over APNs and to the + * user's Expo tokens on Android. Pure orchestrator — all IO is injected via + * `deps` so tests substitute in-memory fakes. + */ + +import { + genericPushContentForPushData, + resolvePushLocale, + type PushData, +} from '@kilocode/notifications'; + +import type { LiveActivityEvent } from './apns-live-activity'; +import type { ExpoPushMessage } from './expo-push'; + +export type ActiveAgentsGlanceable = Extract; +/** The APNs `content-state` is the snapshot without the `type` discriminator and without `accountEpoch`. */ +export type GlanceableContentState = Omit; + +export type IosActivityToken = { token: string; kind: 'ios_activity' | 'ios_push_to_start' }; +export type AndroidPushToken = { token: string; locale: string | null }; + +export function apnsEventForTokenKind(kind: IosActivityToken['kind']): LiveActivityEvent { + return kind === 'ios_push_to_start' ? 'start' : 'update'; +} + +export function toGlanceableContentState(snapshot: ActiveAgentsGlanceable): GlanceableContentState { + const { type: _type, ...contentState } = snapshot; + return contentState; +} + +export function buildAndroidGlanceableMessages( + tokens: readonly AndroidPushToken[], + snapshot: ActiveAgentsGlanceable +): ExpoPushMessage[] { + return tokens.map(({ token, locale }) => { + const { title, body } = genericPushContentForPushData(snapshot, resolvePushLocale(locale)); + return { + to: token, + title, + body, + data: snapshot, + // The aggregate push is a data carrier for the ongoing notification, so + // it never rings or interrupts: no sound, default (not high) priority. + sound: null, + priority: 'default', + channelId: 'active-agents', + // Android collapse key = the opaque scope key, so every aggregate update + // for one user+org collapses into the same ongoing notification. + tag: snapshot.scopeKey, + } satisfies ExpoPushMessage; + }); +} + +export type GlanceableDeliveryDeps = { + /** + * Build the fresh snapshot via the web internal route. `null` means the + * snapshot could not be built (route failure, missing config, invalid + * payload) and the caller must skip delivery. + */ + buildSnapshot: ( + userId: string, + organizationId: string | null + ) => Promise; + listIosActivityTokens: ( + userId: string, + organizationId: string | null + ) => Promise; + sendIosLiveActivity: ( + tokens: readonly { token: string; event: LiveActivityEvent }[], + contentState: GlanceableContentState + ) => Promise; + listAndroidExpoTokens: ( + userId: string, + organizationId: string | null + ) => Promise; + hasAndroidOngoingToken: (userId: string, organizationId: string | null) => Promise; + sendAndroidPush: (messages: ExpoPushMessage[]) => Promise; +}; + +export async function deliverGlanceableSnapshot( + params: { userId: string; organizationId: string | null }, + deps: GlanceableDeliveryDeps +): Promise { + const snapshot = await deps.buildSnapshot(params.userId, params.organizationId); + if (snapshot === null) { + return; + } + const contentState = toGlanceableContentState(snapshot); + + const iosTokens = await deps.listIosActivityTokens(params.userId, params.organizationId); + if (iosTokens.length > 0) { + await deps.sendIosLiveActivity( + iosTokens.map(({ token, kind }) => ({ token, event: apnsEventForTokenKind(kind) })), + contentState + ); + } + + if (await deps.hasAndroidOngoingToken(params.userId, params.organizationId)) { + const expoTokens = await deps.listAndroidExpoTokens(params.userId, params.organizationId); + if (expoTokens.length > 0) { + await deps.sendAndroidPush(buildAndroidGlanceableMessages(expoTokens, snapshot)); + } + } +} From e645a67f0150925492e0ab7f9e77b96189be972c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 16:29:05 +0200 Subject: [PATCH 02/43] fix(glanceable): send renderable Live Activity content-state --- .../active-agents-live-activity.tsx | 193 +++++++++--------- .../src/glanceable-ios/ios-sink.test.ts | 27 +-- apps/mobile/src/glanceable-ios/ios-sink.ts | 27 ++- apps/mobile/src/glanceable-ios/view-props.ts | 18 ++ packages/notifications/src/push-data.ts | 12 ++ .../src/lib/apns-live-activity.test.ts | 38 +++- .../src/lib/apns-live-activity.ts | 11 + .../src/lib/glanceable-delivery.test.ts | 42 ++-- .../src/lib/glanceable-delivery.ts | 35 +++- 9 files changed, 269 insertions(+), 134 deletions(-) diff --git a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx index 9d161c2537..0a7ae0f1d8 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx @@ -8,105 +8,116 @@ import { import { createLiveActivity } from 'expo-widgets'; import { PlatformColor } from 'react-native'; -import { type GlanceableViewProps } from './view-props'; +import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; /* eslint-disable new-cap -- PlatformColor is a React Native factory function, not a constructor */ // The layout function below is marked with the `'widget'` directive, so Babel // stringifies it and the watcher extension re-evaluates the source. Everything // it references must be a watcher global (`Text`, `VStack`, the modifiers, -// `PlatformColor`) or a built-in. Do not call `@/` helpers or i18n from here — -// translated copy arrives through `props`. The inlined English fallbacks below -// only render while the gallery placeholder has no snapshot props. +// `PlatformColor`) or a built-in. Do not call `@/` helpers or i18n from here. +// The server pushes raw counts + status (it cannot translate), and the +// foreground app passes the same raw shape, so the inlined English copy below +// is the single producer of the displayed Live Activity copy. -export const ActiveAgentsLiveActivity = createLiveActivity>( - 'ActiveAgentsLiveActivity', - (props, environment) => { - 'widget'; +export const ActiveAgentsLiveActivity = createLiveActivity< + Partial +>('ActiveAgentsLiveActivity', (props, environment) => { + 'widget'; - const dark = environment.colorScheme === 'dark'; - const counts = props.countLines ?? []; - const hasCounts = counts.length > 0; - const primaryLabel = props.primaryLabel ?? null; - const primaryCount = String(props.primaryCount ?? 0); - const statusLine = props.statusLine ?? (hasCounts ? null : 'No work in progress'); - const elapsedAnchor = props.elapsedAnchor ?? null; + const dark = environment.colorScheme === 'dark'; - const primaryForeground = foregroundStyle(PlatformColor('label')); - const mutedForeground = foregroundStyle( - dark ? PlatformColor('secondaryLabel') : PlatformColor('tertiaryLabel') - ); + const status = props.status ?? 'empty'; + const STATUS_LINE = { + waiting: 'Updating agents', + empty: 'No work in progress', + stale: "Can't update now", + expired: 'Status expired', + signed_out: 'Sign in to see agents', + privacy: 'Agents hidden', + } as const; + const statusLine = status === 'happy' ? null : STATUS_LINE[status]; - const countRows = counts.map(line => ( - - {`${line.count} ${line.label}`} - - )); + const countLines = [ + { label: 'Needs input', count: props.needsInput ?? 0 }, + { label: 'Reconnecting', count: props.reconnecting ?? 0 }, + { label: 'Running', count: props.running ?? 0 }, + ].filter(line => line.count > 0); + const hasCounts = countLines.length > 0; + const primary = countLines[0] ?? null; + const primaryLabel = primary === null ? null : primary.label; + const primaryCount = String(primary === null ? 0 : primary.count); + const elapsedAnchor = status === 'happy' ? (props.eligibleStartedAt ?? null) : null; + + const spokenParts = + status === 'happy' || status === 'stale' + ? [...countLines.map(line => line.label), 'Open agents'] + : [statusLine ?? '', 'Open agents'].filter(part => part !== ''); + const accessibility = spokenParts.join(', '); + + const primaryForeground = foregroundStyle(PlatformColor('label')); + const mutedForeground = foregroundStyle( + dark ? PlatformColor('secondaryLabel') : PlatformColor('tertiaryLabel') + ); + + const countRows = countLines.map(line => ( + + {`${line.count} ${line.label}`} + + )); - return { - banner: ( - - {hasCounts ? ( - - {countRows} - - ) : null} - {statusLine !== null ? {statusLine} : null} - {elapsedAnchor !== null ? ( - - ) : null} - - ), - compactLeading: ( - - {hasCounts ? primaryCount : statusLine} - - ), - compactTrailing: ( - - {hasCounts ? (primaryLabel ?? primaryCount) : ''} - - ), - minimal: ( - - {hasCounts ? primaryCount : ''} - - ), - expandedLeading: ( - - {countRows} - - ), - expandedTrailing: ( - - {statusLine !== null ? {statusLine} : null} - {elapsedAnchor !== null ? ( - - ) : null} - - ), - expandedBottom: ( - - {statusLine !== null && !hasCounts ? ( - {statusLine} - ) : null} - - ), - }; - } -); + return { + banner: ( + + {hasCounts ? ( + + {countRows} + + ) : null} + {statusLine !== null ? {statusLine} : null} + {elapsedAnchor !== null ? ( + + ) : null} + + ), + compactLeading: ( + + {hasCounts ? primaryCount : statusLine} + + ), + compactTrailing: ( + + {hasCounts ? (primaryLabel ?? primaryCount) : ''} + + ), + minimal: ( + + {hasCounts ? primaryCount : ''} + + ), + expandedLeading: ( + + {countRows} + + ), + expandedTrailing: ( + + {statusLine !== null ? {statusLine} : null} + {elapsedAnchor !== null ? ( + + ) : null} + + ), + expandedBottom: ( + + {statusLine !== null && !hasCounts ? ( + {statusLine} + ) : null} + + ), + }; +}); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index c1cd6531b4..943651ef8d 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -5,6 +5,7 @@ import { buildGlanceableSnapshot, type GlanceableAgentsSnapshot, } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; import { writeSignedOutSnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { GlanceablePublisher } from '@/lib/glanceable/publisher'; @@ -151,7 +152,7 @@ describe('iosSink start and update', () => { Date.parse(newer.updatedAt) ); expect( - (mockState.ended[0]?.props as GlanceableViewProps | undefined)?.countLines[0]?.count + (mockState.ended[0]?.props as GlanceableLiveActivityContentState | undefined)?.running ).toBe(1); }); @@ -312,25 +313,27 @@ describe('iosSink widget publish', () => { }); }); -describe('iosSink Live Activity copy', () => { - it('mirrors empty copy onto the Live Activity without starting a second one', () => { +describe('iosSink Live Activity content-state', () => { + it('mirrors the empty content-state without starting a second activity', () => { iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); iosSink.publish(snapshotFor([], 1)); expect(mockState.started.length).toBe(1); - const updated = mockState.updated.at(-1) as GlanceableViewProps | undefined; - expect(updated?.statusLine).toBe('No work in progress'); - expect(updated?.countLines).toEqual([]); + const updated = mockState.updated.at(-1) as GlanceableLiveActivityContentState | undefined; + expect(updated?.status).toBe('empty'); + expect(updated?.running).toBe(0); + expect(updated?.needsInput).toBe(0); + expect(updated?.reconnecting).toBe(0); }); - it('mirrors stale copy with counts onto the Live Activity', () => { + it('mirrors the stale content-state with counts onto the Live Activity', () => { iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); iosSink.publish(snapshotFor([{ status: 'busy' }], 1, 'stale')); expect(mockState.started.length).toBe(1); - const updated = mockState.updated.at(-1) as GlanceableViewProps | undefined; - expect(updated?.statusLine).toBe("Can't update now"); - expect(updated?.countLines).toHaveLength(1); + const updated = mockState.updated.at(-1) as GlanceableLiveActivityContentState | undefined; + expect(updated?.status).toBe('stale'); + expect(updated?.running).toBe(1); }); it('adopts and updates a leftover activity from publish when the handle is null', () => { @@ -339,8 +342,8 @@ describe('iosSink Live Activity copy', () => { iosSink.publish(snapshotFor([], 1, 'empty')); expect(mockState.started.length).toBe(0); - const updated = mockState.updated.at(-1) as GlanceableViewProps | undefined; - expect(updated?.statusLine).toBe('No work in progress'); + const updated = mockState.updated.at(-1) as GlanceableLiveActivityContentState | undefined; + expect(updated?.status).toBe('empty'); expect(delivery.registerTokens).not.toHaveBeenCalled(); }); }); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index 243823823d..cbcb9e5c67 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -2,6 +2,7 @@ import { type GlanceableAgentsSnapshot, isEligibleGlanceableWork, } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; import { type LiveActivity } from 'expo-widgets'; import { i18n } from '@/i18n'; @@ -9,18 +10,22 @@ import { getGlanceableDelivery, type GlanceableSink } from '@/lib/glanceable/sin import { ActiveAgentsLiveActivity } from './active-agents-live-activity'; import { ActiveAgentsWidget } from './active-agents-widget'; -import { buildGlanceableViewProps, type GlanceableViewProps } from './view-props'; +import { + buildGlanceableLiveActivityContentState, + buildGlanceableViewProps, + type GlanceableViewProps, +} from './view-props'; /** Open-agents destination, kept in step with the inlined widget URL. */ const OPEN_AGENTS_URL = '/(app)/(tabs)/(2_agents)'; -type Activity = LiveActivity>; +type Activity = LiveActivity>; let activityKitDeniedState = false; let activity: Activity | null = null; let revision = 0; let lastUpdatedAt: string | null = null; -let lastProps: Partial | null = null; +let lastProps: Partial | null = null; function translate(key: string): string { return i18n.t(key); @@ -118,8 +123,8 @@ export const iosSink: GlanceableSink = { activity ??= adoptExistingActivity(); if (activity !== null) { lastUpdatedAt = snapshot.updatedAt; - lastProps = props; - void activity.update(props); + lastProps = buildGlanceableLiveActivityContentState(snapshot); + void activity.update(lastProps); } }, @@ -128,7 +133,7 @@ export const iosSink: GlanceableSink = { return; } - const props = buildGlanceableViewProps(snapshot, {}, translate); + const contentState = buildGlanceableLiveActivityContentState(snapshot); if (activity === null) { // Adopt the newest existing instance before starting a second one, so a @@ -152,7 +157,7 @@ export const iosSink: GlanceableSink = { if (activity === null) { try { - activity = ActiveAgentsLiveActivity.start(props, OPEN_AGENTS_URL); + activity = ActiveAgentsLiveActivity.start(contentState, OPEN_AGENTS_URL); } catch (error) { // Only ActivityKit unavailability is permanent; a transient // StartLiveActivityException leaves denial unset so a later emit retries. @@ -165,11 +170,11 @@ export const iosSink: GlanceableSink = { } lastUpdatedAt = snapshot.updatedAt; - lastProps = props; + lastProps = contentState; revision = snapshot.revision; getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId); if (adopted) { - void activity.update(props); + void activity.update(contentState); } return; } @@ -180,8 +185,8 @@ export const iosSink: GlanceableSink = { return; } lastUpdatedAt = snapshot.updatedAt; - lastProps = props; - void activity.update(props); + lastProps = contentState; + void activity.update(contentState); revision = snapshot.revision; }, diff --git a/apps/mobile/src/glanceable-ios/view-props.ts b/apps/mobile/src/glanceable-ios/view-props.ts index 2e793c2789..a053b853e8 100644 --- a/apps/mobile/src/glanceable-ios/view-props.ts +++ b/apps/mobile/src/glanceable-ios/view-props.ts @@ -1,4 +1,5 @@ import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; import { glanceableCountLines, @@ -62,3 +63,20 @@ export function buildGlanceableViewProps( .join(', '), }; } + +/** + * Build the Live Activity content-state from a snapshot. The server pushes the + * same raw shape, so the widget extension's `active-agents-live-activity.tsx` + * renders it directly with inlined English copy (the server cannot translate). + */ +export function buildGlanceableLiveActivityContentState( + snapshot: GlanceableAgentsSnapshot +): GlanceableLiveActivityContentState { + return { + status: snapshot.status, + running: snapshot.running, + needsInput: snapshot.needsInput, + reconnecting: snapshot.reconnecting, + eligibleStartedAt: snapshot.eligibleStartedAt, + }; +} diff --git a/packages/notifications/src/push-data.ts b/packages/notifications/src/push-data.ts index 5f901c5cb4..26957bec2e 100644 --- a/packages/notifications/src/push-data.ts +++ b/packages/notifications/src/push-data.ts @@ -99,3 +99,15 @@ export const pushDataSchema = z.discriminatedUnion('type', [ ]); export type PushData = z.infer; + +/** + * The raw content-state the Active Agents Live Activity renders. The server + * pushes exactly this shape (counts + status + the safe eligible-start + * timestamp) and the widget extension renders it directly with inlined English + * copy. It must never carry a title, session id, repository name, organization + * name, generated text, or a raw account id. + */ +export type GlanceableLiveActivityContentState = Pick< + Extract, + 'status' | 'running' | 'needsInput' | 'reconnecting' | 'eligibleStartedAt' +>; diff --git a/services/notifications/src/lib/apns-live-activity.test.ts b/services/notifications/src/lib/apns-live-activity.test.ts index b0488d5a9a..75ce06807e 100644 --- a/services/notifications/src/lib/apns-live-activity.test.ts +++ b/services/notifications/src/lib/apns-live-activity.test.ts @@ -46,11 +46,47 @@ describe('buildLiveActivityApnsRequest', () => { }); const body = JSON.parse(request.body) as { - aps: { timestamp: number; event: string; 'content-state': Record }; + aps: { + timestamp: number; + event: string; + 'content-state': Record; + 'attributes-type'?: string; + attributes?: Record; + }; }; expect(body.aps.timestamp).toBe(1_750_000_000); expect(body.aps.event).toBe('update'); expect(body.aps['content-state']).toEqual({ revision: 7, running: 1 }); + expect(body.aps['attributes-type']).toBeUndefined(); + expect(body.aps.attributes).toBeUndefined(); + }); + + it('adds attributes-type and attributes to a push-to-start payload', () => { + const request = buildLiveActivityApnsRequest({ + token: 'device-token-1', + event: 'start', + contentState: { name: 'ActiveAgentsLiveActivity', props: '{"running":1}' }, + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem: 'pem', topic: TOPIC }, + authorizationJwt: 'header.payload.sig', + timestampSeconds: 1_750_000_000, + }); + + const body = JSON.parse(request.body) as { + aps: { + timestamp: number; + event: string; + 'content-state': Record; + 'attributes-type'?: string; + attributes?: Record; + }; + }; + expect(body.aps.event).toBe('start'); + expect(body.aps['attributes-type']).toBe('LiveActivityAttributes'); + expect(body.aps.attributes).toEqual({}); + expect(body.aps['content-state']).toEqual({ + name: 'ActiveAgentsLiveActivity', + props: '{"running":1}', + }); }); }); diff --git a/services/notifications/src/lib/apns-live-activity.ts b/services/notifications/src/lib/apns-live-activity.ts index 0e44af929e..0e0f1c8647 100644 --- a/services/notifications/src/lib/apns-live-activity.ts +++ b/services/notifications/src/lib/apns-live-activity.ts @@ -19,6 +19,12 @@ const APNS_BASE_URL = 'https://api.push.apple.com'; const APNS_KEY_PREFIX = '-----BEGIN PRIVATE KEY-----'; const APNS_KEY_SUFFIX = '-----END PRIVATE KEY-----'; +/** + * The `ActivityAttributes` type name the widget extension declares. Push-to-start + * uses this as `attributes-type` so iOS knows which activity to create. + */ +const LIVE_ACTIVITY_ATTRIBUTES_TYPE = 'LiveActivityAttributes'; + const encoder = new TextEncoder(); function base64Url(bytes: Uint8Array): string { @@ -91,6 +97,11 @@ export function buildLiveActivityApnsRequest(params: { aps: { timestamp: params.timestampSeconds, event: params.event, + // Push-to-start must name the attributes type and supply its values so + // iOS can create the activity. Updates only replace the content-state. + ...(params.event === 'start' + ? { 'attributes-type': LIVE_ACTIVITY_ATTRIBUTES_TYPE, attributes: {} } + : {}), 'content-state': params.contentState, }, }), diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts index ebec737f4e..bca8351615 100644 --- a/services/notifications/src/lib/glanceable-delivery.test.ts +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -7,6 +7,7 @@ import { deliverGlanceableSnapshot, toGlanceableContentState, type ActiveAgentsGlanceable, + type GlanceableApnsContentState, type GlanceableDeliveryDeps, type IosActivityToken, } from './glanceable-delivery'; @@ -60,23 +61,32 @@ describe('apnsEventForTokenKind', () => { }); describe('toGlanceableContentState', () => { - it('strips the type discriminator and keeps every content-state field', () => { + it('wraps the renderable counts + status in the expo-widgets name/props envelope', () => { const contentState = toGlanceableContentState(snapshot); - expect(contentState).not.toHaveProperty('type'); - expect(contentState).toEqual({ - schemaVersion: 1, - revision: 3, - scopeKey: 'deadbeef', - organizationBound: false, + expect(contentState.name).toBe('ActiveAgentsLiveActivity'); + const props = JSON.parse(contentState.props) as Record; + expect(props).toEqual({ status: 'happy', running: 2, needsInput: 1, reconnecting: 0, - updatedAt: '2026-08-27T10:00:00.000Z', - expiresAt: '2026-08-27T18:00:00.000Z', eligibleStartedAt: '2026-08-27T09:00:00.000Z', }); }); + + it('never leaks snapshot bookkeeping, ids, or titles into the pushed content-state', () => { + const contentState = toGlanceableContentState(snapshot); + const raw = JSON.stringify(contentState); + expect(raw).not.toContain('schemaVersion'); + expect(raw).not.toContain('revision'); + expect(raw).not.toContain('scopeKey'); + expect(raw).not.toContain('deadbeef'); + expect(raw).not.toContain('organizationBound'); + expect(raw).not.toContain('updatedAt'); + expect(raw).not.toContain('expiresAt'); + expect(raw).not.toContain('accountEpoch'); + expect(raw).not.toContain('title'); + }); }); describe('buildAndroidGlanceableMessages', () => { @@ -129,15 +139,21 @@ describe('deliverGlanceableSnapshot', () => { expect(calls.iosSends).toHaveLength(1); const [tokens, contentState] = calls.iosSends[0] as [ { token: string; event: string }[], - Record, + GlanceableApnsContentState, ]; expect(tokens).toEqual([ { token: 'ptt-token', event: 'start' }, { token: 'activity-token', event: 'update' }, ]); - expect(contentState).not.toHaveProperty('type'); - expect(contentState).not.toHaveProperty('accountEpoch'); - expect(contentState.revision).toBe(3); + expect(contentState.name).toBe('ActiveAgentsLiveActivity'); + const props = JSON.parse(contentState.props) as Record; + expect(props.status).toBe('happy'); + expect(props.running).toBe(2); + expect(props.needsInput).toBe(1); + expect(props.reconnecting).toBe(0); + expect(props).not.toHaveProperty('type'); + expect(props).not.toHaveProperty('accountEpoch'); + expect(props).not.toHaveProperty('scopeKey'); expect(calls.androidSends).toHaveLength(0); }); diff --git a/services/notifications/src/lib/glanceable-delivery.ts b/services/notifications/src/lib/glanceable-delivery.ts index bd7ab2f1f3..6cf8a12a60 100644 --- a/services/notifications/src/lib/glanceable-delivery.ts +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -10,6 +10,7 @@ import { genericPushContentForPushData, resolvePushLocale, + type GlanceableLiveActivityContentState, type PushData, } from '@kilocode/notifications'; @@ -17,8 +18,19 @@ import type { LiveActivityEvent } from './apns-live-activity'; import type { ExpoPushMessage } from './expo-push'; export type ActiveAgentsGlanceable = Extract; -/** The APNs `content-state` is the snapshot without the `type` discriminator and without `accountEpoch`. */ -export type GlanceableContentState = Omit; + +/** Matches the first argument to `createLiveActivity` in the widget extension. */ +const ACTIVE_AGENTS_LIVE_ACTIVITY_NAME = 'ActiveAgentsLiveActivity'; + +/** + * The APNs Live Activity `content-state`. expo-widgets wraps the renderable + * props in a JSON string under `props` and routes on `name`, so iOS decodes + * `{ name, props }` into the widget extension's `LiveActivityAttributes`. + */ +export type GlanceableApnsContentState = { + name: string; + props: string; +}; export type IosActivityToken = { token: string; kind: 'ios_activity' | 'ios_push_to_start' }; export type AndroidPushToken = { token: string; locale: string | null }; @@ -27,9 +39,20 @@ export function apnsEventForTokenKind(kind: IosActivityToken['kind']): LiveActiv return kind === 'ios_push_to_start' ? 'start' : 'update'; } -export function toGlanceableContentState(snapshot: ActiveAgentsGlanceable): GlanceableContentState { - const { type: _type, ...contentState } = snapshot; - return contentState; +export function toGlanceableContentState( + snapshot: ActiveAgentsGlanceable +): GlanceableApnsContentState { + const contentState: GlanceableLiveActivityContentState = { + status: snapshot.status, + running: snapshot.running, + needsInput: snapshot.needsInput, + reconnecting: snapshot.reconnecting, + eligibleStartedAt: snapshot.eligibleStartedAt, + }; + return { + name: ACTIVE_AGENTS_LIVE_ACTIVITY_NAME, + props: JSON.stringify(contentState), + }; } export function buildAndroidGlanceableMessages( @@ -71,7 +94,7 @@ export type GlanceableDeliveryDeps = { ) => Promise; sendIosLiveActivity: ( tokens: readonly { token: string; event: LiveActivityEvent }[], - contentState: GlanceableContentState + contentState: GlanceableApnsContentState ) => Promise; listAndroidExpoTokens: ( userId: string, From b45355261576f489ce9de699e89a39a70cbd6222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 17:01:31 +0200 Subject: [PATCH 03/43] fix(glanceable): apply remote snapshot revision and org scope --- ENVIRONMENT.md | 1 + apps/mobile/src/lib/notification-path.test.ts | 19 +++ apps/mobile/src/lib/notifications.test.ts | 112 ++++++++++++++++++ apps/mobile/src/lib/notifications.ts | 46 +++++-- .../glanceable-agents-snapshot-server.test.ts | 58 +++++++++ .../src/push-presentation.test.ts | 15 +++ services/notifications/wrangler.jsonc | 5 +- 7 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/lib/glanceable-agents-snapshot-server.test.ts diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index c3208de065..621fcf9ad4 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -328,6 +328,7 @@ When `VERCEL_TARGET_ENV` is absent in local development or a script process, tra - `APNS_KEY_ID` - APNs key identifier (`kid`) for the Live Activity push key. [SERVER] - `APNS_PRIVATE_KEY` - PKCS#8 ES256 `.p8` private key contents for APNs provider-token signing. `[SECRET]` - `APNS_TOPIC` - iOS app bundle id (`com.kilocode.kiloapp`); Live Activity pushes use `.push-type.liveactivity`. [SERVER] +- `KILO_WEB_API_BASE_URL` - Base origin of the web app, used to reach the internal `glanceable-agents-snapshot` route; `https://app.kilo.ai` in production. [SERVER] ### KiloClaw Controller diff --git a/apps/mobile/src/lib/notification-path.test.ts b/apps/mobile/src/lib/notification-path.test.ts index bb7a9376f2..234ca30c42 100644 --- a/apps/mobile/src/lib/notification-path.test.ts +++ b/apps/mobile/src/lib/notification-path.test.ts @@ -55,6 +55,25 @@ describe('notificationPathForData', () => { ).toBe('/(app)/agent-chat/ses_1?via=push'); }); + it('routes active_agents_glanceable notifications to the agents tab', () => { + expect( + notificationPathForData({ + type: 'active_agents_glanceable', + schemaVersion: 1, + revision: 1, + scopeKey: 'scope-1', + organizationBound: false, + status: 'happy', + running: 1, + needsInput: 0, + reconnecting: 0, + updatedAt: '2026-01-01T00:00:00.000Z', + expiresAt: '2026-01-01T08:00:00.000Z', + eligibleStartedAt: '2026-01-01T00:00:00.000Z', + }) + ).toBe('/(app)/(tabs)/(2_agents)'); + }); + it('routes low_balance notifications to organization credit activity with via=push', () => { expect( notificationPathForData({ diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index e72d1efd44..2947a615b8 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -1,5 +1,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { + _resetGlanceablePersistForTests, + _setLastGlanceableSnapshotForTests, +} from '@/lib/glanceable/persist'; +import { registerGlanceableSink, unregisterGlanceableSink } from '@/lib/glanceable/sink-registry'; +import { applyGlanceablePushData } from './notifications'; + const mocks = vi.hoisted(() => { const platform = { OS: 'android' as string }; return { @@ -14,6 +22,7 @@ const mocks = vi.hoisted(() => { notificationPathForData: vi.fn(), setPendingDeepLink: vi.fn(), safeParse: vi.fn(), + getItemAsync: vi.fn(), }; }); @@ -42,6 +51,12 @@ vi.mock('expo-constants', () => ({ default: { expoConfig: { extra: { eas: { projectId: 'proj-1' } } } }, })); +vi.mock('expo-secure-store', () => ({ + getItemAsync: mocks.getItemAsync, + setItemAsync: vi.fn(), + deleteItemAsync: vi.fn(), +})); + vi.mock('@kilocode/notifications', () => ({ ANDROID_NOTIFICATION_CHANNELS: [ { id: 'agent', name: 'Agent sessions', importance: 'high' }, @@ -238,3 +253,100 @@ describe('setupNotificationResponseHandler', () => { expect(mocks.clearLastNotificationResponse).not.toHaveBeenCalled(); }); }); + +function glanceableSnapshot( + overrides: Partial = {} +): GlanceableAgentsSnapshot { + return { + schemaVersion: 1, + revision: 1, + updatedAt: '2026-01-01T00:00:00.000Z', + expiresAt: '2026-01-01T08:00:00.000Z', + scopeKey: 'scope-1', + organizationBound: false, + status: 'happy', + running: 1, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: '2026-01-01T00:00:00.000Z', + ...overrides, + }; +} + +type GlanceablePushData = Parameters[0]; + +function activeGlanceablePush( + overrides: Partial = {} +): GlanceablePushData { + return { + type: 'active_agents_glanceable', + ...glanceableSnapshot(overrides), + }; +} + +function makeFakeSink() { + return { + publish: vi.fn(), + endImmediate: vi.fn(), + startOrUpdate: vi.fn(), + }; +} + +describe('applyGlanceablePushData', () => { + beforeEach(() => { + _resetGlanceablePersistForTests(); + mocks.getItemAsync.mockResolvedValue(null); + }); + + it('discards a remote snapshot that is not newer than the last applied snapshot', async () => { + _setLastGlanceableSnapshotForTests( + glanceableSnapshot({ + scopeKey: 'scope-1', + revision: 3, + updatedAt: '2026-01-02T00:00:00.000Z', + }) + ); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + const result = await applyGlanceablePushData( + activeGlanceablePush({ scopeKey: 'scope-1', updatedAt: '2026-01-01T00:00:00.000Z' }) + ); + + expect(result).toBe(false); + expect(sink.publish).not.toHaveBeenCalled(); + expect(sink.startOrUpdate).not.toHaveBeenCalled(); + + unregisterGlanceableSink(sink); + }); + + it('applies a newer remote snapshot and re-registers under the selected organization', async () => { + _setLastGlanceableSnapshotForTests( + glanceableSnapshot({ + scopeKey: 'scope-1', + revision: 3, + updatedAt: '2026-01-01T00:00:00.000Z', + }) + ); + mocks.getItemAsync.mockResolvedValue('org-9'); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + const result = await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey: 'scope-1', + updatedAt: '2026-01-02T00:00:00.000Z', + organizationBound: true, + }) + ); + + expect(result).toBe(true); + // The rebased revision continues the local monotonic sequence. + expect(sink.publish).toHaveBeenCalledWith(expect.objectContaining({ revision: 4 })); + expect(sink.startOrUpdate).toHaveBeenCalledWith(expect.objectContaining({ revision: 4 }), { + organizationId: 'org-9', + }); + + unregisterGlanceableSink(sink); + }); +}); diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index 1e26752e11..a6167ba6c6 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -1,5 +1,6 @@ import expoConstants from 'expo-constants'; import * as Notifications from 'expo-notifications'; +import * as SecureStore from 'expo-secure-store'; import { Platform } from 'react-native'; import { z } from 'zod'; @@ -13,12 +14,12 @@ import { import { type GlanceableAgentsSnapshot, isEligibleGlanceableWork, - shouldDiscardGlanceableRevision, } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; import { getLastGlanceableSnapshot, getLocalScopeKey } from '@/lib/glanceable/persist'; import { getGlanceableSinks } from '@/lib/glanceable/sink-registry'; +import { ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { i18n } from '@/i18n'; import { setPendingDeepLink } from './deep-link-launch'; import { notificationPathForData } from './notification-path'; @@ -58,30 +59,40 @@ export function parseNotificationData(data: unknown): PushData | null { * Apply an `active_agents_glanceable` background push to the glanceable sinks * (widgets, Android ongoing, iOS Live Activity). Returns false when the push * must be dropped: its opaque scope key does not match the persisted local - * scope key, or its revision is older than the last applied snapshot. + * scope key, or it is not newer than the last applied snapshot. + * + * The server builds every remote snapshot with revision 1 (it never chains + * `previousRevision` across requests), so the revision cannot fence against the + * local monotonic sequence. Fence on `updatedAt` instead and rebase the remote + * revision onto the local sequence so the sinks' monotonic guards keep + * accepting it. * * The server omits `accountEpoch`, so it is set to the current local epoch * before publishing. Never opens a session chat. */ -export function applyGlanceablePushData( +export async function applyGlanceablePushData( data: Extract -): boolean { +): Promise { if (data.scopeKey !== getLocalScopeKey()) { return false; } const { type: _type, ...fields } = data; + const current = getLastGlanceableSnapshot(); + + if (current !== null && fields.updatedAt < current.updatedAt) { + return false; + } + const snapshot: GlanceableAgentsSnapshot = { ...fields, + revision: current === null ? fields.revision : current.revision + 1, accountEpoch: currentAuthEpoch(), }; - const current = getLastGlanceableSnapshot(); - if (current !== null && shouldDiscardGlanceableRevision(snapshot, current)) { - return false; - } + const organizationId = await getSelectedOrganizationId(); - const ctx = { organizationId: null }; + const ctx = { organizationId }; if (isEligibleGlanceableWork(snapshot)) { for (const sink of getGlanceableSinks()) { sink.publish(snapshot); @@ -95,6 +106,20 @@ export function applyGlanceablePushData( return true; } +/** + * Read the selected organization id from SecureStore. The scope-key fence above + * already proved the incoming snapshot belongs to the current scope, so this id + * (a string for an org scope, null for personal) keeps org-scoped APNs token + * lookups finding the token when `startOrUpdate` re-registers it. + */ +async function getSelectedOrganizationId(): Promise { + try { + return await SecureStore.getItemAsync(ORGANIZATION_STORAGE_KEY); + } catch { + return null; + } +} + const shown = { shouldPlaySound: true, shouldSetBadge: true, @@ -111,7 +136,6 @@ const suppressed = { export function setupNotificationHandler() { Notifications.setNotificationHandler({ - // eslint-disable-next-line require-await -- expo-notifications requires async callback type but logic is synchronous handleNotification: async notification => { const data = parseNotificationData(notification.request.content.data); @@ -119,7 +143,7 @@ export function setupNotificationHandler() { // The aggregate glanceable push is a data carrier for the ongoing // notification/widgets, never a visible banner: the local ongoing owns // the display. Apply it to the sinks regardless of the discard outcome. - applyGlanceablePushData(data); + await applyGlanceablePushData(data); return suppressed; } diff --git a/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts b/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts new file mode 100644 index 0000000000..f4595a9f40 --- /dev/null +++ b/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts @@ -0,0 +1,58 @@ +import type { ActiveSession } from '@/lib/active-sessions-list'; +import { listActiveSessions } from '@/lib/active-sessions-list'; + +jest.mock('@/lib/active-sessions-list', () => ({ + listActiveSessions: jest.fn(), +})); + +import { buildGlanceableSnapshotForUser } from './glanceable-agents-snapshot-server'; + +const mockedListActiveSessions = listActiveSessions as jest.MockedFunction< + typeof listActiveSessions +>; + +describe('buildGlanceableSnapshotForUser', () => { + beforeEach(() => { + mockedListActiveSessions.mockReset(); + }); + + it('copies no forbidden session field into the snapshot', async () => { + const sessions: (ActiveSession & { organizationName?: string })[] = [ + { + id: 'ses_raw_1', + status: 'busy', + title: 'Secret prompt', + connectionId: 'conn-1', + gitUrl: 'github.com/acme/repo', + organizationName: 'Acme Org', + organizationId: 'org-9', + }, + { + id: 'ses_raw_2', + status: 'question', + title: 'Another secret', + connectionId: 'conn-2', + }, + ]; + mockedListActiveSessions.mockResolvedValue({ sessions }); + + const snapshot = await buildGlanceableSnapshotForUser({ + userId: 'oauth/user-1', + organizationId: 'org-9', + }); + + const json = JSON.stringify(snapshot); + expect(json).not.toContain('Secret prompt'); + expect(json).not.toContain('Another secret'); + expect(json).not.toContain('github.com/acme/repo'); + expect(json).not.toContain('ses_raw_1'); + expect(json).not.toContain('ses_raw_2'); + expect(json).not.toContain('Acme Org'); + expect(json).not.toContain('oauth/user-1'); + expect(json).not.toContain('org-9'); + + expect(snapshot.status).toBe('happy'); + expect(snapshot.running).toBe(1); + expect(snapshot.needsInput).toBe(1); + }); +}); diff --git a/packages/notifications/src/push-presentation.test.ts b/packages/notifications/src/push-presentation.test.ts index b2f2dc17c4..9de3852551 100644 --- a/packages/notifications/src/push-presentation.test.ts +++ b/packages/notifications/src/push-presentation.test.ts @@ -19,6 +19,20 @@ const variants = [ { type: 'low_balance', organizationId: 'org1' }, { type: 'security_finding', findingId: 'f1', scope: 'org' }, { type: 'security_lifecycle', event: 'analysis_completed', findingId: 'f1', scope: 'org' }, + { + type: 'active_agents_glanceable', + schemaVersion: 1, + revision: 1, + scopeKey: 'scope-1', + organizationBound: false, + status: 'happy', + running: 1, + needsInput: 0, + reconnecting: 0, + updatedAt: '2026-01-01T00:00:00.000Z', + expiresAt: '2026-01-01T08:00:00.000Z', + eligibleStartedAt: '2026-01-01T00:00:00.000Z', + }, ] as const; describe('androidChannelIdForPushData', () => { @@ -44,6 +58,7 @@ describe('androidChannelIdForPushData', () => { low_balance: 'balance', security_finding: 'security', security_lifecycle: 'security', + active_agents_glanceable: 'active-agents', }; for (const variant of variants) { diff --git a/services/notifications/wrangler.jsonc b/services/notifications/wrangler.jsonc index 0cd77a2460..3fa241e4fe 100644 --- a/services/notifications/wrangler.jsonc +++ b/services/notifications/wrangler.jsonc @@ -8,7 +8,10 @@ "dev": { "port": 8804 }, "placement": { "mode": "smart" }, "observability": { "enabled": true }, - "vars": { "WORKER_ENV": "production" }, + "vars": { + "WORKER_ENV": "production", + "KILO_WEB_API_BASE_URL": "https://app.kilo.ai", + }, "routes": [ { From a9fe19f35b976fd7788692f510fd3928ed489cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 18:36:57 +0200 Subject: [PATCH 04/43] fix(glanceable): apply snapshot in the background --- apps/mobile/app.config.ts | 4 + apps/mobile/package.json | 1 + apps/mobile/src/app/_layout.tsx | 4 + apps/mobile/src/lib/notifications.test.ts | 148 +++++++++++++++++- apps/mobile/src/lib/notifications.ts | 124 ++++++++++++++- pnpm-lock.yaml | 20 +++ services/notifications/src/index.ts | 15 +- .../src/lib/glanceable-delivery.test.ts | 59 ++++--- .../src/lib/glanceable-delivery.ts | 67 ++++---- 9 files changed, 386 insertions(+), 56 deletions(-) diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index e672018c66..ca29520c35 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -190,6 +190,10 @@ const config: ExpoConfig = { { icon: './assets/images/android-notification-icon.png', color: '#FAF74F', + // iOS requires `remote-notification` in UIBackgroundModes for the + // headless background task (`registerTaskAsync`) to deliver a data-only + // `active_agents_glanceable` push while the app is not in the foreground. + enableBackgroundRemoteNotifications: true, }, ], 'expo-web-browser', diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 82c6387f3c..26e87e943e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -93,6 +93,7 @@ "expo-sqlite": "~57.0.1", "expo-status-bar": "57.0.1", "expo-store-review": "~57.0.1", + "expo-task-manager": "57.0.12", "expo-tracking-transparency": "~57.0.1", "expo-web-browser": "~57.0.2", "expo-widgets": "57.0.11", diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 43d42eda02..450a1b25ff 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -90,6 +90,7 @@ import { checkInitialNotification, ensureAndroidNotificationChannels, renameAndroidNotificationChannels, + setupNotificationBackgroundHandler, setupNotificationHandler, setupNotificationResponseHandler, } from '@/lib/notifications'; @@ -214,6 +215,9 @@ function preloadStartupFonts(): void { void SplashScreen.preventAutoHideAsync(); void ensureAndroidNotificationChannels(); setupNotificationHandler(); +// Applies the aggregate glanceable push while backgrounded/killed via a +// headless expo-notifications task; see setupNotificationBackgroundHandler. +setupNotificationBackgroundHandler(); checkInitialNotification(); captureLaunchDeepLink(); prefetchCurrentUser(); diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index 2947a615b8..deb083bc6d 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -1,12 +1,18 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +/* eslint-disable max-lines -- one cohesive notification suite sharing the glanceable sink and native module mock harness. */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests, + _setSecureStoreForTests, } from '@/lib/glanceable/persist'; import { registerGlanceableSink, unregisterGlanceableSink } from '@/lib/glanceable/sink-registry'; -import { applyGlanceablePushData } from './notifications'; +import { + _setGlanceableSinksLoaderForTests, + applyGlanceablePushData, + setupNotificationBackgroundHandler, +} from './notifications'; const mocks = vi.hoisted(() => { const platform = { OS: 'android' as string }; @@ -23,6 +29,8 @@ const mocks = vi.hoisted(() => { setPendingDeepLink: vi.fn(), safeParse: vi.fn(), getItemAsync: vi.fn(), + defineTask: vi.fn(), + registerTaskAsync: vi.fn(), }; }); @@ -39,10 +47,16 @@ vi.mock('expo-notifications', () => ({ addNotificationResponseReceivedListener: mocks.addNotificationResponseReceivedListener, getLastNotificationResponse: vi.fn(), clearLastNotificationResponse: mocks.clearLastNotificationResponse, + registerTaskAsync: mocks.registerTaskAsync, + BackgroundNotificationTaskResult: { NewData: 0, NoData: 1, Failed: 2 }, AndroidImportance: { HIGH: 4, DEFAULT: 3 }, PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' }, })); +vi.mock('expo-task-manager', () => ({ + defineTask: mocks.defineTask, +})); + vi.mock('@sentry/react-native', () => ({ captureException: mocks.captureException, })); @@ -292,6 +306,22 @@ function makeFakeSink() { }; } +// Map-backed SecureStore surface for the persist module's restore path. The +// persist module lazy-`require`s `expo-secure-store` (a native module), which +// cannot load in the pure-vitest suite, so the restore tests inject this store +// through the test-only setter — the same pattern persist.test.ts uses. +const secureStore = new Map(); +const secureStoreMock = { + setItemAsync: vi.fn(async (key: string, value: string) => { + secureStore.set(key, value); + await Promise.resolve(); + }), + getItemAsync: vi.fn(async (key: string) => { + await Promise.resolve(); + return secureStore.get(key) ?? null; + }), +}; + describe('applyGlanceablePushData', () => { beforeEach(() => { _resetGlanceablePersistForTests(); @@ -350,3 +380,117 @@ describe('applyGlanceablePushData', () => { unregisterGlanceableSink(sink); }); }); + +describe('setupNotificationBackgroundHandler', () => { + type HeadlessExecutor = (body: { + data: unknown; + error: unknown; + executionInfo: unknown; + }) => Promise; + + function executorFor(mock: typeof mocks.defineTask): HeadlessExecutor { + const firstCall = mock.mock.calls[0]; + if (!firstCall) { + throw new Error('defineTask was not called before executorFor'); + } + return firstCall[1] as HeadlessExecutor; + } + + beforeEach(() => { + _resetGlanceablePersistForTests(); + _setSecureStoreForTests(secureStoreMock); + secureStore.clear(); + mocks.getItemAsync.mockResolvedValue(null); + mocks.defineTask.mockReset(); + mocks.registerTaskAsync.mockResolvedValue(null); + }); + + afterEach(() => { + _resetGlanceablePersistForTests(); + secureStore.clear(); + }); + + it('restores the persisted fence then applies a glanceable push via applyGlanceablePushData', async () => { + // Leave in-memory state empty and persist the fence in SecureStore instead, + // exactly as a killed process finds it. The executor must call + // `restorePersistedGlanceable` before applying; without it the scope-key + // fence discards the push and the sink never publishes. + const persisted = glanceableSnapshot({ + scopeKey: 'scope-1', + revision: 1, + updatedAt: '2026-01-01T00:00:00.000Z', + }); + secureStore.set('glanceable-snapshot', JSON.stringify(persisted)); + secureStore.set('glanceable-scope-key', 'scope-1'); + mocks.safeParse.mockImplementation((data: unknown) => ({ success: true, data })); + _setGlanceableSinksLoaderForTests(() => undefined); + + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + setupNotificationBackgroundHandler(); + + expect(mocks.defineTask).toHaveBeenCalledTimes(1); + expect(mocks.defineTask).toHaveBeenCalledWith( + 'active-agents-glanceable-background-task', + expect.any(Function) + ); + expect(mocks.registerTaskAsync).toHaveBeenCalledWith( + 'active-agents-glanceable-background-task' + ); + + const executor = executorFor(mocks.defineTask); + const result = await executor({ + data: { + notification: null, + data: { + dataString: JSON.stringify( + activeGlanceablePush({ + scopeKey: 'scope-1', + updatedAt: '2026-01-02T00:00:00.000Z', + organizationBound: true, + }) + ), + }, + }, + error: null, + executionInfo: { eventId: 'e1', taskName: 'active-agents-glanceable-background-task' }, + }); + + // A successful apply delivered new sink data, so the executor reports + // NewData (0), not NoData (1), which throttles iOS content-available wakes. + expect(result).toBe(0); + // The rebased revision proves the restored fence and the single apply code + // path ran, not a duplicated one. + expect(sink.publish).toHaveBeenCalledWith(expect.objectContaining({ revision: 2 })); + expect(sink.startOrUpdate).toHaveBeenCalledWith(expect.objectContaining({ revision: 2 }), { + organizationId: null, + }); + + unregisterGlanceableSink(sink); + }); + + it('ignores a headless payload that is not a glanceable push', async () => { + mocks.safeParse.mockImplementation((data: unknown) => ({ success: true, data })); + _setGlanceableSinksLoaderForTests(() => undefined); + + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + setupNotificationBackgroundHandler(); + + const executor = executorFor(mocks.defineTask); + await executor({ + data: { + notification: null, + data: { dataString: JSON.stringify({ type: 'chat.message' }) }, + }, + error: null, + executionInfo: { eventId: 'e2', taskName: 'active-agents-glanceable-background-task' }, + }); + + expect(sink.publish).not.toHaveBeenCalled(); + + unregisterGlanceableSink(sink); + }); +}); diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index a6167ba6c6..bcd125418a 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -1,6 +1,8 @@ +/* eslint-disable max-lines -- notification wiring: foreground/background handlers, channels, and push-token plumbing are kept together. */ import expoConstants from 'expo-constants'; import * as Notifications from 'expo-notifications'; import * as SecureStore from 'expo-secure-store'; +import * as TaskManager from 'expo-task-manager'; import { Platform } from 'react-native'; import { z } from 'zod'; @@ -17,8 +19,13 @@ import { } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; -import { getLastGlanceableSnapshot, getLocalScopeKey } from '@/lib/glanceable/persist'; -import { getGlanceableSinks } from '@/lib/glanceable/sink-registry'; +import { + getLastGlanceableSnapshot, + getLocalScopeKey, + persistGlanceableSink, + restorePersistedGlanceable, +} from '@/lib/glanceable/persist'; +import { getGlanceableSinks, registerGlanceableSink } from '@/lib/glanceable/sink-registry'; import { ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { i18n } from '@/i18n'; import { setPendingDeepLink } from './deep-link-launch'; @@ -159,6 +166,119 @@ export function setupNotificationHandler() { }); } +const GLANCEABLE_BACKGROUND_TASK = 'active-agents-glanceable-background-task'; + +// Expo wraps the data payload of a background notification in a JSON string on +// both platforms; decode that envelope before parsing the push data itself. +const headlessTaskDataSchema = z.object({ dataString: z.string() }); + +// Test-only override so the background-handler suite never loads the platform +// sink register files (expo-widgets / react-native-android-widget native loads). +let glanceableSinksLoaderForTests: (() => void) | null = null; + +export function _setGlanceableSinksLoaderForTests(loader: (() => void) | null): void { + glanceableSinksLoaderForTests = loader; +} + +/** + * Register the persist sink and the platform sinks so a headless apply has + * somewhere to publish. The root layout imports the platform register files in + * the foreground; the headless task context loads only this module, so the + * sinks must be registered here before `applyGlanceablePushData` runs. + */ +function ensureGlanceableSinksLoaded(): void { + if (glanceableSinksLoaderForTests) { + glanceableSinksLoaderForTests(); + return; + } + registerGlanceableSink(persistGlanceableSink); + // Side-effect imports register the platform sinks. + // eslint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires, unicorn/prefer-module -- lazy platform sink load + require('@/glanceable-ios/register'); + try { + // eslint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires, unicorn/prefer-module -- lazy platform sink load + require('@/glanceable-android/register'); + } catch { + // react-native-android-widget is absent on iOS; the iOS sink still loaded. + } +} + +/** Recover the typed push data from the headless payload envelope. */ +function parseHeadlessPushData(data: unknown): PushData | null { + const envelope = headlessTaskDataSchema.safeParse(data); + if (!envelope.success) { + return parseNotificationData(data); + } + try { + return parseNotificationData(JSON.parse(envelope.data.dataString)); + } catch { + return null; + } +} + +/** + * Headless background-notification executor. Runs when a data-only push is + * delivered while the app is backgrounded or killed. Reuses + * `applyGlanceablePushData` so the scope-key fence, revision discard, and org + * re-register behave identically to the foreground path. + */ +async function handleBackgroundNotificationTask( + body: TaskManager.TaskManagerTaskBody +): Promise { + const { data, error } = body; + if (error) { + return Notifications.BackgroundNotificationTaskResult.Failed; + } + // A notification *response* (a tap) is not a delivered push; the glanceable + // apply runs only for a delivered data-only push. + if ('actionIdentifier' in data) { + return Notifications.BackgroundNotificationTaskResult.NoData; + } + + const pushData = parseHeadlessPushData(data.data); + if (pushData?.type !== 'active_agents_glanceable') { + return Notifications.BackgroundNotificationTaskResult.NoData; + } + + // The headless process is fresh: restore the persisted snapshot and scope key + // so the fence and revision discard below compare against durable state. + await restorePersistedGlanceable(); + const applied = await applyGlanceablePushData(pushData); + // A successful apply delivered new sink data: report NewData so iOS does not + // throttle later content-available wakes (repeated NoData reduces them). + return applied + ? Notifications.BackgroundNotificationTaskResult.NewData + : Notifications.BackgroundNotificationTaskResult.NoData; +} + +async function registerBackgroundNotificationTask(): Promise { + try { + await Notifications.registerTaskAsync(GLANCEABLE_BACKGROUND_TASK); + } catch (error) { + Sentry.captureException(error, { + tags: { + 'error.subsystem': 'notifications', + 'error.operation': 'register_background_task', + }, + }); + } +} + +/** + * Register the background notification task so a data-only + * `active_agents_glanceable` push is applied while the app is backgrounded or + * killed. `defineTask` must run at module scope of the root layout, not inside + * a React effect. + */ +export function setupNotificationBackgroundHandler(): void { + ensureGlanceableSinksLoaded(); + TaskManager.defineTask( + GLANCEABLE_BACKGROUND_TASK, + handleBackgroundNotificationTask + ); + void registerBackgroundNotificationTask(); +} + export function setupNotificationResponseHandler() { const subscription = Notifications.addNotificationResponseReceivedListener(response => { const data = parseNotificationData(response.notification.request.content.data); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4ee52ed88..29ef61b089 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -526,6 +526,9 @@ importers: expo-store-review: specifier: ~57.0.1 version: 57.0.1(expo@57.0.10)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-task-manager: + specifier: 57.0.12 + version: 57.0.12(expo@57.0.10)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-tracking-transparency: specifier: ~57.0.1 version: 57.0.1(expo@57.0.10)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) @@ -12881,6 +12884,12 @@ packages: react: '*' react-native: '*' + expo-task-manager@57.0.12: + resolution: {integrity: sha512-Cs9JYqPle7TzPjfFxN7ym96arCB9/Izgaq22UuADF8dHZgsNM0yT0ucMj3/Wp2Yh6mXFruArtWgCd5b48AZWZw==} + peerDependencies: + expo: '*' + react-native: '*' + expo-tracking-transparency@57.0.1: resolution: {integrity: sha512-gL4sIKFaXfvlLQYpuOPjY5HhdfMgbZNCA4UVO9cS9kNt1TuyrEAfa/O1nCSFXv6VRwQoFxPsXEWQ6ru7sFortA==} peerDependencies: @@ -18052,6 +18061,9 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unimodules-app-loader@57.0.1: + resolution: {integrity: sha512-wey5ChoJkCTq0j0JWdIMu2QB81vVrdhmrNAP14ZZ6WDslnZ7ff7Ezv8rMdEnVHaCKz3xK4mIVXbVU51xHgdyCA==} + unimport@6.3.0: resolution: {integrity: sha512-M+Dxk5W9WRd+8j56W9tp8lGW/dmMc7g5zj7BWQnEjKQhryBstqsi1V0izb0zHwSkEN8cSYV7K75/bykairV2tA==} engines: {node: '>=18.12.0'} @@ -30458,6 +30470,12 @@ snapshots: sf-symbols-typescript: 2.2.0 optional: true + expo-task-manager@57.0.12(expo@57.0.10)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + dependencies: + expo: 57.0.10(@babel/core@7.29.7)(@expo/metro-runtime@57.0.8)(bufferutil@4.1.0)(expo-router@57.0.10)(expo-widgets@57.0.11)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + react-native: 0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + unimodules-app-loader: 57.0.1 + expo-tracking-transparency@57.0.1(expo@57.0.10)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: expo: 57.0.10(@babel/core@7.29.7)(@expo/metro-runtime@57.0.8)(bufferutil@4.1.0)(expo-router@57.0.10)(expo-widgets@57.0.11)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) @@ -37402,6 +37420,8 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unimodules-app-loader@57.0.1: {} + unimport@6.3.0(oxc-parser@0.140.0)(rolldown@1.0.3): dependencies: acorn: 8.16.0 diff --git a/services/notifications/src/index.ts b/services/notifications/src/index.ts index 058dce76ec..8fbca64a01 100644 --- a/services/notifications/src/index.ts +++ b/services/notifications/src/index.ts @@ -458,12 +458,23 @@ export class NotificationsService extends WorkerEntrypoint { }); } }, + listIosExpoTokens: async userId => { + const rows = await getDbForCall() + .select({ token: user_push_tokens.token, locale: user_push_tokens.locale }) + .from(user_push_tokens) + .where(and(eq(user_push_tokens.user_id, userId), eq(user_push_tokens.platform, 'ios'))); + return rows.map(row => ({ token: row.token, locale: row.locale })); + }, listAndroidExpoTokens: async userId => { const rows = await getDbForCall() .select({ token: user_push_tokens.token, locale: user_push_tokens.locale }) .from(user_push_tokens) .where( - and(eq(user_push_tokens.user_id, userId), isNotNull(user_push_tokens.app_version)) + and( + eq(user_push_tokens.user_id, userId), + eq(user_push_tokens.platform, 'android'), + isNotNull(user_push_tokens.app_version) + ) ); return rows.map(row => ({ token: row.token, locale: row.locale })); }, @@ -485,7 +496,7 @@ export class NotificationsService extends WorkerEntrypoint { .limit(1); return row !== undefined; }, - sendAndroidPush: async messages => { + sendExpoPush: async messages => { const accessToken = await this.env.EXPO_ACCESS_TOKEN.get(); await sendPushNotifications(messages, accessToken); }, diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts index bca8351615..776399e189 100644 --- a/services/notifications/src/lib/glanceable-delivery.test.ts +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import type { ExpoPushMessage } from './expo-push'; import { apnsEventForTokenKind, - buildAndroidGlanceableMessages, + buildGlanceableExpoMessages, deliverGlanceableSnapshot, toGlanceableContentState, type ActiveAgentsGlanceable, @@ -29,9 +29,9 @@ const snapshot: ActiveAgentsGlanceable = { function fakeDeps(overrides: Partial = {}): { deps: GlanceableDeliveryDeps; - calls: { iosSends: unknown[][]; androidSends: ExpoPushMessage[][] }; + calls: { iosSends: unknown[][]; expoSends: ExpoPushMessage[][] }; } { - const calls = { iosSends: [] as unknown[][], androidSends: [] as ExpoPushMessage[][] }; + const calls = { iosSends: [] as unknown[][], expoSends: [] as ExpoPushMessage[][] }; const deps: GlanceableDeliveryDeps = { buildSnapshot: vi.fn(async () => snapshot), @@ -39,10 +39,11 @@ function fakeDeps(overrides: Partial = {}): { sendIosLiveActivity: vi.fn(async (_tokens, _contentState) => { calls.iosSends.push([_tokens, _contentState]); }), + listIosExpoTokens: vi.fn(async () => []), listAndroidExpoTokens: vi.fn(async () => []), hasAndroidOngoingToken: vi.fn(async () => false), - sendAndroidPush: vi.fn(async messages => { - calls.androidSends.push(messages); + sendExpoPush: vi.fn(async messages => { + calls.expoSends.push(messages); }), ...overrides, }; @@ -89,9 +90,9 @@ describe('toGlanceableContentState', () => { }); }); -describe('buildAndroidGlanceableMessages', () => { - it('emits one low-interruption, tag-collapsed message per Expo token', () => { - const messages = buildAndroidGlanceableMessages( +describe('buildGlanceableExpoMessages', () => { + it('emits one data-only, tag-collapsed message per Expo token', () => { + const messages = buildGlanceableExpoMessages( [ { token: 'ExponentPushToken[aaa]', locale: null }, { token: 'ExponentPushToken[bbb]', locale: 'es' }, @@ -102,12 +103,13 @@ describe('buildAndroidGlanceableMessages', () => { expect(messages).toHaveLength(2); for (const message of messages) { expect(message.data).toEqual(snapshot); + expect(message._contentAvailable).toBe(true); + expect(message.title).toBeUndefined(); + expect(message.body).toBeUndefined(); expect(message.sound).toBeNull(); expect(message.priority).toBe('default'); expect(message.channelId).toBe('active-agents'); expect(message.tag).toBe('deadbeef'); - expect(typeof message.title).toBe('string'); - expect(typeof message.body).toBe('string'); } expect(messages.map(m => m.to)).toEqual(['ExponentPushToken[aaa]', 'ExponentPushToken[bbb]']); }); @@ -120,9 +122,10 @@ describe('deliverGlanceableSnapshot', () => { await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); expect(deps.listIosActivityTokens).not.toHaveBeenCalled(); + expect(deps.listIosExpoTokens).not.toHaveBeenCalled(); expect(deps.hasAndroidOngoingToken).not.toHaveBeenCalled(); expect(calls.iosSends).toHaveLength(0); - expect(calls.androidSends).toHaveLength(0); + expect(calls.expoSends).toHaveLength(0); }); it('delivers the content-state to iOS tokens with the right start/update events', async () => { @@ -154,7 +157,7 @@ describe('deliverGlanceableSnapshot', () => { expect(props).not.toHaveProperty('type'); expect(props).not.toHaveProperty('accountEpoch'); expect(props).not.toHaveProperty('scopeKey'); - expect(calls.androidSends).toHaveLength(0); + expect(calls.expoSends).toHaveLength(0); }); it('skips Android when no android_ongoing activity token exists', async () => { @@ -166,7 +169,7 @@ describe('deliverGlanceableSnapshot', () => { await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); expect(deps.listAndroidExpoTokens).not.toHaveBeenCalled(); - expect(calls.androidSends).toHaveLength(0); + expect(calls.expoSends).toHaveLength(0); }); it('sends the Android Expo push only when an ongoing token and Expo tokens both exist', async () => { @@ -177,10 +180,11 @@ describe('deliverGlanceableSnapshot', () => { await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); - expect(calls.androidSends).toHaveLength(1); - expect(calls.androidSends[0]).toHaveLength(1); - expect(calls.androidSends[0][0].to).toBe('ExponentPushToken[aaa]'); - expect(calls.androidSends[0][0].tag).toBe('deadbeef'); + expect(calls.expoSends).toHaveLength(1); + expect(calls.expoSends[0]).toHaveLength(1); + expect(calls.expoSends[0][0].to).toBe('ExponentPushToken[aaa]'); + expect(calls.expoSends[0][0].tag).toBe('deadbeef'); + expect(calls.expoSends[0][0]._contentAvailable).toBe(true); }); it('sends nothing on Android when the user has no Expo tokens even with an ongoing token', async () => { @@ -191,7 +195,24 @@ describe('deliverGlanceableSnapshot', () => { await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); - expect(deps.sendAndroidPush).not.toHaveBeenCalled(); - expect(calls.androidSends).toHaveLength(0); + expect(deps.sendExpoPush).not.toHaveBeenCalled(); + expect(calls.expoSends).toHaveLength(0); + }); + + it('sends the data-only iOS Expo push regardless of the android_ongoing token', async () => { + const { deps, calls } = fakeDeps({ + hasAndroidOngoingToken: vi.fn(async () => false), + listIosExpoTokens: vi.fn(async () => [{ token: 'ExponentPushToken[ios]', locale: null }]), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); + + expect(deps.listIosExpoTokens).toHaveBeenCalledWith('u1', null); + expect(calls.expoSends).toHaveLength(1); + expect(calls.expoSends[0]).toHaveLength(1); + expect(calls.expoSends[0][0].to).toBe('ExponentPushToken[ios]'); + expect(calls.expoSends[0][0]._contentAvailable).toBe(true); + expect(calls.expoSends[0][0].title).toBeUndefined(); + expect(calls.expoSends[0][0].body).toBeUndefined(); }); }); diff --git a/services/notifications/src/lib/glanceable-delivery.ts b/services/notifications/src/lib/glanceable-delivery.ts index 6cf8a12a60..25deea8865 100644 --- a/services/notifications/src/lib/glanceable-delivery.ts +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -3,16 +3,11 @@ * widgets, and Android ongoing notification. Runs after a cloud-agent session * notification send: it fetches the fresh snapshot from the web internal route, * then pushes it to the registered iOS activity tokens over APNs and to the - * user's Expo tokens on Android. Pure orchestrator — all IO is injected via - * `deps` so tests substitute in-memory fakes. + * user's Expo tokens on iOS and Android. Pure orchestrator — all IO is injected + * via `deps` so tests substitute in-memory fakes. */ -import { - genericPushContentForPushData, - resolvePushLocale, - type GlanceableLiveActivityContentState, - type PushData, -} from '@kilocode/notifications'; +import { type GlanceableLiveActivityContentState, type PushData } from '@kilocode/notifications'; import type { LiveActivityEvent } from './apns-live-activity'; import type { ExpoPushMessage } from './expo-push'; @@ -33,7 +28,7 @@ export type GlanceableApnsContentState = { }; export type IosActivityToken = { token: string; kind: 'ios_activity' | 'ios_push_to_start' }; -export type AndroidPushToken = { token: string; locale: string | null }; +export type ExpoPushToken = { token: string; locale: string | null }; export function apnsEventForTokenKind(kind: IosActivityToken['kind']): LiveActivityEvent { return kind === 'ios_push_to_start' ? 'start' : 'update'; @@ -55,27 +50,29 @@ export function toGlanceableContentState( }; } -export function buildAndroidGlanceableMessages( - tokens: readonly AndroidPushToken[], +export function buildGlanceableExpoMessages( + tokens: readonly ExpoPushToken[], snapshot: ActiveAgentsGlanceable ): ExpoPushMessage[] { - return tokens.map(({ token, locale }) => { - const { title, body } = genericPushContentForPushData(snapshot, resolvePushLocale(locale)); - return { - to: token, - title, - body, - data: snapshot, - // The aggregate push is a data carrier for the ongoing notification, so - // it never rings or interrupts: no sound, default (not high) priority. - sound: null, - priority: 'default', - channelId: 'active-agents', - // Android collapse key = the opaque scope key, so every aggregate update - // for one user+org collapses into the same ongoing notification. - tag: snapshot.scopeKey, - } satisfies ExpoPushMessage; - }); + return tokens.map( + ({ token }) => + ({ + to: token, + data: snapshot, + // Data-only wake: `_contentAvailable` makes the OS deliver the message to + // the background task while the app is backgrounded/killed, and omitting + // title/body keeps it from becoming a visible FCM notification that skips + // the task. The ongoing notification and widget content come from the local + // `applyGlanceablePushData` path, so the push never rings or interrupts. + _contentAvailable: true, + sound: null, + priority: 'default', + channelId: 'active-agents', + // Android collapse key = the opaque scope key, so every aggregate update + // for one user+org collapses into the same ongoing notification. + tag: snapshot.scopeKey, + }) satisfies ExpoPushMessage + ); } export type GlanceableDeliveryDeps = { @@ -96,12 +93,13 @@ export type GlanceableDeliveryDeps = { tokens: readonly { token: string; event: LiveActivityEvent }[], contentState: GlanceableApnsContentState ) => Promise; + listIosExpoTokens: (userId: string, organizationId: string | null) => Promise; listAndroidExpoTokens: ( userId: string, organizationId: string | null - ) => Promise; + ) => Promise; hasAndroidOngoingToken: (userId: string, organizationId: string | null) => Promise; - sendAndroidPush: (messages: ExpoPushMessage[]) => Promise; + sendExpoPush: (messages: ExpoPushMessage[]) => Promise; }; export async function deliverGlanceableSnapshot( @@ -122,10 +120,17 @@ export async function deliverGlanceableSnapshot( ); } + // iOS Expo tokens always need the data-only wake: it drives the widget + // timeline through the background task while the app is not foregrounded. + const iosExpoTokens = await deps.listIosExpoTokens(params.userId, params.organizationId); + if (iosExpoTokens.length > 0) { + await deps.sendExpoPush(buildGlanceableExpoMessages(iosExpoTokens, snapshot)); + } + if (await deps.hasAndroidOngoingToken(params.userId, params.organizationId)) { const expoTokens = await deps.listAndroidExpoTokens(params.userId, params.organizationId); if (expoTokens.length > 0) { - await deps.sendAndroidPush(buildAndroidGlanceableMessages(expoTokens, snapshot)); + await deps.sendExpoPush(buildGlanceableExpoMessages(expoTokens, snapshot)); } } } From 81a7066adcc714eff0c6dbe15bf75abcce73bd14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 20:11:34 +0200 Subject: [PATCH 05/43] fix(glanceable): await activity-token unregister on logout --- .../glanceable-android/android-sink.test.ts | 2 +- .../src/glanceable-ios/ios-sink.test.ts | 2 +- apps/mobile/src/glanceable-ios/ios-sink.ts | 4 +- .../src/lib/auth/logout-cleanup.test.ts | 79 ++++++++++++- apps/mobile/src/lib/auth/logout-cleanup.ts | 27 +++-- .../lib/auth/logout-reconciliation.test.ts | 73 ++++++++++++ .../src/lib/auth/logout-reconciliation.ts | 65 ++++++++++- .../glanceable/delivery-registration.test.ts | 96 ++++++++++++++++ .../lib/glanceable/delivery-registration.ts | 79 +++++++++---- .../src/lib/glanceable/sink-registry.ts | 21 +++- apps/mobile/src/lib/notifications.test.ts | 25 ++++- apps/mobile/src/lib/notifications.ts | 19 +++- apps/web/src/routers/user-router.test.ts | 105 ++++++++++++++++++ 13 files changed, 550 insertions(+), 47 deletions(-) create mode 100644 apps/mobile/src/lib/glanceable/delivery-registration.test.ts diff --git a/apps/mobile/src/glanceable-android/android-sink.test.ts b/apps/mobile/src/glanceable-android/android-sink.test.ts index c1119ec966..802008c0ca 100644 --- a/apps/mobile/src/glanceable-android/android-sink.test.ts +++ b/apps/mobile/src/glanceable-android/android-sink.test.ts @@ -39,7 +39,7 @@ vi.mock('react-native-android-widget', () => ({ })); const NOW = 1_750_000_000_000; -const CTX = { organizationId: null }; +const CTX = { organizationId: null, userId: 'u1' }; function snapshotFor( sessions: { status: string }[], diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index 943651ef8d..f5853f7371 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -86,7 +86,7 @@ const CTX = { userId: 'u1', organizationId: null }; const delivery = { registerTokens: vi.fn(), - unregisterTokens: vi.fn(), + unregisterTokens: vi.fn().mockResolvedValue({ ok: true, tokens: [] }), }; function snapshotFor( diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index cbcb9e5c67..20c6e96432 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -87,7 +87,7 @@ function endNow(): void { void activity.end('immediate', lastProps ?? undefined, contentDate); activity = null; revision = 0; - getGlanceableDelivery().unregisterTokens(); + void getGlanceableDelivery().unregisterTokens(); } /** True once ActivityKit reported the surface unavailable (see slice psh for the alert). */ @@ -172,7 +172,7 @@ export const iosSink: GlanceableSink = { lastUpdatedAt = snapshot.updatedAt; lastProps = contentState; revision = snapshot.revision; - getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId); + getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId); if (adopted) { void activity.update(contentState); } diff --git a/apps/mobile/src/lib/auth/logout-cleanup.test.ts b/apps/mobile/src/lib/auth/logout-cleanup.test.ts index 2c5b9fe7f0..daeda1513e 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.test.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.test.ts @@ -21,10 +21,19 @@ const trpcMock = vi.hoisted(() => ({ unregisterPushToken: { mutate: vi.fn() }, })); +const deliveryMock = vi.hoisted(() => ({ + registerTokens: vi.fn(), + unregisterTokens: vi.fn(), +})); + vi.mock('@/lib/trpc', () => ({ trpcClient: { user: trpcMock }, })); +vi.mock('@/lib/glanceable/sink-registry', () => ({ + getGlanceableDelivery: () => deliveryMock, +})); + vi.mock('@/lib/notifications', () => ({ getDevicePushTokenOutcome: vi.fn(), })); @@ -86,6 +95,7 @@ describe('runLogoutCleanup', () => { expiresAtMs: null, }); seedUser('u1'); + deliveryMock.unregisterTokens.mockResolvedValue({ ok: true, tokens: [] }); }); it('revokes the session and unregisters the token, then deletes any existing tombstone on full success', async () => { @@ -127,6 +137,8 @@ describe('runLogoutCleanup', () => { userId: 'u1', pushToken: 'push-1', needsPushUnregister: true, + needsActivityUnregister: false, + activityTokens: [], failedAt: expect.any(Number), }); }); @@ -181,6 +193,55 @@ describe('runLogoutCleanup', () => { expect(store.has(LOGOUT_CLEANUP_TOMBSTONE_KEY)).toBe(false); }); + it('awaits the activity unregister and tombstones its recorded tokens when it fails', async () => { + pushOutcome('none'); + trpcMock.revokeCurrentDeviceSession.mutate.mockResolvedValue({ outcome: 'revoked' }); + deliveryMock.unregisterTokens.mockResolvedValue({ + ok: false, + tokens: ['activity-token-1', 'activity-token-2'], + }); + + await runLogoutCleanup(); + + expect(deliveryMock.unregisterTokens).toHaveBeenCalledTimes(1); + const tombstone = await readLogoutCleanupTombstone(); + expect(tombstone).toMatchObject({ + needsPushUnregister: false, + needsActivityUnregister: true, + activityTokens: ['activity-token-1', 'activity-token-2'], + }); + }); + + it('does not write the tombstone until the activity unregister settles', async () => { + pushOutcome('none'); + trpcMock.revokeCurrentDeviceSession.mutate.mockResolvedValue({ outcome: 'revoked' }); + const gate = { release: null as (() => void) | null }; + const unregisterGate = new Promise(resolve => { + gate.release = resolve; + }); + deliveryMock.unregisterTokens.mockImplementation(async () => { + await unregisterGate; + return { ok: false, tokens: ['activity-token-1'] }; + }); + + const run = runLogoutCleanup(); + // Flush microtasks and a macrotask: the activity unregister is still in + // flight, so the tombstone must not be written yet. + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + expect(store.has(LOGOUT_CLEANUP_TOMBSTONE_KEY)).toBe(false); + + gate.release?.(); + await run; + + const tombstone = await readLogoutCleanupTombstone(); + expect(tombstone).toMatchObject({ + needsActivityUnregister: true, + activityTokens: ['activity-token-1'], + }); + }); + it('still resolves when a tombstone write fails and reports it to Sentry', async () => { pushOutcome('token'); trpcMock.revokeCurrentDeviceSession.mutate.mockRejectedValue(new Error('network down')); @@ -234,12 +295,26 @@ describe('runLogoutCleanup', () => { [ 'is fully valid', { userId: 'u1', pushToken: null, needsPushUnregister: false, failedAt: 1_700_000_000_000 }, - { userId: 'u1', pushToken: null, needsPushUnregister: false, failedAt: 1_700_000_000_000 }, + { + userId: 'u1', + pushToken: null, + needsPushUnregister: false, + needsActivityUnregister: false, + activityTokens: [], + failedAt: 1_700_000_000_000, + }, ], [ 'is valid with a null userId (identity unknown)', { userId: null, pushToken: 'push-1', needsPushUnregister: true, failedAt: 1_700_000_000_000 }, - { userId: null, pushToken: 'push-1', needsPushUnregister: true, failedAt: 1_700_000_000_000 }, + { + userId: null, + pushToken: 'push-1', + needsPushUnregister: true, + needsActivityUnregister: false, + activityTokens: [], + failedAt: 1_700_000_000_000, + }, ], ])('reads a persisted tombstone that %s', async (_label, persisted, expected) => { store.set(LOGOUT_CLEANUP_TOMBSTONE_KEY, JSON.stringify(persisted)); diff --git a/apps/mobile/src/lib/auth/logout-cleanup.ts b/apps/mobile/src/lib/auth/logout-cleanup.ts index c5ad0bf386..c9029327b1 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.ts @@ -36,6 +36,13 @@ const logoutCleanupTombstoneSchema = z.object({ /** Device push token at logout; null when lookup failed or permission missing. */ pushToken: z.string().nullable(), needsPushUnregister: z.boolean(), + /** + * Activity-token unregister (Live Activity / push-to-start) outstanding at + * logout. Defaults keep pre-change tombstones parseable. + */ + needsActivityUnregister: z.boolean().default(false), + /** The exact activity tokens captured at logout; retried verbatim, never the current session's. */ + activityTokens: z.array(z.string()).default([]), /** Epoch ms of the failed logout; reconciliation discards past 30 days. */ failedAt: z.number(), }); @@ -67,7 +74,9 @@ export async function deleteLogoutCleanupTombstone(): Promise { await SecureStore.deleteItemAsync(LOGOUT_CLEANUP_TOMBSTONE_KEY); } -async function writeLogoutCleanupTombstone(tombstone: LogoutCleanupTombstone): Promise { +export async function writeLogoutCleanupTombstone( + tombstone: LogoutCleanupTombstone +): Promise { await SecureStore.setItemAsync(LOGOUT_CLEANUP_TOMBSTONE_KEY, JSON.stringify(tombstone)); } @@ -80,8 +89,8 @@ async function writeLogoutCleanupTombstone(tombstone: LogoutCleanupTombstone): P * - Revokes the current device session and unregisters the device push token * concurrently, bounded at 15 s each by the tRPC client's `deadlineFetch`. * - A failed revoke is not recorded: see the tombstone type for why. A failed - * push unregister writes a tombstone; a successful one deletes any existing - * tombstone, and is never retried later. + * push or activity-token unregister writes a tombstone; a fully successful + * one deletes any existing tombstone, and is never retried later. */ export async function runLogoutCleanup(): Promise { try { @@ -112,10 +121,10 @@ export async function runLogoutCleanup(): Promise { ]); // Unregister activity tokens (Live Activity / push-to-start) before the - // epoch bump. Best-effort: the delivery re-registers tokens on the next - // activity start, so a failed unregister is not tombstoned — a tombstone - // has no reconciliation retry for activity tokens. - getGlanceableDelivery().unregisterTokens(); + // epoch bump. A failed unregister is tombstoned and retried at the next + // authenticated opportunity against the recorded tokens only. + const activityResult = await getGlanceableDelivery().unregisterTokens(); + const needsActivityUnregister = !activityResult.ok; const unregister = results[1]; @@ -127,11 +136,13 @@ export async function runLogoutCleanup(): Promise { } try { - await (needsPushUnregister + await (needsPushUnregister || needsActivityUnregister ? writeLogoutCleanupTombstone({ userId, pushToken, needsPushUnregister, + needsActivityUnregister, + activityTokens: activityResult.tokens, failedAt: Date.now(), }) : deleteLogoutCleanupTombstone()); diff --git a/apps/mobile/src/lib/auth/logout-reconciliation.test.ts b/apps/mobile/src/lib/auth/logout-reconciliation.test.ts index 9b0e35890e..2fb28536e6 100644 --- a/apps/mobile/src/lib/auth/logout-reconciliation.test.ts +++ b/apps/mobile/src/lib/auth/logout-reconciliation.test.ts @@ -5,6 +5,7 @@ import { type LogoutCleanupTombstone } from '@/lib/auth/logout-cleanup'; const cleanupMock = vi.hoisted(() => ({ readLogoutCleanupTombstone: vi.fn<() => Promise>(), deleteLogoutCleanupTombstone: vi.fn().mockResolvedValue(undefined), + writeLogoutCleanupTombstone: vi.fn().mockResolvedValue(undefined), isNotFoundTrpcError: (error: unknown) => { if (typeof error !== 'object' || error === null) { return false; @@ -16,6 +17,7 @@ const cleanupMock = vi.hoisted(() => ({ const trpcMock = vi.hoisted(() => ({ unregisterPushToken: { mutate: vi.fn() }, + unregisterActivityToken: { mutate: vi.fn() }, })); const notificationsMock = vi.hoisted(() => ({ @@ -44,6 +46,8 @@ function makeTombstone(overrides: Partial = {}): LogoutC userId: 'u1', pushToken: 'push-stored', needsPushUnregister: true, + needsActivityUnregister: false, + activityTokens: [], failedAt: Date.now(), ...overrides, }; @@ -148,6 +152,75 @@ describe('attemptLogoutReconciliation', () => { expect(outcome).toEqual({ kind: 'attempted', tombstoneDeleted: false }); }); + it('unregisters each recorded activity token and deletes the tombstone when all succeed', async () => { + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue( + makeTombstone({ + needsPushUnregister: false, + needsActivityUnregister: true, + activityTokens: ['activity-1', 'activity-2'], + }) + ); + trpcMock.unregisterActivityToken.mutate.mockResolvedValue({ success: true }); + + const outcome = await attemptLogoutReconciliation('u1'); + + expect(outcome).toEqual({ kind: 'attempted', tombstoneDeleted: true }); + expect(trpcMock.unregisterActivityToken.mutate).toHaveBeenCalledWith({ token: 'activity-1' }); + expect(trpcMock.unregisterActivityToken.mutate).toHaveBeenCalledWith({ token: 'activity-2' }); + expect(trpcMock.unregisterPushToken.mutate).not.toHaveBeenCalled(); + expect(cleanupMock.deleteLogoutCleanupTombstone).toHaveBeenCalledTimes(1); + }); + + it('keeps the tombstone when any recorded activity token unregister rejects', async () => { + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue( + makeTombstone({ + needsPushUnregister: false, + needsActivityUnregister: true, + activityTokens: ['activity-1', 'activity-2'], + }) + ); + trpcMock.unregisterActivityToken.mutate + .mockResolvedValueOnce({ success: true }) + .mockRejectedValueOnce(new Error('server 500')); + + const outcome = await attemptLogoutReconciliation('u1'); + + expect(outcome).toEqual({ kind: 'attempted', tombstoneDeleted: false }); + expect(trpcMock.unregisterActivityToken.mutate).toHaveBeenCalledTimes(2); + expect(cleanupMock.deleteLogoutCleanupTombstone).not.toHaveBeenCalled(); + }); + + it('clears the activity part after success while the push part stays outstanding', async () => { + const failedAt = Date.now(); + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue( + makeTombstone({ + pushToken: 'push-stored', + needsPushUnregister: true, + needsActivityUnregister: true, + activityTokens: ['activity-1'], + failedAt, + }) + ); + trpcMock.unregisterPushToken.mutate.mockRejectedValue(new Error('server 500')); + trpcMock.unregisterActivityToken.mutate.mockResolvedValue({ success: true }); + + const outcome = await attemptLogoutReconciliation('u1'); + + expect(outcome).toEqual({ kind: 'attempted', tombstoneDeleted: false }); + expect(trpcMock.unregisterActivityToken.mutate).toHaveBeenCalledWith({ token: 'activity-1' }); + expect(cleanupMock.deleteLogoutCleanupTombstone).not.toHaveBeenCalled(); + // The activity part is cleared so a later retry never re-unregisters the + // same token, while the push part stays for the next attempt. + expect(cleanupMock.writeLogoutCleanupTombstone).toHaveBeenCalledWith({ + userId: 'u1', + pushToken: 'push-stored', + needsPushUnregister: true, + needsActivityUnregister: false, + activityTokens: [], + failedAt, + }); + }); + it('skips a second attempt within the 60 s spacing window', async () => { cleanupMock.readLogoutCleanupTombstone.mockResolvedValue(makeTombstone()); trpcMock.unregisterPushToken.mutate.mockResolvedValue({ success: true }); diff --git a/apps/mobile/src/lib/auth/logout-reconciliation.ts b/apps/mobile/src/lib/auth/logout-reconciliation.ts index e1c80647e1..130b5ec625 100644 --- a/apps/mobile/src/lib/auth/logout-reconciliation.ts +++ b/apps/mobile/src/lib/auth/logout-reconciliation.ts @@ -3,6 +3,7 @@ import { deleteLogoutCleanupTombstone, type LogoutCleanupTombstone, readLogoutCleanupTombstone, + writeLogoutCleanupTombstone, } from '@/lib/auth/logout-cleanup'; import { getDevicePushTokenOutcome } from '@/lib/notifications'; import { trpcClient } from '@/lib/trpc'; @@ -102,14 +103,50 @@ async function runReconciliation(userId: string): Promise { + if (!isCurrentAuthEpoch(epoch)) { + return; + } + try { + await writeLogoutCleanupTombstone({ + ...tombstone, + needsActivityUnregister: false, + activityTokens: [], + }); + } catch { + // Storage failure keeps the part for the next attempt. + } +} + /** * Deletes the tombstone unless the auth epoch moved: a sign-out or sign-in * during the attempt owns the record now, so a stale reconciliation must not @@ -168,3 +205,27 @@ async function reconcilePushUnregister(tombstone: LogoutCleanupTombstone): Promi return false; } } + +/** + * Attempts the outstanding activity-token unregisters recorded in the + * tombstone at logout. Returns true when every recorded token unregistered. + * Only the tombstone's `activityTokens` are retried — never the current + * session's live tokens, which a later sign-in re-registers under its own + * ownership. A retryable failure keeps the part and the tombstone. + */ +async function reconcileActivityUnregister(tombstone: LogoutCleanupTombstone): Promise { + if (!tombstone.needsActivityUnregister || tombstone.activityTokens.length === 0) { + return true; + } + try { + await Promise.all( + tombstone.activityTokens.map(async token => { + await trpcClient.user.unregisterActivityToken.mutate({ token }); + }) + ); + return true; + } catch { + // Retryable failure keeps the part. + return false; + } +} diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts new file mode 100644 index 0000000000..b8459e4c19 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts @@ -0,0 +1,96 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { buildGlanceableSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; + +const logoutMock = vi.hoisted(() => ({ + attemptLogoutReconciliation: vi.fn(), + awaitLogoutReconciliationSettled: vi.fn(), +})); + +const trpcMock = vi.hoisted(() => ({ + registerActivityToken: { mutate: vi.fn() }, +})); + +const activityMock = vi.hoisted(() => ({ + getPushToken: vi.fn(), +})); + +/* eslint-disable import/first */ +vi.mock('@/lib/auth/logout-reconciliation', () => logoutMock); +vi.mock('@/lib/trpc', () => ({ + trpcClient: { user: { registerActivityToken: trpcMock.registerActivityToken } }, +})); +vi.mock('expo-widgets', () => ({ + addPushToStartTokenListener: vi.fn(), +})); +vi.mock('@/glanceable-ios/active-agents-live-activity', () => ({ + ActiveAgentsLiveActivity: { + getInstances: () => [activityMock], + }, +})); +vi.mock('react-native', () => ({ + Platform: { OS: 'ios' }, +})); + +import { getGlanceableDelivery } from './sink-registry'; +// Import side effect: registers the real iOS delivery under the mocks above. +import './delivery-registration'; +/* eslint-enable import/first */ + +const NOW = 1_750_000_000_000; + +function snapshot() { + return buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }], + userId: 'u1', + organizationId: null, + now: NOW, + previousRevision: 0, + }); +} + +describe('delivery registerTokens', () => { + beforeEach(() => { + vi.clearAllMocks(); + activityMock.getPushToken.mockResolvedValue('token-1'); + trpcMock.registerActivityToken.mutate.mockResolvedValue({ success: true }); + logoutMock.attemptLogoutReconciliation.mockResolvedValue({ kind: 'no-tombstone' }); + logoutMock.awaitLogoutReconciliationSettled.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('does not register the activity token while logout reconciliation for this sign-in is still running', async () => { + const gate = { release: null as (() => void) | null }; + const settledGate = new Promise(resolve => { + gate.release = resolve; + }); + logoutMock.attemptLogoutReconciliation.mockReturnValue({ kind: 'in-flight' }); + logoutMock.awaitLogoutReconciliationSettled.mockImplementation(async () => { + await settledGate; + }); + + getGlanceableDelivery().registerTokens(snapshot(), null, 'u1'); + + // Flush microtasks and a macrotask: logout reconciliation is still in + // flight, so the activity token must not have registered. + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + + expect(logoutMock.attemptLogoutReconciliation).toHaveBeenCalledWith('u1'); + expect(trpcMock.registerActivityToken.mutate).not.toHaveBeenCalled(); + + gate.release?.(); + await vi.waitFor(() => { + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalledWith({ + token: 'token-1', + kind: 'ios_activity', + platform: 'ios', + organizationId: null, + }); + }); + }); +}); diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.ts b/apps/mobile/src/lib/glanceable/delivery-registration.ts index 77c8542acf..9eade4a84b 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.ts @@ -3,6 +3,10 @@ import { Platform } from 'react-native'; import { addPushToStartTokenListener } from 'expo-widgets'; import { ActiveAgentsLiveActivity } from '@/glanceable-ios/active-agents-live-activity'; +import { + attemptLogoutReconciliation, + awaitLogoutReconciliationSettled, +} from '@/lib/auth/logout-reconciliation'; import { trpcClient } from '@/lib/trpc'; import { type GlanceableDelivery, setGlanceableDelivery } from './sink-registry'; @@ -33,11 +37,13 @@ async function register( } } -async function unregister(token: string): Promise { +async function unregister(token: string): Promise { try { await trpcClient.user.unregisterActivityToken.mutate({ token }); + return true; } catch { - // Best effort: a stale token row is pruned server-side. + // The caller aggregates success and tombstones the token on failure. + return false; } } @@ -50,11 +56,21 @@ if (Platform.OS === 'ios') { } const delivery: GlanceableDelivery = { - registerTokens(_snapshot, organizationId) { + registerTokens(_snapshot, organizationId, userId) { if (Platform.OS !== 'ios') { return; } void (async () => { + // Order against logout cleanup: an in-flight logout unregister for the + // activity tokens must settle before this session re-registers them, or + // a later retry could delete this session's rows. Trigger the logout + // attempt first (it starts a fresh run only when none is running), then + // await its settle so registration cannot start until any in-flight + // unregister for this sign-in has settled. + if (userId !== null) { + void attemptLogoutReconciliation(userId); + } + await awaitLogoutReconciliationSettled(); if (pushToStartToken !== null) { await register(pushToStartToken, 'ios_push_to_start', organizationId); } @@ -72,27 +88,48 @@ const delivery: GlanceableDelivery = { })(); }, - unregisterTokens() { + async unregisterTokens() { if (Platform.OS !== 'ios') { - return; + return { ok: true, tokens: [] }; } - void (async () => { - if (pushToStartToken !== null) { - await unregister(pushToStartToken); - } - try { - const activity = ActiveAgentsLiveActivity.getInstances().at(-1); - if (activity) { - const token = await activity.getPushToken(); - if (token) { - await unregister(token); - } - } - } catch { - // Nothing to unregister when no activity survives. - } - })(); + const result = await unregisterActivityTokens(); + return result; }, }; +/** + * Gathers the push-to-start token plus the current activity's push token, runs + * each `unregister(token)` in parallel, and reports success plus every token + * it attempted. Never rejects: the caller tombstones `tokens` when `ok` is + * false and retries them at the next authenticated opportunity. + */ +async function unregisterActivityTokens(): Promise<{ ok: boolean; tokens: string[] }> { + const tokens: string[] = []; + if (pushToStartToken !== null) { + tokens.push(pushToStartToken); + } + try { + const activity = ActiveAgentsLiveActivity.getInstances().at(-1); + if (activity) { + const token = await activity.getPushToken(); + if (token) { + tokens.push(token); + } + } + } catch { + // Nothing to unregister when no activity survives. + } + if (tokens.length === 0) { + return { ok: true, tokens }; + } + const results = await Promise.allSettled( + tokens.map(async token => { + const ok = await unregister(token); + return ok; + }) + ); + const ok = results.every(result => result.status === 'fulfilled' && result.value); + return { ok, tokens }; +} + setGlanceableDelivery(delivery); diff --git a/apps/mobile/src/lib/glanceable/sink-registry.ts b/apps/mobile/src/lib/glanceable/sink-registry.ts index 30228c3dbe..e340bce554 100644 --- a/apps/mobile/src/lib/glanceable/sink-registry.ts +++ b/apps/mobile/src/lib/glanceable/sink-registry.ts @@ -9,6 +9,9 @@ import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-a export type GlanceableSinkContext = { /** For token registration only; must never enter the snapshot. */ organizationId: string | null; + /** For token registration only; must never enter the snapshot. `null` in + * the headless background apply when the active-user hint is unavailable. */ + userId: string | null; }; export type GlanceableSink = { @@ -31,18 +34,28 @@ export function getGlanceableSinks(): readonly GlanceableSink[] { return [...sinks]; } -/** Activity-token registrar, set by a later token slice. No-op by default. */ +/** + * Activity-token registrar, set by a later token slice. No-op by default. + * `unregisterTokens` reports success plus the tokens it attempted, so logout + * can tombstone a failed unregister and retry those exact tokens later. + */ export type GlanceableDelivery = { - registerTokens(snapshot: GlanceableAgentsSnapshot, organizationId: string | null): void; - unregisterTokens(): void; + registerTokens( + snapshot: GlanceableAgentsSnapshot, + organizationId: string | null, + userId: string | null + ): void; + unregisterTokens(): Promise<{ ok: boolean; tokens: string[] }>; }; const noopDelivery: GlanceableDelivery = { registerTokens() { // No-op until a token slice registers a delivery. }, - unregisterTokens() { + async unregisterTokens() { // No-op until a token slice registers a delivery. + await Promise.resolve(); + return { ok: true, tokens: [] }; }, }; diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index deb083bc6d..80857cb8c1 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -8,6 +8,7 @@ import { _setSecureStoreForTests, } from '@/lib/glanceable/persist'; import { registerGlanceableSink, unregisterGlanceableSink } from '@/lib/glanceable/sink-registry'; +import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { _setGlanceableSinksLoaderForTests, applyGlanceablePushData, @@ -306,6 +307,21 @@ function makeFakeSink() { }; } +// Key-aware expo-secure-store surface: `applyGlanceablePushData` reads both the +// selected-organization id and the active-user id hint through the module-level +// `SecureStore.getItemAsync`, so the mock must answer each key separately. +function mockSecureStoreKeys() { + mocks.getItemAsync.mockImplementation(async (key: string) => { + if (key === ACTIVE_USER_ID_KEY) { + return 'u1'; + } + if (key === ORGANIZATION_STORAGE_KEY) { + return 'org-9'; + } + return null; + }); +} + // Map-backed SecureStore surface for the persist module's restore path. The // persist module lazy-`require`s `expo-secure-store` (a native module), which // cannot load in the pure-vitest suite, so the restore tests inject this store @@ -325,7 +341,7 @@ const secureStoreMock = { describe('applyGlanceablePushData', () => { beforeEach(() => { _resetGlanceablePersistForTests(); - mocks.getItemAsync.mockResolvedValue(null); + mockSecureStoreKeys(); }); it('discards a remote snapshot that is not newer than the last applied snapshot', async () => { @@ -358,7 +374,6 @@ describe('applyGlanceablePushData', () => { updatedAt: '2026-01-01T00:00:00.000Z', }) ); - mocks.getItemAsync.mockResolvedValue('org-9'); const sink = makeFakeSink(); registerGlanceableSink(sink); @@ -374,6 +389,7 @@ describe('applyGlanceablePushData', () => { // The rebased revision continues the local monotonic sequence. expect(sink.publish).toHaveBeenCalledWith(expect.objectContaining({ revision: 4 })); expect(sink.startOrUpdate).toHaveBeenCalledWith(expect.objectContaining({ revision: 4 }), { + userId: 'u1', organizationId: 'org-9', }); @@ -400,7 +416,7 @@ describe('setupNotificationBackgroundHandler', () => { _resetGlanceablePersistForTests(); _setSecureStoreForTests(secureStoreMock); secureStore.clear(); - mocks.getItemAsync.mockResolvedValue(null); + mockSecureStoreKeys(); mocks.defineTask.mockReset(); mocks.registerTaskAsync.mockResolvedValue(null); }); @@ -464,7 +480,8 @@ describe('setupNotificationBackgroundHandler', () => { // path ran, not a duplicated one. expect(sink.publish).toHaveBeenCalledWith(expect.objectContaining({ revision: 2 })); expect(sink.startOrUpdate).toHaveBeenCalledWith(expect.objectContaining({ revision: 2 }), { - organizationId: null, + userId: 'u1', + organizationId: 'org-9', }); unregisterGlanceableSink(sink); diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index bcd125418a..9b9fc38171 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -26,7 +26,7 @@ import { restorePersistedGlanceable, } from '@/lib/glanceable/persist'; import { getGlanceableSinks, registerGlanceableSink } from '@/lib/glanceable/sink-registry'; -import { ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; +import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { i18n } from '@/i18n'; import { setPendingDeepLink } from './deep-link-launch'; import { notificationPathForData } from './notification-path'; @@ -98,8 +98,9 @@ export async function applyGlanceablePushData( }; const organizationId = await getSelectedOrganizationId(); + const userId = await getActiveUserId(); - const ctx = { organizationId }; + const ctx = { userId, organizationId }; if (isEligibleGlanceableWork(snapshot)) { for (const sink of getGlanceableSinks()) { sink.publish(snapshot); @@ -127,6 +128,20 @@ async function getSelectedOrganizationId(): Promise { } } +/** + * Read the active-user id hint from SecureStore. Null when the hint is + * unavailable (headless background apply before the identity resolves, or a + * failed read). It only feeds logout reconciliation ordering, never the + * snapshot. + */ +async function getActiveUserId(): Promise { + try { + return await SecureStore.getItemAsync(ACTIVE_USER_ID_KEY); + } catch { + return null; + } +} + const shown = { shouldPlaySound: true, shouldSetBadge: true, diff --git a/apps/web/src/routers/user-router.test.ts b/apps/web/src/routers/user-router.test.ts index b6b7987030..576e056143 100644 --- a/apps/web/src/routers/user-router.test.ts +++ b/apps/web/src/routers/user-router.test.ts @@ -9,6 +9,7 @@ import { magic_link_tokens, organization_memberships, organizations, + user_activity_tokens, user_notification_preferences, user_push_tokens, } from '@kilocode/db/schema'; @@ -1260,6 +1261,110 @@ describe('user router - register push token', () => { }); }); +describe('user router - register activity token', () => { + let tokenUser: User; + let otherUser: User; + + beforeAll(async () => { + tokenUser = await insertTestUser({ + google_user_email: 'activity-token-register@example.com', + google_user_name: 'Activity Token Register', + }); + otherUser = await insertTestUser({ + google_user_email: 'activity-token-other@example.com', + google_user_name: 'Activity Token Other', + }); + }); + + afterEach(async () => { + await db + .delete(user_activity_tokens) + .where(inArray(user_activity_tokens.user_id, [tokenUser.id, otherUser.id])); + }); + + afterAll(async () => { + await db.delete(kilocode_users).where(inArray(kilocode_users.id, [tokenUser.id, otherUser.id])); + }); + + it('upserts the single row when the same token re-registers with a different kind, platform, and organizationId', async () => { + const caller = await createCallerForUser(tokenUser.id); + const token = 'activity-token-upsert'; + + await expect( + caller.user.registerActivityToken({ + token, + kind: 'ios_activity', + platform: 'ios', + organizationId: null, + }) + ).resolves.toEqual({ success: true }); + + await expect( + caller.user.registerActivityToken({ + token, + kind: 'ios_push_to_start', + platform: 'ios', + organizationId: 'org-1', + }) + ).resolves.toEqual({ success: true }); + + const rows = await db + .select() + .from(user_activity_tokens) + .where(eq(user_activity_tokens.token, token)); + expect(rows).toHaveLength(1); + expect(rows[0]?.user_id).toBe(tokenUser.id); + expect(rows[0]?.kind).toBe('ios_push_to_start'); + expect(rows[0]?.platform).toBe('ios'); + expect(rows[0]?.organization_id).toBe('org-1'); + }); + + it('unregisterActivityToken deletes only the authenticated user matching token', async () => { + const caller = await createCallerForUser(tokenUser.id); + const otherCaller = await createCallerForUser(otherUser.id); + const ownToken = 'activity-token-own'; + const otherToken = 'activity-token-other'; + + await caller.user.registerActivityToken({ + token: ownToken, + kind: 'ios_activity', + platform: 'ios', + organizationId: null, + }); + await otherCaller.user.registerActivityToken({ + token: otherToken, + kind: 'ios_activity', + platform: 'ios', + organizationId: null, + }); + + // The authenticated user cannot delete another user's token. + await caller.user.unregisterActivityToken({ token: otherToken }); + + const otherRows = await db + .select() + .from(user_activity_tokens) + .where(eq(user_activity_tokens.user_id, otherUser.id)); + expect(otherRows).toHaveLength(1); + expect(otherRows[0]?.token).toBe(otherToken); + + // The user's own token is untouched by the cross-user delete attempt. + const ownRows = await db + .select() + .from(user_activity_tokens) + .where(eq(user_activity_tokens.user_id, tokenUser.id)); + expect(ownRows).toHaveLength(1); + + // Deleting the own token removes only that row. + await caller.user.unregisterActivityToken({ token: ownToken }); + const afterOwn = await db + .select() + .from(user_activity_tokens) + .where(eq(user_activity_tokens.user_id, tokenUser.id)); + expect(afterOwn).toHaveLength(0); + }); +}); + describe('user router - device sessions', () => { let owner: User; let otherUser: User; From c315b5fddaf7077edcf8ff4c80b211fb52bd3f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 21:57:54 +0200 Subject: [PATCH 06/43] fix(glanceable): recover iOS Live Activity and spoken labels --- .../src/app/(app)/(tabs)/(2_agents)/index.tsx | 11 +- .../active-agents-live-activity.tsx | 62 +++++- .../src/glanceable-ios/ios-sink.test.ts | 209 ++++++++++++++++-- apps/mobile/src/glanceable-ios/ios-sink.ts | 71 ++++-- apps/mobile/src/glanceable-ios/view-props.ts | 17 +- .../glanceable/activity-kit-prompt.test.ts | 149 +++++++++++++ .../src/lib/glanceable/activity-kit-prompt.ts | 40 +++- .../src/lib/glanceable/presentation.test.ts | 34 +++ .../mobile/src/lib/glanceable/presentation.ts | 23 ++ 9 files changed, 552 insertions(+), 64 deletions(-) create mode 100644 apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx index 5f03d58c56..4ee3c63f2d 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx @@ -12,7 +12,10 @@ import { type GitHubInstallReturnOutcome, subscribeToGitHubInstallReturnOutcome, } from '@/lib/github-install-return'; -import { showActivityKitDisabledAlertOnce } from '@/lib/glanceable/activity-kit-prompt'; +import { + recoverGlanceableActivityKit, + showActivityKitDisabledAlertOnce, +} from '@/lib/glanceable/activity-kit-prompt'; import { trpcClient } from '@/lib/trpc'; export type GitHubInstallOutcomeAlertButton = { @@ -138,11 +141,13 @@ export default function AgentSessionList() { }, [consumeReturnOutcome]); // Show the one-time "turn on Live Activities" alert when the Agents tab - // regains focus and ActivityKit is unavailable. Never auto-alerts from the - // publisher; this tab focus is the single prompt site. + // regains focus and ActivityKit is unavailable, and recover the surface when + // it became available again. Never auto-alerts from the publisher; this tab + // focus is the single prompt and recovery site. useFocusEffect( useCallback(() => { showActivityKitDisabledAlertOnce(); + void recoverGlanceableActivityKit(); }, []) ); diff --git a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx index 0a7ae0f1d8..87f2238305 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx @@ -4,6 +4,7 @@ import { accessibilityLabel, font, foregroundStyle, + frame, } from '@expo/ui/swift-ui/modifiers'; import { createLiveActivity } from 'expo-widgets'; import { PlatformColor } from 'react-native'; @@ -47,12 +48,18 @@ export const ActiveAgentsLiveActivity = createLiveActivity< const primary = countLines[0] ?? null; const primaryLabel = primary === null ? null : primary.label; const primaryCount = String(primary === null ? 0 : primary.count); - const elapsedAnchor = status === 'happy' ? (props.eligibleStartedAt ?? null) : null; + // Elapsed time shows while eligible counts exist, including the stale status, + // so the running work keeps its elapsed timer when updates stop. + const elapsedAnchor = hasCounts ? (props.eligibleStartedAt ?? null) : null; - const spokenParts = - status === 'happy' || status === 'stale' - ? [...countLines.map(line => line.label), 'Open agents'] - : [statusLine ?? '', 'Open agents'].filter(part => part !== ''); + // Spoken label: status word, numeric counts, then Open agents. Stale keeps + // its status word; happy (no status line) speaks counts then Open agents. + const openAgentsCopy = 'Open agents'; + const spokenParts = [ + ...(statusLine !== null ? [statusLine] : []), + ...countLines.map(line => `${line.count} ${line.label}`), + openAgentsCopy, + ]; const accessibility = spokenParts.join(', '); const primaryForeground = foregroundStyle(PlatformColor('label')); @@ -66,6 +73,19 @@ export const ActiveAgentsLiveActivity = createLiveActivity< )); + const showOpenAgents = status === 'happy' || status === 'stale'; + const openAgentsControl = ( + + {openAgentsCopy} + + ); + return { banner: ( ) : null} + {showOpenAgents ? openAgentsControl : null} ), compactLeading: ( - + {hasCounts ? primaryCount : statusLine} ), compactTrailing: ( - + {hasCounts ? (primaryLabel ?? primaryCount) : ''} ), minimal: ( - + {hasCounts ? primaryCount : ''} ), expandedLeading: ( - + {countRows} ), expandedTrailing: ( - + {statusLine !== null ? {statusLine} : null} {elapsedAnchor !== null ? ( @@ -113,10 +152,11 @@ export const ActiveAgentsLiveActivity = createLiveActivity< ), expandedBottom: ( - + {statusLine !== null && !hasCounts ? ( {statusLine} ) : null} + {showOpenAgents ? openAgentsControl : null} ), }; diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index f5853f7371..f8ec0cee93 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -15,7 +15,12 @@ import { unregisterGlanceableSink, } from '@/lib/glanceable/sink-registry'; -import { _resetIosSinkForTests, getActivityKitDenied, iosSink } from './ios-sink'; +import { + _resetIosSinkForTests, + clearActivityKitDeniedIfAvailable, + getActivityKitDenied, + iosSink, +} from './ios-sink'; import { buildGlanceableViewProps, type GlanceableViewProps } from './view-props'; // Native surfaces are unreachable under vitest: expo-widgets factories, the @@ -40,12 +45,14 @@ vi.mock('react-native', () => ({ PlatformColor: (name: string) => name })); const mockState = vi.hoisted(() => ({ startError: null as { code: string; message: string } | null, + instancesError: null as { code: string; message: string } | null, instances: [] as unknown[], started: [] as { props: unknown; url?: string }[], updated: [] as unknown[], snapshots: [] as unknown[], timelines: [] as { date: Date; props: unknown }[][], ended: [] as { policy: unknown; props?: unknown; contentDate?: unknown }[], + updatePromise: null as Promise | null, })); vi.mock('expo-widgets', () => ({ @@ -59,15 +66,25 @@ vi.mock('expo-widgets', () => ({ } mockState.started.push({ props, url }); return { - update: (next: unknown) => { + update: async (next: unknown) => { mockState.updated.push(next); + if (mockState.updatePromise !== null) { + await mockState.updatePromise; + } }, end: (policy: unknown, finalProps?: unknown, contentDate?: unknown) => { mockState.ended.push({ policy, props: finalProps, contentDate }); }, }; }, - getInstances: () => mockState.instances, + getInstances: () => { + if (mockState.instancesError !== null) { + const error = new Error(mockState.instancesError.message) as Error & { code: string }; + error.code = mockState.instancesError.code; + throw error; + } + return mockState.instances; + }, }), createWidget: () => ({ updateSnapshot: (props: unknown) => { @@ -107,12 +124,14 @@ function snapshotFor( beforeEach(() => { _resetIosSinkForTests(); mockState.startError = null; + mockState.instancesError = null; mockState.instances = []; mockState.started = []; mockState.updated = []; mockState.snapshots = []; mockState.timelines = []; mockState.ended = []; + mockState.updatePromise = null; setGlanceableDelivery(delivery); registerGlanceableSink(iosSink); vi.clearAllMocks(); @@ -134,7 +153,7 @@ describe('iosSink start and update', () => { expect(delivery.unregisterTokens).not.toHaveBeenCalled(); }); - it('discards an older revision without overwriting the newest updatedAt and props', () => { + it('discards an older revision without overwriting the newest props', () => { const newer = snapshotFor([{ status: 'busy' }], 1); const older = { ...snapshotFor([{ status: 'busy' }, { status: 'busy' }], 0), @@ -148,9 +167,7 @@ describe('iosSink start and update', () => { iosSink.endImmediate(); expect(mockState.ended.length).toBe(1); - expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBe( - Date.parse(newer.updatedAt) - ); + expect(mockState.ended[0]?.contentDate).toBeInstanceOf(Date); expect( (mockState.ended[0]?.props as GlanceableLiveActivityContentState | undefined)?.running ).toBe(1); @@ -195,7 +212,13 @@ describe('iosSink start and update', () => { }); it('adopts the newest existing instance instead of starting a second activity', () => { - mockState.instances = [{ update: (next: unknown) => mockState.updated.push(next) }]; + mockState.instances = [ + { + update: (next: unknown) => { + mockState.updated.push(next); + }, + }, + ]; iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); @@ -206,40 +229,106 @@ describe('iosSink start and update', () => { }); describe('iosSink end', () => { - it('ends immediately with contentDate from the last updatedAt on signed-out', () => { - // The eligible snapshot's updatedAt is the fixed NOW; the signed-out publish - // must advance `lastUpdatedAt`, so fake a later clock and prove `end` uses - // the terminal snapshot's timestamp, not the eligible one. + it('ends with a contentDate not older than the last native write', () => { + const writeTime = NOW + 120_000; + vi.useFakeTimers(); + vi.setSystemTime(new Date(writeTime)); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + + iosSink.endImmediate(); + + const contentDate = mockState.ended[0]?.contentDate as Date | undefined; + expect(contentDate).toBeInstanceOf(Date); + // The snapshot's updatedAt (NOW) is older than the write wall-clock; an end + // carrying NOW instead would be discarded by ActivityKit. + expect(contentDate?.getTime()).toBe(writeTime); + }); + + it('unregisters tokens even when no activity handle exists', () => { + mockState.instances = []; + + iosSink.endImmediate(); + + expect(mockState.ended.length).toBe(0); + expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + }); + + it('ends immediately on signed-out with a wall-clock contentDate', async () => { + // The eligible snapshot's updatedAt is the fixed NOW; the native writes run + // at the faked later wall-clock, so `end` must carry that write time, not + // the snapshot's logical updatedAt. const terminalTime = NOW + 120_000; vi.useFakeTimers(); vi.setSystemTime(new Date(terminalTime)); iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); writeSignedOutSnapshotAndEnd(); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); - expect(mockState.ended.length).toBe(1); expect(mockState.ended[0]?.policy).toBe('immediate'); expect(mockState.ended[0]?.contentDate).toBeInstanceOf(Date); - expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBe(terminalTime); + expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBeGreaterThanOrEqual( + terminalTime + ); expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); }); - it('ends with the published empty snapshot contentDate, not the eligible one', () => { + it('ends with the wall-clock of the last publish, not the eligible start', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(NOW)); iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); - const later = new Date(NOW + 60_000).toISOString(); - iosSink.publish({ ...snapshotFor([], 1, 'empty'), updatedAt: later }); + const publishTime = NOW + 60_000; + vi.setSystemTime(new Date(publishTime)); + iosSink.publish(snapshotFor([], 1, 'empty')); iosSink.endImmediate(); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); - expect(mockState.ended.length).toBe(1); expect(mockState.ended[0]?.policy).toBe('immediate'); - expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBe(NOW + 60_000); + expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBeGreaterThanOrEqual( + publishTime + ); + }); + + it('awaits the in-flight publish update so the end contentDate is not older than the native write', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(NOW)); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + + // The native update does not settle at the JS publish stamp: ActivityKit + // stamps its own later wall-clock at native execution. Simulate that gap. + let resolveUpdate: () => void = undefined as unknown as () => void; + mockState.updatePromise = new Promise(resolve => { + resolveUpdate = resolve; + }); + iosSink.publish(snapshotFor([], 1, 'empty')); + + const nativeWriteTime = NOW + 50; + vi.setSystemTime(new Date(nativeWriteTime)); + resolveUpdate(); + + iosSink.endImmediate(); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); + + const contentDate = mockState.ended[0]?.contentDate as Date | undefined; + expect(contentDate).toBeInstanceOf(Date); + // The end must not carry the earlier JS publish stamp (NOW), which ActivityKit + // discards as older than the native write. + expect(contentDate?.getTime()).toBeGreaterThanOrEqual(nativeWriteTime); }); it('adopts and ends a leftover activity when the handle is null after restart', () => { mockState.instances = [ { - update: (next: unknown) => mockState.updated.push(next), + update: (next: unknown) => { + mockState.updated.push(next); + }, end: (policy: unknown, props?: unknown, contentDate?: unknown) => mockState.ended.push({ policy, props, contentDate }), }, @@ -252,7 +341,7 @@ describe('iosSink end', () => { expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); }); - it('ends after the 8s terminal window when work becomes empty', () => { + it('ends after the 8s terminal window when work becomes empty', async () => { vi.useFakeTimers(); const publisher = new GlanceablePublisher({ sinks: [iosSink], now: () => NOW }); @@ -264,7 +353,9 @@ describe('iosSink end', () => { expect(mockState.ended.length).toBe(0); vi.advanceTimersByTime(8000); - expect(mockState.ended.length).toBe(1); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); expect(mockState.ended[0]?.policy).toBe('immediate'); publisher.dispose(); }); @@ -337,7 +428,13 @@ describe('iosSink Live Activity content-state', () => { }); it('adopts and updates a leftover activity from publish when the handle is null', () => { - mockState.instances = [{ update: (next: unknown) => mockState.updated.push(next) }]; + mockState.instances = [ + { + update: (next: unknown) => { + mockState.updated.push(next); + }, + }, + ]; iosSink.publish(snapshotFor([], 1, 'empty')); @@ -348,6 +445,42 @@ describe('iosSink Live Activity content-state', () => { }); }); +describe('clearActivityKitDeniedIfAvailable', () => { + it('returns false when the surface was never denied', () => { + expect(clearActivityKitDeniedIfAvailable()).toBe(false); + expect(getActivityKitDenied()).toBe(false); + }); + + it('clears the denied latch and returns true when ActivityKit is available again', () => { + mockState.startError = { + code: 'ERR_LIVE_ACTIVITIES_NOT_SUPPORTED', + message: 'Live Activities are not supported on this device', + }; + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + expect(getActivityKitDenied()).toBe(true); + + expect(clearActivityKitDeniedIfAvailable()).toBe(true); + expect(getActivityKitDenied()).toBe(false); + }); + + it('keeps the denied latch when the probe still reports unavailability', () => { + mockState.startError = { + code: 'ERR_LIVE_ACTIVITIES_NOT_SUPPORTED', + message: 'Live Activities are not supported on this device', + }; + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + expect(getActivityKitDenied()).toBe(true); + + mockState.startError = null; + mockState.instancesError = { + code: 'ERR_LIVE_ACTIVITIES_NOT_SUPPORTED', + message: 'still unavailable', + }; + expect(clearActivityKitDeniedIfAvailable()).toBe(false); + expect(getActivityKitDenied()).toBe(true); + }); +}); + describe('buildGlanceableViewProps', () => { it('ranks the compact primary count as needs-input, then reconnecting, then running', () => { const props = buildGlanceableViewProps( @@ -399,4 +532,34 @@ describe('buildGlanceableViewProps', () => { expect(json).not.toContain('revision'); expect(json).not.toContain('title'); }); + + it('shows the elapsed anchor for stale with eligible counts', () => { + const stale = snapshotFor([{ status: 'busy' }], 1, 'stale'); + const props = buildGlanceableViewProps(stale, {}, key => key); + + expect(props.elapsedAnchor).toBe(stale.eligibleStartedAt); + }); + + it('hides the elapsed anchor when no eligible counts exist', () => { + const props = buildGlanceableViewProps(snapshotFor([], 1, 'empty'), {}, key => key); + + expect(props.elapsedAnchor).toBeNull(); + }); + + it('speaks the status word, numeric counts, then Open agents', () => { + const stale = buildGlanceableViewProps( + snapshotFor([{ status: 'busy' }, { status: 'busy' }, { status: 'question' }], 1, 'stale'), + {}, + key => key + ); + expect(stale.accessibilityLabel).toBe( + 'glanceable.stale, 1 glanceable.needsInput, 2 glanceable.running, glanceable.openAgents' + ); + + const happy = buildGlanceableViewProps(snapshotFor([{ status: 'busy' }], 0), {}, key => key); + expect(happy.accessibilityLabel).toBe('1 glanceable.running, glanceable.openAgents'); + + const empty = buildGlanceableViewProps(snapshotFor([], 1, 'empty'), {}, key => key); + expect(empty.accessibilityLabel).toBe('glanceable.empty, glanceable.openAgents'); + }); }); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index 20c6e96432..741af93f2a 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -24,7 +24,8 @@ type Activity = LiveActivity>; let activityKitDeniedState = false; let activity: Activity | null = null; let revision = 0; -let lastUpdatedAt: string | null = null; +/** In-flight native `update`; `end` awaits it so its contentDate is never older. */ +let inFlightUpdate: Promise | null = null; let lastProps: Partial | null = null; function translate(key: string): string { @@ -76,18 +77,32 @@ function buildExpiredProps(snapshot: GlanceableAgentsSnapshot): Partial { + // Unregister push-to-start tokens even when no Live Activity handle exists: + // an account or org switch with no live handle must not keep the prior + // scope's token registered. + void getGlanceableDelivery().unregisterTokens(); // A process restart leaves the JS handle null while ActivityKit still // holds the activity; adopt it so the end actually clears the Lock Screen. activity ??= adoptExistingActivity(); if (activity === null) { return; } - const contentDate = lastUpdatedAt === null ? undefined : new Date(lastUpdatedAt); - void activity.end('immediate', lastProps ?? undefined, contentDate); + // ActivityKit (iOS 17.2+) discards an end whose contentDate is older than the + // last content write. Native `update` stamps its own later wall-clock, so wait + // for the in-flight update and pass a fresh `Date()` — never the earlier JS + // stamp or the snapshot's logical `updatedAt`, which is recorded beforehand. + if (inFlightUpdate !== null) { + try { + await inFlightUpdate; + } catch { + // A rejected update must not block the end; the contentDate still advances. + } + } + void activity.end('immediate', lastProps ?? undefined, new Date()); + inFlightUpdate = null; activity = null; revision = 0; - void getGlanceableDelivery().unregisterTokens(); } /** True once ActivityKit reported the surface unavailable (see slice psh for the alert). */ @@ -95,12 +110,33 @@ export function getActivityKitDenied(): boolean { return activityKitDeniedState; } +/** + * Re-probe ActivityKit after the user may have re-enabled it in Settings. + * Clears the denied latch when the surface is available again and returns true; + * keeps the latch and returns false when it is still unavailable (or the probe + * is a transient read failure). The caller then re-emits eligible work through + * `startOrUpdate`, whose `start` re-checks availability authoritatively. + */ +export function clearActivityKitDeniedIfAvailable(): boolean { + if (!activityKitDeniedState) { + return false; + } + try { + ActiveAgentsLiveActivity.getInstances(); + activityKitDeniedState = false; + return true; + } catch { + // Still unavailable (or transient): keep the latch. + return false; + } +} + /** Test-only: drop all sink state between cases. */ export function _resetIosSinkForTests(): void { activityKitDeniedState = false; activity = null; revision = 0; - lastUpdatedAt = null; + inFlightUpdate = null; lastProps = null; } @@ -115,16 +151,15 @@ export const iosSink: GlanceableSink = { // Mirror the published snapshot onto a present Live Activity so the empty // "No work in progress" and stale "Can't update now" copy shows during the // terminal window before `endImmediate` ends it. Never start an activity - // here: start is reserved for the first eligible emit. Record the applied - // snapshot so a later `end` carries a contentDate that is not older than - // this update (ActivityKit ignores an end older than the last update). - // Adopt a leftover instance first: after a process restart the JS handle is - // null while ActivityKit still holds the activity. + // here: start is reserved for the first eligible emit. Track the update's + // promise so a later `end` awaits it and carries a contentDate not older + // than the native write (ActivityKit ignores an older end). Adopt a + // leftover instance first: after a process restart the JS handle is null + // while ActivityKit still holds the activity. activity ??= adoptExistingActivity(); if (activity !== null) { - lastUpdatedAt = snapshot.updatedAt; lastProps = buildGlanceableLiveActivityContentState(snapshot); - void activity.update(lastProps); + inFlightUpdate = activity.update(lastProps); } }, @@ -144,6 +179,7 @@ export const iosSink: GlanceableSink = { const newest = instances.at(-1); if (newest !== undefined) { activity = newest; + inFlightUpdate = null; adopted = true; } } catch (error) { @@ -158,6 +194,7 @@ export const iosSink: GlanceableSink = { if (activity === null) { try { activity = ActiveAgentsLiveActivity.start(contentState, OPEN_AGENTS_URL); + inFlightUpdate = null; } catch (error) { // Only ActivityKit unavailability is permanent; a transient // StartLiveActivityException leaves denial unset so a later emit retries. @@ -169,12 +206,11 @@ export const iosSink: GlanceableSink = { } } - lastUpdatedAt = snapshot.updatedAt; lastProps = contentState; revision = snapshot.revision; getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId); if (adopted) { - void activity.update(contentState); + inFlightUpdate = activity.update(contentState); } return; } @@ -184,13 +220,12 @@ export const iosSink: GlanceableSink = { if (snapshot.revision <= revision) { return; } - lastUpdatedAt = snapshot.updatedAt; lastProps = contentState; - void activity.update(contentState); + inFlightUpdate = activity.update(contentState); revision = snapshot.revision; }, endImmediate() { - endNow(); + void endNow(); }, }; diff --git a/apps/mobile/src/glanceable-ios/view-props.ts b/apps/mobile/src/glanceable-ios/view-props.ts index a053b853e8..6f91ad4a24 100644 --- a/apps/mobile/src/glanceable-ios/view-props.ts +++ b/apps/mobile/src/glanceable-ios/view-props.ts @@ -1,9 +1,12 @@ -import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { + type GlanceableAgentsSnapshot, + isEligibleGlanceableWork, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; import { glanceableCountLines, - glanceableSpokenLabelKeys, + glanceableSpokenLabel, glanceableStatusCopyKey, type GlanceableSurfaceFlags, primaryGlanceableCount, @@ -27,13 +30,13 @@ export type GlanceableViewProps = { primaryLabel: string | null; /** Top-ranked count value for compact surfaces; 0 when no eligible work. */ primaryCount: number; - /** ISO anchor for the elapsed timer; only happy with eligible work. */ + /** ISO anchor for the elapsed timer; shows while eligible work runs, incl. stale. */ elapsedAnchor: string | null; /** Translated "Open agents" affordance. */ openAgentsLabel: string; /** True for happy and stale — the only statuses that show counts. */ showOpenAgents: boolean; - /** Spoken label: status words, counts, then Open agents. Never a title or id. */ + /** Spoken label: status word, numeric counts, then Open agents. Never a title or id. */ accessibilityLabel: string; }; @@ -55,12 +58,10 @@ export function buildGlanceableViewProps( })), primaryLabel: primary === null ? null : translate(primary.key), primaryCount: primary === null ? 0 : primary.count, - elapsedAnchor: status === 'happy' ? snapshot.eligibleStartedAt : null, + elapsedAnchor: isEligibleGlanceableWork(snapshot) ? snapshot.eligibleStartedAt : null, openAgentsLabel: translate('glanceable.openAgents'), showOpenAgents: status === 'happy' || status === 'stale', - accessibilityLabel: glanceableSpokenLabelKeys(snapshot, flags) - .map(key => translate(key)) - .join(', '), + accessibilityLabel: glanceableSpokenLabel(snapshot, flags, translate), }; } diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts new file mode 100644 index 0000000000..c758bd5a84 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts @@ -0,0 +1,149 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { + _resetGlanceablePersistForTests, + _setLastGlanceableSnapshotForTests, +} from '@/lib/glanceable/persist'; +import { registerGlanceableSink, unregisterGlanceableSink } from '@/lib/glanceable/sink-registry'; +import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; + +import { recoverGlanceableActivityKit } from './activity-kit-prompt'; + +const mocks = vi.hoisted(() => ({ + platform: { OS: 'ios' }, + alert: vi.fn(), + openSettings: vi.fn(), + getItemAsync: vi.fn(), + clearActivityKitDeniedIfAvailable: vi.fn(), + getActivityKitDenied: vi.fn(), +})); + +vi.mock('react-native', () => ({ + Platform: mocks.platform, + Alert: { alert: mocks.alert }, + Linking: { openSettings: mocks.openSettings }, +})); + +vi.mock('expo-secure-store', () => ({ + getItemAsync: mocks.getItemAsync, +})); + +vi.mock('@/glanceable-ios/ios-sink', () => ({ + clearActivityKitDeniedIfAvailable: mocks.clearActivityKitDeniedIfAvailable, + getActivityKitDenied: mocks.getActivityKitDenied, +})); + +vi.mock('@/i18n', () => ({ + i18n: { t: (key: string) => key }, +})); + +const NOW = 1_750_000_000_000; + +function eligibleSnapshot(): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }], + userId: 'u1', + organizationId: null, + now: NOW, + }); +} + +function emptySnapshot(): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW, + }); +} + +function makeFakeSink() { + return { + publish: vi.fn(), + endImmediate: vi.fn(), + startOrUpdate: vi.fn(), + }; +} + +beforeEach(() => { + vi.clearAllMocks(); + _resetGlanceablePersistForTests(); + mocks.platform.OS = 'ios'; + mocks.getItemAsync.mockResolvedValue(null); + mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(false); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('recoverGlanceableActivityKit', () => { + it('does nothing when the denied latch was not cleared', async () => { + mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(false); + _setLastGlanceableSnapshotForTests(eligibleSnapshot()); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + await recoverGlanceableActivityKit(); + + expect(sink.startOrUpdate).not.toHaveBeenCalled(); + unregisterGlanceableSink(sink); + }); + + it('does not re-emit when the persisted snapshot has no eligible work', async () => { + mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(true); + _setLastGlanceableSnapshotForTests(emptySnapshot()); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + await recoverGlanceableActivityKit(); + + expect(sink.startOrUpdate).not.toHaveBeenCalled(); + unregisterGlanceableSink(sink); + }); + + it('re-emits the persisted eligible snapshot with the SecureStore identity', async () => { + mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(true); + const snapshot = eligibleSnapshot(); + _setLastGlanceableSnapshotForTests(snapshot); + mocks.getItemAsync.mockImplementation(async (key: string) => { + await Promise.resolve(); + if (key === ACTIVE_USER_ID_KEY) { + return 'u1'; + } + if (key === ORGANIZATION_STORAGE_KEY) { + return 'org-9'; + } + return null; + }); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + await recoverGlanceableActivityKit(); + + expect(sink.startOrUpdate).toHaveBeenCalledWith(snapshot, { + userId: 'u1', + organizationId: 'org-9', + }); + unregisterGlanceableSink(sink); + }); + + it('does not re-emit on a non-iOS platform', async () => { + mocks.platform.OS = 'android'; + mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(true); + _setLastGlanceableSnapshotForTests(eligibleSnapshot()); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + await recoverGlanceableActivityKit(); + + expect(mocks.clearActivityKitDeniedIfAvailable).not.toHaveBeenCalled(); + expect(sink.startOrUpdate).not.toHaveBeenCalled(); + unregisterGlanceableSink(sink); + }); +}); diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts index aa69e6a4d8..1fda2362e3 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -1,6 +1,12 @@ +import * as SecureStore from 'expo-secure-store'; import { Alert, Linking, Platform } from 'react-native'; -import { getActivityKitDenied } from '@/glanceable-ios/ios-sink'; +import { isEligibleGlanceableWork } from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { clearActivityKitDeniedIfAvailable, getActivityKitDenied } from '@/glanceable-ios/ios-sink'; +import { getLastGlanceableSnapshot } from '@/lib/glanceable/persist'; +import { getGlanceableSinks } from '@/lib/glanceable/sink-registry'; +import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { i18n } from '@/i18n'; /** @@ -29,6 +35,38 @@ export function showActivityKitDisabledAlertOnce(): void { ); } +async function readSecureStoreValue(key: string): Promise { + try { + return await SecureStore.getItemAsync(key); + } catch { + return null; + } +} + +/** + * Re-emit the last eligible snapshot after a once-denied ActivityKit surface + * became available again. Reads the active-user and selected-organization hints + * from SecureStore exactly as `notifications.ts` does, so the re-emitted token + * registration keeps the right scope. No-op when the latch was never denied, + * was not cleared, or the persisted snapshot has no eligible work. + */ +export async function recoverGlanceableActivityKit(): Promise { + if (Platform.OS !== 'ios' || !clearActivityKitDeniedIfAvailable()) { + return; + } + const snapshot = getLastGlanceableSnapshot(); + if (snapshot === null || !isEligibleGlanceableWork(snapshot)) { + return; + } + const [userId, organizationId] = await Promise.all([ + readSecureStoreValue(ACTIVE_USER_ID_KEY), + readSecureStoreValue(ORGANIZATION_STORAGE_KEY), + ]); + for (const sink of getGlanceableSinks()) { + sink.startOrUpdate(snapshot, { userId, organizationId }); + } +} + /** Test-only: drop the once-per-process latch between cases. */ export function _resetActivityKitPromptForTests(): void { alertShown = false; diff --git a/apps/mobile/src/lib/glanceable/presentation.test.ts b/apps/mobile/src/lib/glanceable/presentation.test.ts index 87177b1294..0241461d43 100644 --- a/apps/mobile/src/lib/glanceable/presentation.test.ts +++ b/apps/mobile/src/lib/glanceable/presentation.test.ts @@ -6,6 +6,7 @@ import { } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { + glanceableSpokenLabel, glanceableSpokenLabelKeys, glanceableStatusCopyKey, primaryGlanceableCount, @@ -106,3 +107,36 @@ describe('spoken label shape', () => { ]); }); }); + +describe('numeric spoken label', () => { + it('speaks numeric counts then Open agents for happy', () => { + const happy = snapshot({ + sessions: [{ status: 'busy' }, { status: 'busy' }, { status: 'question' }], + }); + expect(glanceableSpokenLabel(happy, {}, key => key)).toBe( + '1 glanceable.needsInput, 2 glanceable.running, glanceable.openAgents' + ); + }); + + it('speaks the status word, numeric counts, then Open agents for stale', () => { + const stale = snapshot({ sessions: [{ status: 'busy' }], status: 'stale' }); + expect(glanceableSpokenLabel(stale, {}, key => key)).toBe( + 'glanceable.stale, 1 glanceable.running, glanceable.openAgents' + ); + }); + + it('speaks the status word then Open agents when no counts exist', () => { + expect(glanceableSpokenLabel(snapshot({ status: 'empty' }), {}, key => key)).toBe( + 'glanceable.empty, glanceable.openAgents' + ); + }); + + it('never speaks a title, organization name, or raw id', () => { + const spoken = glanceableSpokenLabel( + snapshot({ sessions: [{ status: 'busy' }] }), + {}, + key => key + ); + expect(spoken).not.toContain('u1'); + }); +}); diff --git a/apps/mobile/src/lib/glanceable/presentation.ts b/apps/mobile/src/lib/glanceable/presentation.ts index 5935d60d46..6ba6768434 100644 --- a/apps/mobile/src/lib/glanceable/presentation.ts +++ b/apps/mobile/src/lib/glanceable/presentation.ts @@ -105,3 +105,26 @@ export function glanceableSpokenLabelKeys( parts.push('glanceable.openAgents'); return parts; } + +/** + * The full spoken label with numbers: the status word (for non-happy statuses, + * including stale), then each non-zero count as "N label", then Open agents. + * `translate` resolves the copy keys, so the iOS widget (`view-props.ts`) can + * speak translated copy. Never a title, organization name, or id. + */ +export function glanceableSpokenLabel( + snapshot: GlanceableAgentsSnapshot, + flags: GlanceableSurfaceFlags, + translate: (key: string) => string +): string { + const status = resolveGlanceableStatus(snapshot, flags); + const parts: string[] = []; + if (status !== 'happy') { + parts.push(translate(GLANCEABLE_STATUS_COPY_KEY[status])); + } + for (const { key, count } of glanceableCountLines(snapshot)) { + parts.push(`${count} ${translate(key)}`); + } + parts.push(translate('glanceable.openAgents')); + return parts.join(', '); +} From ffd061c0b75321c290285506f23598821dc985f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 23:17:44 +0200 Subject: [PATCH 07/43] fix(glanceable): register Android ongoing and restore widget state --- .../ActiveAgentsLiveUpdateModule.kt | 41 +++- .../glanceable-android/android-sink.test.ts | 103 ++++++++- .../src/glanceable-android/android-sink.ts | 33 ++- .../src/glanceable-android/live-update.ts | 12 +- .../src/glanceable-android/register.test.ts | 136 +++++++++++ .../mobile/src/glanceable-android/register.ts | 37 ++- .../glanceable/delivery-registration.test.ts | 217 +++++++++++++++++- .../lib/glanceable/delivery-registration.ts | 163 +++++++++++-- 8 files changed, 695 insertions(+), 47 deletions(-) create mode 100644 apps/mobile/src/glanceable-android/register.test.ts diff --git a/apps/mobile/modules/active-agents-live-update/android/src/main/java/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule.kt b/apps/mobile/modules/active-agents-live-update/android/src/main/java/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule.kt index 9444a5fea8..748711d89b 100644 --- a/apps/mobile/modules/active-agents-live-update/android/src/main/java/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule.kt +++ b/apps/mobile/modules/active-agents-live-update/android/src/main/java/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule.kt @@ -3,7 +3,11 @@ package com.kilocode.activeagentsliveupdate import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import android.net.Uri import android.os.Build import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition @@ -13,7 +17,8 @@ import expo.modules.kotlin.modules.ModuleDefinition * * The JS side owns the translated copy and the revision guard; this module owns * the fixed notification id, the dedicated `active-agents` channel (default - * importance, silent, no heads-up), and the API 36.1+ promotion gate. + * importance, silent, no heads-up), the API 36.1+ promotion gate, and the + * content intent plus named action that open the Agents tab via a deep link. */ class ActiveAgentsLiveUpdateModule : Module() { override fun definition() = ModuleDefinition { @@ -23,12 +28,12 @@ class ActiveAgentsLiveUpdateModule : Module() { isPromotionCapable() } - Function("start") { title: String, text: String, promotion: Boolean -> - post(title, text, promotion) + Function("start") { title: String, text: String, openAgentsLabel: String, promotion: Boolean -> + post(title, text, openAgentsLabel, promotion) } - Function("update") { title: String, text: String, promotion: Boolean -> - post(title, text, promotion) + Function("update") { title: String, text: String, openAgentsLabel: String, promotion: Boolean -> + post(title, text, openAgentsLabel, promotion) } Function("end") { @@ -73,15 +78,37 @@ class ActiveAgentsLiveUpdateModule : Module() { @Suppress("DEPRECATION") private fun legacyBuilder(): Notification.Builder = Notification.Builder(context) - private fun post(title: String, text: String, promotion: Boolean) { + /** A PendingIntent that deep-links the app to the Open agents route. */ + private fun openAgentsPendingIntent(): PendingIntent { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(OPEN_AGENTS_DEEP_LINK)).apply { + setPackage(context.packageName) + } + return PendingIntent.getActivity( + context, + OPEN_AGENTS_REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + private fun post(title: String, text: String, openAgentsLabel: String, promotion: Boolean) { + val contentIntent = openAgentsPendingIntent() val builder = newBuilder(title) .setSmallIcon(smallIconId()) .setContentTitle(title) .setContentText(text) + .setContentIntent(contentIntent) .setOngoing(true) .setOnlyAlertOnce(true) .setSound(null) .setCategory(Notification.CATEGORY_STATUS) + .addAction( + Notification.Action.Builder( + Icon.createWithResource(context, smallIconId()), + openAgentsLabel, + contentIntent + ).build() + ) // API 36.1+ Live Update: promote only when the device reports the capability. // setRequestPromotedOngoing does not exist; use the documented flag setter. @@ -100,5 +127,7 @@ class ActiveAgentsLiveUpdateModule : Module() { private companion object { const val CHANNEL_ID = "active-agents" const val NOTIFICATION_ID = 1001 + const val OPEN_AGENTS_DEEP_LINK = "kiloapp:///cloud/sessions" + const val OPEN_AGENTS_REQUEST_CODE = 1002 } } \ No newline at end of file diff --git a/apps/mobile/src/glanceable-android/android-sink.test.ts b/apps/mobile/src/glanceable-android/android-sink.test.ts index 802008c0ca..a689c8ade7 100644 --- a/apps/mobile/src/glanceable-android/android-sink.test.ts +++ b/apps/mobile/src/glanceable-android/android-sink.test.ts @@ -5,7 +5,14 @@ import { } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { _resetAndroidSinkForTests, androidSink, getCurrentWidgetProps } from './android-sink'; +import { setGlanceableDelivery } from '@/lib/glanceable/sink-registry'; + +import { + _resetAndroidSinkForTests, + androidSink, + getCurrentWidgetProps, + handleAppStateActive, +} from './android-sink'; import { _setPermissionReaderForTests, type NotificationPermissionStatus } from './permission'; import { _resetAndroidPermissionAlertForTests } from './permission-alert'; @@ -41,6 +48,11 @@ vi.mock('react-native-android-widget', () => ({ const NOW = 1_750_000_000_000; const CTX = { organizationId: null, userId: 'u1' }; +const delivery = { + registerTokens: vi.fn(), + unregisterTokens: vi.fn().mockResolvedValue({ ok: true, tokens: [] }), +}; + function snapshotFor( sessions: { status: string }[], revision = 0, @@ -84,6 +96,9 @@ beforeEach(() => { _resetAndroidPermissionAlertForTests(); // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules _setPermissionReaderForTests(() => Promise.resolve('granted')); + setGlanceableDelivery(delivery); + delivery.registerTokens.mockClear(); + delivery.unregisterTokens.mockClear(); mocks.native.isPromotionCapable.mockReturnValue(true); mocks.native.start.mockClear(); mocks.native.update.mockClear(); @@ -105,8 +120,18 @@ describe('androidSink start and update', () => { expect(mocks.native.start).toHaveBeenCalledTimes(1); expect(mocks.native.update).toHaveBeenCalledTimes(1); - expect(mocks.native.start).toHaveBeenCalledWith('Active agents', '1 Running', true); - expect(mocks.native.update).toHaveBeenCalledWith('Active agents', '1 Running', true); + expect(mocks.native.start).toHaveBeenCalledWith( + 'Active agents', + '1 Running', + 'Open agents', + true + ); + expect(mocks.native.update).toHaveBeenCalledWith( + 'Active agents', + '1 Running', + 'Open agents', + true + ); }); it('passes promotion false when the device is not capable', async () => { @@ -115,7 +140,12 @@ describe('androidSink start and update', () => { await flushAsync(); expect(mocks.native.start).toHaveBeenCalledTimes(1); - expect(mocks.native.start).toHaveBeenCalledWith(expect.any(String), expect.any(String), false); + expect(mocks.native.start).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.any(String), + false + ); }); it('does not start ongoing when notification permission is denied', async () => { @@ -177,6 +207,55 @@ describe('androidSink start and update', () => { expect(mocks.native.start).not.toHaveBeenCalled(); expect(mocks.native.update).not.toHaveBeenCalled(); }); + + it('registers the android_ongoing token on a successful start', async () => { + const snapshot = snapshotFor([{ status: 'busy' }], 0); + androidSink.startOrUpdate(snapshot, CTX); + await flushAsync(); + + expect(delivery.registerTokens).toHaveBeenCalledTimes(1); + expect(delivery.registerTokens).toHaveBeenCalledWith(snapshot, CTX.organizationId, CTX.userId); + expect(delivery.unregisterTokens).not.toHaveBeenCalled(); + }); + + it('does not register tokens when permission is denied', async () => { + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setPermissionReaderForTests(() => Promise.resolve('denied')); + androidSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + await flushAsync(); + + expect(delivery.registerTokens).not.toHaveBeenCalled(); + }); +}); + +describe('androidSink app-state retry', () => { + it('restarts pending work and registers tokens once permission is granted', async () => { + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setPermissionReaderForTests(() => Promise.resolve('denied')); + const snapshot = snapshotFor([{ status: 'busy' }], 0); + androidSink.startOrUpdate(snapshot, CTX); + await flushAsync(); + expect(mocks.native.start).not.toHaveBeenCalled(); + + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setPermissionReaderForTests(() => Promise.resolve('granted')); + await handleAppStateActive(); + + expect(mocks.native.start).toHaveBeenCalledTimes(1); + expect(delivery.registerTokens).toHaveBeenCalledWith(snapshot, CTX.organizationId, CTX.userId); + }); + + it('does not restart pending work while permission is still denied', async () => { + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setPermissionReaderForTests(() => Promise.resolve('denied')); + androidSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + await flushAsync(); + + await handleAppStateActive(); + + expect(mocks.native.start).not.toHaveBeenCalled(); + expect(delivery.registerTokens).not.toHaveBeenCalled(); + }); }); describe('androidSink widget publish and end', () => { @@ -199,7 +278,12 @@ describe('androidSink widget publish and end', () => { androidSink.publish(snapshotFor([], 1, 'privacy')); expect(getCurrentWidgetProps()?.statusLine).toBe('Agents hidden'); expect(getCurrentWidgetProps()?.countLines).toEqual([]); - expect(mocks.native.update).toHaveBeenCalledWith('Active agents', 'Agents hidden', true); + expect(mocks.native.update).toHaveBeenCalledWith( + 'Active agents', + 'Agents hidden', + 'Open agents', + true + ); androidSink.endImmediate(); expect(mocks.native.end).toHaveBeenCalledTimes(1); @@ -222,6 +306,15 @@ describe('androidSink widget publish and end', () => { expect(getCurrentWidgetProps()?.primaryCount).toBe(1); }); + it('unregisters the token on endImmediate', async () => { + androidSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + await flushAsync(); + expect(delivery.registerTokens).toHaveBeenCalledTimes(1); + + androidSink.endImmediate(); + expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + }); + it('schedules a single future redraw at expiresAt with expired copy', () => { vi.useFakeTimers(); vi.setSystemTime(NOW); diff --git a/apps/mobile/src/glanceable-android/android-sink.ts b/apps/mobile/src/glanceable-android/android-sink.ts index a5ee286d5a..91ee32d962 100644 --- a/apps/mobile/src/glanceable-android/android-sink.ts +++ b/apps/mobile/src/glanceable-android/android-sink.ts @@ -5,7 +5,11 @@ import { import { requestWidgetUpdate } from 'react-native-android-widget'; import { i18n } from '@/i18n'; -import { type GlanceableSink, type GlanceableSinkContext } from '@/lib/glanceable/sink-registry'; +import { + getGlanceableDelivery, + type GlanceableSink, + type GlanceableSinkContext, +} from '@/lib/glanceable/sink-registry'; import { renderActiveAgentsWidget, WIDGET_NAME } from './active-agents-widget'; import { @@ -32,6 +36,7 @@ import { type TimerHandle = ReturnType; const NOTIFICATION_TITLE_KEY = 'glanceable.channelName'; +const OPEN_AGENTS_LABEL_KEY = 'glanceable.openAgents'; function translate(key: string): string { return i18n.t(key); @@ -95,9 +100,10 @@ async function tryStartOrUpdate( } const title = translate(NOTIFICATION_TITLE_KEY); const text = buildOngoingNotificationText(snapshot, {}, translate); + const openAgentsLabel = translate(OPEN_AGENTS_LABEL_KEY); if (notificationActive) { - updateLiveUpdate(title, text); + updateLiveUpdate(title, text, openAgentsLabel); revision = snapshot.revision; return; } @@ -111,15 +117,16 @@ async function tryStartOrUpdate( // eslint-disable-next-line typescript-eslint/no-unnecessary-condition -- a concurrent start/retry can set notificationActive while awaiting permission if (notificationActive) { if (snapshot.revision > revision) { - updateLiveUpdate(title, text); + updateLiveUpdate(title, text, openAgentsLabel); revision = snapshot.revision; } return; } - startLiveUpdate(title, text); + startLiveUpdate(title, text, openAgentsLabel); notificationActive = true; revision = snapshot.revision; pending = null; + getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId); return; } pending = { snapshot, ctx }; @@ -132,10 +139,22 @@ export function retryPendingStart(): void { return; } const title = translate(NOTIFICATION_TITLE_KEY); - startLiveUpdate(title, buildOngoingNotificationText(p.snapshot, {}, translate)); + const openAgentsLabel = translate(OPEN_AGENTS_LABEL_KEY); + startLiveUpdate(title, buildOngoingNotificationText(p.snapshot, {}, translate), openAgentsLabel); notificationActive = true; revision = p.snapshot.revision; pending = null; + getGlanceableDelivery().registerTokens(p.snapshot, p.ctx.organizationId, p.ctx.userId); +} + +/** Retry a pending start when the app returns to the foreground with permission granted. */ +export async function handleAppStateActive(): Promise { + if (pending === null) { + return; + } + if (await isNotificationPermissionGranted()) { + retryPendingStart(); + } } /** @@ -167,7 +186,8 @@ export const androidSink: GlanceableSink = { if (notificationActive && snapshot.revision > revision) { updateLiveUpdate( translate(NOTIFICATION_TITLE_KEY), - buildOngoingNotificationText(snapshot, {}, translate) + buildOngoingNotificationText(snapshot, {}, translate), + translate(OPEN_AGENTS_LABEL_KEY) ); revision = snapshot.revision; } @@ -184,6 +204,7 @@ export const androidSink: GlanceableSink = { revision = 0; pending = null; startEpoch += 1; + void getGlanceableDelivery().unregisterTokens(); // Widget props intentionally kept: the Home widget stays truthful. }, }; diff --git a/apps/mobile/src/glanceable-android/live-update.ts b/apps/mobile/src/glanceable-android/live-update.ts index fb3b059fcb..a67e0aae7c 100644 --- a/apps/mobile/src/glanceable-android/live-update.ts +++ b/apps/mobile/src/glanceable-android/live-update.ts @@ -8,8 +8,8 @@ import { requireOptionalNativeModule } from 'expo'; export type LiveUpdateNativeModule = { isPromotionCapable(): boolean; - start(title: string, text: string, promotion: boolean): void; - update(title: string, text: string, promotion: boolean): void; + start(title: string, text: string, openAgentsLabel: string, promotion: boolean): void; + update(title: string, text: string, openAgentsLabel: string, promotion: boolean): void; end(): void; }; @@ -23,12 +23,12 @@ export function isPromotionCapable(): boolean { return nativeModule?.isPromotionCapable() ?? false; } -export function start(title: string, text: string): void { - nativeModule?.start(title, text, isPromotionCapable()); +export function start(title: string, text: string, openAgentsLabel: string): void { + nativeModule?.start(title, text, openAgentsLabel, isPromotionCapable()); } -export function update(title: string, text: string): void { - nativeModule?.update(title, text, isPromotionCapable()); +export function update(title: string, text: string, openAgentsLabel: string): void { + nativeModule?.update(title, text, openAgentsLabel, isPromotionCapable()); } export function end(): void { diff --git a/apps/mobile/src/glanceable-android/register.test.ts b/apps/mobile/src/glanceable-android/register.test.ts new file mode 100644 index 0000000000..cbd987f777 --- /dev/null +++ b/apps/mobile/src/glanceable-android/register.test.ts @@ -0,0 +1,136 @@ +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { type AndroidWidgetProps } from './widget-props'; + +const taskHandlerMock = vi.hoisted(() => ({ + handler: null as ((props: Record) => Promise) | null, +})); + +const persistMock = vi.hoisted(() => ({ + restorePersistedGlanceable: vi.fn().mockResolvedValue(undefined), + getLastGlanceableSnapshot: vi.fn((): GlanceableAgentsSnapshot | null => null), +})); + +const sinkMock = vi.hoisted(() => ({ + androidSink: {}, + getCurrentWidgetProps: vi.fn((): AndroidWidgetProps | null => null), + handleAppStateActive: vi.fn(), + handleWidgetOpenTap: vi.fn(), +})); + +const openAgentsMock = vi.hoisted(() => ({ + openGlanceableAgents: vi.fn(), +})); + +const registerSinkMock = vi.hoisted(() => ({ + registerGlanceableSink: vi.fn(), +})); + +vi.mock('react-native', () => ({ + AppState: { addEventListener: vi.fn(() => ({ remove: vi.fn() })) }, +})); + +vi.mock('react-native-android-widget', () => ({ + registerWidgetTaskHandler: (handler: unknown) => { + taskHandlerMock.handler = handler as (props: Record) => Promise; + }, +})); + +vi.mock('@/lib/glanceable/persist', () => persistMock); +vi.mock('@/lib/glanceable/sink-registry', () => registerSinkMock); +vi.mock('@/lib/glanceable/open-agents', () => openAgentsMock); +vi.mock('./android-sink', () => sinkMock); +vi.mock('./active-agents-widget', () => ({ + OPEN_AGENTS_CLICK: 'OPEN_AGENTS', + renderActiveAgentsWidget: (props: unknown) => props, +})); + +// eslint-disable-next-line import/first -- mocks must register before the module under test +import './register'; + +const NOW = 1_750_000_000_000; + +function snapshotFor(sessions: { status: string }[]): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + sessions, + userId: 'u1', + organizationId: null, + now: NOW, + previousRevision: 0, + }); +} + +function runRenderTask(): ReturnType { + const renderWidget = vi.fn(); + const task = { + widgetInfo: {}, + widgetAction: 'WIDGET_UPDATE', + clickAction: undefined, + renderWidget, + }; + void (taskHandlerMock.handler as (props: Record) => Promise)(task); + return renderWidget; +} + +describe('android register widget task handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + persistMock.restorePersistedGlanceable.mockResolvedValue(undefined); + persistMock.getLastGlanceableSnapshot.mockReturnValue(null); + sinkMock.getCurrentWidgetProps.mockReturnValue(null); + }); + + it('restores the persisted snapshot and rebuilds props from it after a restart', async () => { + persistMock.getLastGlanceableSnapshot.mockReturnValue(snapshotFor([{ status: 'busy' }])); + const renderWidget = runRenderTask(); + + await vi.waitFor(() => { + expect(renderWidget).toHaveBeenCalledTimes(1); + }); + expect(persistMock.restorePersistedGlanceable).toHaveBeenCalledTimes(1); + + const props = renderWidget.mock.calls[0]?.[0] as AndroidWidgetProps | undefined; + expect(props?.statusLine).toBeNull(); + expect(props?.primaryCount).toBe(1); + expect(props?.countLines).toHaveLength(1); + }); + + it('uses the generic empty placeholder only when no snapshot exists', async () => { + persistMock.getLastGlanceableSnapshot.mockReturnValue(null); + const renderWidget = runRenderTask(); + + await vi.waitFor(() => { + expect(renderWidget).toHaveBeenCalledTimes(1); + }); + + const props = renderWidget.mock.calls[0]?.[0] as AndroidWidgetProps | undefined; + expect(props?.statusLine).toBe('No work in progress'); + expect(props?.countLines).toEqual([]); + expect(props?.primaryCount).toBe(0); + expect(props?.showOpenAgents).toBe(false); + }); + + it('renders in-memory props on a redraw without restoring persist', async () => { + const liveProps: AndroidWidgetProps = { + statusLine: null, + countLines: [], + primaryLabel: null, + primaryCount: 0, + openAgentsLabel: 'Open agents', + showOpenAgents: false, + accessibilityLabel: '', + }; + sinkMock.getCurrentWidgetProps.mockReturnValue(liveProps); + const renderWidget = runRenderTask(); + + await vi.waitFor(() => { + expect(renderWidget).toHaveBeenCalledTimes(1); + }); + expect(persistMock.restorePersistedGlanceable).not.toHaveBeenCalled(); + expect(renderWidget).toHaveBeenCalledWith(liveProps); + }); +}); diff --git a/apps/mobile/src/glanceable-android/register.ts b/apps/mobile/src/glanceable-android/register.ts index 2cb8e13331..fc7594feac 100644 --- a/apps/mobile/src/glanceable-android/register.ts +++ b/apps/mobile/src/glanceable-android/register.ts @@ -1,15 +1,22 @@ +import { AppState } from 'react-native'; import { registerWidgetTaskHandler, type WidgetTaskHandlerProps, } from 'react-native-android-widget'; import { i18n } from '@/i18n'; +import { getLastGlanceableSnapshot, restorePersistedGlanceable } from '@/lib/glanceable/persist'; import { registerGlanceableSink } from '@/lib/glanceable/sink-registry'; import { openGlanceableAgents } from '@/lib/glanceable/open-agents'; import { OPEN_AGENTS_CLICK, renderActiveAgentsWidget } from './active-agents-widget'; -import { androidSink, getCurrentWidgetProps, handleWidgetOpenTap } from './android-sink'; -import { buildGenericWidgetProps } from './widget-props'; +import { + androidSink, + getCurrentWidgetProps, + handleAppStateActive, + handleWidgetOpenTap, +} from './android-sink'; +import { buildAndroidWidgetProps, buildGenericWidgetProps } from './widget-props'; // Register the Android sink at import time. The main-app import of the local // live-update module loads this file, so the sink subscribes before any widget @@ -20,6 +27,15 @@ function translate(key: string): string { return i18n.t(key); } +// Restart a permission-denied pending start when the app returns to the +// foreground and notification permission is now granted. Plain AppState, no +// React component: this module loads once for the whole process. +AppState.addEventListener('change', state => { + if (state === 'active') { + void handleAppStateActive(); + } +}); + registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { const { widgetInfo, widgetAction, clickAction, renderWidget } = task; @@ -31,6 +47,21 @@ registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { return; } - const props = getCurrentWidgetProps() ?? buildGenericWidgetProps(translate); + // A process restart loses the in-memory widget props. Render them directly + // when present: a live redraw's in-memory props are newer than any + // SecureStore record, so a redraw must never restore a stale snapshot. The + // persisted snapshot is restored only on first load, when no in-memory props + // exist, and the generic empty placeholder covers a never-persisted state. + const liveProps = getCurrentWidgetProps(); + if (liveProps !== null) { + renderWidget(renderActiveAgentsWidget(liveProps, widgetInfo)); + return; + } + await restorePersistedGlanceable(); + const snapshot = getLastGlanceableSnapshot(); + const props = + snapshot === null + ? buildGenericWidgetProps(translate) + : buildAndroidWidgetProps(snapshot, {}, translate); renderWidget(renderActiveAgentsWidget(props, widgetInfo)); }); diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts index b8459e4c19..da67a81acc 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts @@ -9,16 +9,24 @@ const logoutMock = vi.hoisted(() => ({ const trpcMock = vi.hoisted(() => ({ registerActivityToken: { mutate: vi.fn() }, + unregisterActivityToken: { mutate: vi.fn() }, })); const activityMock = vi.hoisted(() => ({ getPushToken: vi.fn(), })); +const platformMock = vi.hoisted(() => ({ OS: 'ios' as string })); + /* eslint-disable import/first */ vi.mock('@/lib/auth/logout-reconciliation', () => logoutMock); vi.mock('@/lib/trpc', () => ({ - trpcClient: { user: { registerActivityToken: trpcMock.registerActivityToken } }, + trpcClient: { + user: { + registerActivityToken: trpcMock.registerActivityToken, + unregisterActivityToken: trpcMock.unregisterActivityToken, + }, + }, })); vi.mock('expo-widgets', () => ({ addPushToStartTokenListener: vi.fn(), @@ -29,12 +37,15 @@ vi.mock('@/glanceable-ios/active-agents-live-activity', () => ({ }, })); vi.mock('react-native', () => ({ - Platform: { OS: 'ios' }, + Platform: platformMock, })); import { getGlanceableDelivery } from './sink-registry'; -// Import side effect: registers the real iOS delivery under the mocks above. -import './delivery-registration'; +// Import side effect: registers the real delivery under the mocks above. +import { + _resetAndroidOngoingTokenForTests, + _setGetDevicePushTokenForTests, +} from './delivery-registration'; /* eslint-enable import/first */ const NOW = 1_750_000_000_000; @@ -52,8 +63,12 @@ function snapshot() { describe('delivery registerTokens', () => { beforeEach(() => { vi.clearAllMocks(); + platformMock.OS = 'ios'; + _setGetDevicePushTokenForTests(null); + _resetAndroidOngoingTokenForTests(); activityMock.getPushToken.mockResolvedValue('token-1'); trpcMock.registerActivityToken.mutate.mockResolvedValue({ success: true }); + trpcMock.unregisterActivityToken.mutate.mockResolvedValue({ success: true }); logoutMock.attemptLogoutReconciliation.mockResolvedValue({ kind: 'no-tombstone' }); logoutMock.awaitLogoutReconciliationSettled.mockResolvedValue(undefined); }); @@ -93,4 +108,198 @@ describe('delivery registerTokens', () => { }); }); }); + + it('registers the device Expo push token as android_ongoing on a successful Android start', async () => { + platformMock.OS = 'android'; + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setGetDevicePushTokenForTests(() => Promise.resolve('android-token-1')); + + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await vi.waitFor(() => { + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalledWith({ + token: 'android-token-1', + kind: 'android_ongoing', + platform: 'android', + organizationId: 'org-1', + }); + }); + }); + + it('does not register on Android when the device has no push token', async () => { + platformMock.OS = 'android'; + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setGetDevicePushTokenForTests(() => Promise.resolve(null)); + + getGlanceableDelivery().registerTokens(snapshot(), null, 'u1'); + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + + expect(trpcMock.registerActivityToken.mutate).not.toHaveBeenCalled(); + }); + + it('unregisters the recorded android_ongoing token and clears it on success', async () => { + platformMock.OS = 'android'; + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setGetDevicePushTokenForTests(() => Promise.resolve('android-token-1')); + getGlanceableDelivery().registerTokens(snapshot(), null, 'u1'); + await vi.waitFor(() => { + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalled(); + }); + + const result = await getGlanceableDelivery().unregisterTokens(); + expect(result).toEqual({ ok: true, tokens: ['android-token-1'] }); + expect(trpcMock.unregisterActivityToken.mutate).toHaveBeenCalledWith({ + token: 'android-token-1', + }); + + // A second unregister has nothing recorded to attempt. + const second = await getGlanceableDelivery().unregisterTokens(); + expect(second).toEqual({ ok: true, tokens: [] }); + }); + + it('reports a failed android_ongoing unregister and keeps the token for retry', async () => { + platformMock.OS = 'android'; + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setGetDevicePushTokenForTests(() => Promise.resolve('android-token-1')); + getGlanceableDelivery().registerTokens(snapshot(), null, 'u1'); + await vi.waitFor(() => { + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalled(); + }); + + trpcMock.unregisterActivityToken.mutate.mockRejectedValueOnce(new Error('network')); + const result = await getGlanceableDelivery().unregisterTokens(); + expect(result).toEqual({ ok: false, tokens: ['android-token-1'] }); + + // The failed unregister kept the token: a following unregister still + // targets the same token. + const retry = await getGlanceableDelivery().unregisterTokens(); + expect(retry).toEqual({ ok: true, tokens: ['android-token-1'] }); + expect(trpcMock.unregisterActivityToken.mutate).toHaveBeenLastCalledWith({ + token: 'android-token-1', + }); + }); + + it('blocks a late register and does not unregister the token it recorded while in flight', async () => { + platformMock.OS = 'android'; + const tokenResolver: { resolve: ((value: string | null) => void) | null } = { + resolve: null, + }; + let tokenLookupCalled = false; + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + const deferredToken = (): Promise => + new Promise(resolve => { + tokenLookupCalled = true; + tokenResolver.resolve = resolve; + }); + _setGetDevicePushTokenForTests(deferredToken); + + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await vi.waitFor(() => { + expect(tokenLookupCalled).toBe(true); + }); + + // Unregister while the register is still in flight at the token lookup: + // the unregister snapshots the (empty) recorded token and does not await + // the in-flight register. + const result = await getGlanceableDelivery().unregisterTokens(); + tokenResolver.resolve?.('android-token-1'); + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + + expect(result).toEqual({ ok: true, tokens: [] }); + expect(trpcMock.registerActivityToken.mutate).not.toHaveBeenCalled(); + expect(trpcMock.unregisterActivityToken.mutate).not.toHaveBeenCalled(); + }); + + it("keeps a later start's row when a register is in flight during unregister", async () => { + platformMock.OS = 'android'; + const firstResolver: { resolve: ((value: string | null) => void) | null } = { + resolve: null, + }; + let firstLookupCalled = false; + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + const deferredFirst = (): Promise => + new Promise(resolve => { + firstLookupCalled = true; + firstResolver.resolve = resolve; + }); + _setGetDevicePushTokenForTests(deferredFirst); + + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await vi.waitFor(() => { + expect(firstLookupCalled).toBe(true); + }); + + // End while the first register is still in flight at the token lookup. + const unregisterPromise = getGlanceableDelivery().unregisterTokens(); + + // Immediate restart: a second register records and registers its token + // while the unregister is still settling. + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setGetDevicePushTokenForTests(() => Promise.resolve('android-token-2')); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await vi.waitFor(() => { + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalledWith({ + token: 'android-token-2', + kind: 'android_ongoing', + platform: 'android', + organizationId: 'org-1', + }); + }); + + // Let the first register's stalled lookup finish; it must abort and must + // not delete the second register's row. + firstResolver.resolve?.('android-token-1'); + const unregisterResult = await unregisterPromise; + + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalledTimes(1); + expect(trpcMock.unregisterActivityToken.mutate).not.toHaveBeenCalled(); + expect(unregisterResult).toEqual({ ok: true, tokens: [] }); + }); + + it("keeps a later start's row for the same device token (end-then-restart)", async () => { + platformMock.OS = 'android'; + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setGetDevicePushTokenForTests(() => Promise.resolve('android-token-1')); + + // First start registers the device token. + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await vi.waitFor(() => { + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalledTimes(1); + }); + + // End: hold the server delete in flight. + const deleteGateState = { release: undefined as ((value: unknown) => void) | undefined }; + const deleteGate = new Promise(resolve => { + deleteGateState.release = resolve; + }); + // eslint-disable-next-line promise-function-async -- controllable promise for the race test + trpcMock.unregisterActivityToken.mutate.mockImplementationOnce(() => deleteGate); + const unregisterPromise = getGlanceableDelivery().unregisterTokens(); + + // Immediate restart with the same device token while the delete is in flight. + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + // The re-register is serialized behind the in-flight delete, so it has not + // run yet. + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalledTimes(1); + + // Release the delete; the serialized re-register then runs and wins. + deleteGateState.release?.({ success: true }); + await vi.waitFor(() => { + expect(trpcMock.registerActivityToken.mutate).toHaveBeenCalledTimes(2); + }); + + const result = await unregisterPromise; + expect(result).toEqual({ ok: true, tokens: ['android-token-1'] }); + expect(trpcMock.unregisterActivityToken.mutate).toHaveBeenCalledTimes(1); + + // The final state is re-registered: a later unregister targets the token. + const final = await getGlanceableDelivery().unregisterTokens(); + expect(final).toEqual({ ok: true, tokens: ['android-token-1'] }); + }); }); diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.ts b/apps/mobile/src/lib/glanceable/delivery-registration.ts index 9eade4a84b..1b928a02e6 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.ts @@ -12,26 +12,73 @@ import { trpcClient } from '@/lib/trpc'; import { type GlanceableDelivery, setGlanceableDelivery } from './sink-registry'; /** - * iOS activity-token registrar. Wires the glanceable publisher's delivery - * hooks to `user.registerActivityToken`/`user.unregisterActivityToken` so the - * server can reach this device's Live Activity and push-to-start token via - * APNs. Android uses Expo push tokens and never calls this delivery. + * Activity-token registrar. Wires the glanceable publisher's delivery hooks to + * `user.registerActivityToken`/`user.unregisterActivityToken` so the server can + * reach this device's surface token: on iOS the Live Activity and push-to-start + * token via APNs, on Android the per-device Expo push token (`android_ongoing`). */ let pushToStartToken: string | null = null; -async function register( - token: string, - kind: 'ios_push_to_start' | 'ios_activity', - organizationId: string | null -): Promise { +/** The last Android device token registered, so end/cleanup can unregister it. */ +let androidOngoingToken: string | null = null; + +/** Epoch bumped on every Android unregister/end. A register that started before + * the bump must abort instead of recreating the row after end/logout. */ +let androidRegisterEpoch = 0; + +/** FIFO chain serializing Android register/unregister mutations so the last + * client intent wins: an upsert and a delete of the same per-device token must + * never race, because the device token is stable across sign-ins. */ +let androidMutationTail: Promise | null = null; + +const NOOP = (): void => undefined; + +/** Serialize one mutation; a rejected prior mutation never blocks the next. */ +async function enqueueAndroidMutation(op: () => Promise): Promise { + const previous = androidMutationTail; + let release: () => void = NOOP; + const gate = new Promise(resolve => { + release = resolve; + }); + androidMutationTail = gate; + if (previous !== null) { + try { + await previous; + } catch { + // A prior mutation failure must not block the next one. + } + } try { - await trpcClient.user.registerActivityToken.mutate({ - token, - kind, - platform: 'ios', - organizationId, - }); + return await op(); + } finally { + release(); + } +} + +// Test-only override so pure suites never load @/lib/notifications +// (→ expo-notifications → expo-modules-core → RN). +let getDevicePushTokenForTests: (() => Promise) | null = null; + +function getDevicePushTokenLazy(): () => Promise { + if (getDevicePushTokenForTests !== null) { + return getDevicePushTokenForTests; + } + // eslint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires, unicorn/prefer-module -- lazy native load + const { getDevicePushToken } = require('@/lib/notifications') as { + getDevicePushToken: () => Promise; + }; + return getDevicePushToken; +} + +async function register(input: { + token: string; + kind: 'ios_push_to_start' | 'ios_activity' | 'android_ongoing'; + platform: 'ios' | 'android'; + organizationId: string | null; +}): Promise { + try { + await trpcClient.user.registerActivityToken.mutate(input); } catch { // Best effort: a failed registration is retried on the next start. } @@ -55,8 +102,71 @@ if (Platform.OS === 'ios') { }); } +/** + * Android: register the device Expo push token as the `android_ongoing` + * activity token. The device-token lookup happens outside the mutation chain + * (the chain must never await logout reconciliation or it can deadlock against + * `runLogoutCleanup`); only the server mutation and the slot write are + * serialized, so the last client intent wins even for a stable token. + */ +async function registerAndroidOngoingToken( + organizationId: string | null, + userId: string | null +): Promise { + // Capture the epoch before the first await so an unregister/end that lands + // during reconciliation or the token lookup aborts this stale register. + const epoch = androidRegisterEpoch; + if (userId !== null) { + void attemptLogoutReconciliation(userId); + } + try { + await awaitLogoutReconciliationSettled(); + if (epoch !== androidRegisterEpoch) { + return; + } + const token = await getDevicePushTokenLazy()(); + if (token === null) { + return; + } + if (epoch !== androidRegisterEpoch) { + return; + } + await enqueueAndroidMutation(async () => { + await register({ token, kind: 'android_ongoing', platform: 'android', organizationId }); + androidOngoingToken = token; + }); + } catch { + // Best effort: a failed lookup is retried on the next start. + } +} + +/** Android: unregister the recorded device token, tombstoning it on failure. + * Bumps the epoch (invalidating in-flight registers) and serializes the delete + * against register so a delete never races an upsert of the same token: the + * FIFO order decides the final state. */ +async function unregisterAndroidOngoingToken(): Promise<{ ok: boolean; tokens: string[] }> { + androidRegisterEpoch += 1; + const result = enqueueAndroidMutation(async () => { + const token = androidOngoingToken; + if (token === null) { + return { ok: true, tokens: [] as string[] }; + } + const ok = await unregister(token); + if (ok) { + androidOngoingToken = null; + } + return { ok, tokens: [token] }; + }); + await result; + return result; +} + const delivery: GlanceableDelivery = { registerTokens(_snapshot, organizationId, userId) { + if (Platform.OS === 'android') { + void registerAndroidOngoingToken(organizationId, userId); + return; + } if (Platform.OS !== 'ios') { return; } @@ -72,14 +182,19 @@ const delivery: GlanceableDelivery = { } await awaitLogoutReconciliationSettled(); if (pushToStartToken !== null) { - await register(pushToStartToken, 'ios_push_to_start', organizationId); + await register({ + token: pushToStartToken, + kind: 'ios_push_to_start', + platform: 'ios', + organizationId, + }); } try { const activity = ActiveAgentsLiveActivity.getInstances().at(-1); if (activity) { const token = await activity.getPushToken(); if (token) { - await register(token, 'ios_activity', organizationId); + await register({ token, kind: 'ios_activity', platform: 'ios', organizationId }); } } } catch { @@ -89,6 +204,9 @@ const delivery: GlanceableDelivery = { }, async unregisterTokens() { + if (Platform.OS === 'android') { + return unregisterAndroidOngoingToken(); + } if (Platform.OS !== 'ios') { return { ok: true, tokens: [] }; } @@ -133,3 +251,14 @@ async function unregisterActivityTokens(): Promise<{ ok: boolean; tokens: string } setGlanceableDelivery(delivery); + +// ── Test-only helpers ────────────────────────────────────────────────────── + +export function _setGetDevicePushTokenForTests(fn: (() => Promise) | null): void { + getDevicePushTokenForTests = fn; +} + +export function _resetAndroidOngoingTokenForTests(): void { + androidOngoingToken = null; + androidMutationTail = null; +} From 5734c5541c3c1236a762d34e21b6e19bff9b2ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 00:41:50 +0200 Subject: [PATCH 08/43] fix(glanceable): unregister tokens on switch and end remote-empty --- .../mobile/src/lib/auth/auth-context.test.tsx | 17 ++ apps/mobile/src/lib/auth/auth-context.tsx | 7 +- .../src/lib/auth/logout-cleanup.test.ts | 74 ++++++++- apps/mobile/src/lib/auth/logout-cleanup.ts | 40 +++++ apps/mobile/src/lib/notifications.test.ts | 96 +++++++++++ apps/mobile/src/lib/notifications.ts | 44 +++++ .../src/lib/organization-context.test.ts | 152 ++++++++++++++++++ apps/mobile/src/lib/organization-context.tsx | 6 + 8 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 apps/mobile/src/lib/organization-context.test.ts diff --git a/apps/mobile/src/lib/auth/auth-context.test.tsx b/apps/mobile/src/lib/auth/auth-context.test.tsx index dafe95c348..ddbc76d86e 100644 --- a/apps/mobile/src/lib/auth/auth-context.test.tsx +++ b/apps/mobile/src/lib/auth/auth-context.test.tsx @@ -91,6 +91,7 @@ const readCacheMock = vi.hoisted(() => ({ // force it to reject without loading the tRPC/notifications chain. const logoutCleanupMock = vi.hoisted(() => ({ runLogoutCleanup: vi.fn().mockResolvedValue(undefined), + unregisterActivityTokensAndTombstone: vi.fn().mockResolvedValue(undefined), })); // ---- all vi.mock calls ---- @@ -419,6 +420,22 @@ describe('sign-out teardown ordering', () => { unmount(); }); + it('unregisters the prior account activity tokens on sign-in (account switch)', async () => { + const { ctx, unmount } = await mountAndGetContext(); + + await act(async () => { + await ctx.signIn(makeToken({ kiloUserId: 'user-2' })); + }); + + // The switch unregisters the prior scope's activity tokens (tombstone on + // failure) without revoking the device session — runLogoutCleanup must not + // run on a plain sign-in. + expect(logoutCleanupMock.unregisterActivityTokensAndTombstone).toHaveBeenCalledTimes(1); + expect(logoutCleanupMock.runLogoutCleanup).not.toHaveBeenCalled(); + + unmount(); + }); + it('clears the trusted hosts and image confirmations on sign-in', async () => { const { ctx, unmount } = await mountAndGetContext(); const trustedHosts = await import('@/lib/hooks/use-trusted-hosts'); diff --git a/apps/mobile/src/lib/auth/auth-context.tsx b/apps/mobile/src/lib/auth/auth-context.tsx index bd25a10051..bb76377170 100644 --- a/apps/mobile/src/lib/auth/auth-context.tsx +++ b/apps/mobile/src/lib/auth/auth-context.tsx @@ -17,7 +17,7 @@ import { resetAppsFlyerState, trackEvent } from '@/lib/appsflyer'; import { clearAccountBoundPendingDeepLink, setCurrentDeepLinkUserId } from '@/lib/deep-link-launch'; import { writeSignedOutSnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { deleteAccountMetadata } from '@/lib/auth/account-metadata-write'; -import { runLogoutCleanup } from '@/lib/auth/logout-cleanup'; +import { runLogoutCleanup, unregisterActivityTokensAndTombstone } from '@/lib/auth/logout-cleanup'; import { queryClient } from '@/lib/query-client'; import { setTrpcUnauthorizedHandler } from '@/lib/auth/trpc-unauthorized'; import { exchangeLegacyToken } from '@/lib/auth/exchange-legacy-token'; @@ -208,6 +208,11 @@ export function AuthProvider({ children }: { readonly children: ReactNode }) { // Blank the prior account's glanceable surface before any credential // persist, so a direct account switch never shows the previous account. writeSignedOutSnapshotAndEnd(); + // Unregister the prior account's activity tokens (Live Activity / + // push-to-start) BEFORE persisting the new credentials, so the + // unregister runs under the old token owner's auth. This never revokes + // the device session or unregisters the Expo push token (logout-only). + await unregisterActivityTokensAndTombstone(); setAuthEpoch(currentAuthEpoch()); // Bind the pending deep-link slot to the new user id at the same // place the auth epoch advances, so a destination captured while this diff --git a/apps/mobile/src/lib/auth/logout-cleanup.test.ts b/apps/mobile/src/lib/auth/logout-cleanup.test.ts index daeda1513e..c0ba8e37b9 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.test.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines -- one cohesive logout-cleanup suite: runLogoutCleanup and the account/org-switch activity unregister share the SecureStore and delivery mocks */ import { beforeEach, describe, expect, it, vi } from 'vitest'; const store = new Map(); @@ -53,7 +54,11 @@ vi.mock('@/lib/persist/encrypted-kv', () => ({ clearScopePrefix: vi.fn(), })); -import { readLogoutCleanupTombstone, runLogoutCleanup } from '@/lib/auth/logout-cleanup'; +import { + readLogoutCleanupTombstone, + runLogoutCleanup, + unregisterActivityTokensAndTombstone, +} from '@/lib/auth/logout-cleanup'; import { getDevicePushTokenOutcome } from '@/lib/notifications'; import { getActiveToken } from '@/lib/auth/token-owner'; import { queryClient } from '@/lib/query-client'; @@ -322,3 +327,70 @@ describe('runLogoutCleanup', () => { await expect(readLogoutCleanupTombstone()).resolves.toEqual(expected); }); }); + +describe('unregisterActivityTokensAndTombstone', () => { + beforeEach(() => { + vi.clearAllMocks(); + store.clear(); + seedUser('u1'); + deliveryMock.unregisterTokens.mockResolvedValue({ ok: true, tokens: [] }); + }); + + it('unregisters the activity tokens and writes no tombstone on success', async () => { + deliveryMock.unregisterTokens.mockResolvedValue({ ok: true, tokens: ['a1', 'a2'] }); + + await expect(unregisterActivityTokensAndTombstone()).resolves.toBeUndefined(); + + expect(deliveryMock.unregisterTokens).toHaveBeenCalledTimes(1); + expect(store.has(LOGOUT_CLEANUP_TOMBSTONE_KEY)).toBe(false); + }); + + it('tombstones the recorded activity tokens when the unregister fails', async () => { + deliveryMock.unregisterTokens.mockResolvedValue({ + ok: false, + tokens: ['activity-token-1', 'activity-token-2'], + }); + + await unregisterActivityTokensAndTombstone(); + + const tombstone = await readLogoutCleanupTombstone(); + expect(tombstone).toMatchObject({ + userId: 'u1', + pushToken: null, + needsPushUnregister: false, + needsActivityUnregister: true, + activityTokens: ['activity-token-1', 'activity-token-2'], + }); + }); + + it('leaves an existing tombstone untouched on success so a pending push unregister survives', async () => { + store.set( + LOGOUT_CLEANUP_TOMBSTONE_KEY, + JSON.stringify({ + userId: 'u1', + pushToken: 'push-1', + needsPushUnregister: true, + needsActivityUnregister: false, + activityTokens: [], + failedAt: 1_700_000_000_000, + }) + ); + deliveryMock.unregisterTokens.mockResolvedValue({ ok: true, tokens: ['a1'] }); + + await unregisterActivityTokensAndTombstone(); + + const tombstone = await readLogoutCleanupTombstone(); + expect(tombstone).toMatchObject({ + needsPushUnregister: true, + pushToken: 'push-1', + needsActivityUnregister: false, + }); + }); + + it('never throws when the unregister itself rejects', async () => { + deliveryMock.unregisterTokens.mockRejectedValue(new Error('network down')); + + await expect(unregisterActivityTokensAndTombstone()).resolves.toBeUndefined(); + expect(store.has(LOGOUT_CLEANUP_TOMBSTONE_KEY)).toBe(false); + }); +}); diff --git a/apps/mobile/src/lib/auth/logout-cleanup.ts b/apps/mobile/src/lib/auth/logout-cleanup.ts index c9029327b1..b8bb3e7c8c 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.ts @@ -161,3 +161,43 @@ export async function runLogoutCleanup(): Promise { }); } } + +/** + * Unregister the recorded activity tokens (Live Activity / push-to-start) and + * tombstone a failure, WITHOUT revoking the device session or unregistering + * the Expo push token (those are logout-only). Never throws by contract. + * + * Called on account switch (`signIn`) and org switch (`setOrganizationId`), + * where the prior scope's activity tokens must stop receiving APNs before the + * new scope registers its own. The cached user id is read before any switch + * clears it, so a failed unregister tombstones the prior account's identity — + * the same ordering `runLogoutCleanup` relies on. A successful unregister + * leaves any existing tombstone untouched: only a full logout deletes it, so a + * pending push unregister survives a switch. + */ +export async function unregisterActivityTokensAndTombstone(): Promise { + try { + const userId = readCachedUserId(queryClient); + const result = await getGlanceableDelivery().unregisterTokens(); + if (result.ok) { + return; + } + await writeLogoutCleanupTombstone({ + userId, + pushToken: null, + needsPushUnregister: false, + needsActivityUnregister: true, + activityTokens: result.tokens, + failedAt: Date.now(), + }); + } catch (error) { + // Never throw: a failed unregister or tombstone write must not block the + // account or org switch. + Sentry.captureException(error, { + tags: { + 'error.subsystem': 'auth', + 'error.operation': 'unregister_activity_tokens', + }, + }); + } +} diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index 80857cb8c1..c76760a081 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -6,6 +6,7 @@ import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests, _setSecureStoreForTests, + persistGlanceableSink, } from '@/lib/glanceable/persist'; import { registerGlanceableSink, unregisterGlanceableSink } from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; @@ -344,6 +345,10 @@ describe('applyGlanceablePushData', () => { mockSecureStoreKeys(); }); + afterEach(() => { + vi.useRealTimers(); + }); + it('discards a remote snapshot that is not newer than the last applied snapshot', async () => { _setLastGlanceableSnapshotForTests( glanceableSnapshot({ @@ -395,6 +400,97 @@ describe('applyGlanceablePushData', () => { unregisterGlanceableSink(sink); }); + + it('ends the sinks after the terminal window for a non-eligible remote snapshot', async () => { + vi.useFakeTimers(); + _setLastGlanceableSnapshotForTests( + glanceableSnapshot({ + scopeKey: 'scope-1', + revision: 3, + updatedAt: '2026-01-01T00:00:00.000Z', + }) + ); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + const result = await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey: 'scope-1', + updatedAt: '2026-01-02T00:00:00.000Z', + status: 'empty', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }) + ); + + expect(result).toBe(true); + // The empty snapshot is published (widgets keep the latest counts), but + // the Live Activity / ongoing is not ended until the terminal window. + expect(sink.publish).toHaveBeenCalledTimes(1); + expect(sink.startOrUpdate).not.toHaveBeenCalled(); + expect(sink.endImmediate).not.toHaveBeenCalled(); + + // The persist sink writes the empty snapshot before the terminal fires, so + // the fire-time eligibility guard sees non-eligible work and ends it. + _setLastGlanceableSnapshotForTests( + glanceableSnapshot({ + scopeKey: 'scope-1', + revision: 4, + updatedAt: '2026-01-02T00:00:00.000Z', + status: 'empty', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }) + ); + + vi.advanceTimersByTime(8000); + expect(sink.endImmediate).toHaveBeenCalledTimes(1); + + unregisterGlanceableSink(sink); + }); + + it('cancels the pending terminal when a newer eligible snapshot arrives', async () => { + vi.useFakeTimers(); + _setLastGlanceableSnapshotForTests( + glanceableSnapshot({ + scopeKey: 'scope-1', + revision: 3, + updatedAt: '2026-01-01T00:00:00.000Z', + }) + ); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey: 'scope-1', + updatedAt: '2026-01-02T00:00:00.000Z', + status: 'empty', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }) + ); + await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey: 'scope-1', + updatedAt: '2026-01-03T00:00:00.000Z', + organizationBound: true, + }) + ); + + vi.advanceTimersByTime(8000); + // The later eligible snapshot cancelled the 8s terminal: work restarted, + // so the activity must not end. + expect(sink.endImmediate).not.toHaveBeenCalled(); + + unregisterGlanceableSink(sink); + }); }); describe('setupNotificationBackgroundHandler', () => { diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index 9b9fc38171..2e6093a829 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -14,11 +14,13 @@ import { pushDataSchema, } from '@kilocode/notifications'; import { + GLANCEABLE_TERMINAL_MS, type GlanceableAgentsSnapshot, isEligibleGlanceableWork, } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; +import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; import { getLastGlanceableSnapshot, getLocalScopeKey, @@ -62,6 +64,43 @@ export function parseNotificationData(data: unknown): PushData | null { return parsed.success ? parsed.data : null; } +// Pending 8 s terminal end for a non-eligible remote snapshot. Mirrors the +// in-app publisher's terminal window: publish the empty counts, then end the +// Live Activity / Android ongoing after GLANCEABLE_TERMINAL_MS. A newer +// eligible snapshot cancels it, and the terminal-blank epoch gate skips a +// stale end after a logout/org switch already ended the surface. +let glanceableTerminalTimer: ReturnType | null = null; + +function cancelGlanceableTerminalEnd(): void { + if (glanceableTerminalTimer !== null) { + clearTimeout(glanceableTerminalTimer); + glanceableTerminalTimer = null; + } +} + +function scheduleGlanceableTerminalEnd(): void { + cancelGlanceableTerminalEnd(); + const blankEpoch = getTerminalBlankEpoch(); + glanceableTerminalTimer = setTimeout(() => { + glanceableTerminalTimer = null; + // A terminal blank (logout/org switch) that landed during the window + // already ended the surface; do not end the new scope's activity. + if (getTerminalBlankEpoch() !== blankEpoch) { + return; + } + // Eligible work published during the window restarted the activity (the + // in-app publisher owns the foreground path and never cancels this timer); + // do not end a restarted activity. + const last = getLastGlanceableSnapshot(); + if (last !== null && isEligibleGlanceableWork(last)) { + return; + } + for (const sink of getGlanceableSinks()) { + sink.endImmediate(); + } + }, GLANCEABLE_TERMINAL_MS); +} + /** * Apply an `active_agents_glanceable` background push to the glanceable sinks * (widgets, Android ongoing, iOS Live Activity). Returns false when the push @@ -102,6 +141,7 @@ export async function applyGlanceablePushData( const ctx = { userId, organizationId }; if (isEligibleGlanceableWork(snapshot)) { + cancelGlanceableTerminalEnd(); for (const sink of getGlanceableSinks()) { sink.publish(snapshot); sink.startOrUpdate(snapshot, ctx); @@ -110,6 +150,10 @@ export async function applyGlanceablePushData( for (const sink of getGlanceableSinks()) { sink.publish(snapshot); } + // A remote snapshot with no eligible work must end the Live Activity and + // the Android ongoing after the terminal window; widgets keep the last + // published counts (their endImmediate is a no-op). + scheduleGlanceableTerminalEnd(); } return true; } diff --git a/apps/mobile/src/lib/organization-context.test.ts b/apps/mobile/src/lib/organization-context.test.ts new file mode 100644 index 0000000000..2460af298c --- /dev/null +++ b/apps/mobile/src/lib/organization-context.test.ts @@ -0,0 +1,152 @@ +/* oxlint-disable typescript-eslint/no-deprecated -- react-test-renderer is the DOM-free renderer for RN trees under vitest (node env, no jsdom) */ +/* oxlint-disable @typescript-eslint/no-unsafe-call @typescript-eslint/no-unsafe-member-access */ +import { createElement } from 'react'; +import TestRenderer, { act } from 'react-test-renderer'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const hoisted = vi.hoisted(() => ({ + useAuth: vi.fn(), + setAccountMetadata: vi.fn(), + deleteAccountMetadata: vi.fn(), + writePrivacySnapshotAndEnd: vi.fn(), + unregisterActivityTokensAndTombstone: vi.fn(), + getItemAsync: vi.fn(), +})); + +vi.mock('@/lib/auth/auth-context', () => ({ + useAuth: hoisted.useAuth, +})); + +vi.mock('@/lib/auth/account-metadata-write', () => ({ + setAccountMetadata: hoisted.setAccountMetadata, + deleteAccountMetadata: hoisted.deleteAccountMetadata, +})); + +vi.mock('@/lib/glanceable/cleanup', () => ({ + writePrivacySnapshotAndEnd: hoisted.writePrivacySnapshotAndEnd, +})); + +vi.mock('@/lib/auth/logout-cleanup', () => ({ + unregisterActivityTokensAndTombstone: hoisted.unregisterActivityTokensAndTombstone, +})); + +vi.mock('@/lib/storage-keys', () => ({ + ORGANIZATION_STORAGE_KEY: 'organization', +})); + +vi.mock('expo-secure-store', () => ({ + getItemAsync: hoisted.getItemAsync, + setItemAsync: vi.fn(), + deleteItemAsync: vi.fn(), +})); + +type OrganizationContextValue = { + organizationId: string | null; + isLoaded: boolean; + setOrganizationId: (id: string | null) => void; +}; + +async function mountProvider(): Promise<{ + getCtx: () => OrganizationContextValue; + unmount: () => void; +}> { + vi.resetModules(); + const mod = await import('./organization-context'); + + let capturedCtx: OrganizationContextValue | undefined = undefined; + function Consumer(): null { + capturedCtx = mod.useOrganization(); + return null; + } + + let renderer: TestRenderer.ReactTestRenderer | undefined = undefined; + await act(async () => { + renderer = TestRenderer.create( + createElement(mod.OrganizationProvider, null, createElement(Consumer)) + ); + await Promise.resolve(); + }); + await act(async () => { + await new Promise(resolve => { + void setTimeout(resolve, 0); + }); + }); + + // oxlint-disable-next-line @typescript-eslint/no-unnecessary-condition -- safety net for test failures + if (!capturedCtx) { + throw new Error('organization context not captured'); + } + + return { + getCtx: () => { + // oxlint-disable-next-line @typescript-eslint/no-unnecessary-condition -- safety net for test failures + if (!capturedCtx) { + throw new Error('organization context not captured'); + } + return capturedCtx; + }, + unmount: () => { + renderer?.unmount(); + }, + }; +} + +describe('OrganizationProvider.setOrganizationId', () => { + beforeEach(() => { + vi.clearAllMocks(); + hoisted.useAuth.mockReturnValue({ token: 't' }); + hoisted.getItemAsync.mockResolvedValue(null); + hoisted.setAccountMetadata.mockResolvedValue(undefined); + hoisted.deleteAccountMetadata.mockResolvedValue(undefined); + hoisted.unregisterActivityTokensAndTombstone.mockResolvedValue(undefined); + }); + + it('blanks, unregisters the prior org activity tokens, and persists the new selection', async () => { + const { getCtx, unmount } = await mountProvider(); + + act(() => { + getCtx().setOrganizationId('org-2'); + }); + + expect(hoisted.writePrivacySnapshotAndEnd).toHaveBeenCalledTimes(1); + expect(hoisted.unregisterActivityTokensAndTombstone).toHaveBeenCalledTimes(1); + expect(hoisted.setAccountMetadata).toHaveBeenCalledWith('organization', 'org-2'); + expect(getCtx().organizationId).toBe('org-2'); + + unmount(); + }); + + it('no-ops a same-value org selection', async () => { + const { getCtx, unmount } = await mountProvider(); + + act(() => { + getCtx().setOrganizationId('org-2'); + }); + act(() => { + getCtx().setOrganizationId('org-2'); + }); + + expect(hoisted.writePrivacySnapshotAndEnd).toHaveBeenCalledTimes(1); + expect(hoisted.unregisterActivityTokensAndTombstone).toHaveBeenCalledTimes(1); + expect(hoisted.setAccountMetadata).toHaveBeenCalledTimes(1); + + unmount(); + }); + + it('clears the persisted org and unregisters tokens when switching to personal', async () => { + const { getCtx, unmount } = await mountProvider(); + + act(() => { + getCtx().setOrganizationId('org-2'); + }); + act(() => { + getCtx().setOrganizationId(null); + }); + + expect(hoisted.deleteAccountMetadata).toHaveBeenCalledWith('organization'); + expect(hoisted.unregisterActivityTokensAndTombstone).toHaveBeenCalledTimes(2); + expect(getCtx().organizationId).toBeNull(); + + unmount(); + }); +}); diff --git a/apps/mobile/src/lib/organization-context.tsx b/apps/mobile/src/lib/organization-context.tsx index 2e485bb317..1feaa76c4c 100644 --- a/apps/mobile/src/lib/organization-context.tsx +++ b/apps/mobile/src/lib/organization-context.tsx @@ -11,6 +11,7 @@ import { import { useAuth } from '@/lib/auth/auth-context'; import { deleteAccountMetadata, setAccountMetadata } from '@/lib/auth/account-metadata-write'; +import { unregisterActivityTokensAndTombstone } from '@/lib/auth/logout-cleanup'; import { writePrivacySnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; @@ -70,6 +71,11 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod // Blank the current surface before the selection changes so the prior // org's counts are never shown under the next org. writePrivacySnapshotAndEnd(); + // Unregister the prior org's activity tokens (Live Activity / + // push-to-start) so APNs stops targeting this device for the old scope. + // Same-account switch: never revokes the device session or unregisters + // the Expo push token (logout-only). + void unregisterActivityTokensAndTombstone(); setOrgState(id); if (id) { void setAccountMetadata(ORGANIZATION_STORAGE_KEY, id); From a292bacb7ba7050c4721539dd8b8036870d33f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 00:58:35 +0200 Subject: [PATCH 09/43] chore(db): renumber activity-token migration after main merge --- .../0234_smiling_natasha_romanoff.sql | 14 + .../db/src/migrations/meta/0234_snapshot.json | 39889 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + 3 files changed, 39910 insertions(+) create mode 100644 packages/db/src/migrations/0234_smiling_natasha_romanoff.sql create mode 100644 packages/db/src/migrations/meta/0234_snapshot.json diff --git a/packages/db/src/migrations/0234_smiling_natasha_romanoff.sql b/packages/db/src/migrations/0234_smiling_natasha_romanoff.sql new file mode 100644 index 0000000000..de6246d0e5 --- /dev/null +++ b/packages/db/src/migrations/0234_smiling_natasha_romanoff.sql @@ -0,0 +1,14 @@ +CREATE TABLE "user_activity_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "token" text NOT NULL, + "kind" text NOT NULL, + "platform" text NOT NULL, + "organization_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "user_activity_tokens" ADD CONSTRAINT "user_activity_tokens_user_id_kilocode_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_user_activity_tokens_token" ON "user_activity_tokens" USING btree ("token");--> statement-breakpoint +CREATE INDEX "IDX_user_activity_tokens_user_org" ON "user_activity_tokens" USING btree ("user_id","organization_id"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0234_snapshot.json b/packages/db/src/migrations/meta/0234_snapshot.json new file mode 100644 index 0000000000..2927f43b0e --- /dev/null +++ b/packages/db/src/migrations/meta/0234_snapshot.json @@ -0,0 +1,39889 @@ +{ + "id": "57726a46-8ba3-46e0-afb3-7fb0de4a0564", + "prevId": "e0607648-c887-430b-805e-ecbf3342f762", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_configs": { + "name": "agent_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "runtime_state": { + "name": "runtime_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "IDX_agent_configs_org_id": { + "name": "IDX_agent_configs_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_owned_by_user_id": { + "name": "IDX_agent_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_agent_type": { + "name": "IDX_agent_configs_agent_type", + "columns": [ + { + "expression": "agent_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_platform": { + "name": "IDX_agent_configs_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_configs_owned_by_organization_id_organizations_id_fk": { + "name": "agent_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_configs_org_agent_platform": { + "name": "UQ_agent_configs_org_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "agent_type", + "platform" + ] + }, + "UQ_agent_configs_user_agent_platform": { + "name": "UQ_agent_configs_user_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_user_id", + "agent_type", + "platform" + ] + } + }, + "policies": {}, + "checkConstraints": { + "agent_configs_owner_check": { + "name": "agent_configs_owner_check", + "value": "(\n (\"agent_configs\".\"owned_by_user_id\" IS NOT NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_configs\".\"owned_by_user_id\" IS NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "agent_configs_agent_type_check": { + "name": "agent_configs_agent_type_check", + "value": "\"agent_configs\".\"agent_type\" IN ('code_review', 'auto_triage', 'auto_fix', 'security_scan')" + }, + "agent_configs_config_revision_check": { + "name": "agent_configs_config_revision_check", + "value": "\"agent_configs\".\"config_revision\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_agents": { + "name": "agent_environment_profile_agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_agents_profile_id": { + "name": "IDX_agent_env_profile_agents_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_agents", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_agents_profile_slug": { + "name": "UQ_agent_env_profile_agents_profile_slug", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_commands": { + "name": "agent_environment_profile_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_commands_profile_id": { + "name": "IDX_agent_env_profile_commands_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_commands_profile_sequence": { + "name": "UQ_agent_env_profile_commands_profile_sequence", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_kilo_commands": { + "name": "agent_environment_profile_kilo_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtask": { + "name": "subtask", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_kilo_cmds_profile_id": { + "name": "IDX_agent_env_profile_kilo_cmds_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_kilo_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_kilo_cmds_profile_name": { + "name": "UQ_agent_env_profile_kilo_cmds_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_mcp_servers": { + "name": "agent_environment_profile_mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_mcp_servers_profile_id": { + "name": "IDX_agent_env_profile_mcp_servers_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_mcp_servers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_mcp_servers_profile_name": { + "name": "UQ_agent_env_profile_mcp_servers_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_repo_bindings": { + "name": "agent_environment_profile_repo_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profile_repo_bindings_user": { + "name": "UQ_agent_env_profile_repo_bindings_user", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profile_repo_bindings_org": { + "name": "UQ_agent_env_profile_repo_bindings_org", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profile_repo_bindings_owner_check": { + "name": "agent_env_profile_repo_bindings_owner_check", + "value": "(\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_skills": { + "name": "agent_environment_profile_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_markdown": { + "name": "raw_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_skills_profile_id": { + "name": "IDX_agent_env_profile_skills_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_skills", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_skills_profile_name": { + "name": "UQ_agent_env_profile_skills_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_vars": { + "name": "agent_environment_profile_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_vars_profile_id": { + "name": "IDX_agent_env_profile_vars_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_vars", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_vars_profile_key": { + "name": "UQ_agent_env_profile_vars_profile_key", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profiles": { + "name": "agent_environment_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profiles_org_name": { + "name": "UQ_agent_env_profiles_org_name", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_name": { + "name": "UQ_agent_env_profiles_user_name", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_org_default": { + "name": "UQ_agent_env_profiles_org_default", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_default": { + "name": "UQ_agent_env_profiles_user_default", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_org_id": { + "name": "IDX_agent_env_profiles_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_user_id": { + "name": "IDX_agent_env_profiles_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_created_by_user_id": { + "name": "IDX_agent_env_profiles_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profiles_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profiles_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profiles_owner_check": { + "name": "agent_env_profiles_owner_check", + "value": "(\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.analytics_event_outbox": { + "name": "analytics_event_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "event_uuid": { + "name": "event_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_name": { + "name": "event_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "distinct_id": { + "name": "distinct_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_analytics_event_outbox_event_uuid": { + "name": "UQ_analytics_event_outbox_event_uuid", + "columns": [ + { + "expression": "event_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_analytics_event_outbox_status_next_attempt_at": { + "name": "IDX_analytics_event_outbox_status_next_attempt_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_kind": { + "name": "api_kind", + "schema": "", + "columns": { + "api_kind_id": { + "name": "api_kind_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_api_kind": { + "name": "UQ_api_kind", + "columns": [ + { + "expression": "api_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_log": { + "name": "api_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_request_id": { + "name": "vercel_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_api_request_log_created_at": { + "name": "idx_api_request_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_feedback": { + "name": "app_builder_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_status": { + "name": "preview_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_feedback_created_at": { + "name": "IDX_app_builder_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_kilo_user_id": { + "name": "IDX_app_builder_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_project_id": { + "name": "IDX_app_builder_feedback_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "app_builder_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "app_builder_feedback_project_id_app_builder_projects_id_fk": { + "name": "app_builder_feedback_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_project_sessions": { + "name": "app_builder_project_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "worker_version": { + "name": "worker_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'v2'" + } + }, + "indexes": { + "IDX_app_builder_project_sessions_project_id": { + "name": "IDX_app_builder_project_sessions_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_project_sessions_project_id_app_builder_projects_id_fk": { + "name": "app_builder_project_sessions_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_project_sessions", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_app_builder_project_sessions_cloud_agent_session_id": { + "name": "UQ_app_builder_project_sessions_cloud_agent_session_id", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_projects": { + "name": "app_builder_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "git_repo_full_name": { + "name": "git_repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_platform_integration_id": { + "name": "git_platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_projects_created_by_user_id": { + "name": "IDX_app_builder_projects_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_user_id": { + "name": "IDX_app_builder_projects_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_organization_id": { + "name": "IDX_app_builder_projects_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_created_at": { + "name": "IDX_app_builder_projects_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_last_message_at": { + "name": "IDX_app_builder_projects_last_message_at", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_git_repo_integration": { + "name": "IDX_app_builder_projects_git_repo_integration", + "columns": [ + { + "expression": "git_repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"app_builder_projects\".\"git_repo_full_name\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_projects_owned_by_user_id_kilocode_users_id_fk": { + "name": "app_builder_projects_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_owned_by_organization_id_organizations_id_fk": { + "name": "app_builder_projects_owned_by_organization_id_organizations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_deployment_id_deployments_id_fk": { + "name": "app_builder_projects_deployment_id_deployments_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk": { + "name": "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "platform_integrations", + "columnsFrom": [ + "git_platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "app_builder_projects_owner_check": { + "name": "app_builder_projects_owner_check", + "value": "(\n (\"app_builder_projects\".\"owned_by_user_id\" IS NOT NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NULL) OR\n (\"app_builder_projects\".\"owned_by_user_id\" IS NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.app_min_versions": { + "name": "app_min_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ios_min_version": { + "name": "ios_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "android_min_version": { + "name": "android_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_reported_messages": { + "name": "app_reported_messages", + "schema": "", + "columns": { + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "app_reported_messages_cli_session_id_cli_sessions_session_id_fk": { + "name": "app_reported_messages_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "app_reported_messages", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_fix_tickets": { + "name": "auto_fix_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "triage_ticket_id": { + "name": "triage_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'label'" + }, + "review_comment_id": { + "name": "review_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_comment_body": { + "name": "review_comment_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "diff_hunk": { + "name": "diff_hunk", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_ref": { + "name": "pr_head_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_branch": { + "name": "pr_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_fix_tickets_repo_issue": { + "name": "UQ_auto_fix_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"trigger_source\" = 'label'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_fix_tickets_repo_review_comment": { + "name": "UQ_auto_fix_tickets_repo_review_comment", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "review_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"review_comment_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_org": { + "name": "IDX_auto_fix_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_user": { + "name": "IDX_auto_fix_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_status": { + "name": "IDX_auto_fix_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_created_at": { + "name": "IDX_auto_fix_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_triage_ticket_id": { + "name": "IDX_auto_fix_tickets_triage_ticket_id", + "columns": [ + { + "expression": "triage_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_session_id": { + "name": "IDX_auto_fix_tickets_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_fix_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_fix_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "triage_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk": { + "name": "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_fix_tickets_owner_check": { + "name": "auto_fix_tickets_owner_check", + "value": "(\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_fix_tickets_status_check": { + "name": "auto_fix_tickets_status_check", + "value": "\"auto_fix_tickets\".\"status\" IN ('pending', 'running', 'completed', 'failed', 'cancelled')" + }, + "auto_fix_tickets_classification_check": { + "name": "auto_fix_tickets_classification_check", + "value": "\"auto_fix_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'unclear')" + }, + "auto_fix_tickets_confidence_check": { + "name": "auto_fix_tickets_confidence_check", + "value": "\"auto_fix_tickets\".\"confidence\" >= 0 AND \"auto_fix_tickets\".\"confidence\" <= 1" + }, + "auto_fix_tickets_trigger_source_check": { + "name": "auto_fix_tickets_trigger_source_check", + "value": "\"auto_fix_tickets\".\"trigger_source\" IN ('label', 'review_comment')" + } + }, + "isRLSEnabled": false + }, + "public.auto_model": { + "name": "auto_model", + "schema": "", + "columns": { + "auto_model_id": { + "name": "auto_model_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_auto_model": { + "name": "UQ_auto_model", + "columns": [ + { + "expression": "auto_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_top_up_configs": { + "name": "auto_top_up_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_method_id": { + "name": "stripe_payment_method_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "last_auto_top_up_at": { + "name": "last_auto_top_up_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_started_at": { + "name": "attempt_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_top_up_configs_owned_by_user_id": { + "name": "UQ_auto_top_up_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_top_up_configs_owned_by_organization_id": { + "name": "UQ_auto_top_up_configs_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "auto_top_up_configs_owned_by_organization_id_organizations_id_fk": { + "name": "auto_top_up_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_top_up_configs_exactly_one_owner": { + "name": "auto_top_up_configs_exactly_one_owner", + "value": "(\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NULL) OR (\"auto_top_up_configs\".\"owned_by_user_id\" IS NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.auto_triage_tickets": { + "name": "auto_triage_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_duplicate": { + "name": "is_duplicate", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duplicate_of_ticket_id": { + "name": "duplicate_of_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "similarity_score": { + "name": "similarity_score", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "qdrant_point_id": { + "name": "qdrant_point_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "should_auto_fix": { + "name": "should_auto_fix", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "action_taken": { + "name": "action_taken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_metadata": { + "name": "action_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_triage_tickets_repo_issue": { + "name": "UQ_auto_triage_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_org": { + "name": "IDX_auto_triage_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_user": { + "name": "IDX_auto_triage_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_status": { + "name": "IDX_auto_triage_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_created_at": { + "name": "IDX_auto_triage_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_qdrant_point_id": { + "name": "IDX_auto_triage_tickets_qdrant_point_id", + "columns": [ + { + "expression": "qdrant_point_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owner_status_created": { + "name": "IDX_auto_triage_tickets_owner_status_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_user_status_created": { + "name": "IDX_auto_triage_tickets_user_status_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_repo_classification": { + "name": "IDX_auto_triage_tickets_repo_classification", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "classification", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_triage_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_triage_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "duplicate_of_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_triage_tickets_owner_check": { + "name": "auto_triage_tickets_owner_check", + "value": "(\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_triage_tickets_issue_type_check": { + "name": "auto_triage_tickets_issue_type_check", + "value": "\"auto_triage_tickets\".\"issue_type\" IN ('issue', 'pull_request')" + }, + "auto_triage_tickets_classification_check": { + "name": "auto_triage_tickets_classification_check", + "value": "\"auto_triage_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'duplicate', 'unclear')" + }, + "auto_triage_tickets_confidence_check": { + "name": "auto_triage_tickets_confidence_check", + "value": "\"auto_triage_tickets\".\"confidence\" >= 0 AND \"auto_triage_tickets\".\"confidence\" <= 1" + }, + "auto_triage_tickets_similarity_score_check": { + "name": "auto_triage_tickets_similarity_score_check", + "value": "\"auto_triage_tickets\".\"similarity_score\" >= 0 AND \"auto_triage_tickets\".\"similarity_score\" <= 1" + }, + "auto_triage_tickets_status_check": { + "name": "auto_triage_tickets_status_check", + "value": "\"auto_triage_tickets\".\"status\" IN ('pending', 'analyzing', 'actioned', 'failed', 'skipped')" + }, + "auto_triage_tickets_action_taken_check": { + "name": "auto_triage_tickets_action_taken_check", + "value": "\"auto_triage_tickets\".\"action_taken\" IN ('pr_created', 'comment_posted', 'closed_duplicate', 'needs_clarification')" + } + }, + "isRLSEnabled": false + }, + "public.bot_request_cloud_agent_sessions": { + "name": "bot_request_cloud_agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "bot_request_id": { + "name": "bot_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spawn_group_id": { + "name": "spawn_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlab_project": { + "name": "gitlab_project", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_step": { + "name": "callback_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message": { + "name": "final_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message_fetched_at": { + "name": "final_message_fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "final_message_error": { + "name": "final_message_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "continuation_started_at": { + "name": "continuation_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_bot_request_cas_cloud_agent_session_id": { + "name": "UQ_bot_request_cas_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id": { + "name": "IDX_bot_request_cas_bot_request_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id_status": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id_status", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk": { + "name": "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk", + "tableFrom": "bot_request_cloud_agent_sessions", + "tableTo": "bot_requests", + "columnsFrom": [ + "bot_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_requests": { + "name": "bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_thread_id": { + "name": "platform_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_message_id": { + "name": "platform_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_bot_requests_created_at": { + "name": "IDX_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_created_by": { + "name": "IDX_bot_requests_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_organization_id": { + "name": "IDX_bot_requests_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_platform_integration_id": { + "name": "IDX_bot_requests_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_status": { + "name": "IDX_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_requests_created_by_kilocode_users_id_fk": { + "name": "bot_requests_created_by_kilocode_users_id_fk", + "tableFrom": "bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_organization_id_organizations_id_fk": { + "name": "bot_requests_organization_id_organizations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.byok_api_keys": { + "name": "byok_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "management_source": { + "name": "management_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_byok_api_keys_organization_id": { + "name": "IDX_byok_api_keys_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_kilo_user_id": { + "name": "IDX_byok_api_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_provider_id": { + "name": "IDX_byok_api_keys_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "byok_api_keys_organization_id_organizations_id_fk": { + "name": "byok_api_keys_organization_id_organizations_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "byok_api_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "byok_api_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_byok_api_keys_org_provider": { + "name": "UQ_byok_api_keys_org_provider", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "provider_id" + ] + }, + "UQ_byok_api_keys_user_provider": { + "name": "UQ_byok_api_keys_user_provider", + "nullsNotDistinct": false, + "columns": [ + "kilo_user_id", + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "byok_api_keys_management_source_check": { + "name": "byok_api_keys_management_source_check", + "value": "\"byok_api_keys\".\"management_source\" IN ('user', 'coding_plan')" + }, + "byok_api_keys_owner_check": { + "name": "byok_api_keys_owner_check", + "value": "(\n (\"byok_api_keys\".\"kilo_user_id\" IS NOT NULL AND \"byok_api_keys\".\"organization_id\" IS NULL) OR\n (\"byok_api_keys\".\"kilo_user_id\" IS NULL AND \"byok_api_keys\".\"organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cli_sessions": { + "name": "cli_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_mode": { + "name": "last_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_kilo_user_id": { + "name": "IDX_cli_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_created_at": { + "name": "IDX_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_updated_at": { + "name": "IDX_cli_sessions_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_organization_id": { + "name": "IDX_cli_sessions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_user_updated": { + "name": "IDX_cli_sessions_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_forked_from_cli_sessions_session_id_fk": { + "name": "cli_sessions_forked_from_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "forked_from" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_parent_session_id_cli_sessions_session_id_fk": { + "name": "cli_sessions_parent_session_id_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "parent_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_organization_id_organizations_id_fk": { + "name": "cli_sessions_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cli_sessions_cloud_agent_session_id_unique": { + "name": "cli_sessions_cloud_agent_session_id_unique", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_sessions_v2": { + "name": "cli_sessions_v2", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_scope_id": { + "name": "cloud_agent_session_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_updated_at": { + "name": "status_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_v2_parent_session_id_kilo_user_id": { + "name": "IDX_cli_sessions_v2_parent_session_id_kilo_user_id", + "columns": [ + { + "expression": "parent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_public_id": { + "name": "UQ_cli_sessions_v2_public_id", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"public_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_cloud_agent_session_id": { + "name": "UQ_cli_sessions_v2_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"cloud_agent_session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_organization_id": { + "name": "IDX_cli_sessions_v2_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_updated": { + "name": "IDX_cli_sessions_v2_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_created": { + "name": "IDX_cli_sessions_v2_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "cli_sessions_v2_git_url_branch_idx": { + "name": "cli_sessions_v2_git_url_branch_idx", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_v2_organization_id_organizations_id_fk": { + "name": "cli_sessions_v2_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_v2_parent_session_id_kilo_user_id_fk": { + "name": "cli_sessions_v2_parent_session_id_kilo_user_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "cli_sessions_v2", + "columnsFrom": [ + "parent_session_id", + "kilo_user_id" + ], + "columnsTo": [ + "session_id", + "kilo_user_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cli_sessions_v2_session_id_kilo_user_id_pk": { + "name": "cli_sessions_v2_session_id_kilo_user_id_pk", + "columns": [ + "session_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_code_review_attempts": { + "name": "cloud_agent_code_review_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retry_reason": { + "name": "retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analytics_enabled_at_dispatch": { + "name": "analytics_enabled_at_dispatch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_review_attempts_review_attempt_number": { + "name": "UQ_cloud_agent_code_review_attempts_review_attempt_number", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_code_review_id": { + "name": "idx_cloud_agent_code_review_attempts_code_review_id", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_session_id": { + "name": "idx_cloud_agent_code_review_attempts_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_cli_session_id": { + "name": "idx_cloud_agent_code_review_attempts_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_status": { + "name": "idx_cloud_agent_code_review_attempts_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_retry_reason": { + "name": "idx_cloud_agent_code_review_attempts_retry_reason", + "columns": [ + { + "expression": "retry_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "retry_of_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_review_attempts_attempt_number_check": { + "name": "cloud_agent_code_review_attempts_attempt_number_check", + "value": "\"cloud_agent_code_review_attempts\".\"attempt_number\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_code_reviews": { + "name": "cloud_agent_code_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "manual_config": { + "name": "manual_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "review_type": { + "name": "review_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "council_result": { + "name": "council_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author": { + "name": "pr_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author_github_id": { + "name": "pr_author_github_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_ref": { + "name": "head_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "dispatch_reservation_id": { + "name": "dispatch_reservation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'v1'" + }, + "check_run_id": { + "name": "check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_used": { + "name": "repository_review_instructions_used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "repository_review_instructions_ref": { + "name": "repository_review_instructions_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_truncated": { + "name": "repository_review_instructions_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previous_summary_body": { + "name": "previous_summary_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_summary_head_sha": { + "name": "previous_summary_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_tokens_in": { + "name": "total_tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens_out": { + "name": "total_tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cost_musd": { + "name": "total_cost_musd", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha": { + "name": "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"manual_config\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_code_reviews_active_provider_publisher": { + "name": "UQ_cloud_agent_code_reviews_active_provider_publisher", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"platform_integration_id\" IS NOT NULL\n AND \"cloud_agent_code_reviews\".\"status\" IN ('pending', 'queued', 'running')\n AND (\"cloud_agent_code_reviews\".\"manual_config\" IS NULL OR \"cloud_agent_code_reviews\".\"manual_config\"->>'outputMode' = 'provider')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_org_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_user_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_session_id": { + "name": "idx_cloud_agent_code_reviews_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_cli_session_id": { + "name": "idx_cloud_agent_code_reviews_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_status": { + "name": "idx_cloud_agent_code_reviews_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_repo": { + "name": "idx_cloud_agent_code_reviews_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_number": { + "name": "idx_cloud_agent_code_reviews_pr_number", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_created_at": { + "name": "idx_cloud_agent_code_reviews_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_author_github_id": { + "name": "idx_cloud_agent_code_reviews_pr_author_github_id", + "columns": [ + { + "expression": "pr_author_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk": { + "name": "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_reviews_owner_check": { + "name": "cloud_agent_code_reviews_owner_check", + "value": "(\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NULL) OR\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_feedback": { + "name": "cloud_agent_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_type": { + "name": "session_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cloud_agent_feedback_created_at": { + "name": "IDX_cloud_agent_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_kilo_user_id": { + "name": "IDX_cloud_agent_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_cloud_agent_session_id": { + "name": "IDX_cloud_agent_feedback_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "cloud_agent_feedback_organization_id_organizations_id_fk": { + "name": "cloud_agent_feedback_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_pending_uploads": { + "name": "cloud_agent_pending_uploads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_uuid": { + "name": "message_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_cloud_agent_pending_uploads_user_message_status": { + "name": "IDX_cloud_agent_pending_uploads_user_message_status", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_pending_uploads_expired": { + "name": "IDX_cloud_agent_pending_uploads_expired", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_pending_uploads\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cloud_agent_pending_uploads_object_key_unique": { + "name": "cloud_agent_pending_uploads_object_key_unique", + "nullsNotDistinct": false, + "columns": [ + "object_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "cloud_agent_pending_uploads_status_check": { + "name": "cloud_agent_pending_uploads_status_check", + "value": "\"cloud_agent_pending_uploads\".\"status\" IN ('pending', 'linked', 'reaped')" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_session_runs": { + "name": "cloud_agent_session_runs", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapper_run_id": { + "name": "wrapper_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dispatch_accepted_at": { + "name": "dispatch_accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_activity_observed_at": { + "name": "agent_activity_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_responsibility": { + "name": "failure_responsibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_cloud_agent_session_runs_wrapper_run_id": { + "name": "IDX_cloud_agent_session_runs_wrapper_run_id", + "columns": [ + { + "expression": "wrapper_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"wrapper_run_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_session_queued": { + "name": "IDX_cloud_agent_session_runs_session_queued", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_queued_at": { + "name": "IDX_cloud_agent_session_runs_queued_at", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_terminal_at": { + "name": "IDX_cloud_agent_session_runs_terminal_at", + "columns": [ + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_status_terminal": { + "name": "IDX_cloud_agent_session_runs_status_terminal", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_failure_terminal": { + "name": "IDX_cloud_agent_session_runs_failure_terminal", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_responsibility_reason_terminal": { + "name": "IDX_cloud_agent_session_runs_responsibility_reason_terminal", + "columns": [ + { + "expression": "failure_responsibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"status\" = 'failed'", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_error_expires_at": { + "name": "IDX_cloud_agent_session_runs_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk", + "tableFrom": "cloud_agent_session_runs", + "tableTo": "cloud_agent_sessions", + "columnsFrom": [ + "cloud_agent_session_id" + ], + "columnsTo": [ + "cloud_agent_session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk", + "columns": [ + "cloud_agent_session_id", + "message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_session_runs_status_check": { + "name": "cloud_agent_session_runs_status_check", + "value": "\"cloud_agent_session_runs\".\"status\" IN ('queued', 'accepted', 'completed', 'failed', 'interrupted')" + }, + "cloud_agent_session_runs_error_message_bounded_check": { + "name": "cloud_agent_session_runs_error_message_bounded_check", + "value": "\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_session_runs\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_session_runs_error_expiry_check": { + "name": "cloud_agent_session_runs_error_expiry_check", + "value": "(\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_session_runs\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_sessions": { + "name": "cloud_agent_sessions", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initial_message_id": { + "name": "initial_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "failure_at": { + "name": "failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_responsibility": { + "name": "failure_responsibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_cloud_agent_sessions_kilo_session_id": { + "name": "UQ_cloud_agent_sessions_kilo_session_id", + "columns": [ + { + "expression": "kilo_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_sessions_initial_message_id": { + "name": "UQ_cloud_agent_sessions_initial_message_id", + "columns": [ + { + "expression": "initial_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_sandbox_id": { + "name": "IDX_cloud_agent_sessions_sandbox_id", + "columns": [ + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"sandbox_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_created_at": { + "name": "IDX_cloud_agent_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_created": { + "name": "IDX_cloud_agent_sessions_failure_created", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_at": { + "name": "IDX_cloud_agent_sessions_failure_at", + "columns": [ + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_classification_at": { + "name": "IDX_cloud_agent_sessions_failure_classification_at", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_error_expires_at": { + "name": "IDX_cloud_agent_sessions_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_sessions_failure_classification_check": { + "name": "cloud_agent_sessions_failure_classification_check", + "value": "(\"cloud_agent_sessions\".\"failure_at\" IS NULL AND \"cloud_agent_sessions\".\"failure_stage\" IS NULL AND \"cloud_agent_sessions\".\"failure_code\" IS NULL) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'sandbox_identity' AND \"cloud_agent_sessions\".\"failure_code\" = 'sandbox_id_derivation_failed') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'registration' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_registration_rejected') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'initial_admission' AND \"cloud_agent_sessions\".\"failure_code\" IN ('initial_admission_rejected', 'initial_queue_full', 'invalid_initial_intent')) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'transport' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_rpc_outcome_unknown')" + }, + "cloud_agent_sessions_error_message_bounded_check": { + "name": "cloud_agent_sessions_error_message_bounded_check", + "value": "\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_sessions\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_sessions_error_expiry_check": { + "name": "cloud_agent_sessions_error_expiry_check", + "value": "(\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_sessions\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_webhook_triggers": { + "name": "cloud_agent_webhook_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cloud_agent'" + }, + "kiloclaw_instance_id": { + "name": "kiloclaw_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "activation_mode": { + "name": "activation_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'webhook'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_timezone": { + "name": "cron_timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'UTC'" + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_webhook_triggers_user_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_user_trigger", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_webhook_triggers_org_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_org_trigger", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_user": { + "name": "IDX_cloud_agent_webhook_triggers_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_org": { + "name": "IDX_cloud_agent_webhook_triggers_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_active": { + "name": "IDX_cloud_agent_webhook_triggers_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_profile": { + "name": "IDX_cloud_agent_webhook_triggers_profile", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_organization_id_organizations_id_fk": { + "name": "cloud_agent_webhook_triggers_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk": { + "name": "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "kiloclaw_instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk": { + "name": "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_cloud_agent_webhook_triggers_owner": { + "name": "CHK_cloud_agent_webhook_triggers_owner", + "value": "(\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NULL) OR\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_cloud_agent_fields": { + "name": "CHK_cloud_agent_webhook_triggers_cloud_agent_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'cloud_agent' OR\n (\"cloud_agent_webhook_triggers\".\"github_repo\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"profile_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_kiloclaw_fields": { + "name": "CHK_cloud_agent_webhook_triggers_kiloclaw_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'kiloclaw_chat' OR\n \"cloud_agent_webhook_triggers\".\"kiloclaw_instance_id\" IS NOT NULL\n )" + }, + "CHK_cloud_agent_webhook_triggers_scheduled_fields": { + "name": "CHK_cloud_agent_webhook_triggers_scheduled_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"activation_mode\" != 'scheduled' OR\n \"cloud_agent_webhook_triggers\".\"cron_expression\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_billing_sku": { + "name": "cloud_billing_sku", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "accepts_new_usage": { + "name": "accepts_new_usage", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cloud_billing_sku_created_by_user_id_kilocode_users_id_fk": { + "name": "cloud_billing_sku_created_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_billing_sku", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_billing_sku_id_format": { + "name": "cloud_billing_sku_id_format", + "value": "\"cloud_billing_sku\".\"id\" ~ '^[a-z0-9][a-z0-9-]{2,79}$'" + }, + "cloud_billing_sku_name_nonempty": { + "name": "cloud_billing_sku_name_nonempty", + "value": "length(btrim(\"cloud_billing_sku\".\"name\")) > 0" + }, + "cloud_billing_sku_rate_positive": { + "name": "cloud_billing_sku_rate_positive", + "value": "\"cloud_billing_sku\".\"rate_cents_per_unit\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.code_indexing_manifest": { + "name": "code_indexing_manifest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_lines": { + "name": "total_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_ai_lines": { + "name": "total_ai_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_manifest_organization_id": { + "name": "IDX_code_indexing_manifest_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_kilo_user_id": { + "name": "IDX_code_indexing_manifest_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_project_id": { + "name": "IDX_code_indexing_manifest_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_git_branch": { + "name": "IDX_code_indexing_manifest_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_created_at": { + "name": "IDX_code_indexing_manifest_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_manifest", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_indexing_manifest_org_user_project_hash_branch": { + "name": "UQ_code_indexing_manifest_org_user_project_hash_branch", + "nullsNotDistinct": true, + "columns": [ + "organization_id", + "kilo_user_id", + "project_id", + "file_path", + "git_branch" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_indexing_search": { + "name": "code_indexing_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_search_organization_id": { + "name": "IDX_code_indexing_search_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_kilo_user_id": { + "name": "IDX_code_indexing_search_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_project_id": { + "name": "IDX_code_indexing_search_project_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_created_at": { + "name": "IDX_code_indexing_search_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_search_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_search_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_search", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_review_analytics_findings": { + "name": "code_review_analytics_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "analytics_result_id": { + "name": "analytics_result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "security_class": { + "name": "security_class", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk": { + "name": "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk", + "tableFrom": "code_review_analytics_findings", + "tableTo": "code_review_analytics_results", + "columnsFrom": [ + "analytics_result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_findings_result_ordinal": { + "name": "UQ_code_review_analytics_findings_result_ordinal", + "nullsNotDistinct": false, + "columns": [ + "analytics_result_id", + "ordinal" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_findings_severity_check": { + "name": "code_review_analytics_findings_severity_check", + "value": "\"code_review_analytics_findings\".\"severity\" IN ('critical', 'warning', 'suggestion')" + }, + "code_review_analytics_findings_category_check": { + "name": "code_review_analytics_findings_category_check", + "value": "\"code_review_analytics_findings\".\"category\" IN ('security', 'correctness', 'reliability', 'data_integrity', 'performance', 'compatibility', 'maintainability', 'test_quality', 'documentation', 'accessibility', 'other')" + }, + "code_review_analytics_findings_security_class_check": { + "name": "code_review_analytics_findings_security_class_check", + "value": "\"code_review_analytics_findings\".\"security_class\" IN ('auth_access', 'injection', 'data_protection', 'request_resource_boundary', 'deserialization_object_integrity', 'dependency_supply_chain', 'memory_safety', 'availability', 'concurrency', 'security_configuration', 'other')" + }, + "code_review_analytics_findings_ordinal_check": { + "name": "code_review_analytics_findings_ordinal_check", + "value": "\"code_review_analytics_findings\".\"ordinal\" >= 0" + }, + "code_review_analytics_findings_security_class_presence_check": { + "name": "code_review_analytics_findings_security_class_presence_check", + "value": "(\n (\"code_review_analytics_findings\".\"category\" = 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NOT NULL) OR\n (\"code_review_analytics_findings\".\"category\" <> 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_analytics_results": { + "name": "code_review_analytics_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_attempt_id": { + "name": "source_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "capture_status": { + "name": "capture_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "taxonomy_version": { + "name": "taxonomy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_level": { + "name": "impact_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "complexity_level": { + "name": "complexity_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification_confidence": { + "name": "classification_confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finalized_at": { + "name": "finalized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_analytics_results_source_attempt_id": { + "name": "idx_code_review_analytics_results_source_attempt_id", + "columns": [ + { + "expression": "source_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_analytics_results_finalized_at": { + "name": "idx_code_review_analytics_results_finalized_at", + "columns": [ + { + "expression": "finalized_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "source_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_results_code_review_id": { + "name": "UQ_code_review_analytics_results_code_review_id", + "nullsNotDistinct": false, + "columns": [ + "code_review_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_results_capture_status_check": { + "name": "code_review_analytics_results_capture_status_check", + "value": "\"code_review_analytics_results\".\"capture_status\" IN ('captured', 'missing', 'invalid', 'omitted')" + }, + "code_review_analytics_results_change_type_check": { + "name": "code_review_analytics_results_change_type_check", + "value": "\"code_review_analytics_results\".\"change_type\" IN ('bug_fix', 'feature', 'refactor', 'maintenance', 'dependency', 'test', 'documentation', 'mixed', 'other')" + }, + "code_review_analytics_results_impact_level_check": { + "name": "code_review_analytics_results_impact_level_check", + "value": "\"code_review_analytics_results\".\"impact_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_complexity_level_check": { + "name": "code_review_analytics_results_complexity_level_check", + "value": "\"code_review_analytics_results\".\"complexity_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_confidence_check": { + "name": "code_review_analytics_results_classification_confidence_check", + "value": "\"code_review_analytics_results\".\"classification_confidence\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_presence_check": { + "name": "code_review_analytics_results_classification_presence_check", + "value": "(\n (\n \"code_review_analytics_results\".\"capture_status\" = 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NOT NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NOT NULL\n ) OR (\n \"code_review_analytics_results\".\"capture_status\" <> 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NULL\n )\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_feedback_events": { + "name": "code_review_feedback_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "kilo_comment_id": { + "name": "kilo_comment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_excerpt": { + "name": "reply_excerpt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_comment_excerpt": { + "name": "kilo_comment_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedupe_hash": { + "name": "dedupe_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_feedback_events_owned_by_org_id": { + "name": "idx_code_review_feedback_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_owned_by_user_id": { + "name": "idx_code_review_feedback_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_platform_repo": { + "name": "idx_code_review_feedback_events_platform_repo", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_created_at": { + "name": "idx_code_review_feedback_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_feedback_events_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_feedback_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_feedback_events_dedupe_hash": { + "name": "UQ_code_review_feedback_events_dedupe_hash", + "nullsNotDistinct": false, + "columns": [ + "dedupe_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_feedback_events_owner_check": { + "name": "code_review_feedback_events_owner_check", + "value": "(\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_memory_proposals": { + "name": "code_review_memory_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_markdown": { + "name": "proposed_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "positive_count": { + "name": "positive_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "negative_count": { + "name": "negative_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "neutral_count": { + "name": "neutral_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "change_request_url": { + "name": "change_request_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_memory_proposals_owned_by_org_id": { + "name": "idx_code_review_memory_proposals_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_owned_by_user_id": { + "name": "idx_code_review_memory_proposals_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_platform_repo_status": { + "name": "idx_code_review_memory_proposals_platform_repo_status", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_updated_at": { + "name": "idx_code_review_memory_proposals_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_org_active_scope": { + "name": "UQ_code_review_memory_proposals_org_active_scope", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_user_active_scope": { + "name": "UQ_code_review_memory_proposals_user_active_scope", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "code_review_memory_proposals_owner_check": { + "name": "code_review_memory_proposals_owner_check", + "value": "(\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_availability_intents": { + "name": "coding_plan_availability_intents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_availability_intents_user_plan": { + "name": "UQ_coding_plan_availability_intents_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_availability_intents_plan": { + "name": "IDX_coding_plan_availability_intents_plan", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_availability_intents_user_id_kilocode_users_id_fk": { + "name": "coding_plan_availability_intents_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_availability_intents", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coding_plan_key_inventory": { + "name": "coding_plan_key_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_plan_id": { + "name": "upstream_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_usage_id": { + "name": "upstream_usage_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_fingerprint": { + "name": "credential_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "assigned_to_user_id": { + "name": "assigned_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_requested_at": { + "name": "revocation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_attempt_count": { + "name": "revocation_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_revocation_error": { + "name": "last_revocation_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_key_inv_fingerprint": { + "name": "UQ_coding_plan_key_inv_fingerprint", + "columns": [ + { + "expression": "credential_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_coding_plan_key_inv_provider_usage_id": { + "name": "UQ_coding_plan_key_inv_provider_usage_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "upstream_usage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_key_inventory\".\"upstream_usage_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_plan_status": { + "name": "IDX_coding_plan_key_inv_plan_status", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_available": { + "name": "IDX_coding_plan_key_inv_available", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"coding_plan_key_inventory\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk": { + "name": "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_key_inventory", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_to_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_key_inventory_status_check": { + "name": "coding_plan_key_inventory_status_check", + "value": "\"coding_plan_key_inventory\".\"status\" IN ('available', 'assigned', 'revocation_pending', 'revoked', 'revocation_failed')" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_subscriptions": { + "name": "coding_plan_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_inventory_id": { + "name": "key_inventory_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "installed_byok_key_id": { + "name": "installed_byok_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "billing_period_days": { + "name": "billing_period_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "past_due_started_at": { + "name": "past_due_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_grace_expires_at": { + "name": "payment_grace_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_attempted_for_due": { + "name": "auto_top_up_attempted_for_due", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_sub_live_user_plan": { + "name": "UQ_coding_plan_sub_live_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_coding_plan_sub_live_user_provider": { + "name": "UQ_coding_plan_sub_live_user_provider", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_status": { + "name": "IDX_coding_plan_sub_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_renewal": { + "name": "IDX_coding_plan_sub_renewal", + "columns": [ + { + "expression": "credit_renewal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_inventory": { + "name": "IDX_coding_plan_sub_inventory", + "columns": [ + { + "expression": "key_inventory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_subscriptions_user_id_kilocode_users_id_fk": { + "name": "coding_plan_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk": { + "name": "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "coding_plan_key_inventory", + "columnsFrom": [ + "key_inventory_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk": { + "name": "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "byok_api_keys", + "columnsFrom": [ + "installed_byok_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_subscriptions_status_check": { + "name": "coding_plan_subscriptions_status_check", + "value": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due', 'canceled')" + }, + "coding_plan_subscriptions_live_access_check": { + "name": "coding_plan_subscriptions_live_access_check", + "value": "\"coding_plan_subscriptions\".\"status\" = 'canceled' OR \"coding_plan_subscriptions\".\"key_inventory_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_terms": { + "name": "coding_plan_terms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_terms_request": { + "name": "UQ_coding_plan_terms_request", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_terms_subscription": { + "name": "IDX_coding_plan_terms_subscription", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk": { + "name": "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "coding_plan_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_user_id_kilocode_users_id_fk": { + "name": "coding_plan_terms_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk": { + "name": "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_terms_kind_check": { + "name": "coding_plan_terms_kind_check", + "value": "\"coding_plan_terms\".\"kind\" IN ('activation', 'extension', 'renewal')" + } + }, + "isRLSEnabled": false + }, + "public.compute_usage_charge": { + "name": "compute_usage_charge", + "schema": "", + "columns": { + "usage_source": { + "name": "usage_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_source_id": { + "name": "usage_source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_billing_sku_id": { + "name": "cloud_billing_sku_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "settled_quantity_after": { + "name": "settled_quantity_after", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": false + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_compute_usage_charge_user_created": { + "name": "IDX_compute_usage_charge_user_created", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_compute_usage_charge_organization_created": { + "name": "IDX_compute_usage_charge_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "compute_usage_charge_user_id_kilocode_users_id_fk": { + "name": "compute_usage_charge_user_id_kilocode_users_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "compute_usage_charge_organization_id_organizations_id_fk": { + "name": "compute_usage_charge_organization_id_organizations_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "compute_usage_charge_cloud_billing_sku_id_cloud_billing_sku_id_fk": { + "name": "compute_usage_charge_cloud_billing_sku_id_cloud_billing_sku_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "cloud_billing_sku", + "columnsFrom": [ + "cloud_billing_sku_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "compute_usage_charge_usage_source_usage_source_id_created_at_pk": { + "name": "compute_usage_charge_usage_source_usage_source_id_created_at_pk", + "columns": [ + "usage_source", + "usage_source_id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "compute_usage_charge_exactly_one_payer": { + "name": "compute_usage_charge_exactly_one_payer", + "value": "(\"compute_usage_charge\".\"user_id\" IS NULL) <> (\"compute_usage_charge\".\"organization_id\" IS NULL)" + }, + "compute_usage_charge_quantity_positive": { + "name": "compute_usage_charge_quantity_positive", + "value": "\"compute_usage_charge\".\"quantity\" > 0" + }, + "compute_usage_charge_settled_quantity_positive": { + "name": "compute_usage_charge_settled_quantity_positive", + "value": "\"compute_usage_charge\".\"settled_quantity_after\" IS NULL OR \"compute_usage_charge\".\"settled_quantity_after\" > 0" + }, + "compute_usage_charge_rate_positive": { + "name": "compute_usage_charge_rate_positive", + "value": "\"compute_usage_charge\".\"rate_cents_per_unit\" > 0" + }, + "compute_usage_charge_amount_positive": { + "name": "compute_usage_charge_amount_positive", + "value": "\"compute_usage_charge\".\"amount_microdollars\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.container_usage_interval": { + "name": "container_usage_interval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_epoch_ms": { + "name": "start_epoch_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cloud_billing_sku_id": { + "name": "cloud_billing_sku_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_fingerprint": { + "name": "context_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_heartbeat_seq": { + "name": "last_heartbeat_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confirmed_seconds": { + "name": "confirmed_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_mode": { + "name": "billing_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'shadow'" + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": false + }, + "settled_billable_seconds": { + "name": "settled_billable_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_stop_seq": { + "name": "final_stop_seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_container_usage_interval_sweep": { + "name": "IDX_container_usage_interval_sweep", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_container_usage_interval_subject_started": { + "name": "IDX_container_usage_interval_subject_started", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_container_usage_interval_single_open": { + "name": "UQ_container_usage_interval_single_open", + "columns": [ + { + "expression": "service", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"container_usage_interval\".\"status\" = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_usage_interval_cloud_billing_sku_id_cloud_billing_sku_id_fk": { + "name": "container_usage_interval_cloud_billing_sku_id_cloud_billing_sku_id_fk", + "tableFrom": "container_usage_interval", + "tableTo": "cloud_billing_sku", + "columnsFrom": [ + "cloud_billing_sku_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "container_usage_interval_subject_type": { + "name": "container_usage_interval_subject_type", + "value": "\"container_usage_interval\".\"subject_type\" IN ('user', 'org')" + }, + "container_usage_interval_actor_type": { + "name": "container_usage_interval_actor_type", + "value": "\"container_usage_interval\".\"actor_type\" IN ('user', 'bot')" + }, + "container_usage_interval_context_fingerprint": { + "name": "container_usage_interval_context_fingerprint", + "value": "\"container_usage_interval\".\"context_fingerprint\" ~ '^[a-f0-9]{64}$'" + }, + "container_usage_interval_attribution": { + "name": "container_usage_interval_attribution", + "value": "\"container_usage_interval\".\"actor_type\" = 'bot' OR (\"container_usage_interval\".\"actor_type\" = 'user' AND (\"container_usage_interval\".\"subject_type\" <> 'user' OR \"container_usage_interval\".\"actor_id\" = \"container_usage_interval\".\"subject_id\"))" + }, + "container_usage_interval_status": { + "name": "container_usage_interval_status", + "value": "\"container_usage_interval\".\"status\" IN ('open', 'closed')" + }, + "container_usage_interval_billing_mode": { + "name": "container_usage_interval_billing_mode", + "value": "\"container_usage_interval\".\"billing_mode\" IN ('shadow', 'paid')" + }, + "container_usage_interval_paid_rate": { + "name": "container_usage_interval_paid_rate", + "value": "(\"container_usage_interval\".\"billing_mode\" = 'shadow' AND \"container_usage_interval\".\"rate_cents_per_unit\" IS NULL) OR (\"container_usage_interval\".\"billing_mode\" = 'paid' AND \"container_usage_interval\".\"rate_cents_per_unit\" > 0)" + }, + "container_usage_interval_open_closed_shape": { + "name": "container_usage_interval_open_closed_shape", + "value": "(\"container_usage_interval\".\"status\" = 'open' AND \"container_usage_interval\".\"stopped_at\" IS NULL AND \"container_usage_interval\".\"close_reason\" IS NULL) OR (\"container_usage_interval\".\"status\" = 'closed' AND \"container_usage_interval\".\"stopped_at\" IS NOT NULL AND \"container_usage_interval\".\"close_reason\" IS NOT NULL)" + }, + "container_usage_interval_time_order": { + "name": "container_usage_interval_time_order", + "value": "\"container_usage_interval\".\"last_seen_at\" >= \"container_usage_interval\".\"started_at\" AND (\"container_usage_interval\".\"stopped_at\" IS NULL OR (\"container_usage_interval\".\"stopped_at\" >= \"container_usage_interval\".\"started_at\" AND \"container_usage_interval\".\"stopped_at\" <= \"container_usage_interval\".\"last_seen_at\"))" + }, + "container_usage_interval_last_heartbeat_seq_nonnegative": { + "name": "container_usage_interval_last_heartbeat_seq_nonnegative", + "value": "\"container_usage_interval\".\"last_heartbeat_seq\" >= 0" + }, + "container_usage_interval_confirmed_seconds_nonnegative": { + "name": "container_usage_interval_confirmed_seconds_nonnegative", + "value": "\"container_usage_interval\".\"confirmed_seconds\" >= 0" + }, + "container_usage_interval_settled_billable_seconds_nonnegative": { + "name": "container_usage_interval_settled_billable_seconds_nonnegative", + "value": "\"container_usage_interval\".\"settled_billable_seconds\" >= 0 AND \"container_usage_interval\".\"settled_billable_seconds\" <= \"container_usage_interval\".\"confirmed_seconds\"" + }, + "container_usage_interval_final_stop_seq_positive": { + "name": "container_usage_interval_final_stop_seq_positive", + "value": "\"container_usage_interval\".\"final_stop_seq\" IS NULL OR \"container_usage_interval\".\"final_stop_seq\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.container_usage_segment": { + "name": "container_usage_segment", + "schema": "", + "columns": { + "interval_id": { + "name": "interval_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_seconds": { + "name": "reported_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "usage_seconds": { + "name": "usage_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_container_usage_segment_received": { + "name": "IDX_container_usage_segment_received", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_usage_segment_interval_id_container_usage_interval_id_fk": { + "name": "container_usage_segment_interval_id_container_usage_interval_id_fk", + "tableFrom": "container_usage_segment", + "tableTo": "container_usage_interval", + "columnsFrom": [ + "interval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "container_usage_segment_interval_id_seq_pk": { + "name": "container_usage_segment_interval_id_seq_pk", + "columns": [ + "interval_id", + "seq" + ] + } + }, + "uniqueConstraints": { + "container_usage_segment_idempotency_key_unique": { + "name": "container_usage_segment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "container_usage_segment_seq_positive": { + "name": "container_usage_segment_seq_positive", + "value": "\"container_usage_segment\".\"seq\" > 0" + }, + "container_usage_segment_reported_seconds_nonnegative": { + "name": "container_usage_segment_reported_seconds_nonnegative", + "value": "\"container_usage_segment\".\"reported_seconds\" >= 0" + }, + "container_usage_segment_usage_seconds_nonnegative": { + "name": "container_usage_segment_usage_seconds_nonnegative", + "value": "\"container_usage_segment\".\"usage_seconds\" >= 0" + }, + "container_usage_segment_usage_within_reported": { + "name": "container_usage_segment_usage_within_reported", + "value": "\"container_usage_segment\".\"usage_seconds\" <= \"container_usage_segment\".\"reported_seconds\"" + } + }, + "isRLSEnabled": false + }, + "public.content_moderation_reports": { + "name": "content_moderation_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "receipt_id": { + "name": "receipt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "triage_status": { + "name": "triage_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "appeal_status": { + "name": "appeal_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_content_moderation_reports_user_created": { + "name": "IDX_content_moderation_reports_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_content_moderation_reports_target": { + "name": "IDX_content_moderation_reports_target", + "columns": [ + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "content_moderation_reports_receipt_id_unique": { + "name": "content_moderation_reports_receipt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "receipt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_contributors": { + "name": "contributor_champion_contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_profile_url": { + "name": "github_profile_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "first_contribution_at": { + "name": "first_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_contribution_at": { + "name": "last_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "all_time_contributions": { + "name": "all_time_contributions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "manual_email": { + "name": "manual_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_contributors_last_contribution_at": { + "name": "IDX_contributor_champion_contributors_last_contribution_at", + "columns": [ + { + "expression": "last_contribution_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_contributors_manual_email": { + "name": "IDX_contributor_champion_contributors_manual_email", + "columns": [ + { + "expression": "manual_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_contributors_github_login": { + "name": "UQ_contributor_champion_contributors_github_login", + "nullsNotDistinct": false, + "columns": [ + "github_login" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_events": { + "name": "contributor_champion_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_number": { + "name": "github_pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "github_pr_url": { + "name": "github_pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_title": { + "name": "github_pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_login": { + "name": "github_author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_email": { + "name": "github_author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_events_contributor_id": { + "name": "IDX_contributor_champion_events_contributor_id", + "columns": [ + { + "expression": "contributor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_merged_at": { + "name": "IDX_contributor_champion_events_merged_at", + "columns": [ + { + "expression": "merged_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_author_email": { + "name": "IDX_contributor_champion_events_author_email", + "columns": [ + { + "expression": "github_author_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_events", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_events_repo_pr": { + "name": "UQ_contributor_champion_events_repo_pr", + "nullsNotDistinct": false, + "columns": [ + "repo_full_name", + "github_pr_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_memberships": { + "name": "contributor_champion_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selected_tier": { + "name": "selected_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_tier": { + "name": "enrolled_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_amount_microdollars": { + "name": "credit_amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credits_last_granted_at": { + "name": "credits_last_granted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "linked_kilo_user_id": { + "name": "linked_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_memberships_credits_due": { + "name": "IDX_contributor_champion_memberships_credits_due", + "columns": [ + { + "expression": "credits_last_granted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NOT NULL AND \"contributor_champion_memberships\".\"credit_amount_microdollars\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_memberships_linked_kilo_user_id": { + "name": "IDX_contributor_champion_memberships_linked_kilo_user_id", + "columns": [ + { + "expression": "linked_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk": { + "name": "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "kilocode_users", + "columnsFrom": [ + "linked_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_memberships_contributor_id": { + "name": "UQ_contributor_champion_memberships_contributor_id", + "nullsNotDistinct": false, + "columns": [ + "contributor_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contributor_champion_memberships_selected_tier_check": { + "name": "contributor_champion_memberships_selected_tier_check", + "value": "\"contributor_champion_memberships\".\"selected_tier\" IS NULL OR \"contributor_champion_memberships\".\"selected_tier\" IN ('contributor', 'ambassador', 'champion')" + }, + "contributor_champion_memberships_enrolled_tier_check": { + "name": "contributor_champion_memberships_enrolled_tier_check", + "value": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NULL OR \"contributor_champion_memberships\".\"enrolled_tier\" IN ('contributor', 'ambassador', 'champion')" + } + }, + "isRLSEnabled": false + }, + "public.contributor_champion_sync_state": { + "name": "contributor_champion_sync_state", + "schema": "", + "columns": { + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_merged_at": { + "name": "last_merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_campaigns": { + "name": "credit_campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_expiry_hours": { + "name": "credit_expiry_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_ends_at": { + "name": "campaign_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_redemptions_allowed": { + "name": "total_redemptions_allowed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_credit_campaigns_slug": { + "name": "UQ_credit_campaigns_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_credit_campaigns_credit_category": { + "name": "UQ_credit_campaigns_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credit_campaigns_slug_format_check": { + "name": "credit_campaigns_slug_format_check", + "value": "\"credit_campaigns\".\"slug\" ~ '^[a-z0-9-]{5,40}$'" + }, + "credit_campaigns_amount_positive_check": { + "name": "credit_campaigns_amount_positive_check", + "value": "\"credit_campaigns\".\"amount_microdollars\" > 0" + }, + "credit_campaigns_credit_expiry_hours_positive_check": { + "name": "credit_campaigns_credit_expiry_hours_positive_check", + "value": "\"credit_campaigns\".\"credit_expiry_hours\" IS NULL OR \"credit_campaigns\".\"credit_expiry_hours\" > 0" + }, + "credit_campaigns_total_redemptions_allowed_positive_check": { + "name": "credit_campaigns_total_redemptions_allowed_positive_check", + "value": "\"credit_campaigns\".\"total_redemptions_allowed\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.credit_transactions": { + "name": "credit_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expiration_baseline_microdollars_used": { + "name": "expiration_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "original_baseline_microdollars_used": { + "name": "original_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_transaction_id": { + "name": "original_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_id": { + "name": "stripe_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coinbase_credit_block_id": { + "name": "coinbase_credit_block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_date": { + "name": "expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "check_category_uniqueness": { + "name": "check_category_uniqueness", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_credit_transactions_created_at": { + "name": "IDX_credit_transactions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_is_free": { + "name": "IDX_credit_transactions_is_free", + "columns": [ + { + "expression": "is_free", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_kilo_user_id": { + "name": "IDX_credit_transactions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_credit_category": { + "name": "IDX_credit_transactions_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_stripe_payment_id": { + "name": "IDX_credit_transactions_stripe_payment_id", + "columns": [ + { + "expression": "stripe_payment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_original_transaction_id": { + "name": "IDX_credit_transactions_original_transaction_id", + "columns": [ + { + "expression": "original_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_coinbase_credit_block_id": { + "name": "IDX_credit_transactions_coinbase_credit_block_id", + "columns": [ + { + "expression": "coinbase_credit_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_organization_id": { + "name": "IDX_credit_transactions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_unique_category": { + "name": "IDX_credit_transactions_unique_category", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credit_transactions\".\"check_category_uniqueness\" = TRUE", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "credit_transactions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_llm2": { + "name": "custom_llm2", + "schema": "", + "columns": { + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deleted_user_email_tombstones": { + "name": "deleted_user_email_tombstones", + "schema": "", + "columns": { + "normalized_email_hash": { + "name": "normalized_email_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_builds": { + "name": "deployment_builds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_builds_deployment_id": { + "name": "idx_deployment_builds_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_builds_status": { + "name": "idx_deployment_builds_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_builds_deployment_id_deployments_id_fk": { + "name": "deployment_builds_deployment_id_deployments_id_fk", + "tableFrom": "deployment_builds", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_env_vars": { + "name": "deployment_env_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_env_vars_deployment_id": { + "name": "idx_deployment_env_vars_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_env_vars_deployment_id_deployments_id_fk": { + "name": "deployment_env_vars_deployment_id_deployments_id_fk", + "tableFrom": "deployment_env_vars", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployment_env_vars_deployment_key": { + "name": "UQ_deployment_env_vars_deployment_key", + "nullsNotDistinct": false, + "columns": [ + "deployment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_events": { + "name": "deployment_events", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'log'" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_deployment_events_build_id": { + "name": "idx_deployment_events_build_id", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_events_timestamp": { + "name": "idx_deployment_events_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_events_type": { + "name": "idx_deployment_events_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_events_build_id_deployment_builds_id_fk": { + "name": "deployment_events_build_id_deployment_builds_id_fk", + "tableFrom": "deployment_events", + "tableTo": "deployment_builds", + "columnsFrom": [ + "build_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_events_build_id_event_id_pk": { + "name": "deployment_events_build_id_event_id_pk", + "columns": [ + "build_id", + "event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_threat_detections": { + "name": "deployment_threat_detections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "build_id": { + "name": "build_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "threat_type": { + "name": "threat_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_threat_detections_deployment_id": { + "name": "idx_deployment_threat_detections_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_threat_detections_created_at": { + "name": "idx_deployment_threat_detections_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_threat_detections_deployment_id_deployments_id_fk": { + "name": "deployment_threat_detections_deployment_id_deployments_id_fk", + "tableFrom": "deployment_threat_detections", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_threat_detections_build_id_deployment_builds_id_fk": { + "name": "deployment_threat_detections_build_id_deployment_builds_id_fk", + "tableFrom": "deployment_threat_detections", + "tableTo": "deployment_builds", + "columnsFrom": [ + "build_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployments": { + "name": "deployments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deployment_slug": { + "name": "deployment_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_worker_name": { + "name": "internal_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_source": { + "name": "repository_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_url": { + "name": "deployment_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "git_auth_token": { + "name": "git_auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_deployed_at": { + "name": "last_deployed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_build_id": { + "name": "last_build_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "threat_status": { + "name": "threat_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_from": { + "name": "created_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_deployments_owned_by_user_id": { + "name": "idx_deployments_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_owned_by_organization_id": { + "name": "idx_deployments_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_platform_integration_id": { + "name": "idx_deployments_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_repository_source_branch": { + "name": "idx_deployments_repository_source_branch", + "columns": [ + { + "expression": "repository_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_threat_status_pending": { + "name": "idx_deployments_threat_status_pending", + "columns": [ + { + "expression": "threat_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"deployments\".\"threat_status\" = 'pending_scan'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_owned_by_user_id_kilocode_users_id_fk": { + "name": "deployments_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "deployments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployments_owned_by_organization_id_organizations_id_fk": { + "name": "deployments_owned_by_organization_id_organizations_id_fk", + "tableFrom": "deployments", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployments_deployment_slug": { + "name": "UQ_deployments_deployment_slug", + "nullsNotDistinct": false, + "columns": [ + "deployment_slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "deployments_owner_check": { + "name": "deployments_owner_check", + "value": "(\n (\"deployments\".\"owned_by_user_id\" IS NOT NULL AND \"deployments\".\"owned_by_organization_id\" IS NULL) OR\n (\"deployments\".\"owned_by_user_id\" IS NULL AND \"deployments\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "deployments_source_type_check": { + "name": "deployments_source_type_check", + "value": "\"deployments\".\"source_type\" IN ('github', 'git', 'app-builder')" + } + }, + "isRLSEnabled": false + }, + "public.deployments_ephemeral": { + "name": "deployments_ephemeral", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_worker_name": { + "name": "internal_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_slug": { + "name": "deployment_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_cleanup_at": { + "name": "next_cleanup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cleanup_claim_token": { + "name": "cleanup_claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cleanup_claimed_until": { + "name": "cleanup_claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployments_ephemeral_owned_by_user_id": { + "name": "idx_deployments_ephemeral_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_ephemeral_next_cleanup_at": { + "name": "idx_deployments_ephemeral_next_cleanup_at", + "columns": [ + { + "expression": "next_cleanup_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_ephemeral_owned_by_user_id_kilocode_users_id_fk": { + "name": "deployments_ephemeral_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "deployments_ephemeral", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployments_ephemeral_internal_worker_name": { + "name": "UQ_deployments_ephemeral_internal_worker_name", + "nullsNotDistinct": false, + "columns": [ + "internal_worker_name" + ] + }, + "UQ_deployments_ephemeral_deployment_slug": { + "name": "UQ_deployments_ephemeral_deployment_slug", + "nullsNotDistinct": false, + "columns": [ + "deployment_slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "deployments_ephemeral_source_type_check": { + "name": "deployments_ephemeral_source_type_check", + "value": "\"deployments_ephemeral\".\"source_type\" IN ('html')" + }, + "deployments_ephemeral_status_check": { + "name": "deployments_ephemeral_status_check", + "value": "\"deployments_ephemeral\".\"status\" IN ('pending', 'active', 'cleanup_retry')" + }, + "deployments_ephemeral_claim_fields_check": { + "name": "deployments_ephemeral_claim_fields_check", + "value": "(\"deployments_ephemeral\".\"cleanup_claim_token\" IS NULL) = (\"deployments_ephemeral\".\"cleanup_claimed_until\" IS NULL)" + }, + "deployments_ephemeral_active_fields_check": { + "name": "deployments_ephemeral_active_fields_check", + "value": "\"deployments_ephemeral\".\"status\" <> 'active' OR (\"deployments_ephemeral\".\"deployment_slug\" IS NOT NULL AND \"deployments_ephemeral\".\"expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.device_auth_requests": { + "name": "device_auth_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_device_auth_requests_code": { + "name": "UQ_device_auth_requests_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_status": { + "name": "IDX_device_auth_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_expires_at": { + "name": "IDX_device_auth_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_kilo_user_id": { + "name": "IDX_device_auth_requests_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_device_auth_requests_device_code_hash": { + "name": "UQ_device_auth_requests_device_code_hash", + "columns": [ + { + "expression": "device_code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"device_auth_requests\".\"device_code_hash\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_user_code": { + "name": "IDX_device_auth_requests_user_code", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"device_auth_requests\".\"user_code\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_auth_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "device_auth_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_auth_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_refresh_tokens": { + "name": "device_refresh_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_session_id": { + "name": "device_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_device_refresh_tokens_device_session_id": { + "name": "IDX_device_refresh_tokens_device_session_id", + "columns": [ + { + "expression": "device_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_refresh_tokens_expires_at": { + "name": "IDX_device_refresh_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_refresh_tokens_device_session_id_device_sessions_id_fk": { + "name": "device_refresh_tokens_device_session_id_device_sessions_id_fk", + "tableFrom": "device_refresh_tokens", + "tableTo": "device_sessions", + "columnsFrom": [ + "device_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_sessions": { + "name": "device_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_auth_request_id": { + "name": "device_auth_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_device_sessions_kilo_user_id": { + "name": "IDX_device_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_sessions_revoked_at": { + "name": "IDX_device_sessions_revoked_at", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "device_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord_gateway_listener": { + "name": "discord_gateway_listener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "listener_id": { + "name": "listener_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.editor_name": { + "name": "editor_name", + "schema": "", + "columns": { + "editor_name_id": { + "name": "editor_name_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_editor_name": { + "name": "UQ_editor_name", + "columns": [ + { + "expression": "editor_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.enrichment_data": { + "name": "enrichment_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_enrichment_data": { + "name": "github_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linkedin_enrichment_data": { + "name": "linkedin_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "clay_enrichment_data": { + "name": "clay_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_enrichment_data_user_id": { + "name": "IDX_enrichment_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "enrichment_data_user_id_kilocode_users_id_fk": { + "name": "enrichment_data_user_id_kilocode_users_id_fk", + "tableFrom": "enrichment_data", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_enrichment_data_user_id": { + "name": "UQ_enrichment_data_user_id", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_monthly_usage": { + "name": "exa_monthly_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "month": { + "name": "month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_charged_microdollars": { + "name": "total_charged_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "free_allowance_microdollars": { + "name": "free_allowance_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 10000000 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_monthly_usage_personal": { + "name": "idx_exa_monthly_usage_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_exa_monthly_usage_org": { + "name": "idx_exa_monthly_usage_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_usage_log": { + "name": "exa_usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "charged_to_balance": { + "name": "charged_to_balance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_usage_log_user_created": { + "name": "idx_exa_usage_log_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "exa_usage_log_id_created_at_pk": { + "name": "exa_usage_log_id_created_at_pk", + "columns": [ + "id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_side_effect_outbox": { + "name": "external_side_effect_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'send_org_invite_email'" + }, + "invitation_id": { + "name": "invitation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_external_side_effect_outbox_invitation_id": { + "name": "UQ_external_side_effect_outbox_invitation_id", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_external_side_effect_outbox_status_next_attempt_at": { + "name": "IDX_external_side_effect_outbox_status_next_attempt_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feature": { + "name": "feature", + "schema": "", + "columns": { + "feature_id": { + "name": "feature_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_feature": { + "name": "UQ_feature", + "columns": [ + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finish_reason": { + "name": "finish_reason", + "schema": "", + "columns": { + "finish_reason_id": { + "name": "finish_reason_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_finish_reason": { + "name": "UQ_finish_reason", + "columns": [ + { + "expression": "finish_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_model_usage": { + "name": "free_model_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_model_usage_ip_created_at": { + "name": "idx_free_model_usage_ip_created_at", + "columns": [ + { + "expression": "ip_address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_model_usage_created_at": { + "name": "idx_free_model_usage_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_branch_pull_requests": { + "name": "github_branch_pull_requests", + "schema": "", + "columns": { + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_state": { + "name": "pr_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_sha": { + "name": "pr_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_review_decision": { + "name": "pr_review_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_decision_pending": { + "name": "review_decision_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "review_decision_fetching_at": { + "name": "review_decision_fetching_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_last_synced_at": { + "name": "pr_last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_github_branch_prs_org": { + "name": "UQ_github_branch_prs_org", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_github_branch_prs_user": { + "name": "UQ_github_branch_prs_user", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_github_branch_prs_url_branch": { + "name": "IDX_github_branch_prs_url_branch", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk": { + "name": "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_branch_pull_requests_owner_check": { + "name": "github_branch_pull_requests_owner_check", + "value": "(\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NOT NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NULL) OR\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NOT NULL)\n )" + }, + "github_branch_pull_requests_review_decision_check": { + "name": "github_branch_pull_requests_review_decision_check", + "value": "\"github_branch_pull_requests\".\"pr_review_decision\" IS NULL OR \"github_branch_pull_requests\".\"pr_review_decision\" IN ('approved', 'changes_requested', 'review_required')" + } + }, + "isRLSEnabled": false + }, + "public.github_install_states": { + "name": "github_install_states", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_github_install_states_expires_at": { + "name": "IDX_github_install_states_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_install_states_kilo_user_id_kilocode_users_id_fk": { + "name": "github_install_states_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "github_install_states", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_install_states_owner_type_check": { + "name": "github_install_states_owner_type_check", + "value": "\"github_install_states\".\"owner_type\" IN ('org', 'user')" + } + }, + "isRLSEnabled": false + }, + "public.http_ip": { + "name": "http_ip", + "schema": "", + "columns": { + "http_ip_id": { + "name": "http_ip_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_ip": { + "name": "http_ip", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_ip": { + "name": "UQ_http_ip", + "columns": [ + { + "expression": "http_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.http_user_agent": { + "name": "http_user_agent", + "schema": "", + "columns": { + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_user_agent": { + "name": "UQ_http_user_agent", + "columns": [ + { + "expression": "http_user_agent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.impact_advocate_participants": { + "name": "impact_advocate_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_id": { + "name": "advocate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_account_id": { + "name": "advocate_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_referral_identifier": { + "name": "opaque_referral_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_state": { + "name": "registration_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_registration_attempt_at": { + "name": "last_registration_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_impact_advocate_participants_program_referral_identifier": { + "name": "UQ_impact_advocate_participants_program_referral_identifier", + "columns": [ + { + "expression": "program_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opaque_referral_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"impact_advocate_participants\".\"opaque_referral_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_participants_registration_state": { + "name": "IDX_impact_advocate_participants_registration_state", + "columns": [ + { + "expression": "registration_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_participants_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_participants_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_participants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_participants_program_user": { + "name": "UQ_impact_advocate_participants_program_user", + "nullsNotDistinct": false, + "columns": [ + "program_key", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_participants_program_key_check": { + "name": "impact_advocate_participants_program_key_check", + "value": "\"impact_advocate_participants\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_participants_registration_state_check": { + "name": "impact_advocate_participants_registration_state_check", + "value": "\"impact_advocate_participants\".\"registration_state\" IN ('pending', 'retrying', 'registered', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_registration_attempts": { + "name": "impact_advocate_registration_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_cookie_value": { + "name": "opaque_cookie_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cookie_value_length": { + "name": "cookie_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_advocate_registration_attempts_participant_id": { + "name": "IDX_impact_advocate_registration_attempts_participant_id", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_registration_attempts_delivery_state": { + "name": "IDX_impact_advocate_registration_attempts_delivery_state", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk": { + "name": "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk", + "tableFrom": "impact_advocate_registration_attempts", + "tableTo": "impact_advocate_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_registration_attempts_dedupe_key": { + "name": "UQ_impact_advocate_registration_attempts_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_registration_attempts_program_key_check": { + "name": "impact_advocate_registration_attempts_program_key_check", + "value": "\"impact_advocate_registration_attempts\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_registration_attempts_delivery_state_check": { + "name": "impact_advocate_registration_attempts_delivery_state_check", + "value": "\"impact_advocate_registration_attempts\".\"delivery_state\" IN ('queued', 'sending', 'succeeded', 'failed')" + }, + "impact_advocate_registration_attempts_cookie_value_length_non_negative_check": { + "name": "impact_advocate_registration_attempts_cookie_value_length_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"cookie_value_length\" >= 0" + }, + "impact_advocate_registration_attempts_attempt_count_non_negative_check": { + "name": "impact_advocate_registration_attempts_attempt_count_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_reward_redemptions": { + "name": "impact_advocate_reward_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "impact_reward_id": { + "name": "impact_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lookup_response_payload": { + "name": "lookup_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "redeem_response_payload": { + "name": "redeem_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_advocate_reward_redemptions_beneficiary_user_id": { + "name": "IDX_impact_advocate_reward_redemptions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_reward_redemptions_state": { + "name": "IDX_impact_advocate_reward_redemptions_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_reward_redemptions_reward_id": { + "name": "UQ_impact_advocate_reward_redemptions_reward_id", + "nullsNotDistinct": false, + "columns": [ + "reward_id" + ] + }, + "UQ_impact_advocate_reward_redemptions_dedupe_key": { + "name": "UQ_impact_advocate_reward_redemptions_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_reward_redemptions_state_check": { + "name": "impact_advocate_reward_redemptions_state_check", + "value": "\"impact_advocate_reward_redemptions\".\"state\" IN ('queued', 'retrying', 'redeemed', 'failed')" + }, + "impact_advocate_reward_redemptions_attempt_count_non_negative_check": { + "name": "impact_advocate_reward_redemptions_attempt_count_non_negative_check", + "value": "\"impact_advocate_reward_redemptions\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_attribution_touches": { + "name": "impact_attribution_touches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'kiloclaw'" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anonymous_id": { + "name": "anonymous_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touch_type": { + "name": "touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_tracking_value": { + "name": "opaque_tracking_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_value_length": { + "name": "tracking_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_tracking_value_accepted": { + "name": "is_tracking_value_accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rs_code": { + "name": "rs_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_share_medium": { + "name": "rs_share_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_engagement_medium": { + "name": "rs_engagement_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "im_ref": { + "name": "im_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_path": { + "name": "landing_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touched_at": { + "name": "touched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sale_attributed_at": { + "name": "sale_attributed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_attribution_touches_product_user_id": { + "name": "IDX_impact_attribution_touches_product_user_id", + "columns": [ + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_user_id": { + "name": "IDX_impact_attribution_touches_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_anonymous_id": { + "name": "IDX_impact_attribution_touches_anonymous_id", + "columns": [ + { + "expression": "anonymous_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_expires_at": { + "name": "IDX_impact_attribution_touches_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_sale_attributed_at": { + "name": "IDX_impact_attribution_touches_sale_attributed_at", + "columns": [ + { + "expression": "sale_attributed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_attribution_touches_user_id_kilocode_users_id_fk": { + "name": "impact_attribution_touches_user_id_kilocode_users_id_fk", + "tableFrom": "impact_attribution_touches", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_attribution_touches_dedupe_key": { + "name": "UQ_impact_attribution_touches_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_attribution_touches_product_check": { + "name": "impact_attribution_touches_product_check", + "value": "\"impact_attribution_touches\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_program_key_check": { + "name": "impact_attribution_touches_program_key_check", + "value": "\"impact_attribution_touches\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_touch_type_check": { + "name": "impact_attribution_touches_touch_type_check", + "value": "\"impact_attribution_touches\".\"touch_type\" IN ('affiliate', 'referral')" + }, + "impact_attribution_touches_provider_check": { + "name": "impact_attribution_touches_provider_check", + "value": "\"impact_attribution_touches\".\"provider\" IN ('impact_performance', 'impact_advocate')" + }, + "impact_attribution_touches_tracking_value_length_non_negative_check": { + "name": "impact_attribution_touches_tracking_value_length_non_negative_check", + "value": "\"impact_attribution_touches\".\"tracking_value_length\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_conversion_reports": { + "name": "impact_conversion_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_tracker_id": { + "name": "action_tracker_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order_id": { + "name": "order_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_conversion_reports_conversion_id": { + "name": "IDX_impact_conversion_reports_conversion_id", + "columns": [ + { + "expression": "conversion_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_conversion_reports_state": { + "name": "IDX_impact_conversion_reports_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_conversion_reports", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_conversion_reports_dedupe_key": { + "name": "UQ_impact_conversion_reports_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_conversion_reports_state_check": { + "name": "impact_conversion_reports_state_check", + "value": "\"impact_conversion_reports\".\"state\" IN ('queued', 'retrying', 'delivered', 'failed')" + }, + "impact_conversion_reports_attempt_count_non_negative_check": { + "name": "impact_conversion_reports_attempt_count_non_negative_check", + "value": "\"impact_conversion_reports\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_conversions": { + "name": "impact_referral_conversions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winning_touch_type": { + "name": "winning_touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credits'" + }, + "source_payment_id": { + "name": "source_payment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qualified": { + "name": "qualified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disqualification_reason": { + "name": "disqualification_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "converted_at": { + "name": "converted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_conversions_referee_user_id": { + "name": "IDX_impact_referral_conversions_referee_user_id", + "columns": [ + { + "expression": "referee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_conversions_referrer_user_id": { + "name": "IDX_impact_referral_conversions_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_conversions_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_conversions_product_payment_source": { + "name": "UQ_impact_referral_conversions_product_payment_source", + "nullsNotDistinct": false, + "columns": [ + "product", + "payment_provider", + "source_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_conversions_product_check": { + "name": "impact_referral_conversions_product_check", + "value": "\"impact_referral_conversions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_conversions_winning_touch_type_check": { + "name": "impact_referral_conversions_winning_touch_type_check", + "value": "\"impact_referral_conversions\".\"winning_touch_type\" IN ('referral', 'affiliate', 'none')" + }, + "impact_referral_conversions_payment_provider_check": { + "name": "impact_referral_conversions_payment_provider_check", + "value": "\"impact_referral_conversions\".\"payment_provider\" IN ('stripe', 'credits', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_applications": { + "name": "impact_referral_reward_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "previous_renewal_boundary": { + "name": "previous_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "new_renewal_boundary": { + "name": "new_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "local_operation_id": { + "name": "local_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_operation_id": { + "name": "stripe_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_idempotency_key": { + "name": "stripe_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_applications_reward_id": { + "name": "IDX_impact_referral_reward_applications_reward_id", + "columns": [ + { + "expression": "reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_reward_applications_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_applications_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_applications_product_check": { + "name": "impact_referral_reward_applications_product_check", + "value": "\"impact_referral_reward_applications\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_decisions": { + "name": "impact_referral_reward_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_decisions_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_decisions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_reward_decisions_conversion_role": { + "name": "UQ_impact_referral_reward_decisions_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_decisions_product_check": { + "name": "impact_referral_reward_decisions_product_check", + "value": "\"impact_referral_reward_decisions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_reward_decisions_beneficiary_role_check": { + "name": "impact_referral_reward_decisions_beneficiary_role_check", + "value": "\"impact_referral_reward_decisions\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_reward_decisions_outcome_check": { + "name": "impact_referral_reward_decisions_outcome_check", + "value": "\"impact_referral_reward_decisions\".\"outcome\" IN ('granted', 'cap_limited', 'disqualified')" + }, + "impact_referral_reward_decisions_reward_kind_check": { + "name": "impact_referral_reward_decisions_reward_kind_check", + "value": "\"impact_referral_reward_decisions\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_reward_decisions_months_granted_non_negative_check": { + "name": "impact_referral_reward_decisions_months_granted_non_negative_check", + "value": "\"impact_referral_reward_decisions\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_rewards": { + "name": "impact_referral_rewards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applies_to_subscription_id": { + "name": "applies_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applies_to_kilo_pass_subscription_id": { + "name": "applies_to_kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_id": { + "name": "consumed_kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_item_id": { + "name": "consumed_kilo_pass_issuance_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "earned_at": { + "name": "earned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reversed_at": { + "name": "reversed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_reason": { + "name": "review_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_rewards_beneficiary_user_id": { + "name": "IDX_impact_referral_rewards_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_rewards_status": { + "name": "IDX_impact_referral_rewards_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk": { + "name": "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_reward_decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_subscription": { + "name": "FK_impact_referral_rewards_kilo_pass_subscription", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "applies_to_kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "consumed_kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance_item": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance_item", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuance_items", + "columnsFrom": [ + "consumed_kilo_pass_issuance_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_rewards_conversion_role": { + "name": "UQ_impact_referral_rewards_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + }, + "UQ_impact_referral_rewards_decision_id": { + "name": "UQ_impact_referral_rewards_decision_id", + "nullsNotDistinct": false, + "columns": [ + "decision_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_rewards_product_check": { + "name": "impact_referral_rewards_product_check", + "value": "\"impact_referral_rewards\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_rewards_beneficiary_role_check": { + "name": "impact_referral_rewards_beneficiary_role_check", + "value": "\"impact_referral_rewards\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_rewards_reward_kind_check": { + "name": "impact_referral_rewards_reward_kind_check", + "value": "\"impact_referral_rewards\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_rewards_status_check": { + "name": "impact_referral_rewards_status_check", + "value": "\"impact_referral_rewards\".\"status\" IN ('pending', 'earned', 'applied', 'reversed', 'expired', 'canceled', 'review_required')" + }, + "impact_referral_rewards_months_granted_non_negative_check": { + "name": "impact_referral_rewards_months_granted_non_negative_check", + "value": "\"impact_referral_rewards\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referrals": { + "name": "impact_referrals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "impact_referral_id": { + "name": "impact_referral_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referrals_referrer_user_id": { + "name": "IDX_impact_referrals_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referrals_source_touch_id": { + "name": "IDX_impact_referrals_source_touch_id", + "columns": [ + { + "expression": "source_touch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referrals_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referrals_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referrals_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referrals_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referrals_product_referee_user_id": { + "name": "UQ_impact_referrals_product_referee_user_id", + "nullsNotDistinct": false, + "columns": [ + "product", + "referee_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referrals_product_check": { + "name": "impact_referrals_product_check", + "value": "\"impact_referrals\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.ja4_digest": { + "name": "ja4_digest", + "schema": "", + "columns": { + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "ja4_digest": { + "name": "ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_ja4_digest": { + "name": "UQ_ja4_digest", + "columns": [ + { + "expression": "ja4_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_audit_log": { + "name": "kilo_pass_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_credit_transaction_id": { + "name": "related_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_monthly_issuance_id": { + "name": "related_monthly_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_kilo_pass_audit_log_created_at": { + "name": "IDX_kilo_pass_audit_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_user_id": { + "name": "IDX_kilo_pass_audit_log_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_pass_subscription_id": { + "name": "IDX_kilo_pass_audit_log_kilo_pass_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_action": { + "name": "IDX_kilo_pass_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_result": { + "name": "IDX_kilo_pass_audit_log_result", + "columns": [ + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_idempotency_key": { + "name": "IDX_kilo_pass_audit_log_idempotency_key", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_event_id": { + "name": "IDX_kilo_pass_audit_log_stripe_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_invoice_id": { + "name": "IDX_kilo_pass_audit_log_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_subscription_id": { + "name": "IDX_kilo_pass_audit_log_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_credit_transaction_id": { + "name": "IDX_kilo_pass_audit_log_related_credit_transaction_id", + "columns": [ + { + "expression": "related_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_monthly_issuance_id": { + "name": "IDX_kilo_pass_audit_log_related_monthly_issuance_id", + "columns": [ + { + "expression": "related_monthly_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "credit_transactions", + "columnsFrom": [ + "related_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "related_monthly_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_audit_log_action_check": { + "name": "kilo_pass_audit_log_action_check", + "value": "\"kilo_pass_audit_log\".\"action\" IN ('stripe_webhook_received', 'kilo_pass_invoice_paid_handled', 'store_purchase_completed', 'store_notification_received', 'store_subscription_renewed', 'store_subscription_canceled', 'store_subscription_expired', 'store_subscription_refunded', 'base_credits_issued', 'bonus_credits_issued', 'bonus_credits_skipped_idempotent', 'first_month_50pct_promo_issued', 'yearly_monthly_base_cron_started', 'yearly_monthly_base_cron_completed', 'issue_yearly_remaining_credits', 'duplicate_card_subscription_canceled', 'yearly_monthly_bonus_cron_started', 'yearly_monthly_bonus_cron_completed')" + }, + "kilo_pass_audit_log_result_check": { + "name": "kilo_pass_audit_log_result_check", + "value": "\"kilo_pass_audit_log\".\"result\" IN ('success', 'skipped_idempotent', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuance_items": { + "name": "kilo_pass_issuance_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_issuance_id": { + "name": "kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "bonus_percent_applied": { + "name": "bonus_percent_applied", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_issuance_items_issuance_id": { + "name": "IDX_kilo_pass_issuance_items_issuance_id", + "columns": [ + { + "expression": "kilo_pass_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuance_items_credit_transaction_id": { + "name": "IDX_kilo_pass_issuance_items_credit_transaction_id", + "columns": [ + { + "expression": "credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_issuance_items_credit_transaction_id_unique": { + "name": "kilo_pass_issuance_items_credit_transaction_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credit_transaction_id" + ] + }, + "UQ_kilo_pass_issuance_items_issuance_kind": { + "name": "UQ_kilo_pass_issuance_items_issuance_kind", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_issuance_id", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuance_items_bonus_percent_applied_range_check": { + "name": "kilo_pass_issuance_items_bonus_percent_applied_range_check", + "value": "\"kilo_pass_issuance_items\".\"bonus_percent_applied\" IS NULL OR (\"kilo_pass_issuance_items\".\"bonus_percent_applied\" >= 0 AND \"kilo_pass_issuance_items\".\"bonus_percent_applied\" <= 1)" + }, + "kilo_pass_issuance_items_amount_usd_non_negative_check": { + "name": "kilo_pass_issuance_items_amount_usd_non_negative_check", + "value": "\"kilo_pass_issuance_items\".\"amount_usd\" >= 0" + }, + "kilo_pass_issuance_items_kind_check": { + "name": "kilo_pass_issuance_items_kind_check", + "value": "\"kilo_pass_issuance_items\".\"kind\" IN ('base', 'bonus', 'promo_first_month_50pct', 'referral_bonus')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuances": { + "name": "kilo_pass_issuances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_month": { + "name": "issue_month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initial_welcome_promo_eligibility_reason": { + "name": "initial_welcome_promo_eligibility_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_issuances_stripe_invoice_id": { + "name": "UQ_kilo_pass_issuances_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_issuances\".\"stripe_invoice_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_subscription_id": { + "name": "IDX_kilo_pass_issuances_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_issue_month": { + "name": "IDX_kilo_pass_issuances_issue_month", + "columns": [ + { + "expression": "issue_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_issuances", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_issuances_subscription_issue_month": { + "name": "UQ_kilo_pass_issuances_subscription_issue_month", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_subscription_id", + "issue_month" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuances_issue_month_day_one_check": { + "name": "kilo_pass_issuances_issue_month_day_one_check", + "value": "EXTRACT(DAY FROM \"kilo_pass_issuances\".\"issue_month\") = 1" + }, + "kilo_pass_issuances_source_check": { + "name": "kilo_pass_issuances_source_check", + "value": "\"kilo_pass_issuances\".\"source\" IN ('stripe_invoice', 'app_store_transaction', 'google_play_transaction', 'cron')" + }, + "kilo_pass_issuances_initial_welcome_promo_reason_check": { + "name": "kilo_pass_issuances_initial_welcome_promo_reason_check", + "value": "\"kilo_pass_issuances\".\"initial_welcome_promo_eligibility_reason\" IN ('first_payment_fingerprint_claim', 'fingerprint_previously_claimed', 'missing_fingerprint', 'no_supported_fingerprint', 'no_positive_settlement', 'settlement_unresolved')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_agreements": { + "name": "kilo_pass_org_agreements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "term_version_id": { + "name": "term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processing_condition": { + "name": "processing_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "purchase_channel": { + "name": "purchase_channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_pass_capacity": { + "name": "purchased_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "next_purchased_pass_capacity": { + "name": "next_purchased_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_capacity_effective_at": { + "name": "next_capacity_effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_from": { + "name": "paid_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_until": { + "name": "paid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issuance_anchor_at": { + "name": "issuance_anchor_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_seat_add_on_item_id": { + "name": "provider_seat_add_on_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activation_provider_event_id": { + "name": "activation_provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_contract_id": { + "name": "external_contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_review_required_at": { + "name": "payment_review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_effective_at": { + "name": "cancellation_effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manually_issued_through": { + "name": "manually_issued_through", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_agreements_one_non_ended_parent": { + "name": "UQ_kilo_pass_org_agreements_one_non_ended_parent", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_provider_subscription": { + "name": "UQ_kilo_pass_org_agreements_provider_subscription", + "columns": [ + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"provider_subscription_id\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_provider_seat_add_on_item": { + "name": "UQ_kilo_pass_org_agreements_provider_seat_add_on_item", + "columns": [ + { + "expression": "provider_seat_add_on_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"provider_seat_add_on_item_id\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_external_contract": { + "name": "UQ_kilo_pass_org_agreements_external_contract", + "columns": [ + { + "expression": "external_contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"external_contract_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_activation_provider_event": { + "name": "UQ_kilo_pass_org_agreements_activation_provider_event", + "columns": [ + { + "expression": "activation_provider_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"activation_provider_event_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_agreements_processing": { + "name": "IDX_kilo_pass_org_agreements_processing", + "columns": [ + { + "expression": "processing_condition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_agreements_parent_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_agreements_parent_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_agreements", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_agreements_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_agreements_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_agreements", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_agreements_purchased_capacity_non_negative_check": { + "name": "kilo_pass_org_agreements_purchased_capacity_non_negative_check", + "value": "\"kilo_pass_org_agreements\".\"purchased_pass_capacity\" >= 0" + }, + "kilo_pass_org_agreements_next_capacity_check": { + "name": "kilo_pass_org_agreements_next_capacity_check", + "value": "(\"kilo_pass_org_agreements\".\"next_purchased_pass_capacity\" IS NULL AND \"kilo_pass_org_agreements\".\"next_capacity_effective_at\" IS NULL) OR (\"kilo_pass_org_agreements\".\"next_purchased_pass_capacity\" >= 0 AND \"kilo_pass_org_agreements\".\"next_capacity_effective_at\" IS NOT NULL)" + }, + "kilo_pass_org_agreements_paid_interval_check": { + "name": "kilo_pass_org_agreements_paid_interval_check", + "value": "(\"kilo_pass_org_agreements\".\"paid_from\" IS NULL AND \"kilo_pass_org_agreements\".\"paid_until\" IS NULL) OR (\"kilo_pass_org_agreements\".\"paid_from\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"paid_until\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"paid_from\" < \"kilo_pass_org_agreements\".\"paid_until\")" + }, + "kilo_pass_org_agreements_state_check": { + "name": "kilo_pass_org_agreements_state_check", + "value": "\"kilo_pass_org_agreements\".\"state\" IN ('pending_payment', 'active', 'cancel_at_period_end', 'ended')" + }, + "kilo_pass_org_agreements_processing_condition_check": { + "name": "kilo_pass_org_agreements_processing_condition_check", + "value": "\"kilo_pass_org_agreements\".\"processing_condition\" IN ('ready', 'manual', 'blocked', 'overallocated', 'failed', 'suspended_for_review')" + }, + "kilo_pass_org_agreements_purchase_channel_check": { + "name": "kilo_pass_org_agreements_purchase_channel_check", + "value": "\"kilo_pass_org_agreements\".\"purchase_channel\" IN ('self_serve', 'manual')" + }, + "kilo_pass_org_agreements_cadence_check": { + "name": "kilo_pass_org_agreements_cadence_check", + "value": "\"kilo_pass_org_agreements\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_allocation_plan_rows": { + "name": "kilo_pass_org_allocation_plan_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "allocation_plan_id": { + "name": "allocation_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pass_capacity": { + "name": "pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_allocation_plan_rows_positive_container": { + "name": "IDX_kilo_pass_org_allocation_plan_rows_positive_container", + "columns": [ + { + "expression": "allocation_container_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kilo_pass_org_allocation_plan_rows\".\"pass_capacity\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_allocation_plan_rows_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk": { + "name": "kilo_pass_org_allocation_plan_rows_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk", + "tableFrom": "kilo_pass_org_allocation_plan_rows", + "tableTo": "kilo_pass_org_allocation_plans", + "columnsFrom": [ + "allocation_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_allocation_plan_rows_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_allocation_plan_rows_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_allocation_plan_rows", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_allocation_plan_rows_plan_container": { + "name": "UQ_kilo_pass_org_allocation_plan_rows_plan_container", + "nullsNotDistinct": false, + "columns": [ + "allocation_plan_id", + "allocation_container_organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_allocation_plan_rows_capacity_non_negative_check": { + "name": "kilo_pass_org_allocation_plan_rows_capacity_non_negative_check", + "value": "\"kilo_pass_org_allocation_plan_rows\".\"pass_capacity\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_allocation_plans": { + "name": "kilo_pass_org_allocation_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effective_window_start": { + "name": "effective_window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_allocation_plans_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_allocation_plans_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_allocation_plans", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_allocation_plans_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_allocation_plans_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_allocation_plans", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_allocation_plans_agreement_window": { + "name": "UQ_kilo_pass_org_allocation_plans_agreement_window", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "effective_window_start" + ] + }, + "UQ_kilo_pass_org_allocation_plans_agreement_version": { + "name": "UQ_kilo_pass_org_allocation_plans_agreement_version", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "version" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_allocation_plans_version_positive_check": { + "name": "kilo_pass_org_allocation_plans_version_positive_check", + "value": "\"kilo_pass_org_allocation_plans\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_audit_records": { + "name": "kilo_pass_org_audit_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_json": { + "name": "before_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_json": { + "name": "after_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_audit_records_idempotency": { + "name": "UQ_kilo_pass_org_audit_records_idempotency", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_audit_records\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_audit_records_agreement_created": { + "name": "IDX_kilo_pass_org_audit_records_agreement_created", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_audit_records_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_audit_records_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_audit_records", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_audit_records_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_audit_records_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_audit_records", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_org_issuance_snapshots": { + "name": "kilo_pass_org_issuance_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "allocation_plan_id": { + "name": "allocation_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "term_version_id": { + "name": "term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "qualifying_spend_starts_at": { + "name": "qualifying_spend_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tranche_key": { + "name": "tranche_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allocated_pass_capacity": { + "name": "allocated_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "base_credit_microdollars": { + "name": "base_credit_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_credit_microdollars": { + "name": "bonus_credit_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unlock_spend_microdollars": { + "name": "unlock_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "qualifying_spend_microdollars": { + "name": "qualifying_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bonus_mode": { + "name": "bonus_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bonus_unlocked_at": { + "name": "bonus_unlocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "repair_completed_at": { + "name": "repair_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_transaction_id": { + "name": "bonus_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "base_credit_transaction_id": { + "name": "base_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_issuance_snapshots_base_credit_transaction": { + "name": "UQ_kilo_pass_org_issuance_snapshots_base_credit_transaction", + "columns": [ + { + "expression": "base_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_issuance_snapshots\".\"base_credit_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_issuance_snapshots_bonus_credit_transaction": { + "name": "UQ_kilo_pass_org_issuance_snapshots_bonus_credit_transaction", + "columns": [ + { + "expression": "bonus_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_issuance_snapshots\".\"bonus_credit_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_issuance_snapshots_window": { + "name": "IDX_kilo_pass_org_issuance_snapshots_window", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_issuance_snapshots_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_processing_run_id_kilo_pass_org_processing_runs_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_processing_run_id_kilo_pass_org_processing_runs_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_processing_runs", + "columnsFrom": [ + "processing_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_allocation_plans", + "columnsFrom": [ + "allocation_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_bonus_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_bonus_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "credit_transactions", + "columnsFrom": [ + "bonus_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_base_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_base_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "credit_transactions", + "columnsFrom": [ + "base_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_issuance_snapshots_container_window_tranche": { + "name": "UQ_kilo_pass_org_issuance_snapshots_container_window_tranche", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "allocation_container_organization_id", + "window_start", + "tranche_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_issuance_snapshots_window_check": { + "name": "kilo_pass_org_issuance_snapshots_window_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"window_start\" < \"kilo_pass_org_issuance_snapshots\".\"window_end\"" + }, + "kilo_pass_org_issuance_snapshots_qualifying_spend_window_check": { + "name": "kilo_pass_org_issuance_snapshots_qualifying_spend_window_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"window_start\" <= \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_starts_at\" AND \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_starts_at\" < \"kilo_pass_org_issuance_snapshots\".\"window_end\"" + }, + "kilo_pass_org_issuance_snapshots_values_non_negative_check": { + "name": "kilo_pass_org_issuance_snapshots_values_non_negative_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"allocated_pass_capacity\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"base_credit_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"bonus_credit_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"unlock_spend_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_microdollars\" >= 0" + }, + "kilo_pass_org_issuance_snapshots_kind_check": { + "name": "kilo_pass_org_issuance_snapshots_kind_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"kind\" IN ('regular', 'bridge', 'supplement')" + }, + "kilo_pass_org_issuance_snapshots_bonus_mode_check": { + "name": "kilo_pass_org_issuance_snapshots_bonus_mode_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"bonus_mode\" IN ('after_base', 'upfront')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_notification_deliveries": { + "name": "kilo_pass_org_notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_kilo_user_id": { + "name": "recipient_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_notification_deliveries_status": { + "name": "IDX_kilo_pass_org_notification_deliveries_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_notification_deliveries_processing_run_id_kilo_pass_org_processing_runs_id_fk": { + "name": "kilo_pass_org_notification_deliveries_processing_run_id_kilo_pass_org_processing_runs_id_fk", + "tableFrom": "kilo_pass_org_notification_deliveries", + "tableTo": "kilo_pass_org_processing_runs", + "columnsFrom": [ + "processing_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_notification_deliveries_recipient_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_notification_deliveries_recipient_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_notification_deliveries", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_notification_deliveries_run_recipient": { + "name": "UQ_kilo_pass_org_notification_deliveries_run_recipient", + "nullsNotDistinct": false, + "columns": [ + "processing_run_id", + "recipient_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_notification_deliveries_status_check": { + "name": "kilo_pass_org_notification_deliveries_status_check", + "value": "\"kilo_pass_org_notification_deliveries\".\"status\" IN ('pending', 'sending', 'sent', 'failed')" + }, + "kilo_pass_org_notification_deliveries_attempt_count_check": { + "name": "kilo_pass_org_notification_deliveries_attempt_count_check", + "value": "\"kilo_pass_org_notification_deliveries\".\"attempt_count\" >= 0" + }, + "kilo_pass_org_notification_deliveries_sent_check": { + "name": "kilo_pass_org_notification_deliveries_sent_check", + "value": "(\"kilo_pass_org_notification_deliveries\".\"status\" = 'sent' AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NOT NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NULL) OR (\"kilo_pass_org_notification_deliveries\".\"status\" = 'sending' AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NOT NULL) OR (\"kilo_pass_org_notification_deliveries\".\"status\" IN ('pending', 'failed') AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_processing_runs": { + "name": "kilo_pass_org_processing_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_processing_runs_state_lease": { + "name": "IDX_kilo_pass_org_processing_runs_state_lease", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_processing_runs_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_processing_runs_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_processing_runs", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_processing_runs_agreement_window": { + "name": "UQ_kilo_pass_org_processing_runs_agreement_window", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "window_start" + ] + }, + "UQ_kilo_pass_org_processing_runs_idempotency": { + "name": "UQ_kilo_pass_org_processing_runs_idempotency", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_processing_runs_window_check": { + "name": "kilo_pass_org_processing_runs_window_check", + "value": "\"kilo_pass_org_processing_runs\".\"window_start\" < \"kilo_pass_org_processing_runs\".\"window_end\"" + }, + "kilo_pass_org_processing_runs_attempt_count_non_negative_check": { + "name": "kilo_pass_org_processing_runs_attempt_count_non_negative_check", + "value": "\"kilo_pass_org_processing_runs\".\"attempt_count\" >= 0" + }, + "kilo_pass_org_processing_runs_state_check": { + "name": "kilo_pass_org_processing_runs_state_check", + "value": "\"kilo_pass_org_processing_runs\".\"state\" IN ('pending', 'running', 'succeeded', 'blocked', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_qualifying_spend_events": { + "name": "kilo_pass_org_qualifying_spend_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "issuance_snapshot_id": { + "name": "issuance_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spent_microdollars": { + "name": "spent_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_qualifying_spend_events_snapshot_occurred": { + "name": "IDX_kilo_pass_org_qualifying_spend_events_snapshot_occurred", + "columns": [ + { + "expression": "issuance_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_qualifying_spend_events_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "kilo_pass_org_issuance_snapshots", + "columnsFrom": [ + "issuance_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_qualifying_spend_events_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_qualifying_spend_events_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_qualifying_spend_events_snapshot_credit_transaction": { + "name": "UQ_kilo_pass_org_qualifying_spend_events_snapshot_credit_transaction", + "nullsNotDistinct": false, + "columns": [ + "issuance_snapshot_id", + "credit_transaction_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_qualifying_spend_events_amount_positive_check": { + "name": "kilo_pass_org_qualifying_spend_events_amount_positive_check", + "value": "\"kilo_pass_org_qualifying_spend_events\".\"spent_microdollars\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_supplements": { + "name": "kilo_pass_org_supplements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "issuance_snapshot_id": { + "name": "issuance_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_invoice_line_id": { + "name": "provider_invoice_line_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remaining_service_numerator": { + "name": "remaining_service_numerator", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "remaining_service_denominator": { + "name": "remaining_service_denominator", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_supplements_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk": { + "name": "kilo_pass_org_supplements_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk", + "tableFrom": "kilo_pass_org_supplements", + "tableTo": "kilo_pass_org_issuance_snapshots", + "columnsFrom": [ + "issuance_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_supplements_provider_invoice_line": { + "name": "UQ_kilo_pass_org_supplements_provider_invoice_line", + "nullsNotDistinct": false, + "columns": [ + "provider_invoice_line_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_supplements_ratio_check": { + "name": "kilo_pass_org_supplements_ratio_check", + "value": "\"kilo_pass_org_supplements\".\"remaining_service_numerator\" > 0 AND \"kilo_pass_org_supplements\".\"remaining_service_denominator\" > 0 AND \"kilo_pass_org_supplements\".\"remaining_service_numerator\" <= \"kilo_pass_org_supplements\".\"remaining_service_denominator\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_term_transitions": { + "name": "kilo_pass_org_term_transitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_term_version_id": { + "name": "from_term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_term_version_id": { + "name": "to_term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_term_transitions_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_term_transitions_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_from_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_term_transitions_from_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "from_term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_to_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_term_transitions_to_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "to_term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_term_transitions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_term_transitions_agreement_effective": { + "name": "UQ_kilo_pass_org_term_transitions_agreement_effective", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "effective_at" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_term_transitions_changes_version_check": { + "name": "kilo_pass_org_term_transitions_changes_version_check", + "value": "\"kilo_pass_org_term_transitions\".\"from_term_version_id\" <> \"kilo_pass_org_term_transitions\".\"to_term_version_id\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_term_versions": { + "name": "kilo_pass_org_term_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "version_key": { + "name": "version_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_microdollars_per_pass": { + "name": "billing_price_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "base_credit_microdollars_per_pass": { + "name": "base_credit_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_credit_microdollars_per_pass": { + "name": "bonus_credit_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unlock_spend_microdollars_per_pass": { + "name": "unlock_spend_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_mode": { + "name": "bonus_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_term_versions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_term_versions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_term_versions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_term_versions_version_key": { + "name": "UQ_kilo_pass_org_term_versions_version_key", + "nullsNotDistinct": false, + "columns": [ + "version_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_term_versions_amounts_non_negative_check": { + "name": "kilo_pass_org_term_versions_amounts_non_negative_check", + "value": "\"kilo_pass_org_term_versions\".\"billing_price_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"base_credit_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"bonus_credit_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"unlock_spend_microdollars_per_pass\" >= 0" + }, + "kilo_pass_org_term_versions_tier_check": { + "name": "kilo_pass_org_term_versions_tier_check", + "value": "\"kilo_pass_org_term_versions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_org_term_versions_cadence_check": { + "name": "kilo_pass_org_term_versions_cadence_check", + "value": "\"kilo_pass_org_term_versions\".\"cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_org_term_versions_bonus_mode_check": { + "name": "kilo_pass_org_term_versions_bonus_mode_check", + "value": "\"kilo_pass_org_term_versions\".\"bonus_mode\" IN ('after_base', 'upfront')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_pause_events": { + "name": "kilo_pass_pause_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resumes_at": { + "name": "resumes_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resumed_at": { + "name": "resumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_pause_events_subscription_id": { + "name": "IDX_kilo_pass_pause_events_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_pause_events_one_open_per_sub": { + "name": "UQ_kilo_pass_pause_events_one_open_per_sub", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_pause_events", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_pause_events_resumed_at_after_paused_at_check": { + "name": "kilo_pass_pause_events_resumed_at_after_paused_at_check", + "value": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL OR \"kilo_pass_pause_events\".\"resumed_at\" >= \"kilo_pass_pause_events\".\"paused_at\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_scheduled_changes": { + "name": "kilo_pass_scheduled_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_tier": { + "name": "from_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_cadence": { + "name": "from_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_tier": { + "name": "to_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_cadence": { + "name": "to_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_scheduled_changes_kilo_user_id": { + "name": "IDX_kilo_pass_scheduled_changes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_status": { + "name": "IDX_kilo_pass_scheduled_changes_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_stripe_subscription_id": { + "name": "IDX_kilo_pass_scheduled_changes_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id": { + "name": "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_scheduled_changes\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_effective_at": { + "name": "IDX_kilo_pass_scheduled_changes_effective_at", + "columns": [ + { + "expression": "effective_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_deleted_at": { + "name": "IDX_kilo_pass_scheduled_changes_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk": { + "name": "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "stripe_subscription_id" + ], + "columnsTo": [ + "stripe_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_scheduled_changes_from_tier_check": { + "name": "kilo_pass_scheduled_changes_from_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_from_cadence_check": { + "name": "kilo_pass_scheduled_changes_from_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_to_tier_check": { + "name": "kilo_pass_scheduled_changes_to_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_to_cadence_check": { + "name": "kilo_pass_scheduled_changes_to_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_status_check": { + "name": "kilo_pass_scheduled_changes_status_check", + "value": "\"kilo_pass_scheduled_changes\".\"status\" IN ('not_started', 'active', 'completed', 'released', 'canceled')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_events": { + "name": "kilo_pass_store_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_store_events_provider_event": { + "name": "UQ_kilo_pass_store_events_provider_event", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_provider_subscription": { + "name": "IDX_kilo_pass_store_events_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_app_account_token": { + "name": "IDX_kilo_pass_store_events_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_events_payment_provider_check": { + "name": "kilo_pass_store_events_payment_provider_check", + "value": "\"kilo_pass_store_events\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_purchases": { + "name": "kilo_pass_store_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_original_transaction_id": { + "name": "provider_original_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purchase_token": { + "name": "purchase_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_at": { + "name": "purchased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "raw_payload_json": { + "name": "raw_payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_store_purchases_provider_transaction": { + "name": "UQ_kilo_pass_store_purchases_provider_transaction", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_subscription_id": { + "name": "IDX_kilo_pass_store_purchases_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_user_id": { + "name": "IDX_kilo_pass_store_purchases_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_app_account_token": { + "name": "IDX_kilo_pass_store_purchases_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_latest_subscription_purchase": { + "name": "IDX_kilo_pass_store_purchases_latest_subscription_purchase", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchased_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_kilo_pass_store_purchases_subscription_owner_provider": { + "name": "FK_kilo_pass_store_purchases_subscription_owner_provider", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "columnsTo": [ + "id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_purchases_store_provider_check": { + "name": "kilo_pass_store_purchases_store_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('app_store', 'google_play')" + }, + "kilo_pass_store_purchases_payment_provider_check": { + "name": "kilo_pass_store_purchases_payment_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_subscriptions": { + "name": "kilo_pass_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stripe'" + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_streak_months": { + "name": "current_streak_months", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_yearly_issue_at": { + "name": "next_yearly_issue_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_subscriptions_kilo_user_id": { + "name": "IDX_kilo_pass_subscriptions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_payment_provider": { + "name": "IDX_kilo_pass_subscriptions_payment_provider", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_status": { + "name": "IDX_kilo_pass_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_cadence": { + "name": "IDX_kilo_pass_subscriptions_cadence", + "columns": [ + { + "expression": "cadence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_provider_subscription": { + "name": "UQ_kilo_pass_subscriptions_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_store_purchase_reference": { + "name": "UQ_kilo_pass_subscriptions_store_purchase_reference", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_subscriptions_stripe_subscription_id_unique": { + "name": "kilo_pass_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_subscriptions_current_streak_months_non_negative_check": { + "name": "kilo_pass_subscriptions_current_streak_months_non_negative_check", + "value": "\"kilo_pass_subscriptions\".\"current_streak_months\" >= 0" + }, + "kilo_pass_subscriptions_provider_ids_check": { + "name": "kilo_pass_subscriptions_provider_ids_check", + "value": "(\n \"kilo_pass_subscriptions\".\"payment_provider\" = 'stripe'\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" = \"kilo_pass_subscriptions\".\"stripe_subscription_id\"\n ) OR (\n \"kilo_pass_subscriptions\".\"payment_provider\" IN ('app_store', 'google_play')\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NULL\n )" + }, + "kilo_pass_subscriptions_payment_provider_check": { + "name": "kilo_pass_subscriptions_payment_provider_check", + "value": "\"kilo_pass_subscriptions\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + }, + "kilo_pass_subscriptions_tier_check": { + "name": "kilo_pass_subscriptions_tier_check", + "value": "\"kilo_pass_subscriptions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_subscriptions_cadence_check": { + "name": "kilo_pass_subscriptions_cadence_check", + "value": "\"kilo_pass_subscriptions\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_welcome_promo_payment_fingerprint_claims": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims", + "schema": "", + "columns": { + "stripe_payment_method_type": { + "name": "stripe_payment_method_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_stripe_invoice_id": { + "name": "source_stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk", + "columns": [ + "stripe_payment_method_type", + "stripe_fingerprint" + ] + } + }, + "uniqueConstraints": { + "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id": { + "name": "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id", + "nullsNotDistinct": false, + "columns": [ + "source_stripe_invoice_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check", + "value": "\"kilo_pass_welcome_promo_payment_fingerprint_claims\".\"stripe_payment_method_type\" IN ('card', 'sepa_debit', 'us_bank_account', 'bacs_debit', 'au_becs_debit')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_access_codes": { + "name": "kiloclaw_access_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_access_codes_code": { + "name": "UQ_kiloclaw_access_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_access_codes_user_status": { + "name": "IDX_kiloclaw_access_codes_user_status", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_access_codes_one_active_per_user": { + "name": "UQ_kiloclaw_access_codes_one_active_per_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_access_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_admin_audit_logs": { + "name": "kiloclaw_admin_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_admin_audit_logs_target_user_id": { + "name": "IDX_kiloclaw_admin_audit_logs_target_user_id", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_action": { + "name": "IDX_kiloclaw_admin_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_created_at": { + "name": "IDX_kiloclaw_admin_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_cli_runs": { + "name": "kiloclaw_cli_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "initiated_by_admin_id": { + "name": "initiated_by_admin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_cli_runs_user_id": { + "name": "IDX_kiloclaw_cli_runs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_started_at": { + "name": "IDX_kiloclaw_cli_runs_started_at", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_instance_id": { + "name": "IDX_kiloclaw_cli_runs_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_cli_runs_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "initiated_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_earlybird_purchases": { + "name": "kiloclaw_earlybird_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manual_payment_id": { + "name": "manual_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_earlybird_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_earlybird_purchases_user_id_unique": { + "name": "kiloclaw_earlybird_purchases_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "kiloclaw_earlybird_purchases_stripe_charge_id_unique": { + "name": "kiloclaw_earlybird_purchases_stripe_charge_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_charge_id" + ] + }, + "kiloclaw_earlybird_purchases_manual_payment_id_unique": { + "name": "kiloclaw_earlybird_purchases_manual_payment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "manual_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_email_log": { + "name": "kiloclaw_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "'epoch'" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_email_log_user_type_global": { + "name": "UQ_kiloclaw_email_log_user_type_global", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_email_log_user_instance_type_period": { + "name": "UQ_kiloclaw_email_log_user_instance_type_period", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_email_log_type_sent_instance": { + "name": "IDX_kiloclaw_email_log_type_sent_instance", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_email_log_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_google_oauth_connections": { + "name": "kiloclaw_google_oauth_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'google'" + }, + "account_email": { + "name": "account_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_subject": { + "name": "account_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_profile": { + "name": "credential_profile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kilo_owned'" + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "grants_by_source": { + "name": "grants_by_source", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_google_oauth_connections_instance": { + "name": "UQ_kiloclaw_google_oauth_connections_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_status": { + "name": "IDX_kiloclaw_google_oauth_connections_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_provider": { + "name": "IDX_kiloclaw_google_oauth_connections_provider", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_google_oauth_connections", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_google_oauth_connections_status_check": { + "name": "kiloclaw_google_oauth_connections_status_check", + "value": "\"kiloclaw_google_oauth_connections\".\"status\" IN ('active', 'action_required', 'disconnected')" + }, + "kiloclaw_google_oauth_connections_credential_profile_check": { + "name": "kiloclaw_google_oauth_connections_credential_profile_check", + "value": "\"kiloclaw_google_oauth_connections\".\"credential_profile\" IN ('legacy', 'kilo_owned')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_image_catalog": { + "name": "kiloclaw_image_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_digest": { + "name": "image_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rollout_percent": { + "name": "rollout_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_latest": { + "name": "is_latest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kiloclaw_image_catalog_status": { + "name": "IDX_kiloclaw_image_catalog_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_image_catalog_variant": { + "name": "IDX_kiloclaw_image_catalog_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_latest_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_latest_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_candidate_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_candidate_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = false AND \"kiloclaw_image_catalog\".\"rollout_percent\" > 0 AND \"kiloclaw_image_catalog\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_image_catalog_image_tag_unique": { + "name": "kiloclaw_image_catalog_image_tag_unique", + "nullsNotDistinct": false, + "columns": [ + "image_tag" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_aliases": { + "name": "kiloclaw_inbound_email_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_inbound_email_aliases_instance_id": { + "name": "IDX_kiloclaw_inbound_email_aliases_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_inbound_email_aliases_active_instance": { + "name": "UQ_kiloclaw_inbound_email_aliases_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_inbound_email_aliases\".\"retired_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_inbound_email_aliases", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_reserved_aliases": { + "name": "kiloclaw_inbound_email_reserved_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_instances": { + "name": "kiloclaw_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fly'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbound_email_enabled": { + "name": "inbound_email_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inactive_trial_stopped_at": { + "name": "inactive_trial_stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracked_image_tag": { + "name": "tracked_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instance_type": { + "name": "instance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_size_override": { + "name": "admin_size_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_instances_active": { + "name": "UQ_kiloclaw_instances_active", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_personal_by_user": { + "name": "IDX_kiloclaw_instances_active_personal_by_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_user_org": { + "name": "IDX_kiloclaw_instances_active_org_by_user_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_org_created": { + "name": "IDX_kiloclaw_instances_active_org_by_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_user_id_created_at": { + "name": "IDX_kiloclaw_instances_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_tracked_image_tag": { + "name": "IDX_kiloclaw_instances_tracked_image_tag", + "columns": [ + { + "expression": "tracked_image_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_instance_type": { + "name": "IDX_kiloclaw_instances_instance_type", + "columns": [ + { + "expression": "instance_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_admin_size_override": { + "name": "IDX_kiloclaw_instances_admin_size_override", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"admin_size_override\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_instances_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_instances_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_instances_organization_id_organizations_id_fk": { + "name": "kiloclaw_instances_organization_id_organizations_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_kiloclaw_instances_instance_type": { + "name": "CHK_kiloclaw_instances_instance_type", + "value": "\"kiloclaw_instances\".\"instance_type\" IS NULL OR \"kiloclaw_instances\".\"instance_type\" IN ('perf-1-3', 'perf-4-8', 'perf-4-16', 'shared-2-3', 'shared-2-4', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_morning_briefing_configs": { + "name": "kiloclaw_morning_briefing_configs", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0 7 * * *'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "interest_topics": { + "name": "interest_topics", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_morning_briefing_configs_enabled": { + "name": "IDX_kiloclaw_morning_briefing_configs_enabled", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_morning_briefing_configs\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_morning_briefing_configs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_notifications": { + "name": "kiloclaw_scheduled_action_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'notice'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel": { + "name": "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_notifications_pending": { + "name": "IDX_kiloclaw_scheduled_action_notifications_pending", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk": { + "name": "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk", + "tableFrom": "kiloclaw_scheduled_action_notifications", + "tableTo": "kiloclaw_scheduled_action_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_stages": { + "name": "kiloclaw_scheduled_action_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_index": { + "name": "stage_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "notice_sent_at": { + "name": "notice_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_stages_parent_index": { + "name": "UQ_kiloclaw_scheduled_action_stages_parent_index", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_stages_notice_due": { + "name": "IDX_kiloclaw_scheduled_action_stages_notice_due", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_stages\".\"notice_sent_at\" IS NULL AND \"kiloclaw_scheduled_action_stages\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_stages", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_targets": { + "name": "kiloclaw_scheduled_action_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_image_tag": { + "name": "source_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_targets_parent_instance": { + "name": "UQ_kiloclaw_scheduled_action_targets_parent_instance", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_stage": { + "name": "IDX_kiloclaw_scheduled_action_targets_stage", + "columns": [ + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_pending_by_instance": { + "name": "IDX_kiloclaw_scheduled_action_targets_pending_by_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_targets\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk": { + "name": "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_action_stages", + "columnsFrom": [ + "stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_actions": { + "name": "kiloclaw_scheduled_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_pins": { + "name": "override_pins", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notice_lead_hours": { + "name": "notice_lead_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "notice_subject": { + "name": "notice_subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "notice_body": { + "name": "notice_body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_count": { + "name": "total_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "IDX_kiloclaw_scheduled_actions_status": { + "name": "IDX_kiloclaw_scheduled_actions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_action_type": { + "name": "IDX_kiloclaw_scheduled_actions_action_type", + "columns": [ + { + "expression": "action_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_created_by": { + "name": "IDX_kiloclaw_scheduled_actions_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "target_image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_subscription_change_log": { + "name": "kiloclaw_subscription_change_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_subscription_change_log_subscription_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_subscription_created_at", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscription_change_log_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscription_change_log", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscription_change_log_actor_type_check": { + "name": "kiloclaw_subscription_change_log_actor_type_check", + "value": "\"kiloclaw_subscription_change_log\".\"actor_type\" IN ('user', 'system')" + }, + "kiloclaw_subscription_change_log_action_check": { + "name": "kiloclaw_subscription_change_log_action_check", + "value": "\"kiloclaw_subscription_change_log\".\"action\" IN ('created', 'status_changed', 'plan_switched', 'period_advanced', 'canceled', 'reactivated', 'suspended', 'destruction_scheduled', 'reassigned', 'backfilled', 'payment_source_changed', 'schedule_changed', 'admin_override')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_subscriptions": { + "name": "kiloclaw_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transferred_to_subscription_id": { + "name": "transferred_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "access_origin": { + "name": "access_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_source": { + "name": "payment_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kiloclaw_price_version": { + "name": "kiloclaw_price_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_plan": { + "name": "scheduled_plan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_by": { + "name": "scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pending_conversion": { + "name": "pending_conversion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trial_started_at": { + "name": "trial_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "commit_ends_at": { + "name": "commit_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "destruction_deadline": { + "name": "destruction_deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_requested_at": { + "name": "auto_resume_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_retry_after": { + "name": "auto_resume_retry_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_attempt_count": { + "name": "auto_resume_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_top_up_triggered_for_period": { + "name": "auto_top_up_triggered_for_period", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_subscriptions_status": { + "name": "IDX_kiloclaw_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_id": { + "name": "IDX_kiloclaw_subscriptions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_status": { + "name": "IDX_kiloclaw_subscriptions_user_status", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_price_version": { + "name": "IDX_kiloclaw_subscriptions_price_version", + "columns": [ + { + "expression": "kiloclaw_price_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_transferred_to": { + "name": "IDX_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_stripe_schedule_id": { + "name": "IDX_kiloclaw_subscriptions_stripe_schedule_id", + "columns": [ + { + "expression": "stripe_schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_auto_resume_retry_after": { + "name": "IDX_kiloclaw_subscriptions_auto_resume_retry_after", + "columns": [ + { + "expression": "auto_resume_retry_after", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_instance": { + "name": "UQ_kiloclaw_subscriptions_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_transferred_to": { + "name": "UQ_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"transferred_to_subscription_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_earlybird_origin": { + "name": "IDX_kiloclaw_subscriptions_earlybird_origin", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "access_origin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_subscriptions\".\"access_origin\" = 'earlybird'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscriptions_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "transferred_to_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_subscriptions_stripe_subscription_id_unique": { + "name": "kiloclaw_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscriptions_price_version_check": { + "name": "kiloclaw_subscriptions_price_version_check", + "value": "\"kiloclaw_subscriptions\".\"kiloclaw_price_version\" IN ('2026-03-19', '2026-05-10')" + }, + "kiloclaw_subscriptions_plan_check": { + "name": "kiloclaw_subscriptions_plan_check", + "value": "\"kiloclaw_subscriptions\".\"plan\" IN ('trial', 'commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_plan_check": { + "name": "kiloclaw_subscriptions_scheduled_plan_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_plan\" IN ('commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_by_check": { + "name": "kiloclaw_subscriptions_scheduled_by_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_by\" IN ('auto', 'user')" + }, + "kiloclaw_subscriptions_status_check": { + "name": "kiloclaw_subscriptions_status_check", + "value": "\"kiloclaw_subscriptions\".\"status\" IN ('trialing', 'active', 'past_due', 'canceled', 'unpaid')" + }, + "kiloclaw_subscriptions_access_origin_check": { + "name": "kiloclaw_subscriptions_access_origin_check", + "value": "\"kiloclaw_subscriptions\".\"access_origin\" IN ('earlybird')" + }, + "kiloclaw_subscriptions_payment_source_check": { + "name": "kiloclaw_subscriptions_payment_source_check", + "value": "\"kiloclaw_subscriptions\".\"payment_source\" IN ('stripe', 'credits')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_terminal_renewal_failures": { + "name": "kiloclaw_terminal_renewal_failures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "renewal_boundary": { + "name": "renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unresolved'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_failure_at": { + "name": "first_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_failure_message": { + "name": "last_failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_type": { + "name": "resolution_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_id": { + "name": "resolution_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_at": { + "name": "resolution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary": { + "name": "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_unresolved": { + "name": "IDX_kiloclaw_terminal_renewal_failures_unresolved", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_terminal_renewal_failures\".\"status\" = 'unresolved'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at": { + "name": "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_terminal_renewal_failures", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_terminal_renewal_failures_status_check": { + "name": "kiloclaw_terminal_renewal_failures_status_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"status\" IN ('unresolved', 'resolved', 'waived', 'superseded')" + }, + "kiloclaw_terminal_renewal_failures_last_failure_code_check": { + "name": "kiloclaw_terminal_renewal_failures_last_failure_code_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"last_failure_code\" IN ('credit_balance_read_failed', 'renewal_transaction_failed', 'auto_top_up_marker_write_failed', 'worker_timeout', 'poison_payload', 'queue_delivery_exhausted')" + }, + "kiloclaw_terminal_renewal_failures_resolution_actor_type_check": { + "name": "kiloclaw_terminal_renewal_failures_resolution_actor_type_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"resolution_actor_type\" IN ('operator', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_version_pins": { + "name": "kiloclaw_version_pins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_by": { + "name": "pinned_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk": { + "name": "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kilocode_users", + "columnsFrom": [ + "pinned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_version_pins_instance_id_unique": { + "name": "kiloclaw_version_pins_instance_id_unique", + "nullsNotDistinct": false, + "columns": [ + "instance_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilocode_users": { + "name": "kilocode_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "google_user_email": { + "name": "google_user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_name": { + "name": "google_user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_image_url": { + "name": "google_user_image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "kilo_pass_threshold": { + "name": "kilo_pass_threshold", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_store_account_token": { + "name": "app_store_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_super_admin": { + "name": "is_super_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_view_sessions": { + "name": "can_view_sessions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_manage_credits": { + "name": "can_manage_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "has_validation_stytch": { + "name": "has_validation_stytch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_validation_novel_card_with_hold": { + "name": "has_validation_novel_card_with_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_at": { + "name": "blocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_by_kilo_user_id": { + "name": "blocked_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_token_pepper": { + "name": "api_token_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_session_pepper": { + "name": "web_session_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "kiloclaw_early_access": { + "name": "kiloclaw_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cohorts": { + "name": "cohorts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_welcome_form": { + "name": "completed_welcome_form", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "linkedin_url": { + "name": "linkedin_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discord_server_membership_verified_at": { + "name": "discord_server_membership_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "openrouter_upstream_safety_identifier": { + "name": "openrouter_upstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openrouter_downstream_safety_identifier": { + "name": "openrouter_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_downstream_safety_identifier": { + "name": "vercel_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_source": { + "name": "customer_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signup_ip": { + "name": "signup_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_deletion_requested_at": { + "name": "account_deletion_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_domain": { + "name": "email_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "personal_account_disabled": { + "name": "personal_account_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kilocode_users_signup_ip_created_at": { + "name": "IDX_kilocode_users_signup_ip_created_at", + "columns": [ + { + "expression": "signup_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_at": { + "name": "IDX_kilocode_users_blocked_at", + "columns": [ + { + "expression": "blocked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_by_kilo_user_id": { + "name": "IDX_kilocode_users_blocked_by_kilo_user_id", + "columns": [ + { + "expression": "blocked_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_upstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_upstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_upstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_upstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_downstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_downstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_downstream_safety_identifier\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_vercel_downstream_safety_identifier": { + "name": "UQ_kilocode_users_vercel_downstream_safety_identifier", + "columns": [ + { + "expression": "vercel_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"vercel_downstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_normalized_email": { + "name": "IDX_kilocode_users_normalized_email", + "columns": [ + { + "expression": "normalized_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_email_domain": { + "name": "IDX_kilocode_users_email_domain", + "columns": [ + { + "expression": "email_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilocode_users_app_store_account_token_unique": { + "name": "kilocode_users_app_store_account_token_unique", + "nullsNotDistinct": false, + "columns": [ + "app_store_account_token" + ] + }, + "UQ_b1afacbcf43f2c7c4cb9f7e7faa": { + "name": "UQ_b1afacbcf43f2c7c4cb9f7e7faa", + "nullsNotDistinct": false, + "columns": [ + "google_user_email" + ] + } + }, + "policies": {}, + "checkConstraints": { + "blocked_reason_not_empty": { + "name": "blocked_reason_not_empty", + "value": "length(blocked_reason) > 0" + }, + "kilocode_users_is_super_admin_requires_admin_check": { + "name": "kilocode_users_is_super_admin_requires_admin_check", + "value": "NOT \"kilocode_users\".\"is_super_admin\" OR \"kilocode_users\".\"is_admin\"" + }, + "kilocode_users_can_view_sessions_requires_admin_check": { + "name": "kilocode_users_can_view_sessions_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_view_sessions\" OR \"kilocode_users\".\"is_admin\"" + }, + "kilocode_users_can_manage_credits_requires_admin_check": { + "name": "kilocode_users_can_manage_credits_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_manage_credits\" OR \"kilocode_users\".\"is_admin\"" + } + }, + "isRLSEnabled": false + }, + "public.magic_link_tokens": { + "name": "magic_link_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reserved_until": { + "name": "reserved_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'magic_link'" + }, + "challenge_id": { + "name": "challenge_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_magic_link_tokens_email": { + "name": "idx_magic_link_tokens_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_magic_link_tokens_expires_at": { + "name": "idx_magic_link_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_magic_link_tokens_challenge_id": { + "name": "UQ_magic_link_tokens_challenge_id", + "columns": [ + { + "expression": "challenge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"magic_link_tokens\".\"challenge_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_expires_at_future": { + "name": "check_expires_at_future", + "value": "\"magic_link_tokens\".\"expires_at\" > \"magic_link_tokens\".\"created_at\"" + }, + "check_magic_link_tokens_purpose": { + "name": "check_magic_link_tokens_purpose", + "value": "\"magic_link_tokens\".\"purpose\" IN ('magic_link', 'sign_in_code', 'data_export_download')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_assignments": { + "name": "mcp_gateway_assignments", + "schema": "", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_kilo_user_id": { + "name": "assigned_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "single_user_slot": { + "name": "single_user_slot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_assignments_active": { + "name": "UQ_mcp_gateway_assignments_active", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_assignments\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_assignments_single_user_slot": { + "name": "UQ_mcp_gateway_assignments_single_user_slot", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "single_user_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_assignments\".\"revoked_at\" is null and \"mcp_gateway_assignments\".\"single_user_slot\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_assignments_config": { + "name": "IDX_mcp_gateway_assignments_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_assignments_user": { + "name": "IDX_mcp_gateway_assignments_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_assignments_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_assignments_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_assignments_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_assignments_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_assignments_assigned_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_assignments_assigned_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_gateway_audit_events": { + "name": "mcp_gateway_audit_events", + "schema": "", + "columns": { + "audit_event_id": { + "name": "audit_event_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_metadata": { + "name": "correlation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_audit_events_config": { + "name": "IDX_mcp_gateway_audit_events_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_grant": { + "name": "IDX_mcp_gateway_audit_events_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_audit_events\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_owner": { + "name": "IDX_mcp_gateway_audit_events_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_created_at": { + "name": "IDX_mcp_gateway_audit_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_audit_events_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_audit_events_owner_scope": { + "name": "mcp_gateway_audit_events_owner_scope", + "value": "\"mcp_gateway_audit_events\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_audit_events_outcome": { + "name": "mcp_gateway_audit_events_outcome", + "value": "\"mcp_gateway_audit_events\".\"outcome\" IN ('success', 'failure', 'blocked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_codes": { + "name": "mcp_gateway_authorization_codes", + "schema": "", + "columns": { + "authorization_code_id": { + "name": "authorization_code_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_codes_code_hash": { + "name": "UQ_mcp_gateway_authorization_codes_code_hash", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_expires_at": { + "name": "IDX_mcp_gateway_authorization_codes_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_client": { + "name": "IDX_mcp_gateway_authorization_codes_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_grant": { + "name": "IDX_mcp_gateway_authorization_codes_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_authorization_codes\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_codes_owner_scope": { + "name": "mcp_gateway_authorization_codes_owner_scope", + "value": "\"mcp_gateway_authorization_codes\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_requests": { + "name": "mcp_gateway_authorization_requests", + "schema": "", + "columns": { + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_state_hash": { + "name": "request_state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_scopes": { + "name": "requested_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "oauth_state": { + "name": "oauth_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_status": { + "name": "request_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_requests_state_hash": { + "name": "UQ_mcp_gateway_authorization_requests_state_hash", + "columns": [ + { + "expression": "request_state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_config": { + "name": "IDX_mcp_gateway_authorization_requests_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_grant": { + "name": "IDX_mcp_gateway_authorization_requests_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_authorization_requests\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_user": { + "name": "IDX_mcp_gateway_authorization_requests_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_expires_at": { + "name": "IDX_mcp_gateway_authorization_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_requests_owner_scope": { + "name": "mcp_gateway_authorization_requests_owner_scope", + "value": "\"mcp_gateway_authorization_requests\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_authorization_requests_status": { + "name": "mcp_gateway_authorization_requests_status", + "value": "\"mcp_gateway_authorization_requests\".\"request_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_config_secrets": { + "name": "mcp_gateway_config_secrets", + "schema": "", + "columns": { + "config_secret_id": { + "name": "config_secret_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_kind": { + "name": "secret_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_config_secrets_active_kind": { + "name": "UQ_mcp_gateway_config_secrets_active_kind", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_config_secrets\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_config_secrets_config": { + "name": "IDX_mcp_gateway_config_secrets_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_config_secrets", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_config_secrets_version_positive": { + "name": "mcp_gateway_config_secrets_version_positive", + "value": "\"mcp_gateway_config_secrets\".\"secret_version\" > 0" + }, + "mcp_gateway_config_secrets_kind": { + "name": "mcp_gateway_config_secrets_kind", + "value": "\"mcp_gateway_config_secrets\".\"secret_kind\" IN ('static_provider_credentials', 'dynamic_registration', 'static_headers')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_configs": { + "name": "mcp_gateway_configs", + "schema": "", + "columns": { + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharing_mode": { + "name": "sharing_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_scope_source": { + "name": "provider_scope_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "provider_resource": { + "name": "provider_resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "path_passthrough": { + "name": "path_passthrough", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "discovered_provider_metadata": { + "name": "discovered_provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "registry_metadata": { + "name": "registry_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "auxiliary_headers": { + "name": "auxiliary_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_configs_owner": { + "name": "IDX_mcp_gateway_configs_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_enabled": { + "name": "IDX_mcp_gateway_configs_enabled", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_remote_url": { + "name": "IDX_mcp_gateway_configs_remote_url", + "columns": [ + { + "expression": "remote_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_configs_name_not_empty": { + "name": "mcp_gateway_configs_name_not_empty", + "value": "length(trim(\"mcp_gateway_configs\".\"name\")) > 0" + }, + "mcp_gateway_configs_config_version_positive": { + "name": "mcp_gateway_configs_config_version_positive", + "value": "\"mcp_gateway_configs\".\"config_version\" > 0" + }, + "mcp_gateway_configs_personal_single_user": { + "name": "mcp_gateway_configs_personal_single_user", + "value": "\"mcp_gateway_configs\".\"owner_scope\" <> 'personal' OR \"mcp_gateway_configs\".\"sharing_mode\" = 'single_user'" + }, + "mcp_gateway_configs_owner_scope": { + "name": "mcp_gateway_configs_owner_scope", + "value": "\"mcp_gateway_configs\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_configs_auth_mode": { + "name": "mcp_gateway_configs_auth_mode", + "value": "\"mcp_gateway_configs\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_configs_sharing_mode": { + "name": "mcp_gateway_configs_sharing_mode", + "value": "\"mcp_gateway_configs\".\"sharing_mode\" IN ('single_user', 'multi_user')" + }, + "mcp_gateway_configs_provider_scope_source": { + "name": "mcp_gateway_configs_provider_scope_source", + "value": "\"mcp_gateway_configs\".\"provider_scope_source\" IN ('none', 'discovered', 'override')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connect_resources": { + "name": "mcp_gateway_connect_resources", + "schema": "", + "columns": { + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_status": { + "name": "route_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "route_version": { + "name": "route_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connect_resources_route_key": { + "name": "UQ_mcp_gateway_connect_resources_route_key", + "columns": [ + { + "expression": "route_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_connect_resources_active_config": { + "name": "UQ_mcp_gateway_connect_resources_active_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connect_resources\".\"route_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_config": { + "name": "IDX_mcp_gateway_connect_resources_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_canonical_url": { + "name": "IDX_mcp_gateway_connect_resources_canonical_url", + "columns": [ + { + "expression": "canonical_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connect_resources", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connect_resources_route_key_format": { + "name": "mcp_gateway_connect_resources_route_key_format", + "value": "\"mcp_gateway_connect_resources\".\"route_key\" ~ '^[A-Za-z0-9_-]{32,}$'" + }, + "mcp_gateway_connect_resources_route_version_positive": { + "name": "mcp_gateway_connect_resources_route_version_positive", + "value": "\"mcp_gateway_connect_resources\".\"route_version\" > 0" + }, + "mcp_gateway_connect_resources_owner_scope": { + "name": "mcp_gateway_connect_resources_owner_scope", + "value": "\"mcp_gateway_connect_resources\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connect_resources_route_status": { + "name": "mcp_gateway_connect_resources_route_status", + "value": "\"mcp_gateway_connect_resources\".\"route_status\" IN ('active', 'rotated', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connection_instances": { + "name": "mcp_gateway_connection_instances", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_status": { + "name": "instance_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "instance_version": { + "name": "instance_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connection_instances_non_terminal": { + "name": "UQ_mcp_gateway_connection_instances_non_terminal", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_config": { + "name": "IDX_mcp_gateway_connection_instances_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_user": { + "name": "IDX_mcp_gateway_connection_instances_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connection_instances_version_positive": { + "name": "mcp_gateway_connection_instances_version_positive", + "value": "\"mcp_gateway_connection_instances\".\"instance_version\" > 0" + }, + "mcp_gateway_connection_instances_owner_scope": { + "name": "mcp_gateway_connection_instances_owner_scope", + "value": "\"mcp_gateway_connection_instances\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connection_instances_status": { + "name": "mcp_gateway_connection_instances_status", + "value": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth', 'revoked', 'removed')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_clients": { + "name": "mcp_gateway_oauth_clients", + "schema": "", + "columns": { + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_token_hash": { + "name": "registration_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "declared_scopes": { + "name": "declared_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "registration_access_token_expires_at": { + "name": "registration_access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_clients_client_id": { + "name": "UQ_mcp_gateway_oauth_clients_client_id", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_oauth_clients_registration_token_hash": { + "name": "UQ_mcp_gateway_oauth_clients_registration_token_hash", + "columns": [ + { + "expression": "registration_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_clients_deleted_at": { + "name": "IDX_mcp_gateway_oauth_clients_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_clients_client_id_format": { + "name": "mcp_gateway_oauth_clients_client_id_format", + "value": "\"mcp_gateway_oauth_clients\".\"client_id\" ~ '^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+$'" + }, + "mcp_gateway_oauth_clients_auth_method": { + "name": "mcp_gateway_oauth_clients_auth_method", + "value": "\"mcp_gateway_oauth_clients\".\"token_endpoint_auth_method\" IN ('none', 'client_secret_post', 'client_secret_basic')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_grants": { + "name": "mcp_gateway_oauth_grants", + "schema": "", + "columns": { + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_grants_active_binding": { + "name": "UQ_mcp_gateway_oauth_grants_active_binding", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connect_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "redirect_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_oauth_grants\".\"revoked_at\" is null and \"mcp_gateway_oauth_grants\".\"grant_status\" in ('pending', 'active')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_client": { + "name": "IDX_mcp_gateway_oauth_grants_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_user": { + "name": "IDX_mcp_gateway_oauth_grants_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_config": { + "name": "IDX_mcp_gateway_oauth_grants_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_owner": { + "name": "IDX_mcp_gateway_oauth_grants_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_resource": { + "name": "IDX_mcp_gateway_oauth_grants_resource", + "columns": [ + { + "expression": "connect_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_instance": { + "name": "IDX_mcp_gateway_oauth_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_revoked_at": { + "name": "IDX_mcp_gateway_oauth_grants_revoked_at", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_oauth_grants_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_oauth_grants_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_oauth_grants_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_oauth_grants_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_oauth_grants_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_oauth_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_grants_config_version_positive": { + "name": "mcp_gateway_oauth_grants_config_version_positive", + "value": "\"mcp_gateway_oauth_grants\".\"config_version\" > 0" + }, + "mcp_gateway_oauth_grants_owner_scope": { + "name": "mcp_gateway_oauth_grants_owner_scope", + "value": "\"mcp_gateway_oauth_grants\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_oauth_grants_status": { + "name": "mcp_gateway_oauth_grants_status", + "value": "\"mcp_gateway_oauth_grants\".\"grant_status\" IN ('pending', 'active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_pending_provider_authorizations": { + "name": "mcp_gateway_pending_provider_authorizations", + "schema": "", + "columns": { + "pending_provider_authorization_id": { + "name": "pending_provider_authorization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_authorization_endpoint": { + "name": "provider_authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_token_endpoint": { + "name": "provider_token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pending_status": { + "name": "pending_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_pending_provider_authorizations_state_hash": { + "name": "UQ_mcp_gateway_pending_provider_authorizations_state_hash", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_config": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_grant": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_pending_provider_authorizations\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_expires_at": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_pending_provider_authorizations_config_version_positive": { + "name": "mcp_gateway_pending_provider_authorizations_config_version_positive", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"config_version\" > 0" + }, + "mcp_gateway_pending_provider_authorizations_owner_scope": { + "name": "mcp_gateway_pending_provider_authorizations_owner_scope", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_pending_provider_authorizations_auth_mode": { + "name": "mcp_gateway_pending_provider_authorizations_auth_mode", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_pending_provider_authorizations_status": { + "name": "mcp_gateway_pending_provider_authorizations_status", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"pending_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_provider_grants": { + "name": "mcp_gateway_provider_grants", + "schema": "", + "columns": { + "provider_grant_id": { + "name": "provider_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "encrypted_grant": { + "name": "encrypted_grant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject": { + "name": "provider_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_scope": { + "name": "grant_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_provider_grants_active_instance": { + "name": "UQ_mcp_gateway_provider_grants_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_provider_grants\".\"grant_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_provider_grants_instance": { + "name": "IDX_mcp_gateway_provider_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_provider_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_provider_grants_version_positive": { + "name": "mcp_gateway_provider_grants_version_positive", + "value": "\"mcp_gateway_provider_grants\".\"grant_version\" > 0" + }, + "mcp_gateway_provider_grants_status": { + "name": "mcp_gateway_provider_grants_status", + "value": "\"mcp_gateway_provider_grants\".\"grant_status\" IN ('active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_rate_limit_windows": { + "name": "mcp_gateway_rate_limit_windows", + "schema": "", + "columns": { + "rate_limit_window_id": { + "name": "rate_limit_window_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_started_at": { + "name": "window_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_rate_limit_windows_ip_window": { + "name": "UQ_mcp_gateway_rate_limit_windows_ip_window", + "columns": [ + { + "expression": "ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_rate_limit_windows_window": { + "name": "IDX_mcp_gateway_rate_limit_windows_window", + "columns": [ + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_rate_limit_windows_attempt_count_non_negative": { + "name": "mcp_gateway_rate_limit_windows_attempt_count_non_negative", + "value": "\"mcp_gateway_rate_limit_windows\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_refresh_tokens": { + "name": "mcp_gateway_refresh_tokens", + "schema": "", + "columns": { + "refresh_token_id": { + "name": "refresh_token_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotated_from_refresh_token_id": { + "name": "rotated_from_refresh_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_refresh_tokens_token_hash": { + "name": "UQ_mcp_gateway_refresh_tokens_token_hash", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_user": { + "name": "IDX_mcp_gateway_refresh_tokens_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_grant": { + "name": "IDX_mcp_gateway_refresh_tokens_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_refresh_tokens\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_config": { + "name": "IDX_mcp_gateway_refresh_tokens_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_consumed_at": { + "name": "IDX_mcp_gateway_refresh_tokens_consumed_at", + "columns": [ + { + "expression": "consumed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_refresh_tokens_owner_scope": { + "name": "mcp_gateway_refresh_tokens_owner_scope", + "value": "\"mcp_gateway_refresh_tokens\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage": { + "name": "microdollar_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_abuse_classification": { + "name": "idx_abuse_classification", + "columns": [ + { + "expression": "abuse_classification", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id_created_at2": { + "name": "idx_kilo_user_id_created_at2", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_organization_id": { + "name": "idx_microdollar_usage_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily": { + "name": "microdollar_usage_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_microdollar_usage_daily_personal": { + "name": "idx_microdollar_usage_daily_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_daily_org": { + "name": "idx_microdollar_usage_daily_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily_repairs": { + "name": "microdollar_usage_daily_repairs", + "schema": "", + "columns": { + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_microdollar_usage_daily_repairs_claim": { + "name": "IDX_microdollar_usage_daily_repairs_claim", + "columns": [ + { + "expression": "attempt_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_daily_repairs_usage_id_microdollar_usage_id_fk": { + "name": "microdollar_usage_daily_repairs_usage_id_microdollar_usage_id_fk", + "tableFrom": "microdollar_usage_daily_repairs", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "microdollar_usage_daily_repairs_attempt_count_check": { + "name": "microdollar_usage_daily_repairs_attempt_count_check", + "value": "\"microdollar_usage_daily_repairs\".\"attempt_count\" >= 0" + }, + "microdollar_usage_daily_repairs_claim_token_check": { + "name": "microdollar_usage_daily_repairs_claim_token_check", + "value": "(\"microdollar_usage_daily_repairs\".\"claimed_at\" IS NULL AND \"microdollar_usage_daily_repairs\".\"claim_token\" IS NULL) OR (\"microdollar_usage_daily_repairs\".\"claimed_at\" IS NOT NULL AND \"microdollar_usage_daily_repairs\".\"claim_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage_metadata": { + "name": "microdollar_usage_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_ip_id": { + "name": "http_ip_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_latitude": { + "name": "vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_longitude": { + "name": "vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason_id": { + "name": "finish_reason_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name_id": { + "name": "editor_name_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_kind_id": { + "name": "api_kind_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode_id": { + "name": "mode_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_model_id": { + "name": "auto_model_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_microdollar_usage_metadata_created_at": { + "name": "idx_microdollar_usage_metadata_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_metadata_session_id": { + "name": "idx_microdollar_usage_metadata_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage_metadata\".\"session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk": { + "name": "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_user_agent", + "columnsFrom": [ + "http_user_agent_id" + ], + "columnsTo": [ + "http_user_agent_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk": { + "name": "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_ip", + "columnsFrom": [ + "http_ip_id" + ], + "columnsTo": [ + "http_ip_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_city", + "columnsFrom": [ + "vercel_ip_city_id" + ], + "columnsTo": [ + "vercel_ip_city_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_country", + "columnsFrom": [ + "vercel_ip_country_id" + ], + "columnsTo": [ + "vercel_ip_country_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk": { + "name": "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "ja4_digest", + "columnsFrom": [ + "ja4_digest_id" + ], + "columnsTo": [ + "ja4_digest_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk": { + "name": "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "system_prompt_prefix", + "columnsFrom": [ + "system_prompt_prefix_id" + ], + "columnsTo": [ + "system_prompt_prefix_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mode": { + "name": "mode", + "schema": "", + "columns": { + "mode_id": { + "name": "mode_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_mode": { + "name": "UQ_mode", + "columns": [ + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_stats": { + "name": "model_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_stealth": { + "name": "is_stealth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_recommended": { + "name": "is_recommended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "openrouter_id": { + "name": "openrouter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aa_slug": { + "name": "aa_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_creator": { + "name": "model_creator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_slug": { + "name": "creator_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_date": { + "name": "release_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "price_input": { + "name": "price_input", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "price_output": { + "name": "price_output", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "coding_index": { + "name": "coding_index", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "speed_tokens_per_sec": { + "name": "speed_tokens_per_sec", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "openrouter_data": { + "name": "openrouter_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "benchmarks": { + "name": "benchmarks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chart_data": { + "name": "chart_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_stats_openrouter_id": { + "name": "IDX_model_stats_openrouter_id", + "columns": [ + { + "expression": "openrouter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_slug": { + "name": "IDX_model_stats_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_is_active": { + "name": "IDX_model_stats_is_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_creator_slug": { + "name": "IDX_model_stats_creator_slug", + "columns": [ + { + "expression": "creator_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_price_input": { + "name": "IDX_model_stats_price_input", + "columns": [ + { + "expression": "price_input", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_coding_index": { + "name": "IDX_model_stats_coding_index", + "columns": [ + { + "expression": "coding_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_context_length": { + "name": "IDX_model_stats_context_length", + "columns": [ + { + "expression": "context_length", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_stats_openrouter_id_unique": { + "name": "model_stats_openrouter_id_unique", + "nullsNotDistinct": false, + "columns": [ + "openrouter_id" + ] + }, + "model_stats_slug_unique": { + "name": "model_stats_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_eval_ingestions": { + "name": "model_eval_ingestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bench_eval_name": { + "name": "bench_eval_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bench_eval_url": { + "name": "bench_eval_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_stats_id": { + "name": "model_stats_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "n_total_trials": { + "name": "n_total_trials", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "n_attempts": { + "name": "n_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_score": { + "name": "total_score", + "type": "numeric(14, 6)", + "primaryKey": false, + "notNull": true + }, + "overall_score": { + "name": "overall_score", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": true + }, + "n_errored": { + "name": "n_errored", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "avg_cost_microdollars": { + "name": "avg_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_input_tokens": { + "name": "avg_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_output_tokens": { + "name": "avg_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_cache_read_tokens": { + "name": "avg_cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cache_read_tokens": { + "name": "total_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_execution_ms": { + "name": "avg_execution_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promoted_at": { + "name": "promoted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "promoted_by_email": { + "name": "promoted_by_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "promotion_note": { + "name": "promotion_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_eval_ingestions_lookup": { + "name": "IDX_model_eval_ingestions_lookup", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "promoted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_model_stats": { + "name": "IDX_model_eval_ingestions_model_stats", + "columns": [ + { + "expression": "model_stats_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_promoted_by_email_lower": { + "name": "IDX_model_eval_ingestions_promoted_by_email_lower", + "columns": [ + { + "expression": "LOWER(\"promoted_by_email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_eval_ingestions_model_stats_id_model_stats_id_fk": { + "name": "model_eval_ingestions_model_stats_id_model_stats_id_fk", + "tableFrom": "model_eval_ingestions", + "tableTo": "model_stats", + "columnsFrom": [ + "model_stats_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_eval_ingestions_bench_eval_name_unique": { + "name": "model_eval_ingestions_bench_eval_name_unique", + "nullsNotDistinct": false, + "columns": [ + "bench_eval_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_experiment": { + "name": "model_experiment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "public_model_id": { + "name": "public_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_model_experiment_public_model_id_routing": { + "name": "UQ_model_experiment_public_model_id_routing", + "columns": [ + { + "expression": "public_model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"model_experiment\".\"status\" IN ('active', 'paused')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_experiment_status": { + "name": "IDX_model_experiment_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_created_by_user_id_kilocode_users_id_fk": { + "name": "model_experiment_created_by_user_id_kilocode_users_id_fk", + "tableFrom": "model_experiment", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_experiment_status_valid": { + "name": "model_experiment_status_valid", + "value": "\"model_experiment\".\"status\" IN ('draft', 'active', 'paused', 'completed')" + }, + "model_experiment_active_not_archived": { + "name": "model_experiment_active_not_archived", + "value": "\"model_experiment\".\"status\" <> 'active' OR \"model_experiment\".\"is_archived\" = false" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_request": { + "name": "model_experiment_request", + "schema": "", + "columns": { + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "variant_version_id": { + "name": "variant_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_subject": { + "name": "allocation_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_kind": { + "name": "request_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_body_sha256": { + "name": "request_body_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "was_truncated": { + "name": "was_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_request_variant_version_created_at": { + "name": "IDX_model_experiment_request_variant_version_created_at", + "columns": [ + { + "expression": "variant_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_experiment_request_client_request_id": { + "name": "IDX_model_experiment_request_client_request_id", + "columns": [ + { + "expression": "client_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"model_experiment_request\".\"client_request_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_request_usage_id_microdollar_usage_id_fk": { + "name": "model_experiment_request_usage_id_microdollar_usage_id_fk", + "tableFrom": "model_experiment_request", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_experiment_request_variant_version_id_model_experiment_variant_version_id_fk": { + "name": "model_experiment_request_variant_version_id_model_experiment_variant_version_id_fk", + "tableFrom": "model_experiment_request", + "tableTo": "model_experiment_variant_version", + "columnsFrom": [ + "variant_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "model_experiment_request_usage_id_created_at_pk": { + "name": "model_experiment_request_usage_id_created_at_pk", + "columns": [ + "usage_id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_experiment_request_allocation_subject_valid": { + "name": "model_experiment_request_allocation_subject_valid", + "value": "\"model_experiment_request\".\"allocation_subject\" IN ('user', 'machine', 'ip')" + }, + "model_experiment_request_request_kind_valid": { + "name": "model_experiment_request_request_kind_valid", + "value": "\"model_experiment_request\".\"request_kind\" IN ('chat_completions', 'messages', 'responses')" + }, + "model_experiment_request_request_body_sha256_format": { + "name": "model_experiment_request_request_body_sha256_format", + "value": "\"model_experiment_request\".\"request_body_sha256\" ~ '^[0-9a-f]{64}$' OR \"model_experiment_request\".\"request_body_sha256\" IN ('__failed__', '__deleted__')" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_variant": { + "name": "model_experiment_variant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "experiment_id": { + "name": "experiment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_variant_experiment_id": { + "name": "IDX_model_experiment_variant_experiment_id", + "columns": [ + { + "expression": "experiment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_variant_experiment_id_model_experiment_id_fk": { + "name": "model_experiment_variant_experiment_id_model_experiment_id_fk", + "tableFrom": "model_experiment_variant", + "tableTo": "model_experiment", + "columnsFrom": [ + "experiment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_model_experiment_variant_experiment_label": { + "name": "UQ_model_experiment_variant_experiment_label", + "nullsNotDistinct": false, + "columns": [ + "experiment_id", + "label" + ] + } + }, + "policies": {}, + "checkConstraints": { + "model_experiment_variant_weight_positive": { + "name": "model_experiment_variant_weight_positive", + "value": "\"model_experiment_variant\".\"weight\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_variant_version": { + "name": "model_experiment_variant_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "variant_id": { + "name": "variant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_variant_version_variant_effective": { + "name": "IDX_model_experiment_variant_version_variant_effective", + "columns": [ + { + "expression": "variant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_variant_version_variant_id_model_experiment_variant_id_fk": { + "name": "model_experiment_variant_version_variant_id_model_experiment_variant_id_fk", + "tableFrom": "model_experiment_variant_version", + "tableTo": "model_experiment_variant", + "columnsFrom": [ + "variant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_experiment_variant_version_created_by_kilocode_users_id_fk": { + "name": "model_experiment_variant_version_created_by_kilocode_users_id_fk", + "tableFrom": "model_experiment_variant_version", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models_by_provider": { + "name": "models_by_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "openrouter": { + "name": "openrouter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "vercel": { + "name": "vercel", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_admission_challenges": { + "name": "native_admission_challenges", + "schema": "", + "columns": { + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_native_admission_challenges_expires_at": { + "name": "IDX_native_admission_challenges_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_attested_keys": { + "name": "native_attested_keys", + "schema": "", + "columns": { + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sign_count": { + "name": "sign_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attested_at": { + "name": "attested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_native_attested_keys_kilo_user_id": { + "name": "IDX_native_attested_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "native_attested_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "native_attested_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "native_attested_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "native_attested_keys_platform_check": { + "name": "native_attested_keys_platform_check", + "value": "\"native_attested_keys\".\"platform\" IN ('ios', 'android')" + } + }, + "isRLSEnabled": false + }, + "public.operation_ledgers": { + "name": "operation_ledgers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "operation_key": { + "name": "operation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "taxonomy": { + "name": "taxonomy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admitted'" + }, + "outcome_code": { + "name": "outcome_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_result": { + "name": "canonical_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_operation_ledgers_kilo_user_id_domain_operation_key": { + "name": "UQ_operation_ledgers_kilo_user_id_domain_operation_key", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_operation_ledgers_status_expires_at": { + "name": "IDX_operation_ledgers_status_expires_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_operation_ledgers_provider_ref": { + "name": "IDX_operation_ledgers_provider_ref", + "columns": [ + { + "expression": "provider_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"operation_ledgers\".\"provider_ref\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_audit_logs": { + "name": "organization_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_audit_logs_organization_id": { + "name": "IDX_organization_audit_logs_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_action": { + "name": "IDX_organization_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_actor_id": { + "name": "IDX_organization_audit_logs_actor_id", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_created_at": { + "name": "IDX_organization_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_domain_claims": { + "name": "organization_domain_claims", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "workos_organization_id": { + "name": "workos_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workos_domain_id": { + "name": "workos_domain_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_domain_claims_verified_domain": { + "name": "UQ_organization_domain_claims_verified_domain", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_domain_claims\".\"status\" = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_organization_domain_claims_workos_domain_id": { + "name": "UQ_organization_domain_claims_workos_domain_id", + "columns": [ + { + "expression": "workos_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_domain_claims\".\"workos_domain_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_domain_claims_organization_id": { + "name": "IDX_organization_domain_claims_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_domain_claims_organization_id_organizations_id_fk": { + "name": "organization_domain_claims_organization_id_organizations_id_fk", + "tableFrom": "organization_domain_claims", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_domain_claims_organization_domain": { + "name": "UQ_organization_domain_claims_organization_domain", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_domain_claims_canonical_domain_check": { + "name": "organization_domain_claims_canonical_domain_check", + "value": "length(\"organization_domain_claims\".\"domain\") BETWEEN 1 AND 253 AND \"organization_domain_claims\".\"domain\" = lower(btrim(\"organization_domain_claims\".\"domain\"))" + }, + "organization_domain_claims_status_check": { + "name": "organization_domain_claims_status_check", + "value": "\"organization_domain_claims\".\"status\" IN ('pending', 'verified')" + }, + "organization_domain_claims_verification_shape_check": { + "name": "organization_domain_claims_verification_shape_check", + "value": "(\"organization_domain_claims\".\"status\" = 'pending' AND \"organization_domain_claims\".\"verified_at\" IS NULL)\n OR (\"organization_domain_claims\".\"status\" = 'verified' AND \"organization_domain_claims\".\"verified_at\" IS NOT NULL AND \"organization_domain_claims\".\"workos_organization_id\" IS NOT NULL AND \"organization_domain_claims\".\"workos_domain_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.organization_group_memberships": { + "name": "organization_group_memberships", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_kilo_user_id": { + "name": "assigned_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_group_memberships_organization_user": { + "name": "IDX_organization_group_memberships_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "FK_organization_group_memberships_group": { + "name": "FK_organization_group_memberships_group", + "tableFrom": "organization_group_memberships", + "tableTo": "organization_groups", + "columnsFrom": [ + "organization_id", + "group_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_organization_group_memberships_member": { + "name": "FK_organization_group_memberships_member", + "tableFrom": "organization_group_memberships", + "tableTo": "organization_memberships", + "columnsFrom": [ + "organization_id", + "kilo_user_id" + ], + "columnsTo": [ + "organization_id", + "kilo_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "PK_organization_group_memberships": { + "name": "PK_organization_group_memberships", + "columns": [ + "organization_id", + "group_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_group_policy_settings": { + "name": "organization_group_policy_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "default_policies": { + "name": "default_policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[{\"type\":\"model_access\",\"data\":{\"mode\":\"all\"}}]'::jsonb" + }, + "policy_revision": { + "name": "policy_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated_by_kilo_user_id": { + "name": "updated_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_group_policy_settings_organization_id_organizations_id_fk": { + "name": "organization_group_policy_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_group_policy_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_group_policy_settings_revision_check": { + "name": "organization_group_policy_settings_revision_check", + "value": "\"organization_group_policy_settings\".\"policy_revision\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.organization_groups": { + "name": "organization_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policies": { + "name": "policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_groups_organization_id_canonical_name": { + "name": "UQ_organization_groups_organization_id_canonical_name", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(btrim(\"name\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_groups_organization_id": { + "name": "IDX_organization_groups_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_groups_organization_id_organizations_id_fk": { + "name": "organization_groups_organization_id_organizations_id_fk", + "tableFrom": "organization_groups", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_groups_organization_id_id": { + "name": "UQ_organization_groups_organization_id_id", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_groups_name_check": { + "name": "organization_groups_name_check", + "value": "char_length(btrim(\"organization_groups\".\"name\")) BETWEEN 1 AND 80" + }, + "organization_groups_description_check": { + "name": "organization_groups_description_check", + "value": "\"organization_groups\".\"description\" IS NULL OR char_length(\"organization_groups\".\"description\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "authentication_requirement": { + "name": "authentication_requirement", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "sso_source_organization_id": { + "name": "sso_source_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_invitations_token": { + "name": "UQ_organization_invitations_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_org_id": { + "name": "IDX_organization_invitations_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_email": { + "name": "IDX_organization_invitations_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_expires_at": { + "name": "IDX_organization_invitations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_sso_source_organization_id_organizations_id_fk": { + "name": "organization_invitations_sso_source_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "sso_source_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_membership_removals": { + "name": "organization_membership_removals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_by": { + "name": "removed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_role": { + "name": "previous_role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_org_membership_removals_org_id": { + "name": "IDX_org_membership_removals_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_org_membership_removals_user_id": { + "name": "IDX_org_membership_removals_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_membership_removals_org_user": { + "name": "UQ_org_membership_removals_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_memberships": { + "name": "organization_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_memberships_org_id": { + "name": "IDX_organization_memberships_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_memberships_user_id": { + "name": "IDX_organization_memberships_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_memberships_org_user": { + "name": "UQ_organization_memberships_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_recommendation_dismissals": { + "name": "organization_recommendation_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recommendation_key": { + "name": "recommendation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_by_user_id": { + "name": "dismissed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk": { + "name": "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk": { + "name": "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "dismissed_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_recommendation_dismissals_org_key": { + "name": "UQ_org_recommendation_dismissals_org_key", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "recommendation_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_seats_purchases": { + "name": "organization_seats_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subscription_stripe_id": { + "name": "subscription_stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "subscription_status": { + "name": "subscription_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_cycle": { + "name": "billing_cycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + } + }, + "indexes": { + "IDX_organization_seats_org_id": { + "name": "IDX_organization_seats_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_expires_at": { + "name": "IDX_organization_seats_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_created_at": { + "name": "IDX_organization_seats_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_updated_at": { + "name": "IDX_organization_seats_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_starts_at": { + "name": "IDX_organization_seats_starts_at", + "columns": [ + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_seats_idempotency_key": { + "name": "UQ_organization_seats_idempotency_key", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_limits": { + "name": "organization_user_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_limit": { + "name": "microdollar_limit", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_user_limits_org_id": { + "name": "IDX_organization_user_limits_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_limits_user_id": { + "name": "IDX_organization_user_limits_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_limits_org_user": { + "name": "UQ_organization_user_limits_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_usage": { + "name": "organization_user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_usage": { + "name": "microdollar_usage", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_user_daily_usage_org_id": { + "name": "IDX_organization_user_daily_usage_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_daily_usage_user_id": { + "name": "IDX_organization_user_daily_usage_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_daily_usage_org_user_date": { + "name": "UQ_organization_user_daily_usage_org_user_date", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type", + "usage_date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "microdollars_balance": { + "name": "microdollars_balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_seats": { + "name": "require_seats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sso_domain": { + "name": "sso_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'teams'" + }, + "free_trial_end_at": { + "name": "free_trial_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "company_domain": { + "name": "company_domain", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_organizations_sso_domain": { + "name": "IDX_organizations_sso_domain", + "columns": [ + { + "expression": "sso_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organizations_parent_organization_id": { + "name": "IDX_organizations_parent_organization_id", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_organizations_live_sales_demo_per_owner": { + "name": "UQ_organizations_live_sales_demo_per_owner", + "columns": [ + { + "expression": "created_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "(\"organizations\".\"settings\"->>'is_sales_demo')::boolean = true AND \"organizations\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_parent_organization_id_organizations_id_fk": { + "name": "organizations_parent_organization_id_organizations_id_fk", + "tableFrom": "organizations", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organizations_name_not_empty_check": { + "name": "organizations_name_not_empty_check", + "value": "length(trim(\"organizations\".\"name\")) > 0" + }, + "organizations_not_parented_by_self_check": { + "name": "organizations_not_parented_by_self_check", + "value": "\"organizations\".\"parent_organization_id\" IS NULL OR \"organizations\".\"parent_organization_id\" <> \"organizations\".\"id\"" + } + }, + "isRLSEnabled": false + }, + "public.organization_modes": { + "name": "organization_modes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_organization_modes_organization_id": { + "name": "IDX_organization_modes_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_modes_org_id_slug": { + "name": "UQ_organization_modes_org_id_slug", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_methods": { + "name": "payment_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last4": { + "name": "last4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1": { + "name": "address_line1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line2": { + "name": "address_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_state": { + "name": "address_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "three_d_secure_supported": { + "name": "three_d_secure_supported", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regulated_status": { + "name": "regulated_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1_check_status": { + "name": "address_line1_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code_check_status": { + "name": "postal_code_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eligible_for_free_credits": { + "name": "eligible_for_free_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_data": { + "name": "stripe_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_d7d7fb15569674aaadcfbc0428": { + "name": "IDX_d7d7fb15569674aaadcfbc0428", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_e1feb919d0ab8a36381d5d5138": { + "name": "IDX_e1feb919d0ab8a36381d5d5138", + "columns": [ + { + "expression": "stripe_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_payment_methods_organization_id": { + "name": "IDX_payment_methods_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_29df1b0403df5792c96bbbfdbe6": { + "name": "UQ_29df1b0403df5792c96bbbfdbe6", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_impact_sale_reversals": { + "name": "pending_impact_sale_reversals", + "schema": "", + "columns": { + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "dispute_id": { + "name": "dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_impact_sale_reversals_attempt_count_non_negative_check": { + "name": "pending_impact_sale_reversals_attempt_count_non_negative_check", + "value": "\"pending_impact_sale_reversals\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.platform_access_token_credentials": { + "name": "platform_access_token_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_encrypted": { + "name": "token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_credential_type": { + "name": "provider_credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_resource_id": { + "name": "provider_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_verified_at": { + "name": "provider_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_validated_at": { + "name": "last_validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_access_token_credentials_integration_level": { + "name": "UQ_platform_access_token_credentials_integration_level", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_access_token_credentials_resource": { + "name": "UQ_platform_access_token_credentials_resource", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_credential_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_access_token_credentials_authorized_by_user_id": { + "name": "IDX_platform_access_token_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_access_token_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_platform_access_token_credentials_parent": { + "name": "FK_platform_access_token_credentials_parent", + "tableFrom": "platform_access_token_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_access_token_credentials_credential_version_check": { + "name": "platform_access_token_credentials_credential_version_check", + "value": "\"platform_access_token_credentials\".\"credential_version\" > 0" + }, + "platform_access_token_credentials_resource_id_check": { + "name": "platform_access_token_credentials_resource_id_check", + "value": "\"platform_access_token_credentials\".\"provider_resource_id\" IS NULL OR \"platform_access_token_credentials\".\"provider_resource_id\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.platform_integrations": { + "name": "platform_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_installation_id": { + "name": "platform_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_id": { + "name": "platform_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_login": { + "name": "platform_account_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "repository_access": { + "name": "repository_access", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repositories": { + "name": "repositories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repositories_synced_at": { + "name": "repositories_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_at": { + "name": "auth_invalid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_reason": { + "name": "auth_invalid_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "kilo_requester_user_id": { + "name": "kilo_requester_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_requester_account_id": { + "name": "platform_requester_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_status": { + "name": "integration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_by": { + "name": "suspended_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_integrations_owned_by_org_platform_inst": { + "name": "UQ_platform_integrations_owned_by_org_platform_inst", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_owned_by_user_platform_inst": { + "name": "UQ_platform_integrations_owned_by_user_platform_inst", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_slack_platform_inst": { + "name": "UQ_platform_integrations_slack_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'slack' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_linear_platform_inst": { + "name": "UQ_platform_integrations_linear_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'linear' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_platform_inst": { + "name": "UQ_platform_integrations_github_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_pending_target": { + "name": "UQ_platform_integrations_github_pending_target", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"integration_status\" = 'pending' AND \"platform_integrations\".\"platform_installation_id\" IS NULL AND \"platform_integrations\".\"platform_account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_user_bitbucket": { + "name": "UQ_platform_integrations_user_bitbucket", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_org_bitbucket": { + "name": "UQ_platform_integrations_org_bitbucket", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_id": { + "name": "IDX_platform_integrations_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_id": { + "name": "IDX_platform_integrations_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_inst_id": { + "name": "IDX_platform_integrations_platform_inst_id", + "columns": [ + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform": { + "name": "IDX_platform_integrations_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_platform": { + "name": "IDX_platform_integrations_owned_by_org_platform", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_platform": { + "name": "IDX_platform_integrations_owned_by_user_platform", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_integration_status": { + "name": "IDX_platform_integrations_integration_status", + "columns": [ + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_kilo_requester": { + "name": "IDX_platform_integrations_kilo_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_requester_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_requester": { + "name": "IDX_platform_integrations_platform_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_requester_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_integrations_owned_by_organization_id_organizations_id_fk": { + "name": "platform_integrations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_integrations_owned_by_user_id_kilocode_users_id_fk": { + "name": "platform_integrations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_integrations_owner_check": { + "name": "platform_integrations_owner_check", + "value": "(\n (\"platform_integrations\".\"owned_by_user_id\" IS NOT NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NULL) OR\n (\"platform_integrations\".\"owned_by_user_id\" IS NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.platform_oauth_credentials": { + "name": "platform_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject_login": { + "name": "provider_subject_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_oauth_credentials_platform_integration_id": { + "name": "UQ_platform_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_oauth_credentials_authorized_by_user_id": { + "name": "IDX_platform_oauth_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_oauth_credentials_credential_version_check": { + "name": "platform_oauth_credentials_credential_version_check", + "value": "\"platform_oauth_credentials\".\"credential_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.quick_chat_messages": { + "name": "quick_chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_quick_chat_messages_thread_created_at": { + "name": "IDX_quick_chat_messages_thread_created_at", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quick_chat_messages_thread_id_quick_chat_threads_id_fk": { + "name": "quick_chat_messages_thread_id_quick_chat_threads_id_fk", + "tableFrom": "quick_chat_messages", + "tableTo": "quick_chat_threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quick_chat_threads": { + "name": "quick_chat_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quick_chat_threads_user_personal_uidx": { + "name": "quick_chat_threads_user_personal_uidx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"quick_chat_threads\".\"organization_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "quick_chat_threads_user_org_uidx": { + "name": "quick_chat_threads_user_org_uidx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"quick_chat_threads\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quick_chat_threads_user_id_kilocode_users_id_fk": { + "name": "quick_chat_threads_user_id_kilocode_users_id_fk", + "tableFrom": "quick_chat_threads", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "quick_chat_threads_organization_id_organizations_id_fk": { + "name": "quick_chat_threads_organization_id_organizations_id_fk", + "tableFrom": "quick_chat_threads", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_code_usages": { + "name": "referral_code_usages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "referring_kilo_user_id": { + "name": "referring_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeeming_kilo_user_id": { + "name": "redeeming_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_referral_code_usages_redeeming_kilo_user_id": { + "name": "IDX_referral_code_usages_redeeming_kilo_user_id", + "columns": [ + { + "expression": "redeeming_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_referral_code_usages_redeeming_user_id_code": { + "name": "UQ_referral_code_usages_redeeming_user_id_code", + "nullsNotDistinct": false, + "columns": [ + "redeeming_kilo_user_id", + "referring_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_referral_codes_kilo_user_id": { + "name": "UQ_referral_codes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_referral_codes_code": { + "name": "IDX_referral_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sales_demo_spend_ledger": { + "name": "sales_demo_spend_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_kilo_user_id": { + "name": "owner_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sales_demo_spend_ledger_organization_id_organizations_id_fk": { + "name": "sales_demo_spend_ledger_organization_id_organizations_id_fk", + "tableFrom": "sales_demo_spend_ledger", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sales_demo_spend_ledger_spend_positive": { + "name": "sales_demo_spend_ledger_spend_positive", + "value": "\"sales_demo_spend_ledger\".\"microdollars_used\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_check_catalog": { + "name": "security_advisor_check_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_check_catalog_check_id_unique": { + "name": "security_advisor_check_catalog_check_id_unique", + "nullsNotDistinct": false, + "columns": [ + "check_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "security_advisor_check_catalog_severity_check": { + "name": "security_advisor_check_catalog_severity_check", + "value": "\"security_advisor_check_catalog\".\"severity\" in ('critical', 'warn', 'info')" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_content": { + "name": "security_advisor_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_content_key_unique": { + "name": "security_advisor_content_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_kiloclaw_coverage": { + "name": "security_advisor_kiloclaw_coverage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_check_ids": { + "name": "match_check_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_kiloclaw_coverage_area_unique": { + "name": "security_advisor_kiloclaw_coverage_area_unique", + "nullsNotDistinct": false, + "columns": [ + "area" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_scans": { + "name": "security_advisor_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_platform": { + "name": "source_platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_method": { + "name": "source_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_ip": { + "name": "public_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "findings_critical": { + "name": "findings_critical", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_warn": { + "name": "findings_warn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_info": { + "name": "findings_info", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_advisor_scans_user_created_at": { + "name": "idx_security_advisor_scans_user_created_at", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_created_at": { + "name": "idx_security_advisor_scans_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_platform": { + "name": "idx_security_advisor_scans_platform", + "columns": [ + { + "expression": "source_platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_agent_commands": { + "name": "security_agent_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "operation_key": { + "name": "operation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'accepted'" + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_agent_commands_org_created": { + "name": "idx_security_agent_commands_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_user_created": { + "name": "idx_security_agent_commands_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_status_updated": { + "name": "idx_security_agent_commands_status_updated", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_finding_created": { + "name": "idx_security_agent_commands_finding_created", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_commands_org_operation_key": { + "name": "UQ_security_agent_commands_org_operation_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL AND \"security_agent_commands\".\"operation_key\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_security_agent_commands_user_operation_key": { + "name": "UQ_security_agent_commands_user_operation_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"operation_key\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_commands_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_commands_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_commands_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_finding_id_security_findings_id_fk": { + "name": "security_agent_commands_finding_id_security_findings_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_commands_owner_check": { + "name": "security_agent_commands_owner_check", + "value": "(\n (\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_commands\".\"owned_by_user_id\" IS NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_agent_commands_type_check": { + "name": "security_agent_commands_type_check", + "value": "\"security_agent_commands\".\"command_type\" IN ('sync', 'dismiss_finding', 'start_analysis', 'apply_auto_remediation')" + }, + "security_agent_commands_origin_check": { + "name": "security_agent_commands_origin_check", + "value": "\"security_agent_commands\".\"origin\" IN ('manual', 'dashboard_refresh', 'enable_initial_sync', 'settings_include_existing')" + }, + "security_agent_commands_status_check": { + "name": "security_agent_commands_status_check", + "value": "\"security_agent_commands\".\"status\" IN ('accepted', 'running', 'succeeded', 'failed', 'no_op')" + } + }, + "isRLSEnabled": false + }, + "public.security_agent_repository_sync_state": { + "name": "security_agent_repository_sync_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_succeeded_at": { + "name": "last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_agent_repository_sync_state_org_repo": { + "name": "UQ_security_agent_repository_sync_state_org_repo", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_repository_sync_state_user_repo": { + "name": "UQ_security_agent_repository_sync_state_user_repo", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_repository_sync_state_owner_check": { + "name": "security_agent_repository_sync_state_owner_check", + "value": "(\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_owner_state": { + "name": "security_analysis_owner_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_analysis_enabled_at": { + "name": "auto_analysis_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "block_reason": { + "name": "block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_actor_resolution_failures": { + "name": "consecutive_actor_resolution_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_actor_resolution_failure_at": { + "name": "last_actor_resolution_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_owner_state_org_owner": { + "name": "UQ_security_analysis_owner_state_org_owner", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_analysis_owner_state_user_owner": { + "name": "UQ_security_analysis_owner_state_user_owner", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_owner_state_owner_check": { + "name": "security_analysis_owner_state_owner_check", + "value": "(\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_owner_state_block_reason_check": { + "name": "security_analysis_owner_state_block_reason_check", + "value": "\"security_analysis_owner_state\".\"block_reason\" IS NULL OR \"security_analysis_owner_state\".\"block_reason\" IN ('INSUFFICIENT_CREDITS', 'ACTOR_RESOLUTION_FAILED', 'OPERATOR_PAUSE')" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_queue": { + "name": "security_analysis_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queue_status": { + "name": "queue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity_rank": { + "name": "severity_rank", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "admitted_config_revision": { + "name": "admitted_config_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reopen_requeue_count": { + "name": "reopen_requeue_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_queue_finding_id": { + "name": "UQ_security_analysis_queue_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_org": { + "name": "idx_security_analysis_queue_claim_path_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_user": { + "name": "idx_security_analysis_queue_claim_path_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_org": { + "name": "idx_security_analysis_queue_in_flight_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_user": { + "name": "idx_security_analysis_queue_in_flight_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_lag_dashboards": { + "name": "idx_security_analysis_queue_lag_dashboards", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_pending_reconciliation": { + "name": "idx_security_analysis_queue_pending_reconciliation", + "columns": [ + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_running_reconciliation": { + "name": "idx_security_analysis_queue_running_reconciliation", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_failure_trend": { + "name": "idx_security_analysis_queue_failure_trend", + "columns": [ + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"failure_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_queue_finding_id_security_findings_id_fk": { + "name": "security_analysis_queue_finding_id_security_findings_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_queue_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_queue_owner_check": { + "name": "security_analysis_queue_owner_check", + "value": "(\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_queue_status_check": { + "name": "security_analysis_queue_status_check", + "value": "\"security_analysis_queue\".\"queue_status\" IN ('queued', 'pending', 'running', 'failed', 'completed')" + }, + "security_analysis_queue_claim_token_required_check": { + "name": "security_analysis_queue_claim_token_required_check", + "value": "\"security_analysis_queue\".\"queue_status\" NOT IN ('pending', 'running') OR \"security_analysis_queue\".\"claim_token\" IS NOT NULL" + }, + "security_analysis_queue_attempt_count_non_negative_check": { + "name": "security_analysis_queue_attempt_count_non_negative_check", + "value": "\"security_analysis_queue\".\"attempt_count\" >= 0" + }, + "security_analysis_queue_reopen_requeue_count_non_negative_check": { + "name": "security_analysis_queue_reopen_requeue_count_non_negative_check", + "value": "\"security_analysis_queue\".\"reopen_requeue_count\" >= 0" + }, + "security_analysis_queue_severity_rank_check": { + "name": "security_analysis_queue_severity_rank_check", + "value": "\"security_analysis_queue\".\"severity_rank\" IN (0, 1, 2, 3)" + }, + "security_analysis_queue_failure_code_check": { + "name": "security_analysis_queue_failure_code_check", + "value": "\"security_analysis_queue\".\"failure_code\" IS NULL OR \"security_analysis_queue\".\"failure_code\" IN (\n 'NETWORK_TIMEOUT',\n 'UPSTREAM_5XX',\n 'TEMP_TOKEN_FAILURE',\n 'START_CALL_AMBIGUOUS',\n 'REQUEUE_TEMPORARY_PRECONDITION',\n 'ACTOR_RESOLUTION_FAILED',\n 'GITHUB_TOKEN_UNAVAILABLE',\n 'INVALID_CONFIG',\n 'MISSING_OWNERSHIP',\n 'PERMISSION_DENIED_PERMANENT',\n 'UNSUPPORTED_SEVERITY',\n 'INSUFFICIENT_CREDITS',\n 'STATE_GUARD_REJECTED',\n 'SKIPPED_ALREADY_IN_PROGRESS',\n 'SKIPPED_NO_LONGER_ELIGIBLE',\n 'REOPEN_LOOP_GUARD',\n 'RUN_LOST'\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_audit_log": { + "name": "security_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source_occurred_at": { + "name": "source_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "finding_snapshot": { + "name": "finding_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_context": { + "name": "source_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_security_audit_log_org_created": { + "name": "IDX_security_audit_log_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_created": { + "name": "IDX_security_audit_log_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_resource": { + "name": "IDX_security_audit_log_resource", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_actor": { + "name": "IDX_security_audit_log_actor", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_action": { + "name": "IDX_security_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_org_event_key": { + "name": "UQ_security_audit_log_org_event_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_user_event_key": { + "name": "UQ_security_audit_log_user_event_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_org_occurred": { + "name": "IDX_security_audit_log_org_occurred", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_occurred": { + "name": "IDX_security_audit_log_user_occurred", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_audit_log_owned_by_organization_id_organizations_id_fk": { + "name": "security_audit_log_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_audit_log_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_audit_log_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_audit_log_owner_check": { + "name": "security_audit_log_owner_check", + "value": "(\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NULL) OR (\"security_audit_log\".\"owned_by_user_id\" IS NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "security_audit_log_action_check": { + "name": "security_audit_log_action_check", + "value": "\"security_audit_log\".\"action\" IN ('security.finding.created', 'security.finding.severity_changed', 'security.finding.status_change', 'security.finding.dismissed', 'security.finding.auto_dismissed', 'security.finding.superseded', 'security.finding.analysis_started', 'security.finding.analysis_completed', 'security.finding.analysis_failed', 'security.remediation.queued', 'security.remediation.started', 'security.remediation.pr_opened', 'security.remediation.failed', 'security.remediation.blocked', 'security.remediation.no_changes_needed', 'security.remediation.cancelled', 'security.remediation.retried', 'security.finding.deleted', 'security.config.enabled', 'security.config.disabled', 'security.config.updated', 'security.sync.triggered', 'security.sync.completed', 'security.audit_log.exported', 'security.audit_report.generated')" + }, + "security_audit_log_actor_type_check": { + "name": "security_audit_log_actor_type_check", + "value": "\"security_audit_log\".\"actor_type\" IN ('customer_user', 'kilo_admin', 'system')" + }, + "security_audit_log_source_context_check": { + "name": "security_audit_log_source_context_check", + "value": "\"security_audit_log\".\"source_context\" IN ('security_sync', 'web', 'analysis_worker', 'remediation_callback', 'rollout_baseline')" + } + }, + "isRLSEnabled": false + }, + "public.security_finding_notifications": { + "name": "security_finding_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'staged'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_finding_notifications_finding_recipient_kind": { + "name": "uq_security_finding_notifications_finding_recipient_kind", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_pending": { + "name": "idx_security_finding_notifications_pending", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_staged": { + "name": "idx_security_finding_notifications_staged", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'staged'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_finding_id": { + "name": "idx_security_finding_notifications_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_recipient_user_id": { + "name": "idx_security_finding_notifications_recipient_user_id", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_finding_notifications_finding_fk": { + "name": "security_finding_notifications_finding_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_finding_notifications_recipient_fk": { + "name": "security_finding_notifications_recipient_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_finding_notifications_kind_check": { + "name": "security_finding_notifications_kind_check", + "value": "\"security_finding_notifications\".\"kind\" IN ('new_finding', 'sla_warning', 'sla_breach')" + }, + "security_finding_notifications_status_check": { + "name": "security_finding_notifications_status_check", + "value": "\"security_finding_notifications\".\"status\" IN ('staged', 'pending', 'sending', 'sent', 'failed', 'cancelled')" + }, + "security_finding_notifications_attempt_count_check": { + "name": "security_finding_notifications_attempt_count_check", + "value": "\"security_finding_notifications\".\"attempt_count\" >= 0" + }, + "security_finding_notifications_claimed_at_check": { + "name": "security_finding_notifications_claimed_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NULL)\n )" + }, + "security_finding_notifications_sent_at_check": { + "name": "security_finding_notifications_sent_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NULL)\n )" + }, + "security_finding_notifications_error_message_length_check": { + "name": "security_finding_notifications_error_message_length_check", + "value": "\"security_finding_notifications\".\"error_message\" IS NULL OR length(\"security_finding_notifications\".\"error_message\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.security_findings": { + "name": "security_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ghsa_id": { + "name": "ghsa_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cve_id": { + "name": "cve_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_ecosystem": { + "name": "package_ecosystem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vulnerable_version_range": { + "name": "vulnerable_version_range", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patched_version": { + "name": "patched_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest_path": { + "name": "manifest_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "ignored_reason": { + "name": "ignored_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignored_by": { + "name": "ignored_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fixed_at": { + "name": "fixed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sla_due_at": { + "name": "sla_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dependabot_html_url": { + "name": "dependabot_html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwe_ids": { + "name": "cwe_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cvss_score": { + "name": "cvss_score", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": false + }, + "dependency_scope": { + "name": "dependency_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_status": { + "name": "analysis_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_started_at": { + "name": "analysis_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_error": { + "name": "analysis_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis": { + "name": "analysis", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_detected_at": { + "name": "first_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_findings_user_source": { + "name": "uq_security_findings_user_source", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_security_findings_org_source": { + "name": "uq_security_findings_org_source", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_id": { + "name": "idx_security_findings_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_id": { + "name": "idx_security_findings_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_repo": { + "name": "idx_security_findings_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_severity": { + "name": "idx_security_findings_severity", + "columns": [ + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_status": { + "name": "idx_security_findings_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_package": { + "name": "idx_security_findings_package", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_sla_due_at": { + "name": "idx_security_findings_sla_due_at", + "columns": [ + { + "expression": "sla_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_session_id": { + "name": "idx_security_findings_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_cli_session_id": { + "name": "idx_security_findings_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_analysis_status": { + "name": "idx_security_findings_analysis_status", + "columns": [ + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_analysis_in_flight": { + "name": "idx_security_findings_org_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_analysis_in_flight": { + "name": "idx_security_findings_user_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_findings_owned_by_organization_id_organizations_id_fk": { + "name": "security_findings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_findings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_findings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_findings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_platform_integration_id_platform_integrations_id_fk": { + "name": "security_findings_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "security_findings", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_findings_owner_check": { + "name": "security_findings_owner_check", + "value": "(\n (\"security_findings\".\"owned_by_user_id\" IS NOT NULL AND \"security_findings\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_findings\".\"owned_by_user_id\" IS NULL AND \"security_findings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_remediation_attempts": { + "name": "security_remediation_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "remediation_id": { + "name": "remediation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_fingerprint": { + "name": "analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "remediation_model_slug": { + "name": "remediation_model_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_attempt_count": { + "name": "launch_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "callback_attempt_token_hash": { + "name": "callback_attempt_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_result": { + "name": "structured_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "final_assistant_message": { + "name": "final_assistant_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "validation_evidence": { + "name": "validation_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "risk_notes": { + "name": "risk_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_reason": { + "name": "draft_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_by_user_id": { + "name": "cancellation_requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "launched_at": { + "name": "launched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediation_attempts_number": { + "name": "UQ_security_remediation_attempts_number", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_finding": { + "name": "UQ_security_remediation_attempts_active_finding", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_remediation": { + "name": "UQ_security_remediation_attempts_active_remediation", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_finding_fingerprint_terminal": { + "name": "UQ_security_remediation_attempts_finding_fingerprint_terminal", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_claim": { + "name": "idx_security_remediation_attempts_org_claim", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_claim": { + "name": "idx_security_remediation_attempts_user_claim", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_claim": { + "name": "idx_security_remediation_attempts_repo_claim", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_inflight": { + "name": "idx_security_remediation_attempts_org_inflight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_inflight": { + "name": "idx_security_remediation_attempts_user_inflight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_inflight": { + "name": "idx_security_remediation_attempts_repo_inflight", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_cloud_agent_session": { + "name": "idx_security_remediation_attempts_cloud_agent_session", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_finding_fingerprint": { + "name": "idx_security_remediation_attempts_finding_fingerprint", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediation_attempts_remediation_id_security_remediations_id_fk": { + "name": "security_remediation_attempts_remediation_id_security_remediations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_remediations", + "columnsFrom": [ + "remediation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_finding_id_security_findings_id_fk": { + "name": "security_remediation_attempts_finding_id_security_findings_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediation_attempts_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "cancellation_requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediation_attempts_owner_check": { + "name": "security_remediation_attempts_owner_check", + "value": "(\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediation_attempts_status_check": { + "name": "security_remediation_attempts_status_check", + "value": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + }, + "security_remediation_attempts_origin_check": { + "name": "security_remediation_attempts_origin_check", + "value": "\"security_remediation_attempts\".\"origin\" IN ('auto_policy', 'bulk_existing', 'manual')" + }, + "security_remediation_attempts_attempt_number_check": { + "name": "security_remediation_attempts_attempt_number_check", + "value": "\"security_remediation_attempts\".\"attempt_number\" >= 1" + }, + "security_remediation_attempts_launch_attempt_count_check": { + "name": "security_remediation_attempts_launch_attempt_count_check", + "value": "\"security_remediation_attempts\".\"launch_attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.security_remediations": { + "name": "security_remediations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "latest_attempt_id": { + "name": "latest_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_fingerprint": { + "name": "latest_analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_completed_at": { + "name": "latest_analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_summary": { + "name": "outcome_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediations_finding_id": { + "name": "UQ_security_remediations_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_org_status": { + "name": "idx_security_remediations_org_status", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_user_status": { + "name": "idx_security_remediations_user_status", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_repo_status": { + "name": "idx_security_remediations_repo_status", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_latest_attempt": { + "name": "idx_security_remediations_latest_attempt", + "columns": [ + { + "expression": "latest_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediations_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_finding_id_security_findings_id_fk": { + "name": "security_remediations_finding_id_security_findings_id_fk", + "tableFrom": "security_remediations", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediations_owner_check": { + "name": "security_remediations_owner_check", + "value": "(\n (\"security_remediations\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediations\".\"owned_by_user_id\" IS NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediations_status_check": { + "name": "security_remediations_status_check", + "value": "\"security_remediations\".\"status\" IN ('queued', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.shared_cli_sessions": { + "name": "shared_cli_sessions", + "schema": "", + "columns": { + "share_id": { + "name": "share_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_state": { + "name": "shared_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_shared_cli_sessions_session_id": { + "name": "IDX_shared_cli_sessions_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_shared_cli_sessions_created_at": { + "name": "IDX_shared_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_cli_sessions_session_id_cli_sessions_session_id_fk": { + "name": "shared_cli_sessions_session_id_cli_sessions_session_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_cli_sessions_shared_state_check": { + "name": "shared_cli_sessions_shared_state_check", + "value": "\"shared_cli_sessions\".\"shared_state\" IN ('public', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.slack_bot_requests": { + "name": "slack_bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_name": { + "name": "slack_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message_truncated": { + "name": "user_message_truncated", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_calls_made": { + "name": "tool_calls_made", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_slack_bot_requests_created_at": { + "name": "idx_slack_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_slack_team_id": { + "name": "idx_slack_bot_requests_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_org_id": { + "name": "idx_slack_bot_requests_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_user_id": { + "name": "idx_slack_bot_requests_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_status": { + "name": "idx_slack_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_event_type": { + "name": "idx_slack_bot_requests_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_team_created": { + "name": "idx_slack_bot_requests_team_created", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_bot_requests_owned_by_organization_id_organizations_id_fk": { + "name": "slack_bot_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_bot_requests_owner_check": { + "name": "slack_bot_requests_owner_check", + "value": "(\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NOT NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NOT NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.slack_oauth_credentials": { + "name": "slack_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_enterprise_id": { + "name": "slack_enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enterprise_install": { + "name": "is_enterprise_install", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "refresh_claimed_at": { + "name": "refresh_claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_attempt_count": { + "name": "refresh_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_refresh_attempt_at": { + "name": "next_refresh_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_slack_oauth_credentials_platform_integration_id": { + "name": "UQ_slack_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_slack_oauth_credentials_slack_team_id": { + "name": "IDX_slack_oauth_credentials_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_slack_oauth_credentials_refresh_due": { + "name": "IDX_slack_oauth_credentials_refresh_due", + "columns": [ + { + "expression": "access_token_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"slack_oauth_credentials\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_oauth_credentials_credential_version_check": { + "name": "slack_oauth_credentials_credential_version_check", + "value": "\"slack_oauth_credentials\".\"credential_version\" > 0" + }, + "slack_oauth_credentials_refresh_attempt_count_check": { + "name": "slack_oauth_credentials_refresh_attempt_count_check", + "value": "\"slack_oauth_credentials\".\"refresh_attempt_count\" >= 0" + }, + "slack_oauth_credentials_slack_team_id_check": { + "name": "slack_oauth_credentials_slack_team_id_check", + "value": "\"slack_oauth_credentials\".\"slack_team_id\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.source_embeddings": { + "name": "source_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_base_branch": { + "name": "is_base_branch", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_source_embeddings_organization_id": { + "name": "IDX_source_embeddings_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_kilo_user_id": { + "name": "IDX_source_embeddings_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_project_id": { + "name": "IDX_source_embeddings_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_created_at": { + "name": "IDX_source_embeddings_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_updated_at": { + "name": "IDX_source_embeddings_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_file_path_lower": { + "name": "IDX_source_embeddings_file_path_lower", + "columns": [ + { + "expression": "LOWER(\"file_path\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_git_branch": { + "name": "IDX_source_embeddings_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_org_project_branch": { + "name": "IDX_source_embeddings_org_project_branch", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "source_embeddings_organization_id_organizations_id_fk": { + "name": "source_embeddings_organization_id_organizations_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "source_embeddings_kilo_user_id_kilocode_users_id_fk": { + "name": "source_embeddings_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_source_embeddings_org_project_branch_file_lines": { + "name": "UQ_source_embeddings_org_project_branch_file_lines", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "project_id", + "git_branch", + "file_path", + "start_line", + "end_line" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stripe_dispute_actions": { + "name": "stripe_dispute_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_actions_case_id": { + "name": "IDX_stripe_dispute_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_actions_claim_path": { + "name": "IDX_stripe_dispute_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk": { + "name": "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk", + "tableFrom": "stripe_dispute_actions", + "tableTo": "stripe_dispute_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_actions_case_type_target": { + "name": "UQ_stripe_dispute_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_actions_action_type_check": { + "name": "stripe_dispute_actions_action_type_check", + "value": "\"stripe_dispute_actions\".\"action_type\" IN ('stripe_acceptance', 'user_block', 'auto_top_up_disable', 'credit_balance_reset', 'subscription_cancellation', 'access_termination', 'kiloclaw_suspension')" + }, + "stripe_dispute_actions_status_check": { + "name": "stripe_dispute_actions_status_check", + "value": "\"stripe_dispute_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'skipped')" + }, + "stripe_dispute_actions_attempt_count_non_negative_check": { + "name": "stripe_dispute_actions_attempt_count_non_negative_check", + "value": "\"stripe_dispute_actions\".\"attempt_count\" >= 0" + }, + "stripe_dispute_actions_target_key_not_empty_check": { + "name": "stripe_dispute_actions_target_key_not_empty_check", + "value": "length(\"stripe_dispute_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_dispute_cases": { + "name": "stripe_dispute_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_dispute_id": { + "name": "stripe_dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_created_at": { + "name": "stripe_event_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispute_reason": { + "name": "dispute_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_status": { + "name": "stripe_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'needs_action'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_created_at": { + "name": "stripe_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evidence_due_by": { + "name": "evidence_due_by", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by_kilo_user_id": { + "name": "accepted_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance_started_at": { + "name": "acceptance_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enforcement_completed_at": { + "name": "enforcement_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_cases_event_id": { + "name": "IDX_stripe_dispute_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_charge_id": { + "name": "IDX_stripe_dispute_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_payment_intent_id": { + "name": "IDX_stripe_dispute_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_customer_id": { + "name": "IDX_stripe_dispute_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_kilo_user_id": { + "name": "IDX_stripe_dispute_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_organization_id": { + "name": "IDX_stripe_dispute_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_status_due_by": { + "name": "IDX_stripe_dispute_cases_status_due_by", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "evidence_due_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_organization_id_organizations_id_fk": { + "name": "stripe_dispute_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "accepted_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_cases_dispute_id": { + "name": "UQ_stripe_dispute_cases_dispute_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_dispute_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_cases_owner_classification_check": { + "name": "stripe_dispute_cases_owner_classification_check", + "value": "\"stripe_dispute_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_dispute_cases_status_check": { + "name": "stripe_dispute_cases_status_check", + "value": "\"stripe_dispute_cases\".\"status\" IN ('needs_action', 'processing', 'accepted', 'acceptance_failed', 'enforcement_failed', 'review_required', 'closed')" + }, + "stripe_dispute_cases_amount_minor_units_non_negative_check": { + "name": "stripe_dispute_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_dispute_cases\".\"amount_minor_units\" IS NULL OR \"stripe_dispute_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_actions": { + "name": "stripe_early_fraud_warning_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_actions_case_id": { + "name": "IDX_stripe_early_fraud_warning_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_actions_claim_path": { + "name": "IDX_stripe_early_fraud_warning_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk": { + "name": "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk", + "tableFrom": "stripe_early_fraud_warning_actions", + "tableTo": "stripe_early_fraud_warning_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_actions_case_type_target": { + "name": "UQ_stripe_early_fraud_warning_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_actions_action_type_check": { + "name": "stripe_early_fraud_warning_actions_action_type_check", + "value": "\"stripe_early_fraud_warning_actions\".\"action_type\" IN ('containment', 'refund', 'payment_value_clawback', 'subscription_termination', 'access_termination', 'kiloclaw_suspension', 'affiliate_payout_reversal', 'referral_reward_reversal', 'user_notice')" + }, + "stripe_early_fraud_warning_actions_status_check": { + "name": "stripe_early_fraud_warning_actions_status_check", + "value": "\"stripe_early_fraud_warning_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'review_required', 'dismissed')" + }, + "stripe_early_fraud_warning_actions_attempt_count_non_negative_check": { + "name": "stripe_early_fraud_warning_actions_attempt_count_non_negative_check", + "value": "\"stripe_early_fraud_warning_actions\".\"attempt_count\" >= 0" + }, + "stripe_early_fraud_warning_actions_target_key_not_empty_check": { + "name": "stripe_early_fraud_warning_actions_target_key_not_empty_check", + "value": "length(\"stripe_early_fraud_warning_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_cases": { + "name": "stripe_early_fraud_warning_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_early_fraud_warning_id": { + "name": "stripe_early_fraud_warning_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warning_created_at": { + "name": "warning_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "contained_at": { + "name": "contained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remediated_at": { + "name": "remediated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_cases_event_id": { + "name": "IDX_stripe_early_fraud_warning_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_charge_id": { + "name": "IDX_stripe_early_fraud_warning_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_payment_intent_id": { + "name": "IDX_stripe_early_fraud_warning_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_customer_id": { + "name": "IDX_stripe_early_fraud_warning_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_kilo_user_id": { + "name": "IDX_stripe_early_fraud_warning_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_organization_id": { + "name": "IDX_stripe_early_fraud_warning_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_status_created_at": { + "name": "IDX_stripe_early_fraud_warning_cases_status_created_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk": { + "name": "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_cases_warning_id": { + "name": "UQ_stripe_early_fraud_warning_cases_warning_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_early_fraud_warning_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_cases_owner_classification_check": { + "name": "stripe_early_fraud_warning_cases_owner_classification_check", + "value": "\"stripe_early_fraud_warning_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_early_fraud_warning_cases_status_check": { + "name": "stripe_early_fraud_warning_cases_status_check", + "value": "\"stripe_early_fraud_warning_cases\".\"status\" IN ('queued', 'contained', 'processing', 'completed', 'review_required', 'failed', 'remediated', 'dismissed')" + }, + "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check": { + "name": "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_early_fraud_warning_cases\".\"amount_minor_units\" IS NULL OR \"stripe_early_fraud_warning_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stytch_fingerprints": { + "name": "stytch_fingerprints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_fingerprint": { + "name": "visitor_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_fingerprint": { + "name": "browser_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_id": { + "name": "browser_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hardware_fingerprint": { + "name": "hardware_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network_fingerprint": { + "name": "network_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_id": { + "name": "visitor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verdict_action": { + "name": "verdict_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_device_type": { + "name": "detected_device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_authentic_device": { + "name": "is_authentic_device", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"\"}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fingerprint_data": { + "name": "fingerprint_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_free_tier_allowed": { + "name": "kilo_free_tier_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_hardware_fingerprint": { + "name": "idx_hardware_fingerprint", + "columns": [ + { + "expression": "hardware_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id": { + "name": "idx_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_stytch_fingerprints_reasons_gin": { + "name": "idx_stytch_fingerprints_reasons_gin", + "columns": [ + { + "expression": "reasons", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_verdict_action": { + "name": "idx_verdict_action", + "columns": [ + { + "expression": "verdict_action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_visitor_fingerprint": { + "name": "idx_visitor_fingerprint", + "columns": [ + { + "expression": "visitor_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_prompt_prefix": { + "name": "system_prompt_prefix", + "schema": "", + "columns": { + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_system_prompt_prefix": { + "name": "UQ_system_prompt_prefix", + "columns": [ + { + "expression": "system_prompt_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactional_email_log": { + "name": "transactional_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_transactional_email_log_type_idempotency_key": { + "name": "UQ_transactional_email_log_type_idempotency_key", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_user_id": { + "name": "IDX_transactional_email_log_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_organization_id": { + "name": "IDX_transactional_email_log_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactional_email_log_user_id_kilocode_users_id_fk": { + "name": "transactional_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactional_email_log_organization_id_organizations_id_fk": { + "name": "transactional_email_log_organization_id_organizations_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_transactional_email_log_owner": { + "name": "CHK_transactional_email_log_owner", + "value": "\"transactional_email_log\".\"user_id\" IS NOT NULL OR \"transactional_email_log\".\"organization_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.user_activity_tokens": { + "name": "user_activity_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_activity_tokens_token": { + "name": "UQ_user_activity_tokens_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_activity_tokens_user_org": { + "name": "IDX_user_activity_tokens_user_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_activity_tokens_user_id_kilocode_users_id_fk": { + "name": "user_activity_tokens_user_id_kilocode_users_id_fk", + "tableFrom": "user_activity_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_admin_notes": { + "name": "user_admin_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note_content": { + "name": "note_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_kilo_user_id": { + "name": "admin_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_34517df0b385234babc38fe81b": { + "name": "IDX_34517df0b385234babc38fe81b", + "columns": [ + { + "expression": "admin_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_ccbde98c4c14046daa5682ec4f": { + "name": "IDX_ccbde98c4c14046daa5682ec4f", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_d0270eb24ef6442d65a0b7853c": { + "name": "IDX_d0270eb24ef6442d65a0b7853c", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_affiliate_attributions": { + "name": "user_affiliate_attributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_id": { + "name": "tracking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_attributions_user_id": { + "name": "IDX_user_affiliate_attributions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_attributions_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_attributions_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_attributions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_attributions_user_provider": { + "name": "UQ_user_affiliate_attributions_user_provider", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_attributions_provider_check": { + "name": "user_affiliate_attributions_provider_check", + "value": "\"user_affiliate_attributions\".\"provider\" IN ('impact')" + } + }, + "isRLSEnabled": false + }, + "public.user_affiliate_events": { + "name": "user_affiliate_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_event_id": { + "name": "parent_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_action_id": { + "name": "impact_action_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_submission_uri": { + "name": "impact_submission_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_events_claim_path": { + "name": "IDX_user_affiliate_events_claim_path", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_parent_event_id": { + "name": "IDX_user_affiliate_events_parent_event_id", + "columns": [ + { + "expression": "parent_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_provider_event_type_charge": { + "name": "IDX_user_affiliate_events_provider_event_type_charge", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_events_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_events_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_affiliate_events_parent_event_id_fk": { + "name": "user_affiliate_events_parent_event_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "user_affiliate_events", + "columnsFrom": [ + "parent_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_events_dedupe_key": { + "name": "UQ_user_affiliate_events_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_events_provider_check": { + "name": "user_affiliate_events_provider_check", + "value": "\"user_affiliate_events\".\"provider\" IN ('impact')" + }, + "user_affiliate_events_event_type_check": { + "name": "user_affiliate_events_event_type_check", + "value": "\"user_affiliate_events\".\"event_type\" IN ('signup', 'trial_start', 'trial_end', 'sale', 'sale_reversal')" + }, + "user_affiliate_events_delivery_state_check": { + "name": "user_affiliate_events_delivery_state_check", + "value": "\"user_affiliate_events\".\"delivery_state\" IN ('queued', 'blocked', 'sending', 'delivered', 'failed')" + }, + "user_affiliate_events_attempt_count_non_negative_check": { + "name": "user_affiliate_events_attempt_count_non_negative_check", + "value": "\"user_affiliate_events\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_auth_provider": { + "name": "user_auth_provider", + "schema": "", + "columns": { + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_auth_provider_kilo_user_id": { + "name": "IDX_user_auth_provider_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_hosted_domain": { + "name": "IDX_user_auth_provider_hosted_domain", + "columns": [ + { + "expression": "hosted_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_lower_email": { + "name": "IDX_user_auth_provider_lower_email", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_auth_provider_provider_provider_account_id_pk": { + "name": "user_auth_provider_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_data_export_object_deletions": { + "name": "user_data_export_object_deletions", + "schema": "", + "columns": { + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'account_deletion'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_data_export_object_deletions_ready": { + "name": "IDX_user_data_export_object_deletions_ready", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_export_object_deletions_reason_check": { + "name": "user_data_export_object_deletions_reason_check", + "value": "\"user_data_export_object_deletions\".\"reason\" IN ('account_deletion', 'admin_cancel', 'admin_replace')" + }, + "user_data_export_object_deletions_attempt_count_nonnegative": { + "name": "user_data_export_object_deletions_attempt_count_nonnegative", + "value": "\"user_data_export_object_deletions\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_export_outbox": { + "name": "user_data_export_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "export_id": { + "name": "export_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'generate'" + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_data_export_outbox_pending": { + "name": "IDX_user_data_export_outbox_pending", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_export_outbox\".\"sent_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_data_export_outbox_export_id_user_data_exports_id_fk": { + "name": "user_data_export_outbox_export_id_user_data_exports_id_fk", + "tableFrom": "user_data_export_outbox", + "tableTo": "user_data_exports", + "columnsFrom": [ + "export_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_data_export_outbox_generation_operation": { + "name": "UQ_user_data_export_outbox_generation_operation", + "nullsNotDistinct": false, + "columns": [ + "export_id", + "generation", + "operation" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_data_export_outbox_operation_check": { + "name": "user_data_export_outbox_operation_check", + "value": "\"user_data_export_outbox\".\"operation\" = 'generate'" + }, + "user_data_export_outbox_generation_nonnegative": { + "name": "user_data_export_outbox_generation_nonnegative", + "value": "\"user_data_export_outbox\".\"generation\" >= 0" + }, + "user_data_export_outbox_attempt_count_nonnegative": { + "name": "user_data_export_outbox_attempt_count_nonnegative", + "value": "\"user_data_export_outbox\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_export_parts": { + "name": "user_data_export_parts", + "schema": "", + "columns": { + "export_id": { + "name": "export_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "part_number": { + "name": "part_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_data_export_parts_export_id_user_data_exports_id_fk": { + "name": "user_data_export_parts_export_id_user_data_exports_id_fk", + "tableFrom": "user_data_export_parts", + "tableTo": "user_data_exports", + "columnsFrom": [ + "export_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_data_export_parts_export_id_part_number_pk": { + "name": "user_data_export_parts_export_id_part_number_pk", + "columns": [ + "export_id", + "part_number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_export_parts_part_number_positive": { + "name": "user_data_export_parts_part_number_positive", + "value": "\"user_data_export_parts\".\"part_number\" > 0" + }, + "user_data_export_parts_size_bytes_nonnegative": { + "name": "user_data_export_parts_size_bytes_nonnegative", + "value": "\"user_data_export_parts\".\"size_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_exports": { + "name": "user_data_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "snapshot_at": { + "name": "snapshot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "current_source": { + "name": "current_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_cursor": { + "name": "source_cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_part_number": { + "name": "next_part_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "dispatch_generation": { + "name": "dispatch_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "row_count": { + "name": "row_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "r2_object_key": { + "name": "r2_object_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "r2_etag": { + "name": "r2_etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_status": { + "name": "email_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "email_attempt_count": { + "name": "email_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "email_lease_token": { + "name": "email_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_lease_expires_at": { + "name": "email_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_sent_at": { + "name": "email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_data_exports_single_active": { + "name": "UQ_user_data_exports_single_active", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing') AND \"user_data_exports\".\"subject_type\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_data_exports_single_active_org": { + "name": "UQ_user_data_exports_single_active_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing') AND \"user_data_exports\".\"subject_type\" = 'organization'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_user_created": { + "name": "IDX_user_data_exports_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_org_created": { + "name": "IDX_user_data_exports_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_lease_expiry": { + "name": "IDX_user_data_exports_lease_expiry", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" IN ('processing', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_ready_expiry": { + "name": "IDX_user_data_exports_ready_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" = 'ready'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_failed_multipart": { + "name": "IDX_user_data_exports_failed_multipart", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" = 'failed' AND \"user_data_exports\".\"multipart_upload_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_email_lease_expiry": { + "name": "IDX_user_data_exports_email_lease_expiry", + "columns": [ + { + "expression": "email_lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"email_status\" = 'sending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_data_exports_kilo_user_id_kilocode_users_id_fk": { + "name": "user_data_exports_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_data_exports", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "user_data_exports_organization_id_organizations_id_fk": { + "name": "user_data_exports_organization_id_organizations_id_fk", + "tableFrom": "user_data_exports", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_exports_status_check": { + "name": "user_data_exports_status_check", + "value": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing', 'ready', 'failed', 'expired')" + }, + "user_data_exports_subject_type_check": { + "name": "user_data_exports_subject_type_check", + "value": "\"user_data_exports\".\"subject_type\" IN ('user', 'organization')" + }, + "user_data_exports_subject_shape": { + "name": "user_data_exports_subject_shape", + "value": "(\"user_data_exports\".\"subject_type\" = 'user' AND \"user_data_exports\".\"organization_id\" IS NULL)\n OR (\"user_data_exports\".\"subject_type\" = 'organization' AND \"user_data_exports\".\"organization_id\" IS NOT NULL)" + }, + "user_data_exports_schema_version_positive": { + "name": "user_data_exports_schema_version_positive", + "value": "\"user_data_exports\".\"schema_version\" > 0" + }, + "user_data_exports_next_part_number_positive": { + "name": "user_data_exports_next_part_number_positive", + "value": "\"user_data_exports\".\"next_part_number\" > 0" + }, + "user_data_exports_dispatch_generation_nonnegative": { + "name": "user_data_exports_dispatch_generation_nonnegative", + "value": "\"user_data_exports\".\"dispatch_generation\" >= 0" + }, + "user_data_exports_attempt_count_nonnegative": { + "name": "user_data_exports_attempt_count_nonnegative", + "value": "\"user_data_exports\".\"attempt_count\" >= 0" + }, + "user_data_exports_row_count_nonnegative": { + "name": "user_data_exports_row_count_nonnegative", + "value": "\"user_data_exports\".\"row_count\" >= 0" + }, + "user_data_exports_size_bytes_nonnegative": { + "name": "user_data_exports_size_bytes_nonnegative", + "value": "\"user_data_exports\".\"size_bytes\" IS NULL OR \"user_data_exports\".\"size_bytes\" >= 0" + }, + "user_data_exports_lease_shape": { + "name": "user_data_exports_lease_shape", + "value": "(\"user_data_exports\".\"lease_token\" IS NULL) = (\"user_data_exports\".\"lease_expires_at\" IS NULL)" + }, + "user_data_exports_ready_shape": { + "name": "user_data_exports_ready_shape", + "value": "\"user_data_exports\".\"status\" <> 'ready' OR (\"user_data_exports\".\"r2_object_key\" IS NOT NULL AND \"user_data_exports\".\"size_bytes\" IS NOT NULL AND \"user_data_exports\".\"completed_at\" IS NOT NULL AND \"user_data_exports\".\"expires_at\" IS NOT NULL)" + }, + "user_data_exports_last_error_redacted_length": { + "name": "user_data_exports_last_error_redacted_length", + "value": "\"user_data_exports\".\"last_error_redacted\" IS NULL OR length(\"user_data_exports\".\"last_error_redacted\") <= 500" + }, + "user_data_exports_email_attempt_count_nonnegative": { + "name": "user_data_exports_email_attempt_count_nonnegative", + "value": "\"user_data_exports\".\"email_attempt_count\" >= 0" + }, + "user_data_exports_email_status_check": { + "name": "user_data_exports_email_status_check", + "value": "\"user_data_exports\".\"email_status\" IN ('pending', 'sending', 'sent', 'failed')" + }, + "user_data_exports_email_lease_shape": { + "name": "user_data_exports_email_lease_shape", + "value": "(\"user_data_exports\".\"email_status\" = 'sending') = (\"user_data_exports\".\"email_lease_token\" IS NOT NULL AND \"user_data_exports\".\"email_lease_expires_at\" IS NOT NULL)" + }, + "user_data_exports_email_sent_shape": { + "name": "user_data_exports_email_sent_shape", + "value": "(\"user_data_exports\".\"email_status\" = 'sent') = (\"user_data_exports\".\"email_sent_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_activity": { + "name": "user_deletion_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_key": { + "name": "step_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details_json": { + "name": "details_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_deletion_activity_request_created": { + "name": "IDX_user_deletion_activity_request_created", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_activity_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_activity_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_activity", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_deletion_audit_events": { + "name": "user_deletion_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email_hmac": { + "name": "target_email_hmac", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_key": { + "name": "subject_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details_json": { + "name": "details_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_deletion_audit_events_idempotent": { + "name": "UQ_user_deletion_audit_events_idempotent", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_audit_events\".\"request_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_audit_events_request_id": { + "name": "IDX_user_deletion_audit_events_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_audit_events_hmac": { + "name": "IDX_user_deletion_audit_events_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_audit_events_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_audit_events_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_audit_events", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_audit_events_event_type_check": { + "name": "user_deletion_audit_events_event_type_check", + "value": "\"user_deletion_audit_events\".\"event_type\" IN ('request_created', 'intake_refused', 'access_disabled', 'access_absent', 'preflight_disposition', 'task_disposition', 'manual_retry', 'manual_action', 'anonymized', 'deletion_ready_for_customer_reply', 'cancelled', 'completed')" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_provider_credentials": { + "name": "user_deletion_provider_credentials", + "schema": "", + "columns": { + "provider_scope": { + "name": "provider_scope", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "encrypted_material": { + "name": "encrypted_material", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_kilo_user_id": { + "name": "updated_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_deletion_provider_credentials_updated_by_kilo_user_id_kilocode_users_id_fk": { + "name": "user_deletion_provider_credentials_updated_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_provider_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "updated_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_provider_credentials_scope_check": { + "name": "user_deletion_provider_credentials_scope_check", + "value": "\"user_deletion_provider_credentials\".\"provider_scope\" IN ('kiloclaw', 'customerio', 'cloud_storage', 'session_ingest', 'posthog', 'substack', 'pylon', 'csa')" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_requests": { + "name": "user_deletion_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "catalog_version": { + "name": "catalog_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requested_by_kilo_user_id": { + "name": "requested_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_email": { + "name": "requested_by_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email_hmac": { + "name": "target_email_hmac", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pylon_ticket_ref": { + "name": "pylon_ticket_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_subject_resolution": { + "name": "cloud_subject_resolution", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_subject_proof_ref": { + "name": "cloud_subject_proof_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preflight_attention_code": { + "name": "preflight_attention_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_progress_at": { + "name": "last_progress_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "anonymized_at": { + "name": "anonymized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_user_deletion_requests_active_email_hmac": { + "name": "UQ_user_deletion_requests_active_email_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"target_email_hmac\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_deletion_requests_active_user_id": { + "name": "UQ_user_deletion_requests_active_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"user_id\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_deletion_requests_active_pylon_ticket": { + "name": "UQ_user_deletion_requests_active_pylon_ticket", + "columns": [ + { + "expression": "regexp_replace(\"pylon_ticket_ref\", '^#', '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"pylon_ticket_ref\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_fairness": { + "name": "IDX_user_deletion_requests_fairness", + "columns": [ + { + "expression": "last_progress_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_email_hmac": { + "name": "IDX_user_deletion_requests_email_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_user_id": { + "name": "IDX_user_deletion_requests_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_requests_user_id_kilocode_users_id_fk": { + "name": "user_deletion_requests_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_deletion_requests_requested_by_kilo_user_id_kilocode_users_id_fk": { + "name": "user_deletion_requests_requested_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_requests_status_check": { + "name": "user_deletion_requests_status_check", + "value": "\"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing', 'completed', 'cancelled')" + }, + "user_deletion_requests_cloud_subject_resolution_check": { + "name": "user_deletion_requests_cloud_subject_resolution_check", + "value": "\"user_deletion_requests\".\"cloud_subject_resolution\" IN ('current_user', 'authoritative_absence', 'prior_queue_cleanup', 'legacy_identity_unresolved', 'unresolved')" + }, + "user_deletion_requests_catalog_version_positive": { + "name": "user_deletion_requests_catalog_version_positive", + "value": "\"user_deletion_requests\".\"catalog_version\" >= 1" + }, + "user_deletion_requests_completed_at_check": { + "name": "user_deletion_requests_completed_at_check", + "value": "(\"user_deletion_requests\".\"status\" = 'completed') = (\"user_deletion_requests\".\"completed_at\" IS NOT NULL)" + }, + "user_deletion_requests_cancelled_at_check": { + "name": "user_deletion_requests_cancelled_at_check", + "value": "(\"user_deletion_requests\".\"status\" = 'cancelled') = (\"user_deletion_requests\".\"cancelled_at\" IS NOT NULL)" + }, + "user_deletion_requests_active_email_check": { + "name": "user_deletion_requests_active_email_check", + "value": "(\"user_deletion_requests\".\"status\" NOT IN ('in_progress', 'finalizing') OR \"user_deletion_requests\".\"target_email\" IS NOT NULL) AND (\"user_deletion_requests\".\"status\" NOT IN ('completed', 'cancelled') OR \"user_deletion_requests\".\"target_email\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_steps": { + "name": "user_deletion_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_key": { + "name": "step_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "claimed_until": { + "name": "claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "window_attempt_count": { + "name": "window_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lifetime_attempt_count": { + "name": "lifetime_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "progress_json": { + "name": "progress_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rate_limited_since": { + "name": "rate_limited_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manual_evidence_json": { + "name": "manual_evidence_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_deletion_steps_due": { + "name": "IDX_user_deletion_steps_due", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_deletion_steps\".\"status\" IN ('pending', 'retry_wait', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_steps_request_id": { + "name": "IDX_user_deletion_steps_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_steps_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_steps_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_steps", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_deletion_steps_request_step": { + "name": "UQ_user_deletion_steps_request_step", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "step_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_deletion_steps_step_key_check": { + "name": "user_deletion_steps_step_key_check", + "value": "\"user_deletion_steps\".\"step_key\" IN ('kiloclaw_destroy', 'customerio', 'cli_v1_blobs', 'cli_v2_sessions', 'usage_prompt_prefixes', 'posthog', 'substack', 'anonymize', 'pylon_reply', 'pylon_finalize', 'completion_email', 'pylon_contact', 'csa_support_db')" + }, + "user_deletion_steps_status_check": { + "name": "user_deletion_steps_status_check", + "value": "\"user_deletion_steps\".\"status\" IN ('pending', 'running', 'retry_wait', 'needs_attention', 'manual_action_required', 'succeeded', 'not_applicable', 'manually_verified')" + }, + "user_deletion_steps_window_attempt_count_nonnegative": { + "name": "user_deletion_steps_window_attempt_count_nonnegative", + "value": "\"user_deletion_steps\".\"window_attempt_count\" >= 0" + }, + "user_deletion_steps_lifetime_attempt_count_nonnegative": { + "name": "user_deletion_steps_lifetime_attempt_count_nonnegative", + "value": "\"user_deletion_steps\".\"lifetime_attempt_count\" >= 0" + }, + "user_deletion_steps_claim_fields_check": { + "name": "user_deletion_steps_claim_fields_check", + "value": "(\"user_deletion_steps\".\"claim_token\" IS NULL) = (\"user_deletion_steps\".\"claimed_until\" IS NULL)" + }, + "user_deletion_steps_manual_evidence_check": { + "name": "user_deletion_steps_manual_evidence_check", + "value": "(\"user_deletion_steps\".\"status\" = 'manually_verified') = (\"user_deletion_steps\".\"manual_evidence_json\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_feedback": { + "name": "user_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feedback_for": { + "name": "feedback_for", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "feedback_batch": { + "name": "feedback_batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_feedback_created_at": { + "name": "IDX_user_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_kilo_user_id": { + "name": "IDX_user_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_for": { + "name": "IDX_user_feedback_feedback_for", + "columns": [ + { + "expression": "feedback_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_batch": { + "name": "IDX_user_feedback_feedback_batch", + "columns": [ + { + "expression": "feedback_batch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_source": { + "name": "IDX_user_feedback_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "user_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_github_app_tokens": { + "name": "user_github_app_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_github_app_tokens_user_app": { + "name": "UQ_user_github_app_tokens_user_app", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_github_app_tokens_github_user_app": { + "name": "UQ_user_github_app_tokens_github_user_app", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_github_app_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_github_app_tokens_app_type_check": { + "name": "user_github_app_tokens_app_type_check", + "value": "\"user_github_app_tokens\".\"github_app_type\" IN ('standard', 'lite')" + } + }, + "isRLSEnabled": false + }, + "public.user_model_preferences": { + "name": "user_model_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favorites": { + "name": "favorites", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_selected": { + "name": "last_selected", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_model_preferences_user_id": { + "name": "UQ_user_model_preferences_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_model_preferences_user_id_kilocode_users_id_fk": { + "name": "user_model_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_model_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_moderation_blocks": { + "name": "user_moderation_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "blocker_user_id": { + "name": "blocker_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blocked_github_login": { + "name": "blocked_github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_moderation_blocks_blocker_login": { + "name": "UQ_user_moderation_blocks_blocker_login", + "columns": [ + { + "expression": "blocker_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blocked_github_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_moderation_mutes": { + "name": "user_moderation_mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "blocker_user_id": { + "name": "blocker_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "muted_github_login": { + "name": "muted_github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_moderation_mutes_blocker_login": { + "name": "UQ_user_moderation_mutes_blocker_login", + "columns": [ + { + "expression": "blocker_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "muted_github_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_notification_preferences": { + "name": "user_notification_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_push_enabled": { + "name": "agent_push_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "chat_messages_enabled": { + "name": "chat_messages_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "agent_attention_enabled": { + "name": "agent_attention_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "session_status_enabled": { + "name": "session_status_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kiloclaw_activity_enabled": { + "name": "kiloclaw_activity_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "balance_alerts_enabled": { + "name": "balance_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "security_findings_enabled": { + "name": "security_findings_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notification_previews": { + "name": "notification_previews", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'generic'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notification_preferences_user_id_kilocode_users_id_fk": { + "name": "user_notification_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_notification_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_period_cache": { + "name": "user_period_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cache_type": { + "name": "cache_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_type": { + "name": "period_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_key": { + "name": "period_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "shared_url_token": { + "name": "shared_url_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_user_period_cache_kilo_user_id": { + "name": "IDX_user_period_cache_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_period_cache": { + "name": "UQ_user_period_cache", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cache_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_period_cache_lookup": { + "name": "IDX_user_period_cache_lookup", + "columns": [ + { + "expression": "cache_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_period_cache_share_token": { + "name": "UQ_user_period_cache_share_token", + "columns": [ + { + "expression": "shared_url_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_period_cache\".\"shared_url_token\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_period_cache_kilo_user_id_kilocode_users_id_fk": { + "name": "user_period_cache_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_period_cache", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_period_cache_period_type_check": { + "name": "user_period_cache_period_type_check", + "value": "\"user_period_cache\".\"period_type\" IN ('year', 'quarter', 'month', 'week', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.user_push_tokens": { + "name": "user_push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_version": { + "name": "app_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_push_tokens_token": { + "name": "UQ_user_push_tokens_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_push_tokens_user_id": { + "name": "IDX_user_push_tokens_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_push_tokens_user_id_kilocode_users_id_fk": { + "name": "user_push_tokens_user_id_kilocode_users_id_fk", + "tableFrom": "user_push_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_terms_acceptances": { + "name": "user_terms_acceptances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "terms_version": { + "name": "terms_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "age_posture": { + "name": "age_posture", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'13_plus'" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_terms_acceptances_user_version": { + "name": "UQ_user_terms_acceptances_user_version", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terms_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_city": { + "name": "vercel_ip_city", + "schema": "", + "columns": { + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_city": { + "name": "vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_city": { + "name": "UQ_vercel_ip_city", + "columns": [ + { + "expression": "vercel_ip_city", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_country": { + "name": "vercel_ip_country", + "schema": "", + "columns": { + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_country": { + "name": "vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_country": { + "name": "UQ_vercel_ip_country", + "columns": [ + { + "expression": "vercel_ip_country", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_events": { + "name": "webhook_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_action": { + "name": "event_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "handlers_triggered": { + "name": "handlers_triggered", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "event_signature": { + "name": "event_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_webhook_events_owned_by_org_id": { + "name": "IDX_webhook_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_owned_by_user_id": { + "name": "IDX_webhook_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_platform": { + "name": "IDX_webhook_events_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_event_type": { + "name": "IDX_webhook_events_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_created_at": { + "name": "IDX_webhook_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_events_owned_by_organization_id_organizations_id_fk": { + "name": "webhook_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "webhook_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "webhook_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "webhook_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_webhook_events_signature": { + "name": "UQ_webhook_events_signature", + "nullsNotDistinct": false, + "columns": [ + "event_signature" + ] + } + }, + "policies": {}, + "checkConstraints": { + "webhook_events_owner_check": { + "name": "webhook_events_owner_check", + "value": "(\n (\"webhook_events\".\"owned_by_user_id\" IS NOT NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"webhook_events\".\"owned_by_user_id\" IS NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.microdollar_usage_view": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n mu.id,\n mu.kilo_user_id,\n meta.message_id,\n mu.cost,\n mu.input_tokens,\n mu.output_tokens,\n mu.cache_write_tokens,\n mu.cache_hit_tokens,\n mu.created_at,\n ip.http_ip AS http_x_forwarded_for,\n city.vercel_ip_city AS http_x_vercel_ip_city,\n country.vercel_ip_country AS http_x_vercel_ip_country,\n meta.vercel_ip_latitude AS http_x_vercel_ip_latitude,\n meta.vercel_ip_longitude AS http_x_vercel_ip_longitude,\n ja4.ja4_digest AS http_x_vercel_ja4_digest,\n mu.provider,\n mu.model,\n mu.requested_model,\n meta.user_prompt_prefix,\n spp.system_prompt_prefix,\n meta.system_prompt_length,\n ua.http_user_agent,\n mu.cache_discount,\n meta.max_tokens,\n meta.has_middle_out_transform,\n mu.has_error,\n mu.abuse_classification,\n mu.organization_id,\n mu.inference_provider,\n mu.project_id,\n meta.status_code,\n meta.upstream_id,\n frfr.finish_reason,\n meta.latency,\n meta.moderation_latency,\n meta.generation_time,\n meta.is_byok,\n meta.is_user_byok,\n meta.streamed,\n meta.cancelled,\n edit.editor_name,\n ak.api_kind,\n meta.has_tools,\n meta.machine_id,\n feat.feature,\n meta.session_id,\n md.mode,\n am.auto_model,\n meta.market_cost,\n meta.is_free,\n meta.abuse_delay,\n meta.abuse_downgraded_from\n FROM \"microdollar_usage\" mu\n LEFT JOIN \"microdollar_usage_metadata\" meta ON mu.id = meta.id\n LEFT JOIN \"http_ip\" ip ON meta.http_ip_id = ip.http_ip_id\n LEFT JOIN \"vercel_ip_city\" city ON meta.vercel_ip_city_id = city.vercel_ip_city_id\n LEFT JOIN \"vercel_ip_country\" country ON meta.vercel_ip_country_id = country.vercel_ip_country_id\n LEFT JOIN \"ja4_digest\" ja4 ON meta.ja4_digest_id = ja4.ja4_digest_id\n LEFT JOIN \"system_prompt_prefix\" spp ON meta.system_prompt_prefix_id = spp.system_prompt_prefix_id\n LEFT JOIN \"http_user_agent\" ua ON meta.http_user_agent_id = ua.http_user_agent_id\n LEFT JOIN \"finish_reason\" frfr ON meta.finish_reason_id = frfr.finish_reason_id\n LEFT JOIN \"editor_name\" edit ON meta.editor_name_id = edit.editor_name_id\n LEFT JOIN \"api_kind\" ak ON meta.api_kind_id = ak.api_kind_id\n LEFT JOIN \"feature\" feat ON meta.feature_id = feat.feature_id\n LEFT JOIN \"mode\" md ON meta.mode_id = md.mode_id\n LEFT JOIN \"auto_model\" am ON meta.auto_model_id = am.auto_model_id\n", + "name": "microdollar_usage_view", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index c68d7043bd..4b0a1c0756 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1639,6 +1639,13 @@ "when": 1787799247206, "tag": "0233_big_abomination", "breakpoints": true + }, + { + "idx": 234, + "version": "7", + "when": 1787871494851, + "tag": "0234_smiling_natasha_romanoff", + "breakpoints": true } ] } \ No newline at end of file From cdd48d95f7b815fe9a504ccf39ad61bb1b7cd36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 00:59:34 +0200 Subject: [PATCH 10/43] chore(deps): reconcile lockfile after main merge --- pnpm-lock.yaml | 82 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34ee43dde1..14b1f316ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2044,7 +2044,7 @@ importers: version: 0.3.7 '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@kilocode/sdk': specifier: 7.4.20 version: 7.4.20 @@ -2195,7 +2195,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/jest': specifier: 30.0.0 version: 30.0.0 @@ -2357,7 +2357,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/node': specifier: 'catalog:' version: 24.12.4 @@ -2532,7 +2532,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@types/node@25.5.2)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@types/node@25.5.2)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@typescript/native-preview': specifier: 'catalog:' version: 7.0.0-dev.20260514.1 @@ -2639,7 +2639,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/content-disposition': specifier: 0.5.9 version: 0.5.9 @@ -2996,7 +2996,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/node': specifier: 'catalog:' version: 24.12.4 @@ -3033,7 +3033,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/node': specifier: 'catalog:' version: 24.12.4 @@ -3300,7 +3300,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/node': specifier: 'catalog:' version: 24.12.4 @@ -20568,6 +20568,38 @@ snapshots: - bufferutil - utf-8-validate + '@cloudflare/vitest-pool-workers@0.16.13(@types/node@24.12.4)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6)': + dependencies: + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + cjs-module-lexer: 1.2.3 + esbuild: 0.28.1 + miniflare: 4.20260603.0(@types/node@24.12.4)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@29.1.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + wrangler: 4.98.0(@types/node@24.12.4)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + zod: 3.25.76 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@types/node' + - bufferutil + - utf-8-validate + + '@cloudflare/vitest-pool-workers@0.16.13(@types/node@25.5.2)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6)': + dependencies: + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + cjs-module-lexer: 1.2.3 + esbuild: 0.28.1 + miniflare: 4.20260603.0(@types/node@25.5.2)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@29.1.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + wrangler: 4.98.0(@types/node@25.5.2)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + zod: 3.25.76 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@types/node' + - bufferutil + - utf-8-validate + '@cloudflare/workerd-darwin-64@1.20260603.1': optional: true @@ -38382,6 +38414,40 @@ snapshots: - bufferutil - utf-8-validate + wrangler@4.98.0(@types/node@24.12.4)(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260603.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260603.0(@types/node@24.12.4)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + path-to-regexp: 8.4.2 + unenv: 2.0.0-rc.24 + workerd: 1.20260603.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + wrangler@4.98.0(@types/node@25.5.2)(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260603.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260603.0(@types/node@25.5.2)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + path-to-regexp: 8.4.2 + unenv: 2.0.0-rc.24 + workerd: 1.20260603.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + wrap-ansi@10.0.0: dependencies: ansi-styles: 6.2.3 From 99d5319e6d0d7d0e2ba002202187ba42818c9741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 01:10:48 +0200 Subject: [PATCH 11/43] fix(glanceable): resolve lint errors after lint config update --- apps/mobile/src/lib/notifications.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index 486b263690..c19efb26f7 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -6,7 +6,6 @@ import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests, _setSecureStoreForTests, - persistGlanceableSink, } from '@/lib/glanceable/persist'; import { registerGlanceableSink, unregisterGlanceableSink } from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; @@ -317,7 +316,7 @@ function makeFakeSink() { // selected-organization id and the active-user id hint through the module-level // `SecureStore.getItemAsync`, so the mock must answer each key separately. function mockSecureStoreKeys() { - mocks.getItemAsync.mockImplementation(async (key: string) => { + mocks.getItemAsync.mockImplementation((key: string) => { if (key === ACTIVE_USER_ID_KEY) { return 'u1'; } From 00a862845e9cf2c1f2f62bb2178a7dcdca5eeae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 03:02:26 +0200 Subject: [PATCH 12/43] fix(glanceable): settle token races and APNs start stacking --- .../lib/auth/logout-reconciliation.test.ts | 23 +++++++ .../src/lib/auth/logout-reconciliation.ts | 12 ++++ .../src/lib/glanceable/activity-kit-prompt.ts | 5 -- .../glanceable/delivery-registration.test.ts | 55 ++++++++++++++++- .../lib/glanceable/delivery-registration.ts | 26 ++++++-- .../src/lib/glanceable-delivery.test.ts | 60 +++++++++++++++---- .../src/lib/glanceable-delivery.ts | 27 ++++++--- 7 files changed, 179 insertions(+), 29 deletions(-) diff --git a/apps/mobile/src/lib/auth/logout-reconciliation.test.ts b/apps/mobile/src/lib/auth/logout-reconciliation.test.ts index 2fb28536e6..f1f311bae0 100644 --- a/apps/mobile/src/lib/auth/logout-reconciliation.test.ts +++ b/apps/mobile/src/lib/auth/logout-reconciliation.test.ts @@ -34,6 +34,7 @@ vi.mock('@/lib/notifications', () => notificationsMock); import { bumpAuthEpoch } from '@/lib/auth/auth-epoch'; import { attemptLogoutReconciliation, + hasPendingActivityUnregister, resetLogoutReconciliationForTests, TOMBSTONE_MAX_AGE_MS, } from '@/lib/auth/logout-reconciliation'; @@ -304,4 +305,26 @@ describe('attemptLogoutReconciliation', () => { expect(outcome).toEqual({ kind: 'expired-retained' }); }); + + it('reports a pending activity unregister while the tombstone still needs it', async () => { + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue( + makeTombstone({ needsActivityUnregister: true, activityTokens: ['activity-1'] }) + ); + + await expect(hasPendingActivityUnregister()).resolves.toBe(true); + }); + + it('reports no pending activity unregister when the tombstone needs only the push part', async () => { + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue( + makeTombstone({ needsActivityUnregister: false, needsPushUnregister: true }) + ); + + await expect(hasPendingActivityUnregister()).resolves.toBe(false); + }); + + it('reports no pending activity unregister when no tombstone exists', async () => { + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue(null); + + await expect(hasPendingActivityUnregister()).resolves.toBe(false); + }); }); diff --git a/apps/mobile/src/lib/auth/logout-reconciliation.ts b/apps/mobile/src/lib/auth/logout-reconciliation.ts index 130b5ec625..ae5f676f95 100644 --- a/apps/mobile/src/lib/auth/logout-reconciliation.ts +++ b/apps/mobile/src/lib/auth/logout-reconciliation.ts @@ -84,6 +84,18 @@ export async function awaitLogoutReconciliationSettled(): Promise { } } +/** + * True while a tombstone still needs an activity-token unregister. A pending + * reconciliation retry owns those recorded tokens, so a new session must not + * re-register them: the next attempt would delete the new session's rows. This + * covers the spacing-skipped case where `attemptLogoutReconciliation` made no + * new in-flight attempt to await. + */ +export async function hasPendingActivityUnregister(): Promise { + const tombstone = await readLogoutCleanupTombstone(); + return tombstone?.needsActivityUnregister ?? false; +} + async function runReconciliation(userId: string): Promise { const epoch = currentAuthEpoch(); const tombstone = await readLogoutCleanupTombstone(); diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts index 1fda2362e3..3d62b8db85 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -66,8 +66,3 @@ export async function recoverGlanceableActivityKit(): Promise { sink.startOrUpdate(snapshot, { userId, organizationId }); } } - -/** Test-only: drop the once-per-process latch between cases. */ -export function _resetActivityKitPromptForTests(): void { - alertShown = false; -} diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts index da67a81acc..3b31e6fc05 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts @@ -5,6 +5,11 @@ import { buildGlanceableSnapshot } from '@kilocode/app-shared/glanceable-agents- const logoutMock = vi.hoisted(() => ({ attemptLogoutReconciliation: vi.fn(), awaitLogoutReconciliationSettled: vi.fn(), + hasPendingActivityUnregister: vi.fn(), +})); + +const expoWidgetsMock = vi.hoisted(() => ({ + pushToStartListener: null as ((event: { activityPushToStartToken: string }) => void) | null, })); const trpcMock = vi.hoisted(() => ({ @@ -29,7 +34,11 @@ vi.mock('@/lib/trpc', () => ({ }, })); vi.mock('expo-widgets', () => ({ - addPushToStartTokenListener: vi.fn(), + addPushToStartTokenListener: ( + listener: (event: { activityPushToStartToken: string }) => void + ) => { + expoWidgetsMock.pushToStartListener = listener; + }, })); vi.mock('@/glanceable-ios/active-agents-live-activity', () => ({ ActiveAgentsLiveActivity: { @@ -71,6 +80,7 @@ describe('delivery registerTokens', () => { trpcMock.unregisterActivityToken.mutate.mockResolvedValue({ success: true }); logoutMock.attemptLogoutReconciliation.mockResolvedValue({ kind: 'no-tombstone' }); logoutMock.awaitLogoutReconciliationSettled.mockResolvedValue(undefined); + logoutMock.hasPendingActivityUnregister.mockResolvedValue(false); }); afterEach(() => { @@ -302,4 +312,47 @@ describe('delivery registerTokens', () => { const final = await getGlanceableDelivery().unregisterTokens(); expect(final).toEqual({ ok: true, tokens: ['android-token-1'] }); }); + + it('does not register iOS tokens while a pending activity unregister is still recorded', async () => { + logoutMock.hasPendingActivityUnregister.mockResolvedValue(true); + + getGlanceableDelivery().registerTokens(snapshot(), null, 'u1'); + + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + + expect(logoutMock.attemptLogoutReconciliation).toHaveBeenCalledWith('u1'); + expect(trpcMock.registerActivityToken.mutate).not.toHaveBeenCalled(); + }); + + it('does not register the Android token while a pending activity unregister is still recorded', async () => { + platformMock.OS = 'android'; + // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules + _setGetDevicePushTokenForTests(() => Promise.resolve('android-token-1')); + logoutMock.hasPendingActivityUnregister.mockResolvedValue(true); + + getGlanceableDelivery().registerTokens(snapshot(), null, 'u1'); + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + + expect(trpcMock.registerActivityToken.mutate).not.toHaveBeenCalled(); + }); + + it('returns only the failed iOS tokens on a partial unregister failure', async () => { + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'ptt-token' }); + activityMock.getPushToken.mockResolvedValue('activity-token-1'); + + // Tokens are unregistered in gather order (push-to-start first): the first + // unregister fails while the activity unregister succeeds. + trpcMock.unregisterActivityToken.mutate + .mockRejectedValueOnce(new Error('network')) + .mockResolvedValueOnce({ success: true }); + + const result = await getGlanceableDelivery().unregisterTokens(); + + expect(result).toEqual({ ok: false, tokens: ['ptt-token'] }); + expect(trpcMock.unregisterActivityToken.mutate).toHaveBeenCalledTimes(2); + }); }); diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.ts b/apps/mobile/src/lib/glanceable/delivery-registration.ts index 1b928a02e6..663d48154c 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.ts @@ -6,6 +6,7 @@ import { ActiveAgentsLiveActivity } from '@/glanceable-ios/active-agents-live-ac import { attemptLogoutReconciliation, awaitLogoutReconciliationSettled, + hasPendingActivityUnregister, } from '@/lib/auth/logout-reconciliation'; import { trpcClient } from '@/lib/trpc'; @@ -124,6 +125,11 @@ async function registerAndroidOngoingToken( if (epoch !== androidRegisterEpoch) { return; } + if (await hasPendingActivityUnregister()) { + // A pending retry owns the recorded activity tokens; re-registering this + // device token now would only be deleted by the next attempt. + return; + } const token = await getDevicePushTokenLazy()(); if (token === null) { return; @@ -181,6 +187,11 @@ const delivery: GlanceableDelivery = { void attemptLogoutReconciliation(userId); } await awaitLogoutReconciliationSettled(); + if (await hasPendingActivityUnregister()) { + // A pending retry owns the recorded activity tokens; re-registering + // them now would only be deleted by the next reconciliation attempt. + return; + } if (pushToStartToken !== null) { await register({ token: pushToStartToken, @@ -217,9 +228,11 @@ const delivery: GlanceableDelivery = { /** * Gathers the push-to-start token plus the current activity's push token, runs - * each `unregister(token)` in parallel, and reports success plus every token - * it attempted. Never rejects: the caller tombstones `tokens` when `ok` is - * false and retries them at the next authenticated opportunity. + * each `unregister(token)` in parallel, and reports success plus only the + * tokens whose unregister failed. Never rejects: the caller tombstones + * `tokens` when `ok` is false and retries exactly those tokens at the next + * authenticated opportunity, so a partial failure never re-deletes a token + * that already succeeded (and may be re-registered by the new session). */ async function unregisterActivityTokens(): Promise<{ ok: boolean; tokens: string[] }> { const tokens: string[] = []; @@ -246,8 +259,11 @@ async function unregisterActivityTokens(): Promise<{ ok: boolean; tokens: string return ok; }) ); - const ok = results.every(result => result.status === 'fulfilled' && result.value); - return { ok, tokens }; + const failedTokens = tokens.filter((_token, index) => { + const result = results[index]; + return result === undefined || result.status === 'rejected' || !result.value; + }); + return { ok: failedTokens.length === 0, tokens: failedTokens }; } setGlanceableDelivery(delivery); diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts index 776399e189..b05f9ef1a9 100644 --- a/services/notifications/src/lib/glanceable-delivery.test.ts +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import type { ExpoPushMessage } from './expo-push'; import { - apnsEventForTokenKind, + apnsSendsForTokens, buildGlanceableExpoMessages, deliverGlanceableSnapshot, toGlanceableContentState, @@ -51,13 +51,37 @@ function fakeDeps(overrides: Partial = {}): { return { deps, calls }; } -describe('apnsEventForTokenKind', () => { - it('maps the push-to-start token to the start event', () => { - expect(apnsEventForTokenKind('ios_push_to_start')).toBe('start'); +describe('apnsSendsForTokens', () => { + it('sends update only to the activity tokens when one exists, never start to push-to-start', () => { + expect( + apnsSendsForTokens([ + { token: 'ptt-token', kind: 'ios_push_to_start' }, + { token: 'activity-token', kind: 'ios_activity' }, + ]) + ).toEqual([{ token: 'activity-token', event: 'update' }]); }); - it('maps the activity token to the update event', () => { - expect(apnsEventForTokenKind('ios_activity')).toBe('update'); + it('sends start to the push-to-start token when no activity token exists', () => { + expect(apnsSendsForTokens([{ token: 'ptt-token', kind: 'ios_push_to_start' }])).toEqual([ + { token: 'ptt-token', event: 'start' }, + ]); + }); + + it('sends update to every activity token when several are registered', () => { + expect( + apnsSendsForTokens([ + { token: 'ptt-token', kind: 'ios_push_to_start' }, + { token: 'activity-token-1', kind: 'ios_activity' }, + { token: 'activity-token-2', kind: 'ios_activity' }, + ]) + ).toEqual([ + { token: 'activity-token-1', event: 'update' }, + { token: 'activity-token-2', event: 'update' }, + ]); + }); + + it('sends nothing when no iOS token exists', () => { + expect(apnsSendsForTokens([])).toEqual([]); }); }); @@ -128,7 +152,7 @@ describe('deliverGlanceableSnapshot', () => { expect(calls.expoSends).toHaveLength(0); }); - it('delivers the content-state to iOS tokens with the right start/update events', async () => { + it('sends update only to the activity tokens when both kinds are registered', async () => { const iosTokens: IosActivityToken[] = [ { token: 'ptt-token', kind: 'ios_push_to_start' }, { token: 'activity-token', kind: 'ios_activity' }, @@ -144,10 +168,7 @@ describe('deliverGlanceableSnapshot', () => { { token: string; event: string }[], GlanceableApnsContentState, ]; - expect(tokens).toEqual([ - { token: 'ptt-token', event: 'start' }, - { token: 'activity-token', event: 'update' }, - ]); + expect(tokens).toEqual([{ token: 'activity-token', event: 'update' }]); expect(contentState.name).toBe('ActiveAgentsLiveActivity'); const props = JSON.parse(contentState.props) as Record; expect(props.status).toBe('happy'); @@ -160,6 +181,23 @@ describe('deliverGlanceableSnapshot', () => { expect(calls.expoSends).toHaveLength(0); }); + it('sends start to the push-to-start token when no activity token exists', async () => { + const iosTokens: IosActivityToken[] = [{ token: 'ptt-token', kind: 'ios_push_to_start' }]; + const { deps, calls } = fakeDeps({ + listIosActivityTokens: vi.fn(async () => iosTokens), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: 'org-1' }, deps); + + expect(calls.iosSends).toHaveLength(1); + const [tokens] = calls.iosSends[0] as [ + { token: string; event: string }[], + GlanceableApnsContentState, + ]; + expect(tokens).toEqual([{ token: 'ptt-token', event: 'start' }]); + expect(calls.expoSends).toHaveLength(0); + }); + it('skips Android when no android_ongoing activity token exists', async () => { const { deps, calls } = fakeDeps({ hasAndroidOngoingToken: vi.fn(async () => false), diff --git a/services/notifications/src/lib/glanceable-delivery.ts b/services/notifications/src/lib/glanceable-delivery.ts index 25deea8865..62f922dd6f 100644 --- a/services/notifications/src/lib/glanceable-delivery.ts +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -30,8 +30,23 @@ export type GlanceableApnsContentState = { export type IosActivityToken = { token: string; kind: 'ios_activity' | 'ios_push_to_start' }; export type ExpoPushToken = { token: string; locale: string | null }; -export function apnsEventForTokenKind(kind: IosActivityToken['kind']): LiveActivityEvent { - return kind === 'ios_push_to_start' ? 'start' : 'update'; +/** + * Maps the registered iOS activity tokens to the APNs sends for one delivery. + * When an `ios_activity` token exists, that Live Activity is already on screen, + * so send `update` to those tokens only — never `start`, which would stack a + * second activity. Only when no `ios_activity` token exists does the + * still-registered push-to-start token get `start` to create one. + */ +export function apnsSendsForTokens( + tokens: readonly IosActivityToken[] +): { token: string; event: LiveActivityEvent }[] { + const activityTokens = tokens.filter(token => token.kind === 'ios_activity'); + if (activityTokens.length > 0) { + return activityTokens.map(({ token }) => ({ token, event: 'update' })); + } + return tokens + .filter(token => token.kind === 'ios_push_to_start') + .map(({ token }) => ({ token, event: 'start' })); } export function toGlanceableContentState( @@ -113,11 +128,9 @@ export async function deliverGlanceableSnapshot( const contentState = toGlanceableContentState(snapshot); const iosTokens = await deps.listIosActivityTokens(params.userId, params.organizationId); - if (iosTokens.length > 0) { - await deps.sendIosLiveActivity( - iosTokens.map(({ token, kind }) => ({ token, event: apnsEventForTokenKind(kind) })), - contentState - ); + const iosSends = apnsSendsForTokens(iosTokens); + if (iosSends.length > 0) { + await deps.sendIosLiveActivity(iosSends, contentState); } // iOS Expo tokens always need the data-only wake: it drives the widget From ef6ca31cfab66f39b20f3ab16aafad56e8e4aa30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 03:03:35 +0200 Subject: [PATCH 13/43] docs(glanceable): correct unregisterTokens contract comment --- apps/mobile/src/lib/glanceable/sink-registry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/lib/glanceable/sink-registry.ts b/apps/mobile/src/lib/glanceable/sink-registry.ts index e340bce554..e518ff1da5 100644 --- a/apps/mobile/src/lib/glanceable/sink-registry.ts +++ b/apps/mobile/src/lib/glanceable/sink-registry.ts @@ -36,8 +36,8 @@ export function getGlanceableSinks(): readonly GlanceableSink[] { /** * Activity-token registrar, set by a later token slice. No-op by default. - * `unregisterTokens` reports success plus the tokens it attempted, so logout - * can tombstone a failed unregister and retry those exact tokens later. + * `unregisterTokens` reports only the tokens whose unregister failed, so + * logout can tombstone the failed tokens and retry them later. */ export type GlanceableDelivery = { registerTokens( From f617f31cef202333e4dfc6b502eb2e00de777f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 06:17:56 +0200 Subject: [PATCH 14/43] fix(mobile): fence activity teardown and isolate unread tests --- .../src/glanceable-ios/ios-sink.test.ts | 111 +++++++++++++++++- apps/mobile/src/glanceable-ios/ios-sink.ts | 51 ++++---- .../unread-counts-invalidation-mount.test.ts | 44 ++----- 3 files changed, 147 insertions(+), 59 deletions(-) diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index a8c45ba6f4..8a1e4b3074 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -47,7 +47,7 @@ const mockState = vi.hoisted(() => ({ startError: null as { code: string; message: string } | null, instancesError: null as { code: string; message: string } | null, instances: [] as unknown[], - started: [] as { props: unknown; url?: string }[], + started: [] as { props: unknown; url?: string; ended: boolean }[], updated: [] as unknown[], snapshots: [] as unknown[], timelines: [] as { date: Date; props: unknown }[][], @@ -64,18 +64,25 @@ vi.mock('expo-widgets', () => ({ error.code = mockState.startError.code; throw error; } - mockState.started.push({ props, url }); - return { + const state = { props, url, ended: false }; + mockState.started.push(state); + const instance = { update: async (next: unknown) => { mockState.updated.push(next); if (mockState.updatePromise !== null) { await mockState.updatePromise; } + state.props = next; }, end: (policy: unknown, finalProps?: unknown, contentDate?: unknown) => { + state.ended = true; + state.props = finalProps; + mockState.instances = mockState.instances.filter(current => current !== instance); mockState.ended.push({ policy, props: finalProps, contentDate }); }, }; + mockState.instances.push(instance); + return instance; }, getInstances: () => { if (mockState.instancesError !== null) { @@ -323,6 +330,104 @@ describe('iosSink end', () => { expect(contentDate?.getTime()).toBeGreaterThanOrEqual(nativeWriteTime); }); + it('ends once after the pending update when concurrent ends target the same activity', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(NOW)); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + + const update = Promise.withResolvers(); + mockState.updatePromise = update.promise; + iosSink.publish(snapshotFor([], 1, 'empty')); + iosSink.endImmediate(); + iosSink.endImmediate(); + + await Promise.resolve(); + expect(mockState.started[0]?.ended).toBe(false); + expect(mockState.ended).toEqual([]); + + const nativeWriteTime = NOW + 50; + vi.setSystemTime(new Date(nativeWriteTime)); + update.resolve(undefined); + await vi.waitFor(() => { + expect(mockState.started[0]?.ended).toBe(true); + }); + + expect(mockState.ended).toEqual([ + { + policy: 'immediate', + props: expect.objectContaining({ status: 'empty', running: 0 }), + contentDate: expect.any(Date), + }, + ]); + expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBeGreaterThanOrEqual( + nativeWriteTime + ); + }); + + it('keeps a new activity and its pending update when an older end finishes', async () => { + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 4), CTX); + const oldUpdate = Promise.withResolvers(); + mockState.updatePromise = oldUpdate.promise; + iosSink.publish(snapshotFor([], 5, 'empty')); + iosSink.endImmediate(); + + const newSnapshot = snapshotFor([{ status: 'question' }], 0); + iosSink.publish(newSnapshot); + iosSink.startOrUpdate(newSnapshot, CTX); + const newUpdate = Promise.withResolvers(); + mockState.updatePromise = newUpdate.promise; + iosSink.startOrUpdate(snapshotFor([{ status: 'question' }, { status: 'question' }], 1), CTX); + + oldUpdate.resolve(undefined); + await vi.waitFor(() => { + expect(mockState.started[0]?.ended).toBe(true); + }); + + expect(mockState.started).toMatchObject([ + { ended: true, props: { status: 'empty', running: 0, needsInput: 0 } }, + { ended: false, props: { status: 'happy', running: 0, needsInput: 1 } }, + ]); + + // The older end must not reset the new revision or forget its pending update. + mockState.updatePromise = null; + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + iosSink.endImmediate(); + await Promise.resolve(); + expect(mockState.started[1]?.ended).toBe(false); + + newUpdate.resolve(undefined); + await vi.waitFor(() => { + expect(mockState.started[1]?.ended).toBe(true); + }); + expect(mockState.started).toMatchObject([ + { ended: true, props: { status: 'empty', running: 0, needsInput: 0 } }, + { ended: true, props: { status: 'happy', running: 0, needsInput: 2 } }, + ]); + }); + + it('ends with the terminal props and a fresh date after a pending update rejects', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(NOW)); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + + const update = Promise.withResolvers(); + mockState.updatePromise = update.promise; + iosSink.publish(snapshotFor([], 1, 'empty')); + iosSink.endImmediate(); + + const failureTime = NOW + 50; + vi.setSystemTime(new Date(failureTime)); + update.reject(new Error('Native update failed')); + await vi.waitFor(() => { + expect(mockState.started[0]?.ended).toBe(true); + }); + + expect(mockState.started[0]?.props).toMatchObject({ status: 'empty', running: 0 }); + expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBeGreaterThanOrEqual( + failureTime + ); + }); + it('adopts and ends a leftover activity when the handle is null after restart', () => { mockState.instances = [ { diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index c0e628bde0..c953c10367 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -27,6 +27,9 @@ let revision = 0; /** In-flight native `update`; `end` awaits it so its contentDate is never older. */ let inFlightUpdate: Promise | null = null; let lastProps: Partial | null = null; +// Native instances remain discoverable until end settles. Their JS wrappers +// have no stable identity, so do not adopt while any local end is pending. +let pendingEnds = 0; function translate(key: string): string { return i18n.t(key); @@ -52,6 +55,9 @@ function isActivityKitUnavailable(error: unknown): boolean { * leaves denial unset so a later call retries. */ function adoptExistingActivity(): Activity | null { + if (pendingEnds > 0) { + return null; + } try { return ActiveAgentsLiveActivity.getInstances().at(-1) ?? null; } catch (error) { @@ -88,21 +94,32 @@ async function endNow(): Promise { if (activity === null) { return; } + // Detach before yielding so a concurrent start owns independent state. + const endingActivity = activity; + const endingUpdate = inFlightUpdate; + const endingProps = lastProps; + activity = null; + inFlightUpdate = null; + lastProps = null; + revision = 0; + pendingEnds += 1; + // ActivityKit (iOS 17.2+) discards an end whose contentDate is older than the // last content write. Native `update` stamps its own later wall-clock, so wait // for the in-flight update and pass a fresh `Date()` — never the earlier JS // stamp or the snapshot's logical `updatedAt`, which is recorded beforehand. - if (inFlightUpdate !== null) { + if (endingUpdate !== null) { try { - await inFlightUpdate; + await endingUpdate; } catch { // A rejected update must not block the end; the contentDate still advances. } } - void activity.end('immediate', lastProps ?? undefined, new Date()); - inFlightUpdate = null; - activity = null; - revision = 0; + try { + await endingActivity.end('immediate', endingProps ?? undefined, new Date()); + } finally { + pendingEnds -= 1; + } } /** True once ActivityKit reported the surface unavailable (see slice psh for the alert). */ @@ -138,6 +155,7 @@ export function _resetIosSinkForTests(): void { revision = 0; inFlightUpdate = null; lastProps = null; + pendingEnds = 0; } export const iosSink: GlanceableSink = { @@ -182,23 +200,12 @@ export const iosSink: GlanceableSink = { if (activity === null) { // Adopt the newest existing instance before starting a second one, so a // process restart updates the activity it started earlier. - let adopted = false; - try { - const instances = ActiveAgentsLiveActivity.getInstances(); - const newest = instances.at(-1); - if (newest !== undefined) { - activity = newest; - inFlightUpdate = null; - adopted = true; - } - } catch (error) { - if (isActivityKitUnavailable(error)) { - activityKitDeniedState = true; - return; - } - // A transient getInstances failure leaves activity null; the start - // below still runs, so a later emit can retry. + activity = adoptExistingActivity(); + if (getActivityKitDenied()) { + return; } + const adopted = activity !== null; + inFlightUpdate = null; if (activity === null) { try { diff --git a/apps/mobile/src/lib/unread-counts-invalidation-mount.test.ts b/apps/mobile/src/lib/unread-counts-invalidation-mount.test.ts index abebfaea93..548e5ef4d9 100644 --- a/apps/mobile/src/lib/unread-counts-invalidation-mount.test.ts +++ b/apps/mobile/src/lib/unread-counts-invalidation-mount.test.ts @@ -69,50 +69,26 @@ vi.mock('@/components/kilo-chat/hooks/use-current-user-id', () => ({ useCurrentUserId: () => testState.currentUserId, })); -vi.mock('expo-constants', () => ({ - default: { - expoConfig: { - extra: { - eas: { - projectId: 'project-1', - }, - }, - }, - }, -})); - vi.mock('expo-notifications', () => ({ addNotificationReceivedListener: mocks.addNotificationReceivedListener, - PermissionStatus: { - GRANTED: 'granted', - }, -})); - -vi.mock('expo-router', () => ({ - router: { - replace: vi.fn(), - }, })); vi.mock('react-native', () => ({ AppState: { addEventListener: mocks.addAppStateListener, }, - Platform: { - OS: 'ios', - }, })); -vi.mock('@sentry/react-native', () => ({ - captureException: vi.fn(), -})); - -// `@/lib/notifications` imports `@/lib/analytics/posthog`, which imports -// expo-application (and friends), failing this node suite with `__DEV__ is not -// defined`. Mock posthog with the single export `notifications.ts` references. -vi.mock('@/lib/analytics/posthog', () => ({ - captureEvent: vi.fn(), -})); +// Keep the shared payload validation without loading native notification wiring. +vi.mock('@/lib/notifications', async () => { + const { pushDataSchema } = await import('@kilocode/notifications'); + return { + parseNotificationData: (data: unknown) => { + const parsed = pushDataSchema.safeParse(data); + return parsed.success ? parsed.data : null; + }, + }; +}); beforeEach(() => { testState.appStateListeners = []; From 99eb44036e6c758f423527e5e68567788f4c3f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 07:03:26 +0200 Subject: [PATCH 15/43] fix(mobile): preserve cleanup and order scope token mutations --- .../src/lib/auth/logout-cleanup.test.ts | 159 ++++++++++++++++ apps/mobile/src/lib/auth/logout-cleanup.ts | 57 +++++- .../lib/auth/logout-reconciliation.test.ts | 37 +++- .../src/lib/auth/logout-reconciliation.ts | 29 ++- .../glanceable/delivery-registration.test.ts | 178 +++++++++++++++++- .../lib/glanceable/delivery-registration.ts | 107 ++++++----- 6 files changed, 503 insertions(+), 64 deletions(-) diff --git a/apps/mobile/src/lib/auth/logout-cleanup.test.ts b/apps/mobile/src/lib/auth/logout-cleanup.test.ts index 21ca6c39be..501cfb2d80 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.test.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.test.ts @@ -20,6 +20,7 @@ vi.mock('@sentry/react-native', () => ({ captureException: vi.fn() })); const trpcMock = vi.hoisted(() => ({ revokeCurrentDeviceSession: { mutate: vi.fn() }, unregisterPushToken: { mutate: vi.fn() }, + unregisterActivityToken: { mutate: vi.fn() }, })); const deliveryMock = vi.hoisted(() => ({ @@ -60,6 +61,11 @@ import { runLogoutCleanup, unregisterActivityTokensAndTombstone, } from '@/lib/auth/logout-cleanup'; +import { + attemptLogoutReconciliation, + hasPendingActivityUnregister, + resetLogoutReconciliationForTests, +} from '@/lib/auth/logout-reconciliation'; import { getDevicePushTokenOutcome } from '@/lib/notifications'; import { getActiveToken } from '@/lib/auth/token-owner'; import { queryClient } from '@/lib/query-client'; @@ -332,6 +338,7 @@ describe('runLogoutCleanup', () => { describe('unregisterActivityTokensAndTombstone', () => { beforeEach(() => { vi.clearAllMocks(); + resetLogoutReconciliationForTests(); store.clear(); seedUser('u1'); deliveryMock.unregisterTokens.mockResolvedValue({ ok: true, tokens: [] }); @@ -388,6 +395,158 @@ describe('unregisterActivityTokensAndTombstone', () => { }); }); + it.each(['push-1', null])( + 'preserves same-owner push cleanup and failed activity tokens (%s)', + async pushToken => { + store.set( + LOGOUT_CLEANUP_TOMBSTONE_KEY, + JSON.stringify({ + userId: 'u1', + pushToken, + needsPushUnregister: true, + needsActivityUnregister: true, + activityTokens: ['earlier-activity', 'shared-activity'], + failedAt: Date.now(), + }) + ); + deliveryMock.unregisterTokens.mockResolvedValue({ + ok: false, + tokens: ['shared-activity', 'new-activity'], + }); + + await unregisterActivityTokensAndTombstone(); + + expect(await readLogoutCleanupTombstone()).toMatchObject({ + userId: 'u1', + pushToken, + needsPushUnregister: true, + needsActivityUnregister: true, + activityTokens: ['earlier-activity', 'shared-activity', 'new-activity'], + }); + } + ); + + it("does not transfer another known owner's pending tokens into the new cleanup", async () => { + store.set( + LOGOUT_CLEANUP_TOMBSTONE_KEY, + JSON.stringify({ + userId: 'u2', + pushToken: 'other-push', + needsPushUnregister: true, + needsActivityUnregister: true, + activityTokens: ['other-activity'], + failedAt: Date.now(), + }) + ); + deliveryMock.unregisterTokens.mockResolvedValue({ ok: false, tokens: ['current-activity'] }); + + await unregisterActivityTokensAndTombstone(); + + expect(await readLogoutCleanupTombstone()).toMatchObject({ + userId: 'u1', + pushToken: null, + needsPushUnregister: false, + needsActivityUnregister: true, + activityTokens: ['current-activity'], + }); + }); + + it('waits for overlapping cleanup writes before allowing the registration guard to settle', async () => { + const { setItemAsync } = await import('expo-secure-store'); + const writeGate = Promise.withResolvers(); + let writing = false; + vi.mocked(setItemAsync).mockImplementationOnce(async (key, value) => { + writing = true; + await writeGate.promise; + store.set(key, value); + }); + deliveryMock.unregisterTokens + .mockResolvedValueOnce({ ok: false, tokens: ['first-activity'] }) + .mockResolvedValueOnce({ ok: false, tokens: ['second-activity'] }); + + const first = unregisterActivityTokensAndTombstone(); + await vi.waitFor(() => { + expect(writing).toBe(true); + }); + const second = unregisterActivityTokensAndTombstone(); + let pending: boolean | undefined = undefined; + const guard = (async () => { + pending = await hasPendingActivityUnregister('u1'); + })(); + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + const pendingBeforeWrite = pending; + writeGate.resolve(undefined); + await Promise.all([first, second, guard]); + + expect(pendingBeforeWrite).toBeUndefined(); + expect(pending).toBe(true); + expect(await readLogoutCleanupTombstone()).toMatchObject({ + needsActivityUnregister: true, + activityTokens: ['first-activity', 'second-activity'], + }); + }); + + it.each([ + { path: 'successful deletion', pushSucceeds: true, activityTokens: [] }, + { path: 'partial-success rewrite', pushSucceeds: false, activityTokens: ['earlier-activity'] }, + ])( + 'preserves later scope cleanup after reconciliation $path', + async ({ pushSucceeds, activityTokens }) => { + store.set( + LOGOUT_CLEANUP_TOMBSTONE_KEY, + JSON.stringify({ + userId: 'u1', + pushToken: 'push-1', + needsPushUnregister: true, + needsActivityUnregister: activityTokens.length > 0, + activityTokens, + failedAt: Date.now(), + }) + ); + const pushStarted = Promise.withResolvers(); + const pushGate = Promise.withResolvers(); + trpcMock.unregisterPushToken.mutate.mockImplementationOnce(async () => { + pushStarted.resolve(undefined); + await pushGate.promise; + if (!pushSucceeds) { + throw new Error('network down'); + } + return { success: true }; + }); + trpcMock.unregisterActivityToken.mutate.mockResolvedValue({ success: true }); + deliveryMock.unregisterTokens.mockResolvedValue({ ok: false, tokens: ['later-activity'] }); + + // Keep the auth epoch unchanged, as an organization switch does. + const attempt = attemptLogoutReconciliation('u1'); + await pushStarted.promise; + const cleanup = unregisterActivityTokensAndTombstone(); + // Let the failed scope cleanup reach its tombstone merge while the + // reconciliation still holds the older record. + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + pushGate.resolve(undefined); + await Promise.all([attempt, cleanup]); + + const tombstone = await readLogoutCleanupTombstone(); + expect(tombstone).toMatchObject({ + userId: 'u1', + needsActivityUnregister: true, + activityTokens: ['later-activity'], + }); + if (!pushSucceeds) { + expect(tombstone).toMatchObject({ + pushToken: 'push-1', + needsPushUnregister: true, + }); + } + expect(await attemptLogoutReconciliation('u1')).toEqual({ kind: 'spacing-skipped' }); + await expect(hasPendingActivityUnregister('u1')).resolves.toBe(true); + } + ); + it('never throws when the unregister itself rejects', async () => { deliveryMock.unregisterTokens.mockRejectedValue(new Error('network down')); diff --git a/apps/mobile/src/lib/auth/logout-cleanup.ts b/apps/mobile/src/lib/auth/logout-cleanup.ts index 9afb75131e..66d1b88dc3 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.ts @@ -4,6 +4,7 @@ import * as z from 'zod'; import { emitNotificationTokenUpdated, getDevicePushTokenOutcome } from '@/lib/notifications'; import { getGlanceableDelivery } from '@/lib/glanceable/sink-registry'; +import { chainSave } from '@/lib/hooks/save-chain'; import { readCachedUserId } from '@/lib/persist/read-cache'; import { queryClient } from '@/lib/query-client'; import { LOGOUT_CLEANUP_TOMBSTONE_KEY } from '@/lib/storage-keys'; @@ -168,6 +169,15 @@ export async function runLogoutCleanup(): Promise { } } +let activityCleanupInFlight: Promise | null = null; + +/** Wait for scope cleanup, including its tombstone write, before checking registration safety. */ +export async function awaitActivityCleanupSettled(): Promise { + if (activityCleanupInFlight !== null) { + await activityCleanupInFlight; + } +} + /** * Unregister the recorded activity tokens (Live Activity / push-to-start) and * tombstone a failure, WITHOUT revoking the device session or unregistering @@ -178,23 +188,52 @@ export async function runLogoutCleanup(): Promise { * new scope registers its own. The cached user id is read before any switch * clears it, so a failed unregister tombstones the prior account's identity — * the same ordering `runLogoutCleanup` relies on. A successful unregister - * leaves any existing tombstone untouched: only a full logout deletes it, so a - * pending push unregister survives a switch. + * leaves any existing tombstone untouched. A failure merges the same owner's + * pending push cleanup and failed activity tokens so both survive a switch. */ export async function unregisterActivityTokensAndTombstone(): Promise { + const previous = activityCleanupInFlight; + const cleanup = (async () => { + await Promise.all([previous, runActivityCleanup(previous)]); + })(); + activityCleanupInFlight = cleanup; + try { + await cleanup; + } finally { + if (activityCleanupInFlight === cleanup) { + activityCleanupInFlight = null; + } + } +} + +async function runActivityCleanup(previous: Promise | null): Promise { try { const userId = readCachedUserId(queryClient); + // Start the unregister now to fence stale registration intent. Serialize + // only the tombstone merge behind earlier scope cleanup writes. const result = await getGlanceableDelivery().unregisterTokens(); + await previous; if (result.ok) { return; } - await writeLogoutCleanupTombstone({ - userId, - pushToken: null, - needsPushUnregister: false, - needsActivityUnregister: true, - activityTokens: result.tokens, - failedAt: Date.now(), + // Reconciliation must finish with its captured record before this merge + // adds obligations that its deletion or partial-success rewrite cannot see. + await chainSave(LOGOUT_CLEANUP_TOMBSTONE_KEY, async () => { + const tombstone = await readLogoutCleanupTombstone(); + const pending = tombstone?.userId === userId ? tombstone : null; + await writeLogoutCleanupTombstone({ + userId, + pushToken: pending?.pushToken ?? null, + needsPushUnregister: pending?.needsPushUnregister ?? false, + needsActivityUnregister: true, + activityTokens: [ + ...new Set([ + ...(pending?.needsActivityUnregister ? pending.activityTokens : []), + ...result.tokens, + ]), + ], + failedAt: Date.now(), + }); }); } catch (error) { // Never throw: a failed unregister or tombstone write must not block the diff --git a/apps/mobile/src/lib/auth/logout-reconciliation.test.ts b/apps/mobile/src/lib/auth/logout-reconciliation.test.ts index f1f311bae0..4a1d3f489a 100644 --- a/apps/mobile/src/lib/auth/logout-reconciliation.test.ts +++ b/apps/mobile/src/lib/auth/logout-reconciliation.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { type LogoutCleanupTombstone } from '@/lib/auth/logout-cleanup'; const cleanupMock = vi.hoisted(() => ({ + awaitActivityCleanupSettled: vi.fn().mockResolvedValue(undefined), readLogoutCleanupTombstone: vi.fn<() => Promise>(), deleteLogoutCleanupTombstone: vi.fn().mockResolvedValue(undefined), writeLogoutCleanupTombstone: vi.fn().mockResolvedValue(undefined), @@ -311,7 +312,37 @@ describe('attemptLogoutReconciliation', () => { makeTombstone({ needsActivityUnregister: true, activityTokens: ['activity-1'] }) ); - await expect(hasPendingActivityUnregister()).resolves.toBe(true); + await expect(hasPendingActivityUnregister('u1')).resolves.toBe(true); + }); + + it.each([ + { owner: 'u1', currentUser: 'u2', pending: false }, + { owner: null, currentUser: 'u2', pending: true }, + { owner: 'u1', currentUser: null, pending: true }, + ])( + 'scopes the activity guard to owner $owner and current user $currentUser', + async ({ owner, currentUser, pending }) => { + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue( + makeTombstone({ + userId: owner, + needsActivityUnregister: true, + activityTokens: ['activity-1'], + }) + ); + + await expect(hasPendingActivityUnregister(currentUser)).resolves.toBe(pending); + } + ); + + it('does not block a new account when the old tombstone survives deletion and the retry is spacing-skipped', async () => { + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue( + makeTombstone({ needsActivityUnregister: true, activityTokens: ['activity-1'] }) + ); + cleanupMock.deleteLogoutCleanupTombstone.mockRejectedValueOnce(new Error('secure store down')); + + await attemptLogoutReconciliation('u2'); + expect(await attemptLogoutReconciliation('u2')).toEqual({ kind: 'spacing-skipped' }); + await expect(hasPendingActivityUnregister('u2')).resolves.toBe(false); }); it('reports no pending activity unregister when the tombstone needs only the push part', async () => { @@ -319,12 +350,12 @@ describe('attemptLogoutReconciliation', () => { makeTombstone({ needsActivityUnregister: false, needsPushUnregister: true }) ); - await expect(hasPendingActivityUnregister()).resolves.toBe(false); + await expect(hasPendingActivityUnregister('u1')).resolves.toBe(false); }); it('reports no pending activity unregister when no tombstone exists', async () => { cleanupMock.readLogoutCleanupTombstone.mockResolvedValue(null); - await expect(hasPendingActivityUnregister()).resolves.toBe(false); + await expect(hasPendingActivityUnregister('u1')).resolves.toBe(false); }); }); diff --git a/apps/mobile/src/lib/auth/logout-reconciliation.ts b/apps/mobile/src/lib/auth/logout-reconciliation.ts index ae5f676f95..a2e065222c 100644 --- a/apps/mobile/src/lib/auth/logout-reconciliation.ts +++ b/apps/mobile/src/lib/auth/logout-reconciliation.ts @@ -1,11 +1,14 @@ import { currentAuthEpoch, isCurrentAuthEpoch } from '@/lib/auth/auth-epoch'; import { + awaitActivityCleanupSettled, deleteLogoutCleanupTombstone, type LogoutCleanupTombstone, readLogoutCleanupTombstone, writeLogoutCleanupTombstone, } from '@/lib/auth/logout-cleanup'; +import { chainSave } from '@/lib/hooks/save-chain'; import { getDevicePushTokenOutcome } from '@/lib/notifications'; +import { LOGOUT_CLEANUP_TOMBSTONE_KEY } from '@/lib/storage-keys'; import { trpcClient } from '@/lib/trpc'; /** @@ -63,7 +66,13 @@ export async function attemptLogoutReconciliation( return { kind: 'spacing-skipped' }; } lastAttemptAtMs = now; - attemptInFlight = runReconciliation(userId); + const epoch = currentAuthEpoch(); + // Serialize the whole read/cleanup/write attempt with scope-cleanup merges. + // Capture auth before queueing so a later account change still fences it. + attemptInFlight = chainSave(LOGOUT_CLEANUP_TOMBSTONE_KEY, async () => { + const outcome = await runReconciliation(userId, epoch); + return outcome; + }); try { return await attemptInFlight; } finally { @@ -89,15 +98,23 @@ export async function awaitLogoutReconciliationSettled(): Promise { * reconciliation retry owns those recorded tokens, so a new session must not * re-register them: the next attempt would delete the new session's rows. This * covers the spacing-skipped case where `attemptLogoutReconciliation` made no - * new in-flight attempt to await. + * new in-flight attempt to await. Wait for scope cleanup to finish its write. + * A different known owner cannot retry under this user's auth; unknown + * ownership remains conservative. */ -export async function hasPendingActivityUnregister(): Promise { +export async function hasPendingActivityUnregister(userId: string | null): Promise { + await awaitActivityCleanupSettled(); const tombstone = await readLogoutCleanupTombstone(); - return tombstone?.needsActivityUnregister ?? false; + return ( + tombstone?.needsActivityUnregister === true && + (userId === null || tombstone.userId === null || tombstone.userId === userId) + ); } -async function runReconciliation(userId: string): Promise { - const epoch = currentAuthEpoch(); +async function runReconciliation( + userId: string, + epoch: number +): Promise { const tombstone = await readLogoutCleanupTombstone(); if (!tombstone) { return { kind: 'no-tombstone' }; diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts index 3b31e6fc05..ee3a4faa84 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines -- one stateful delivery suite shares native mocks and remote-token state across ordering regressions */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { buildGlanceableSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; @@ -52,13 +53,36 @@ vi.mock('react-native', () => ({ import { getGlanceableDelivery } from './sink-registry'; // Import side effect: registers the real delivery under the mocks above. import { - _resetAndroidOngoingTokenForTests, + _resetDeliveryRegistrationForTests, _setGetDevicePushTokenForTests, } from './delivery-registration'; /* eslint-enable import/first */ const NOW = 1_750_000_000_000; +function trackRemoteTokens(): Map { + const rows = new Map(); + trpcMock.registerActivityToken.mutate.mockImplementation( + async (input: { token: string; organizationId: string | null }) => { + await Promise.resolve(undefined); + rows.set(input.token, input.organizationId); + return { success: true }; + } + ); + trpcMock.unregisterActivityToken.mutate.mockImplementation(async (input: { token: string }) => { + await Promise.resolve(undefined); + rows.delete(input.token); + return { success: true }; + }); + return rows; +} + +async function flushRegistration(): Promise { + await new Promise(resolve => { + setTimeout(resolve, 0); + }); +} + function snapshot() { return buildGlanceableSnapshot({ sessions: [{ status: 'busy' }], @@ -74,7 +98,7 @@ describe('delivery registerTokens', () => { vi.clearAllMocks(); platformMock.OS = 'ios'; _setGetDevicePushTokenForTests(null); - _resetAndroidOngoingTokenForTests(); + _resetDeliveryRegistrationForTests(); activityMock.getPushToken.mockResolvedValue('token-1'); trpcMock.registerActivityToken.mutate.mockResolvedValue({ success: true }); trpcMock.unregisterActivityToken.mutate.mockResolvedValue({ success: true }); @@ -340,6 +364,156 @@ describe('delivery registerTokens', () => { expect(trpcMock.registerActivityToken.mutate).not.toHaveBeenCalled(); }); + it('keeps both stable iOS tokens in the new org after the old deletes settle', async () => { + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'stable-start' }); + activityMock.getPushToken.mockResolvedValue('stable-activity'); + getGlanceableDelivery().registerTokens(snapshot(), 'old-org', 'u1'); + await vi.waitFor(() => { + expect(rows).toEqual( + new Map([ + ['stable-start', 'old-org'], + ['stable-activity', 'old-org'], + ]) + ); + }); + + const deleteGate = Promise.withResolvers(); + trpcMock.unregisterActivityToken.mutate.mockImplementation(async (input: { token: string }) => { + await deleteGate.promise; + rows.delete(input.token); + return { success: true }; + }); + const cleanup = getGlanceableDelivery().unregisterTokens(); + getGlanceableDelivery().registerTokens(snapshot(), 'new-org', 'u1'); + await flushRegistration(); + const rowsBeforeDelete = new Map(rows); + + deleteGate.resolve(undefined); + await cleanup; + await flushRegistration(); + + expect(rowsBeforeDelete).toEqual( + new Map([ + ['stable-start', 'old-org'], + ['stable-activity', 'old-org'], + ]) + ); + expect(rows).toEqual( + new Map([ + ['stable-start', 'new-org'], + ['stable-activity', 'new-org'], + ]) + ); + }); + + it.each(['ios', 'android'])( + 'cancels a queued %s registration when a later end supersedes it', + async platform => { + platformMock.OS = platform; + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'stable-token' }); + activityMock.getPushToken.mockResolvedValue(null); + _setGetDevicePushTokenForTests(async () => { + await Promise.resolve(undefined); + return 'stable-token'; + }); + getGlanceableDelivery().registerTokens(snapshot(), 'old-org', 'u1'); + await vi.waitFor(() => { + expect(rows.get('stable-token')).toBe('old-org'); + }); + + const firstDelete = Promise.withResolvers(); + const lastDelete = Promise.withResolvers(); + trpcMock.unregisterActivityToken.mutate + .mockImplementationOnce(async () => { + await firstDelete.promise; + rows.delete('stable-token'); + return { success: true }; + }) + .mockImplementationOnce(async () => { + await lastDelete.promise; + rows.delete('stable-token'); + return { success: true }; + }); + + const firstEnd = getGlanceableDelivery().unregisterTokens(); + getGlanceableDelivery().registerTokens(snapshot(), 'stale-org', 'u1'); + await flushRegistration(); + const lastEnd = getGlanceableDelivery().unregisterTokens(); + firstDelete.resolve(undefined); + await firstEnd; + await flushRegistration(); + const rowsBeforeLastDelete = new Map(rows); + lastDelete.resolve(undefined); + await lastEnd; + + expect(rowsBeforeLastDelete.size).toBe(0); + expect(rows.size).toBe(0); + } + ); + + it.each(['reconciliation', 'token lookup'])( + 'cancels iOS registration paused at %s after a later end', + async phase => { + const rows = trackRemoteTokens(); + const gate = Promise.withResolvers(); + let paused = false; + if (phase === 'reconciliation') { + logoutMock.awaitLogoutReconciliationSettled.mockImplementationOnce(async () => { + paused = true; + await gate.promise; + }); + } else { + activityMock.getPushToken.mockImplementationOnce(async () => { + paused = true; + await gate.promise; + return 'late-token'; + }); + } + getGlanceableDelivery().registerTokens(snapshot(), 'old-org', 'u1'); + await vi.waitFor(() => { + expect(paused).toBe(true); + }); + + await getGlanceableDelivery().unregisterTokens(); + gate.resolve(undefined); + await flushRegistration(); + + expect(rows.size).toBe(0); + } + ); + + it.each(['ios', 'android'])( + 'allows %s registration for a new known account despite an old account cleanup', + async platform => { + platformMock.OS = platform; + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'stable-token' }); + activityMock.getPushToken.mockResolvedValue(null); + _setGetDevicePushTokenForTests(async () => { + await Promise.resolve(undefined); + return 'stable-token'; + }); + logoutMock.attemptLogoutReconciliation.mockResolvedValue({ kind: 'spacing-skipped' }); + logoutMock.hasPendingActivityUnregister.mockImplementation( + async (currentUser: string | null) => { + await Promise.resolve(undefined); + return currentUser !== 'u2'; + } + ); + + getGlanceableDelivery().registerTokens(snapshot(), 'old-org', 'u1'); + await flushRegistration(); + expect(rows.size).toBe(0); + + getGlanceableDelivery().registerTokens(snapshot(), 'new-org', 'u2'); + await flushRegistration(); + + expect(rows).toEqual(new Map([['stable-token', 'new-org']])); + } + ); + it('returns only the failed iOS tokens on a partial unregister failure', async () => { expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'ptt-token' }); activityMock.getPushToken.mockResolvedValue('activity-token-1'); diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.ts b/apps/mobile/src/lib/glanceable/delivery-registration.ts index 663d48154c..5ac971a0dc 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.ts @@ -24,25 +24,24 @@ let pushToStartToken: string | null = null; /** The last Android device token registered, so end/cleanup can unregister it. */ let androidOngoingToken: string | null = null; -/** Epoch bumped on every Android unregister/end. A register that started before +/** Epoch bumped on every unregister/end. A register that started before * the bump must abort instead of recreating the row after end/logout. */ -let androidRegisterEpoch = 0; +let registerEpoch = 0; -/** FIFO chain serializing Android register/unregister mutations so the last - * client intent wins: an upsert and a delete of the same per-device token must - * never race, because the device token is stable across sign-ins. */ -let androidMutationTail: Promise | null = null; +/** FIFO chain serializing activity-token mutations so the last client intent + * wins: an upsert and a delete must not race for stable iOS or Android tokens. */ +let mutationTail: Promise | null = null; const NOOP = (): void => undefined; /** Serialize one mutation; a rejected prior mutation never blocks the next. */ -async function enqueueAndroidMutation(op: () => Promise): Promise { - const previous = androidMutationTail; +async function enqueueTokenMutation(op: () => Promise): Promise { + const previous = mutationTail; let release: () => void = NOOP; const gate = new Promise(resolve => { release = resolve; }); - androidMutationTail = gate; + mutationTail = gate; if (previous !== null) { try { await previous; @@ -116,16 +115,16 @@ async function registerAndroidOngoingToken( ): Promise { // Capture the epoch before the first await so an unregister/end that lands // during reconciliation or the token lookup aborts this stale register. - const epoch = androidRegisterEpoch; + const epoch = registerEpoch; if (userId !== null) { void attemptLogoutReconciliation(userId); } try { await awaitLogoutReconciliationSettled(); - if (epoch !== androidRegisterEpoch) { + if (epoch !== registerEpoch) { return; } - if (await hasPendingActivityUnregister()) { + if (await hasPendingActivityUnregister(userId)) { // A pending retry owns the recorded activity tokens; re-registering this // device token now would only be deleted by the next attempt. return; @@ -134,10 +133,13 @@ async function registerAndroidOngoingToken( if (token === null) { return; } - if (epoch !== androidRegisterEpoch) { + if (epoch !== registerEpoch) { return; } - await enqueueAndroidMutation(async () => { + await enqueueTokenMutation(async () => { + if (epoch !== registerEpoch) { + return; + } await register({ token, kind: 'android_ongoing', platform: 'android', organizationId }); androidOngoingToken = token; }); @@ -151,8 +153,8 @@ async function registerAndroidOngoingToken( * against register so a delete never races an upsert of the same token: the * FIFO order decides the final state. */ async function unregisterAndroidOngoingToken(): Promise<{ ok: boolean; tokens: string[] }> { - androidRegisterEpoch += 1; - const result = enqueueAndroidMutation(async () => { + registerEpoch += 1; + const result = enqueueTokenMutation(async () => { const token = androidOngoingToken; if (token === null) { return { ok: true, tokens: [] as string[] }; @@ -177,35 +179,44 @@ const delivery: GlanceableDelivery = { return; } void (async () => { - // Order against logout cleanup: an in-flight logout unregister for the - // activity tokens must settle before this session re-registers them, or - // a later retry could delete this session's rows. Trigger the logout - // attempt first (it starts a fresh run only when none is running), then - // await its settle so registration cannot start until any in-flight - // unregister for this sign-in has settled. + const epoch = registerEpoch; + // Keep reconciliation and scope-cleanup waits outside the mutation queue: + // cleanup itself needs that queue to finish its unregister. if (userId !== null) { void attemptLogoutReconciliation(userId); } await awaitLogoutReconciliationSettled(); - if (await hasPendingActivityUnregister()) { - // A pending retry owns the recorded activity tokens; re-registering - // them now would only be deleted by the next reconciliation attempt. + if ((await hasPendingActivityUnregister(userId)) || epoch !== registerEpoch) { return; } - if (pushToStartToken !== null) { - await register({ - token: pushToStartToken, - kind: 'ios_push_to_start', - platform: 'ios', - organizationId, + const startToken = pushToStartToken; + if (startToken !== null) { + await enqueueTokenMutation(async () => { + if (epoch !== registerEpoch) { + return; + } + await register({ + token: startToken, + kind: 'ios_push_to_start', + platform: 'ios', + organizationId, + }); }); } + if (epoch !== registerEpoch) { + return; + } try { const activity = ActiveAgentsLiveActivity.getInstances().at(-1); if (activity) { const token = await activity.getPushToken(); if (token) { - await register({ token, kind: 'ios_activity', platform: 'ios', organizationId }); + await enqueueTokenMutation(async () => { + if (epoch !== registerEpoch) { + return; + } + await register({ token, kind: 'ios_activity', platform: 'ios', organizationId }); + }); } } } catch { @@ -221,20 +232,15 @@ const delivery: GlanceableDelivery = { if (Platform.OS !== 'ios') { return { ok: true, tokens: [] }; } - const result = await unregisterActivityTokens(); + registerEpoch += 1; + const tokens = collectIosActivityTokens(); + const result = await enqueueTokenMutation(async () => unregisterActivityTokens(await tokens)); return result; }, }; -/** - * Gathers the push-to-start token plus the current activity's push token, runs - * each `unregister(token)` in parallel, and reports success plus only the - * tokens whose unregister failed. Never rejects: the caller tombstones - * `tokens` when `ok` is false and retries exactly those tokens at the next - * authenticated opportunity, so a partial failure never re-deletes a token - * that already succeeded (and may be re-registered by the new session). - */ -async function unregisterActivityTokens(): Promise<{ ok: boolean; tokens: string[] }> { +/** Capture the current iOS tokens before a later scope replaces the native instance. */ +async function collectIosActivityTokens(): Promise { const tokens: string[] = []; if (pushToStartToken !== null) { tokens.push(pushToStartToken); @@ -250,6 +256,17 @@ async function unregisterActivityTokens(): Promise<{ ok: boolean; tokens: string } catch { // Nothing to unregister when no activity survives. } + return tokens; +} + +/** + * Unregister the captured iOS tokens in parallel and report only failures. + * The caller tombstones those tokens, so a retry never re-deletes a token + * that already succeeded and can belong to the new session. + */ +async function unregisterActivityTokens( + tokens: string[] +): Promise<{ ok: boolean; tokens: string[] }> { if (tokens.length === 0) { return { ok: true, tokens }; } @@ -274,7 +291,9 @@ export function _setGetDevicePushTokenForTests(fn: (() => Promise getDevicePushTokenForTests = fn; } -export function _resetAndroidOngoingTokenForTests(): void { +export function _resetDeliveryRegistrationForTests(): void { androidOngoingToken = null; - androidMutationTail = null; + pushToStartToken = null; + registerEpoch += 1; + mutationTail = null; } From 87f30ee25b343b27f48331bb22398925a83b5c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 07:17:35 +0200 Subject: [PATCH 16/43] test(mobile): isolate native imports in the mounted Agents suite --- .../(app)/(tabs)/(2_agents)/index.mounted.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx index 244351348c..70c0165813 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx @@ -20,6 +20,20 @@ vi.mock('react-native', () => ({ Platform: platformMock, })); +vi.mock('expo-router', async () => { + const { useEffect } = await import('react'); + return { + useFocusEffect: (effect: () => void) => { + useEffect(effect, [effect]); + }, + }; +}); + +vi.mock('@/lib/glanceable/activity-kit-prompt', () => ({ + showActivityKitDisabledAlertOnce: vi.fn(), + recoverGlanceableActivityKit: vi.fn().mockResolvedValue(undefined), +})); + vi.mock('expo-web-browser', () => ({ openAuthSessionAsync: openAuthSessionMock, openBrowserAsync: openBrowserMock, From 27efe1b48e578d292880a508c041520e39ab8497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 10:54:46 +0200 Subject: [PATCH 17/43] feat(i18n): translate active agents update notifications --- packages/notifications/src/locales/af.json | 3 ++- packages/notifications/src/locales/am.json | 3 ++- packages/notifications/src/locales/ar.json | 3 ++- packages/notifications/src/locales/az.json | 3 ++- packages/notifications/src/locales/be.json | 3 ++- packages/notifications/src/locales/bg.json | 3 ++- packages/notifications/src/locales/bn.json | 3 ++- packages/notifications/src/locales/bs.json | 3 ++- packages/notifications/src/locales/ca.json | 3 ++- packages/notifications/src/locales/ckb.json | 3 ++- packages/notifications/src/locales/cs.json | 3 ++- packages/notifications/src/locales/cy.json | 3 ++- packages/notifications/src/locales/da.json | 3 ++- packages/notifications/src/locales/de.json | 3 ++- packages/notifications/src/locales/el.json | 3 ++- packages/notifications/src/locales/es.json | 3 ++- packages/notifications/src/locales/et.json | 3 ++- packages/notifications/src/locales/eu.json | 3 ++- packages/notifications/src/locales/fa.json | 3 ++- packages/notifications/src/locales/fi.json | 3 ++- packages/notifications/src/locales/fil.json | 3 ++- packages/notifications/src/locales/fr.json | 3 ++- packages/notifications/src/locales/ga.json | 3 ++- packages/notifications/src/locales/gl.json | 3 ++- packages/notifications/src/locales/gu.json | 3 ++- packages/notifications/src/locales/ha.json | 3 ++- packages/notifications/src/locales/he.json | 3 ++- packages/notifications/src/locales/hi.json | 3 ++- packages/notifications/src/locales/hr.json | 3 ++- packages/notifications/src/locales/ht.json | 3 ++- packages/notifications/src/locales/hu.json | 3 ++- packages/notifications/src/locales/hy.json | 3 ++- packages/notifications/src/locales/id.json | 3 ++- packages/notifications/src/locales/ig.json | 3 ++- packages/notifications/src/locales/is.json | 3 ++- packages/notifications/src/locales/it.json | 3 ++- packages/notifications/src/locales/ja.json | 3 ++- packages/notifications/src/locales/ka.json | 3 ++- packages/notifications/src/locales/kk.json | 3 ++- packages/notifications/src/locales/km.json | 3 ++- packages/notifications/src/locales/kn.json | 3 ++- packages/notifications/src/locales/ko.json | 3 ++- packages/notifications/src/locales/lo.json | 3 ++- packages/notifications/src/locales/lt.json | 3 ++- packages/notifications/src/locales/lv.json | 3 ++- packages/notifications/src/locales/mg.json | 3 ++- packages/notifications/src/locales/mi.json | 3 ++- packages/notifications/src/locales/mk.json | 3 ++- packages/notifications/src/locales/ml.json | 3 ++- packages/notifications/src/locales/mn.json | 3 ++- packages/notifications/src/locales/mr.json | 3 ++- packages/notifications/src/locales/ms.json | 3 ++- packages/notifications/src/locales/mt.json | 3 ++- packages/notifications/src/locales/my.json | 3 ++- packages/notifications/src/locales/nb.json | 3 ++- packages/notifications/src/locales/ne.json | 3 ++- packages/notifications/src/locales/nl.json | 3 ++- packages/notifications/src/locales/om.json | 3 ++- packages/notifications/src/locales/or.json | 3 ++- packages/notifications/src/locales/pa.json | 3 ++- packages/notifications/src/locales/pl.json | 3 ++- packages/notifications/src/locales/ps.json | 3 ++- packages/notifications/src/locales/pt-BR.json | 3 ++- packages/notifications/src/locales/pt.json | 3 ++- packages/notifications/src/locales/ro.json | 3 ++- packages/notifications/src/locales/ru.json | 3 ++- packages/notifications/src/locales/si.json | 3 ++- packages/notifications/src/locales/sk.json | 3 ++- packages/notifications/src/locales/sl.json | 3 ++- packages/notifications/src/locales/so.json | 3 ++- packages/notifications/src/locales/sq.json | 3 ++- packages/notifications/src/locales/sr.json | 3 ++- packages/notifications/src/locales/sv.json | 3 ++- packages/notifications/src/locales/sw.json | 3 ++- packages/notifications/src/locales/ta.json | 3 ++- packages/notifications/src/locales/te.json | 3 ++- packages/notifications/src/locales/th.json | 3 ++- packages/notifications/src/locales/tr.json | 3 ++- packages/notifications/src/locales/uk.json | 3 ++- packages/notifications/src/locales/ur.json | 3 ++- packages/notifications/src/locales/uz.json | 3 ++- packages/notifications/src/locales/vi.json | 3 ++- packages/notifications/src/locales/yo.json | 3 ++- packages/notifications/src/locales/zh-Hans.json | 3 ++- packages/notifications/src/locales/zh-Hant.json | 3 ++- packages/notifications/src/locales/zu.json | 3 ++- 86 files changed, 172 insertions(+), 86 deletions(-) diff --git a/packages/notifications/src/locales/af.json b/packages/notifications/src/locales/af.json index be8e5ead19..6c8aa7f334 100644 --- a/packages/notifications/src/locales/af.json +++ b/packages/notifications/src/locales/af.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Jou instansie het 'n opdatering", "scheduledAction": "'n Geskeduleerde aksie het 'n opdatering", "lowBalance": "Jou saldo benodig aandag", - "securityFinding": "'n Sekuriteitsbevinding benodig aandag" + "securityFinding": "'n Sekuriteitsbevinding benodig aandag", + "activeAgentsGlanceable": "Aktiewe agente het 'n opdatering" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/am.json b/packages/notifications/src/locales/am.json index 73905cd1ad..1483f82ee1 100644 --- a/packages/notifications/src/locales/am.json +++ b/packages/notifications/src/locales/am.json @@ -7,7 +7,8 @@ "instanceLifecycle": "ምሳሌዎ ማዘመኛ አለው", "scheduledAction": "የተያዘ ተግባር ማዘመኛ አለው", "lowBalance": "ቀሪ ሂሳብዎ ትኩረት ይፈልጋል", - "securityFinding": "የደህንነት ግኝት ትኩረት ይፈልጋል" + "securityFinding": "የደህንነት ግኝት ትኩረት ይፈልጋል", + "activeAgentsGlanceable": "ንቁ ወኪሎች ማዘመኛ አላቸው" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ar.json b/packages/notifications/src/locales/ar.json index 263b1e8415..a4d9681adc 100644 --- a/packages/notifications/src/locales/ar.json +++ b/packages/notifications/src/locales/ar.json @@ -7,7 +7,8 @@ "instanceLifecycle": "المثيل لديك به تحديث", "scheduledAction": "إجراء مجدول به تحديث", "lowBalance": "رصيدك يحتاج إلى انتباه", - "securityFinding": "نتيجة أمان تحتاج إلى انتباه" + "securityFinding": "نتيجة أمان تحتاج إلى انتباه", + "activeAgentsGlanceable": "الوكلاء النشطون لديهم تحديث" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/az.json b/packages/notifications/src/locales/az.json index 0e4504d350..78562e832a 100644 --- a/packages/notifications/src/locales/az.json +++ b/packages/notifications/src/locales/az.json @@ -7,7 +7,8 @@ "instanceLifecycle": "İnstansiyanızda yenilənmə var", "scheduledAction": "Planlaşdırılmış əməliyyatda yenilənmə var", "lowBalance": "Balansınız diqqət tələb edir", - "securityFinding": "Təhlükəsizlik tapıntısı diqqət tələb edir" + "securityFinding": "Təhlükəsizlik tapıntısı diqqət tələb edir", + "activeAgentsGlanceable": "Aktiv agentlərdə yenilənmə var" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/be.json b/packages/notifications/src/locales/be.json index b2bd8bd78a..aa20871bdc 100644 --- a/packages/notifications/src/locales/be.json +++ b/packages/notifications/src/locales/be.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Ваш інстанс мае абнаўленне", "scheduledAction": "Запланаванае дзеянне мае абнаўленне", "lowBalance": "Ваш баланс патрабуе ўвагі", - "securityFinding": "Знаходка ў бяспецы патрабуе ўвагі" + "securityFinding": "Знаходка ў бяспецы патрабуе ўвагі", + "activeAgentsGlanceable": "Актыўныя агенты маюць абнаўленне" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/bg.json b/packages/notifications/src/locales/bg.json index 6c03ccb16f..a15d91aa75 100644 --- a/packages/notifications/src/locales/bg.json +++ b/packages/notifications/src/locales/bg.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Вашата инстанция има актуализация", "scheduledAction": "Планирано действие има актуализация", "lowBalance": "Вашият баланс изисква внимание", - "securityFinding": "Открит проблем със сигурността изисква внимание" + "securityFinding": "Открит проблем със сигурността изисква внимание", + "activeAgentsGlanceable": "Активните агенти имат актуализация" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/bn.json b/packages/notifications/src/locales/bn.json index 803b8fc2a6..9a2121ce39 100644 --- a/packages/notifications/src/locales/bn.json +++ b/packages/notifications/src/locales/bn.json @@ -7,7 +7,8 @@ "instanceLifecycle": "আপনার ইনস্ট্যান্সে একটি আপডেট আছে", "scheduledAction": "একটি নির্ধারিত কর্মে একটি আপডেট আছে", "lowBalance": "আপনার ব্যালেন্সে মনোযোগ প্রয়োজন", - "securityFinding": "একটি নিরাপত্তা সমস্যায় মনোযোগ প্রয়োজন" + "securityFinding": "একটি নিরাপত্তা সমস্যায় মনোযোগ প্রয়োজন", + "activeAgentsGlanceable": "সক্রিয় এজেন্টগুলির একটি আপডেট আছে" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/bs.json b/packages/notifications/src/locales/bs.json index 33d58dc42f..09e17870ef 100644 --- a/packages/notifications/src/locales/bs.json +++ b/packages/notifications/src/locales/bs.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Vaša instanca ima ažuriranje", "scheduledAction": "Zakazana radnja ima ažuriranje", "lowBalance": "Vaš saldo treba pažnju", - "securityFinding": "Sigurnosni nalaz treba pažnju" + "securityFinding": "Sigurnosni nalaz treba pažnju", + "activeAgentsGlanceable": "Aktivni agenti imaju ažuriranje" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ca.json b/packages/notifications/src/locales/ca.json index fc222d8b89..2fe35e803a 100644 --- a/packages/notifications/src/locales/ca.json +++ b/packages/notifications/src/locales/ca.json @@ -7,7 +7,8 @@ "instanceLifecycle": "La teva instància té una actualització", "scheduledAction": "Una acció programada té una actualització", "lowBalance": "El teu saldo necessita atenció", - "securityFinding": "Una troballa de seguretat necessita atenció" + "securityFinding": "Una troballa de seguretat necessita atenció", + "activeAgentsGlanceable": "Els agents actius tenen una actualització" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ckb.json b/packages/notifications/src/locales/ckb.json index 418bb2e57a..3c9d5126da 100644 --- a/packages/notifications/src/locales/ckb.json +++ b/packages/notifications/src/locales/ckb.json @@ -7,7 +7,8 @@ "instanceLifecycle": "دۆخەکەت نوێکراوەتەوە", "scheduledAction": "کردارێکی دیاریکراو نوێکراوەتەوە", "lowBalance": "تەوازنەکەت پێویستی بە سەرنجە", - "securityFinding": "دۆزینەوەیەکی ئاسایش پێویستی بە سەرنجە" + "securityFinding": "دۆزینەوەیەکی ئاسایش پێویستی بە سەرنجە", + "activeAgentsGlanceable": "ئەجێنتە چالاکەکان نوێکارییەکیان هەیە" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/cs.json b/packages/notifications/src/locales/cs.json index 9c5f58bbf9..6e09350106 100644 --- a/packages/notifications/src/locales/cs.json +++ b/packages/notifications/src/locales/cs.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Vaše instance má aktualizaci", "scheduledAction": "Naplánovaná akce má aktualizaci", "lowBalance": "Váš zůstatek vyžaduje pozornost", - "securityFinding": "Nález zabezpečení vyžaduje pozornost" + "securityFinding": "Nález zabezpečení vyžaduje pozornost", + "activeAgentsGlanceable": "Aktivní agenti mají aktualizaci" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/cy.json b/packages/notifications/src/locales/cy.json index cb27ddc464..2317ec1307 100644 --- a/packages/notifications/src/locales/cy.json +++ b/packages/notifications/src/locales/cy.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Mae gan eich achos ddiweddariad", "scheduledAction": "Mae gan weithred wedi'i hamserlennu ddiweddariad", "lowBalance": "Mae angen sylw ar eich balans", - "securityFinding": "Mae angen sylw ar ganfyddiad diogelwch" + "securityFinding": "Mae angen sylw ar ganfyddiad diogelwch", + "activeAgentsGlanceable": "Mae gan asiantau gweithredol ddiweddariad" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/da.json b/packages/notifications/src/locales/da.json index 22b9e32038..b4b6b6541c 100644 --- a/packages/notifications/src/locales/da.json +++ b/packages/notifications/src/locales/da.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Din instans har en opdatering", "scheduledAction": "En planlagt handling har en opdatering", "lowBalance": "Din saldo kræver opmærksomhed", - "securityFinding": "Et sikkerhedsfund kræver opmærksomhed" + "securityFinding": "Et sikkerhedsfund kræver opmærksomhed", + "activeAgentsGlanceable": "Aktive agenter har en opdatering" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/de.json b/packages/notifications/src/locales/de.json index 2f622da889..4790aea598 100644 --- a/packages/notifications/src/locales/de.json +++ b/packages/notifications/src/locales/de.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Ihre Instanz hat ein Update", "scheduledAction": "Eine geplante Aktion hat ein Update", "lowBalance": "Ihr Guthaben benötigt Aufmerksamkeit", - "securityFinding": "Ein Sicherheitsbefund benötigt Aufmerksamkeit" + "securityFinding": "Ein Sicherheitsbefund benötigt Aufmerksamkeit", + "activeAgentsGlanceable": "Aktive Agenten haben ein Update" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/el.json b/packages/notifications/src/locales/el.json index 1e4bb43b7d..f7e69c40a0 100644 --- a/packages/notifications/src/locales/el.json +++ b/packages/notifications/src/locales/el.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Η παρουσία σας έχει μια ενημέρωση", "scheduledAction": "Μια προγραμματισμένη ενέργεια έχει μια ενημέρωση", "lowBalance": "Το υπόλοιπό σας χρειάζεται προσοχή", - "securityFinding": "Ένα εύρημα ασφαλείας χρειάζεται προσοχή" + "securityFinding": "Ένα εύρημα ασφαλείας χρειάζεται προσοχή", + "activeAgentsGlanceable": "Οι ενεργοί πράκτορες έχουν μια ενημέρωση" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/es.json b/packages/notifications/src/locales/es.json index fd3d4fdf0d..e0ab68130e 100644 --- a/packages/notifications/src/locales/es.json +++ b/packages/notifications/src/locales/es.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Tu instancia tiene una actualización", "scheduledAction": "Una acción programada tiene una actualización", "lowBalance": "Tu saldo necesita atención", - "securityFinding": "Un hallazgo de seguridad necesita atención" + "securityFinding": "Un hallazgo de seguridad necesita atención", + "activeAgentsGlanceable": "Los agentes activos tienen una actualización" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/et.json b/packages/notifications/src/locales/et.json index d224814659..526e5034c8 100644 --- a/packages/notifications/src/locales/et.json +++ b/packages/notifications/src/locales/et.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Sinu eksemplaril on uuendus", "scheduledAction": "Planeeritud toimingul on uuendus", "lowBalance": "Sinu saldo vajab tähelepanu", - "securityFinding": "Turbetuvastus vajab tähelepanu" + "securityFinding": "Turbetuvastus vajab tähelepanu", + "activeAgentsGlanceable": "Aktiivsetel agentidel on uuendus" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/eu.json b/packages/notifications/src/locales/eu.json index d1f6d4875d..8624e5a03d 100644 --- a/packages/notifications/src/locales/eu.json +++ b/packages/notifications/src/locales/eu.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Zure instantziak eguneratze bat du", "scheduledAction": "Programatutako ekintza batek eguneratze bat du", "lowBalance": "Zure saldoak arreta behar du", - "securityFinding": "Aurkitutako segurtasun-arazo batek arreta behar du" + "securityFinding": "Aurkitutako segurtasun-arazo batek arreta behar du", + "activeAgentsGlanceable": "Agente aktiboek eguneratze bat dute" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/fa.json b/packages/notifications/src/locales/fa.json index 651287c08b..3aa316bcb6 100644 --- a/packages/notifications/src/locales/fa.json +++ b/packages/notifications/src/locales/fa.json @@ -7,7 +7,8 @@ "instanceLifecycle": "نمونه شما به‌روزرسانی دارد", "scheduledAction": "یک اقدام زمان‌بندی‌شده به‌روزرسانی دارد", "lowBalance": "موجودی شما نیاز به توجه دارد", - "securityFinding": "یک یافته امنیتی نیاز به توجه دارد" + "securityFinding": "یک یافته امنیتی نیاز به توجه دارد", + "activeAgentsGlanceable": "عامل‌های فعال به‌روزرسانی دارند" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/fi.json b/packages/notifications/src/locales/fi.json index 8f62bced7a..5508d26120 100644 --- a/packages/notifications/src/locales/fi.json +++ b/packages/notifications/src/locales/fi.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Instanssissasi on päivitys", "scheduledAction": "Ajoitetussa toiminnossa on päivitys", "lowBalance": "Saldosi vaatii huomiota", - "securityFinding": "Tietoturvalöydös vaatii huomiota" + "securityFinding": "Tietoturvalöydös vaatii huomiota", + "activeAgentsGlanceable": "Aktiivisilla agenteilla on päivitys" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/fil.json b/packages/notifications/src/locales/fil.json index 844050d0b8..9098551356 100644 --- a/packages/notifications/src/locales/fil.json +++ b/packages/notifications/src/locales/fil.json @@ -7,7 +7,8 @@ "instanceLifecycle": "May update ang iyong instance", "scheduledAction": "May update ang isang naka-schedule na aksyon", "lowBalance": "Kailangan ng atensyon ang iyong balance", - "securityFinding": "Kailangan ng atensyon ang isang security finding" + "securityFinding": "Kailangan ng atensyon ang isang security finding", + "activeAgentsGlanceable": "May update ang mga aktibong agent" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/fr.json b/packages/notifications/src/locales/fr.json index fe0fcfca2b..be70aec8e1 100644 --- a/packages/notifications/src/locales/fr.json +++ b/packages/notifications/src/locales/fr.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Votre instance a une mise à jour", "scheduledAction": "Une action planifiée a une mise à jour", "lowBalance": "Votre solde nécessite une attention", - "securityFinding": "Un résultat de sécurité nécessite une attention" + "securityFinding": "Un résultat de sécurité nécessite une attention", + "activeAgentsGlanceable": "Les agents actifs ont une mise à jour" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ga.json b/packages/notifications/src/locales/ga.json index f1d3c9056d..e242295b74 100644 --- a/packages/notifications/src/locales/ga.json +++ b/packages/notifications/src/locales/ga.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Tá nuashonrú ar do chás", "scheduledAction": "Tá nuashonrú ar ghníomh sceidealta", "lowBalance": "Teastaíonn aird ar do chothromas", - "securityFinding": "Teastaíonn aird ar thátal slándála" + "securityFinding": "Teastaíonn aird ar thátal slándála", + "activeAgentsGlanceable": "Tá nuashonrú ar ghníomhairí gníomhacha" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/gl.json b/packages/notifications/src/locales/gl.json index 3d3bc9b605..a0d1330fda 100644 --- a/packages/notifications/src/locales/gl.json +++ b/packages/notifications/src/locales/gl.json @@ -7,7 +7,8 @@ "instanceLifecycle": "A túa instancia ten unha actualización", "scheduledAction": "Unha acción programada ten unha actualización", "lowBalance": "O teu saldo precisa atención", - "securityFinding": "Un achado de seguridade precisa atención" + "securityFinding": "Un achado de seguridade precisa atención", + "activeAgentsGlanceable": "Os axentes activos teñen unha actualización" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/gu.json b/packages/notifications/src/locales/gu.json index 66e878d431..d05b98da33 100644 --- a/packages/notifications/src/locales/gu.json +++ b/packages/notifications/src/locales/gu.json @@ -7,7 +7,8 @@ "instanceLifecycle": "તમારા ઇન્સ્ટન્સમાં અપડેટ છે", "scheduledAction": "સુનિશ્ચિત ક્રિયામાં અપડેટ છે", "lowBalance": "તમારી બેલેન્સ ધ્યાનની જરૂર છે", - "securityFinding": "સુરક્ષા તારણ ધ્યાનની જરૂર છે" + "securityFinding": "સુરક્ષા તારણ ધ્યાનની જરૂર છે", + "activeAgentsGlanceable": "સક્રિય એજન્ટોમાં અપડેટ છે" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ha.json b/packages/notifications/src/locales/ha.json index 340519f67e..b1a7e8cc92 100644 --- a/packages/notifications/src/locales/ha.json +++ b/packages/notifications/src/locales/ha.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Misalin naka yana da sabuntawa", "scheduledAction": "Wani aiki da aka tsara yana da sabuntawa", "lowBalance": "Ma'auninka yana buƙatar kulawa", - "securityFinding": "Wani binciken tsaro yana buƙatar kulawa" + "securityFinding": "Wani binciken tsaro yana buƙatar kulawa", + "activeAgentsGlanceable": "Wakilai da ke aiki suna da sabuntawa" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/he.json b/packages/notifications/src/locales/he.json index 2711f35d56..f3a06d1a8b 100644 --- a/packages/notifications/src/locales/he.json +++ b/packages/notifications/src/locales/he.json @@ -7,7 +7,8 @@ "instanceLifecycle": "למופע שלך יש עדכון", "scheduledAction": "לפעולה מתוזמנת יש עדכון", "lowBalance": "היתרה שלך דורשת תשומת לב", - "securityFinding": "ממצא אבטחה דורש תשומת לב" + "securityFinding": "ממצא אבטחה דורש תשומת לב", + "activeAgentsGlanceable": "לסוכנים הפעילים יש עדכון" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/hi.json b/packages/notifications/src/locales/hi.json index d696cbccfc..fdd68f6a88 100644 --- a/packages/notifications/src/locales/hi.json +++ b/packages/notifications/src/locales/hi.json @@ -7,7 +7,8 @@ "instanceLifecycle": "आपके इंस्टेंस में एक अपडेट है", "scheduledAction": "एक निर्धारित क्रिया में अपडेट है", "lowBalance": "आपके बैलेंस पर ध्यान देने की आवश्यकता है", - "securityFinding": "एक सुरक्षा निष्कर्ष पर ध्यान देने की आवश्यकता है" + "securityFinding": "एक सुरक्षा निष्कर्ष पर ध्यान देने की आवश्यकता है", + "activeAgentsGlanceable": "सक्रिय एजेंटों में एक अपडेट है" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/hr.json b/packages/notifications/src/locales/hr.json index 6d5d0dd517..65ceb83b40 100644 --- a/packages/notifications/src/locales/hr.json +++ b/packages/notifications/src/locales/hr.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Vaša instanca ima ažuriranje", "scheduledAction": "Zakazana radnja ima ažuriranje", "lowBalance": "Vaš saldo zahtijeva pozornost", - "securityFinding": "Sigurnosni nalaz zahtijeva pozornost" + "securityFinding": "Sigurnosni nalaz zahtijeva pozornost", + "activeAgentsGlanceable": "Aktivni agenti imaju ažuriranje" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ht.json b/packages/notifications/src/locales/ht.json index e4a2074698..8f49115d2d 100644 --- a/packages/notifications/src/locales/ht.json +++ b/packages/notifications/src/locales/ht.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Enstans ou gen yon aktyalizasyon", "scheduledAction": "Yon aksyon pwograme gen yon aktyalizasyon", "lowBalance": "Saldo ou bezwen atansyon", - "securityFinding": "Yon rezilta sekirite bezwen atansyon" + "securityFinding": "Yon rezilta sekirite bezwen atansyon", + "activeAgentsGlanceable": "Ajans aktif yo gen yon aktyalizasyon" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/hu.json b/packages/notifications/src/locales/hu.json index 615c65df00..15f0e79bd7 100644 --- a/packages/notifications/src/locales/hu.json +++ b/packages/notifications/src/locales/hu.json @@ -7,7 +7,8 @@ "instanceLifecycle": "A példányod frissítést kapott", "scheduledAction": "Egy ütemezett művelet frissítést kapott", "lowBalance": "Az egyenleged figyelmet igényel", - "securityFinding": "Egy biztonsági észlelés figyelmet igényel" + "securityFinding": "Egy biztonsági észlelés figyelmet igényel", + "activeAgentsGlanceable": "Az aktív ügynökök frissítést kaptak" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/hy.json b/packages/notifications/src/locales/hy.json index 9b1dbd8bab..4b0a6b5b16 100644 --- a/packages/notifications/src/locales/hy.json +++ b/packages/notifications/src/locales/hy.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Ձեր օրինակն ունի թարմացում", "scheduledAction": "Պլանավորված գործողությունն ունի թարմացում", "lowBalance": "Ձեր մնացորդը ուշադրության կարիք ունի", - "securityFinding": "Անվտանգության հայտնաբերումը ուշադրության կարիք ունի" + "securityFinding": "Անվտանգության հայտնաբերումը ուշադրության կարիք ունի", + "activeAgentsGlanceable": "Ակտիվ գործակալներն ունեն թարմացում" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/id.json b/packages/notifications/src/locales/id.json index aaea8e739e..504a193ad3 100644 --- a/packages/notifications/src/locales/id.json +++ b/packages/notifications/src/locales/id.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Instance Anda memiliki pembaruan", "scheduledAction": "Tindakan terjadwal memiliki pembaruan", "lowBalance": "Saldo Anda perlu diperhatikan", - "securityFinding": "Temuan keamanan perlu diperhatikan" + "securityFinding": "Temuan keamanan perlu diperhatikan", + "activeAgentsGlanceable": "Agen aktif memiliki pembaruan" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ig.json b/packages/notifications/src/locales/ig.json index d72ec058bd..5950263340 100644 --- a/packages/notifications/src/locales/ig.json +++ b/packages/notifications/src/locales/ig.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Ihe atụ gị nwere mmelite", "scheduledAction": "Omume ahaziri nwere mmelite", "lowBalance": "Nguzozi gị chọrọ nlebara anya", - "securityFinding": "Nchọpụta nchekwa chọrọ nlebara anya" + "securityFinding": "Nchọpụta nchekwa chọrọ nlebara anya", + "activeAgentsGlanceable": "Ndị ọrụ na-arụ ọrụ nwere mmelite" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/is.json b/packages/notifications/src/locales/is.json index bcf25bcdf0..6af4d98803 100644 --- a/packages/notifications/src/locales/is.json +++ b/packages/notifications/src/locales/is.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Dæmið þitt hefur uppfærslu", "scheduledAction": "Skipulögð aðgerð hefur uppfærslu", "lowBalance": "Staðan þín þarfnast athygli", - "securityFinding": "Öryggisfundur þarfnast athygli" + "securityFinding": "Öryggisfundur þarfnast athygli", + "activeAgentsGlanceable": "Virk umboð hafa uppfærslu" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/it.json b/packages/notifications/src/locales/it.json index 1eb77874b1..9780f24f88 100644 --- a/packages/notifications/src/locales/it.json +++ b/packages/notifications/src/locales/it.json @@ -7,7 +7,8 @@ "instanceLifecycle": "La tua istanza ha un aggiornamento", "scheduledAction": "Un'azione pianificata ha un aggiornamento", "lowBalance": "Il tuo saldo richiede attenzione", - "securityFinding": "Un risultato di sicurezza richiede attenzione" + "securityFinding": "Un risultato di sicurezza richiede attenzione", + "activeAgentsGlanceable": "Gli agenti attivi hanno un aggiornamento" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ja.json b/packages/notifications/src/locales/ja.json index dd687af0f4..25c65a1aca 100644 --- a/packages/notifications/src/locales/ja.json +++ b/packages/notifications/src/locales/ja.json @@ -7,7 +7,8 @@ "instanceLifecycle": "インスタンスに更新があります", "scheduledAction": "スケジュールされたアクションに更新があります", "lowBalance": "残高の確認が必要です", - "securityFinding": "セキュリティの検出結果の確認が必要です" + "securityFinding": "セキュリティの検出結果の確認が必要です", + "activeAgentsGlanceable": "アクティブなエージェントに更新があります" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ka.json b/packages/notifications/src/locales/ka.json index fa1894ddda..b21b1b9580 100644 --- a/packages/notifications/src/locales/ka.json +++ b/packages/notifications/src/locales/ka.json @@ -7,7 +7,8 @@ "instanceLifecycle": "თქვენს ინსტანსს განახლება აქვს", "scheduledAction": "დაგეგმილ მოქმედებას განახლება აქვს", "lowBalance": "თქვენი ბალანსი ყურადღებას საჭიროებს", - "securityFinding": "უსაფრთხოების დასკვნა ყურადღებას საჭიროებს" + "securityFinding": "უსაფრთხოების დასკვნა ყურადღებას საჭიროებს", + "activeAgentsGlanceable": "აქტიურ აგენტებს განახლება აქვთ" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/kk.json b/packages/notifications/src/locales/kk.json index a59bb3cd9d..5a8680a4f6 100644 --- a/packages/notifications/src/locales/kk.json +++ b/packages/notifications/src/locales/kk.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Данаңызда жаңарту бар", "scheduledAction": "Жоспарланған әрекетте жаңарту бар", "lowBalance": "Балансыңыз назар қажет етеді", - "securityFinding": "Қауіпсіздік табылғаны назар қажет етеді" + "securityFinding": "Қауіпсіздік табылғаны назар қажет етеді", + "activeAgentsGlanceable": "Белсенді агенттерде жаңарту бар" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/km.json b/packages/notifications/src/locales/km.json index 2a97595144..824c798c5b 100644 --- a/packages/notifications/src/locales/km.json +++ b/packages/notifications/src/locales/km.json @@ -7,7 +7,8 @@ "instanceLifecycle": "ឧទាហរណ៍របស់អ្នកមានការធ្វើបច្ចុប្បន្នភាព", "scheduledAction": "សកម្មភាពដែលបានកំណត់ពេលមានការធ្វើបច្ចុប្បន្នភាព", "lowBalance": "សមតុល្យរបស់អ្នកត្រូវការការយកចិត្តទុកដាក់", - "securityFinding": "ការរកឃើញសុវត្ថិភាពត្រូវការការយកចិត្តទុកដាក់" + "securityFinding": "ការរកឃើញសុវត្ថិភាពត្រូវការការយកចិត្តទុកដាក់", + "activeAgentsGlanceable": "ភ្នាក់ងារសកម្មមានការធ្វើបច្ចុប្បន្នភាព" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/kn.json b/packages/notifications/src/locales/kn.json index b61405c3f1..303ee6b7b0 100644 --- a/packages/notifications/src/locales/kn.json +++ b/packages/notifications/src/locales/kn.json @@ -7,7 +7,8 @@ "instanceLifecycle": "ನಿಮ್ಮ ಇನ್‌ಸ್ಟಾನ್ಸ್‌ಗೆ ನವೀಕರಣವಿದೆ", "scheduledAction": "ನಿಗದಿತ ಕ್ರಿಯೆಗೆ ನವೀಕರಣವಿದೆ", "lowBalance": "ನಿಮ್ಮ ಬ್ಯಾಲೆನ್ಸ್‌ಗೆ ಗಮನ ಬೇಕು", - "securityFinding": "ಭದ್ರತಾ ಸಂಶೋಧನೆಗೆ ಗಮನ ಬೇಕು" + "securityFinding": "ಭದ್ರತಾ ಸಂಶೋಧನೆಗೆ ಗಮನ ಬೇಕು", + "activeAgentsGlanceable": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳಿಗೆ ನವೀಕರಣವಿದೆ" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ko.json b/packages/notifications/src/locales/ko.json index c29633d3fd..c2ee568bd9 100644 --- a/packages/notifications/src/locales/ko.json +++ b/packages/notifications/src/locales/ko.json @@ -7,7 +7,8 @@ "instanceLifecycle": "인스턴스에 업데이트가 있습니다", "scheduledAction": "예약된 작업에 업데이트가 있습니다", "lowBalance": "잔액 확인이 필요합니다", - "securityFinding": "보안 발견 사항 확인이 필요합니다" + "securityFinding": "보안 발견 사항 확인이 필요합니다", + "activeAgentsGlanceable": "활성 에이전트에 업데이트가 있습니다" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/lo.json b/packages/notifications/src/locales/lo.json index b869976d77..e57d90ad5d 100644 --- a/packages/notifications/src/locales/lo.json +++ b/packages/notifications/src/locales/lo.json @@ -7,7 +7,8 @@ "instanceLifecycle": "instance ຂອງທ່ານມີການອັບເດດ", "scheduledAction": "ການດຳເນີນການທີ່ກຳນົດໄວ້ມີການອັບເດດ", "lowBalance": "ຍອດຂອງທ່ານຕ້ອງການຄວາມສົນໃຈ", - "securityFinding": "ການກວດພົບຄວາມປອດໄພຕ້ອງການຄວາມສົນໃຈ" + "securityFinding": "ການກວດພົບຄວາມປອດໄພຕ້ອງການຄວາມສົນໃຈ", + "activeAgentsGlanceable": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກມີການອັບເດດ" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/lt.json b/packages/notifications/src/locales/lt.json index 8b06d62585..78e9e42731 100644 --- a/packages/notifications/src/locales/lt.json +++ b/packages/notifications/src/locales/lt.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Jūsų egzempliorius turi atnaujinimą", "scheduledAction": "Suplanuotas veiksmas turi atnaujinimą", "lowBalance": "Jūsų balansui reikia dėmesio", - "securityFinding": "Saugumo radiniui reikia dėmesio" + "securityFinding": "Saugumo radiniui reikia dėmesio", + "activeAgentsGlanceable": "Aktyvūs agentai turi atnaujinimą" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/lv.json b/packages/notifications/src/locales/lv.json index 28dbc042fc..a939f2ad3f 100644 --- a/packages/notifications/src/locales/lv.json +++ b/packages/notifications/src/locales/lv.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Tavas instances ir atjauninājums", "scheduledAction": "Plānotajai darbībai ir atjauninājums", "lowBalance": "Tava bilancei nepieciešama uzmanība", - "securityFinding": "Drošības atradumam nepieciešama uzmanība" + "securityFinding": "Drošības atradumam nepieciešama uzmanība", + "activeAgentsGlanceable": "Aktīvajiem aģentiem ir atjauninājums" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/mg.json b/packages/notifications/src/locales/mg.json index bfb040da22..e65bd8e9eb 100644 --- a/packages/notifications/src/locales/mg.json +++ b/packages/notifications/src/locales/mg.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Misy fanavaozana ny instance anao", "scheduledAction": "Misy fanavaozana ny hetsika voalahatra", "lowBalance": "Mila jerena ny balan-nao", - "securityFinding": "Misy hitan'ny fiarovana mila jerena" + "securityFinding": "Misy hitan'ny fiarovana mila jerena", + "activeAgentsGlanceable": "Misy fanavaozana ny agent mavitrika" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/mi.json b/packages/notifications/src/locales/mi.json index fdace74c40..dfb5f04e6c 100644 --- a/packages/notifications/src/locales/mi.json +++ b/packages/notifications/src/locales/mi.json @@ -7,7 +7,8 @@ "instanceLifecycle": "He whakahoutanga tā tō wae", "scheduledAction": "He whakahoutanga tā tētahi mahi kua whakaritea", "lowBalance": "Me aro ki tō toenga", - "securityFinding": "Me aro ki tētahi kitenga haumaru" + "securityFinding": "Me aro ki tētahi kitenga haumaru", + "activeAgentsGlanceable": "He whakahoutanga tā ngā māngai hohe" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/mk.json b/packages/notifications/src/locales/mk.json index e2f445e6cc..faa5b6bc17 100644 --- a/packages/notifications/src/locales/mk.json +++ b/packages/notifications/src/locales/mk.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Вашата инстанца има ажурирање", "scheduledAction": "Закажана акција има ажурирање", "lowBalance": "Вашата состојба бара внимание", - "securityFinding": "Безбедносно наоѓање бара внимание" + "securityFinding": "Безбедносно наоѓање бара внимание", + "activeAgentsGlanceable": "Активните агенти имаат ажурирање" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ml.json b/packages/notifications/src/locales/ml.json index d966b80a52..2e5c9ef50d 100644 --- a/packages/notifications/src/locales/ml.json +++ b/packages/notifications/src/locales/ml.json @@ -7,7 +7,8 @@ "instanceLifecycle": "നിങ്ങളുടെ ഇൻസ്റ്റൻസിൽ ഒരു അപ്ഡേറ്റ് ഉണ്ട്", "scheduledAction": "ഒരു ഷെഡ്യൂൾ ചെയ്ത പ്രവർത്തനത്തിൽ ഒരു അപ്ഡേറ്റ് ഉണ്ട്", "lowBalance": "നിങ്ങളുടെ ബാലൻസിന് ശ്രദ്ധ ആവശ്യമാണ്", - "securityFinding": "ഒരു സുരക്ഷാ കണ്ടെത്തലിന് ശ്രദ്ധ ആവശ്യമാണ്" + "securityFinding": "ഒരു സുരക്ഷാ കണ്ടെത്തലിന് ശ്രദ്ധ ആവശ്യമാണ്", + "activeAgentsGlanceable": "സജീവ ഏജന്റുകൾക്ക് ഒരു അപ്ഡേറ്റ് ഉണ്ട്" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/mn.json b/packages/notifications/src/locales/mn.json index 59ff0305cf..fbf97f63c4 100644 --- a/packages/notifications/src/locales/mn.json +++ b/packages/notifications/src/locales/mn.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Таны инстанц шинэчлэлттэй байна", "scheduledAction": "Төлөвлөсөн үйлдэл шинэчлэлттэй байна", "lowBalance": "Таны үлдэгдэл анхаарал шаарддаг", - "securityFinding": "Аюулгүй байдлын олдолт анхаарал шаарддаг" + "securityFinding": "Аюулгүй байдлын олдолт анхаарал шаарддаг", + "activeAgentsGlanceable": "Идэвхтэй агентуудад шинэчлэлт бий" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/mr.json b/packages/notifications/src/locales/mr.json index f9debd6188..8cabb6532f 100644 --- a/packages/notifications/src/locales/mr.json +++ b/packages/notifications/src/locales/mr.json @@ -7,7 +7,8 @@ "instanceLifecycle": "तुमच्या इंस्टन्समध्ये अपडेट आहे", "scheduledAction": "नियोजित क्रियेत अपडेट आहे", "lowBalance": "तुमच्या शिल्लकीकडे लक्ष आवश्यक आहे", - "securityFinding": "सुरक्षा निष्कर्षाकडे लक्ष आवश्यक आहे" + "securityFinding": "सुरक्षा निष्कर्षाकडे लक्ष आवश्यक आहे", + "activeAgentsGlanceable": "सक्रिय एजंट्सकडे अपडेट आहे" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ms.json b/packages/notifications/src/locales/ms.json index 1866f29f7f..d2f1d75105 100644 --- a/packages/notifications/src/locales/ms.json +++ b/packages/notifications/src/locales/ms.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Contoh anda ada kemas kini", "scheduledAction": "Tindakan berjadual ada kemas kini", "lowBalance": "Baki anda memerlukan perhatian", - "securityFinding": "Penemuan keselamatan memerlukan perhatian" + "securityFinding": "Penemuan keselamatan memerlukan perhatian", + "activeAgentsGlanceable": "Ejen aktif ada kemas kini" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/mt.json b/packages/notifications/src/locales/mt.json index 24023c6a13..192ab3ff76 100644 --- a/packages/notifications/src/locales/mt.json +++ b/packages/notifications/src/locales/mt.json @@ -7,7 +7,8 @@ "instanceLifecycle": "L-instance tiegħek għandha aġġornament", "scheduledAction": "Azzjoni skedata għandha aġġornament", "lowBalance": "Il-bilanċ tiegħek għandu bżonn attenzjoni", - "securityFinding": "Seba ta' sigurtà għandu bżonn attenzjoni" + "securityFinding": "Seba ta' sigurtà għandu bżonn attenzjoni", + "activeAgentsGlanceable": "L-aġenti attivi għandhom aġġornament" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/my.json b/packages/notifications/src/locales/my.json index ec4bcfbca6..99479bedab 100644 --- a/packages/notifications/src/locales/my.json +++ b/packages/notifications/src/locales/my.json @@ -7,7 +7,8 @@ "instanceLifecycle": "သင့် instance တွင် အသစ်အဆန်း ရှိသည်", "scheduledAction": "စီစဉ်ထားသော လုပ်ဆောင်ချက်တွင် အသစ်အဆန်း ရှိသည်", "lowBalance": "သင့်လက်ကျန် အာရုံစိုက်ရန် လိုအပ်သည်", - "securityFinding": "လုံခြုံရေး တွေ့ရှိချက်တစ်ခု အာရုံစိုက်ရန် လိုအပ်သည်" + "securityFinding": "လုံခြုံရေး တွေ့ရှိချက်တစ်ခု အာရုံစိုက်ရန် လိုအပ်သည်", + "activeAgentsGlanceable": "လုပ်ဆောင်နေသော agent များတွင် အသစ်အဆန်း ရှိသည်" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/nb.json b/packages/notifications/src/locales/nb.json index 1ebac6b012..7e163e363e 100644 --- a/packages/notifications/src/locales/nb.json +++ b/packages/notifications/src/locales/nb.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Forekomsten din har en oppdatering", "scheduledAction": "En planlagt handling har en oppdatering", "lowBalance": "Saldoen din krever oppmerksomhet", - "securityFinding": "Et sikkerhetsfunn krever oppmerksomhet" + "securityFinding": "Et sikkerhetsfunn krever oppmerksomhet", + "activeAgentsGlanceable": "Aktive agenter har en oppdatering" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ne.json b/packages/notifications/src/locales/ne.json index f679d4beba..bf0cdb8c57 100644 --- a/packages/notifications/src/locales/ne.json +++ b/packages/notifications/src/locales/ne.json @@ -7,7 +7,8 @@ "instanceLifecycle": "तपाईंको इन्स्ट्यान्समा अद्यावधिक छ", "scheduledAction": "निर्धारित कार्यमा अद्यावधिक छ", "lowBalance": "तपाईंको ब्यालेन्समा ध्यान चाहिन्छ", - "securityFinding": "सुरक्षा फेला परेकोमा ध्यान चाहिन्छ" + "securityFinding": "सुरक्षा फेला परेकोमा ध्यान चाहिन्छ", + "activeAgentsGlanceable": "सक्रिय एजेन्टहरूमा अद्यावधिक छ" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/nl.json b/packages/notifications/src/locales/nl.json index 56f97be9cd..13b92b9c2f 100644 --- a/packages/notifications/src/locales/nl.json +++ b/packages/notifications/src/locales/nl.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Je instance heeft een update", "scheduledAction": "Een geplande actie heeft een update", "lowBalance": "Je saldo heeft aandacht nodig", - "securityFinding": "Een beveiligingsbevinding heeft aandacht nodig" + "securityFinding": "Een beveiligingsbevinding heeft aandacht nodig", + "activeAgentsGlanceable": "Actieve agents hebben een update" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/om.json b/packages/notifications/src/locales/om.json index b489952794..928576a128 100644 --- a/packages/notifications/src/locales/om.json +++ b/packages/notifications/src/locales/om.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Instance kee keessa bakka jijjiiramni jira", "scheduledAction": "Sochiin karoorfame keessa bakka jijjiiramni jira", "lowBalance": "Baalansii kee xiyyeeffannaa barbaada", - "securityFinding": "Arganni nagaa xiyyeeffannaa barbaada" + "securityFinding": "Arganni nagaa xiyyeeffannaa barbaada", + "activeAgentsGlanceable": "Eejentoonni hojii irra jiran odeeffannoo haaraa qabu" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/or.json b/packages/notifications/src/locales/or.json index 76923d4b76..27dd7ce1c9 100644 --- a/packages/notifications/src/locales/or.json +++ b/packages/notifications/src/locales/or.json @@ -7,7 +7,8 @@ "instanceLifecycle": "ଆପଣଙ୍କ ଇନସ୍ଟାନ୍ସରେ ଅପଡେଟ୍ ଅଛି", "scheduledAction": "ଏକ ନିର୍ଦ୍ଧାରିତ କାର୍ଯ୍ୟରେ ଅପଡେଟ୍ ଅଛି", "lowBalance": "ଆପଣଙ୍କ ବ୍ୟାଲାନ୍ସର ଧ୍ୟାନ ଦରକାର", - "securityFinding": "ଏକ ସୁରକ୍ଷା ନିଷ୍କର୍ଷର ଧ୍ୟାନ ଦରକାର" + "securityFinding": "ଏକ ସୁରକ୍ଷା ନିଷ୍କର୍ଷର ଧ୍ୟାନ ଦରକାର", + "activeAgentsGlanceable": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକରେ ଅପଡେଟ୍ ଅଛି" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/pa.json b/packages/notifications/src/locales/pa.json index c81da04877..f6e65f2815 100644 --- a/packages/notifications/src/locales/pa.json +++ b/packages/notifications/src/locales/pa.json @@ -7,7 +7,8 @@ "instanceLifecycle": "ਤੁਹਾਡੀ ਇੰਸਟੈਂਸ ਵਿੱਚ ਇੱਕ ਅੱਪਡੇਟ ਹੈ", "scheduledAction": "ਇੱਕ ਤਹਿ ਕੀਤੀ ਕਾਰਵਾਈ ਵਿੱਚ ਇੱਕ ਅੱਪਡੇਟ ਹੈ", "lowBalance": "ਤੁਹਾਡੇ ਬੈਲੰਸ ਨੂੰ ਧਿਆਨ ਦੀ ਲੋੜ ਹੈ", - "securityFinding": "ਇੱਕ ਸੁਰੱਖਿਆ ਲੱਭਤ ਨੂੰ ਧਿਆਨ ਦੀ ਲੋੜ ਹੈ" + "securityFinding": "ਇੱਕ ਸੁਰੱਖਿਆ ਲੱਭਤ ਨੂੰ ਧਿਆਨ ਦੀ ਲੋੜ ਹੈ", + "activeAgentsGlanceable": "ਸਰਗਰਮ ਏਜੰਟਾਂ ਵਿੱਚ ਇੱਕ ਅੱਪਡੇਟ ਹੈ" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/pl.json b/packages/notifications/src/locales/pl.json index 5d74418d9f..6ebc8830f2 100644 --- a/packages/notifications/src/locales/pl.json +++ b/packages/notifications/src/locales/pl.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Twoja instancja ma aktualizację", "scheduledAction": "Zaplanowana akcja ma aktualizację", "lowBalance": "Twoje saldo wymaga uwagi", - "securityFinding": "Wynik bezpieczeństwa wymaga uwagi" + "securityFinding": "Wynik bezpieczeństwa wymaga uwagi", + "activeAgentsGlanceable": "Aktywni agenci mają aktualizację" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ps.json b/packages/notifications/src/locales/ps.json index 80a52eb1bc..a8823f2d50 100644 --- a/packages/notifications/src/locales/ps.json +++ b/packages/notifications/src/locales/ps.json @@ -7,7 +7,8 @@ "instanceLifecycle": "ستاسو نمونه تازه شوې ده", "scheduledAction": "یو ټاکلی عمل تازه شوی دی", "lowBalance": "ستاسو توازن ته پاملرنه اړینه ده", - "securityFinding": "یو امنیتي موندنې ته پاملرنه اړینه ده" + "securityFinding": "یو امنیتي موندنې ته پاملرنه اړینه ده", + "activeAgentsGlanceable": "فعال اجنټان تازه معلومات لري" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/pt-BR.json b/packages/notifications/src/locales/pt-BR.json index 3c515e451c..5ad8f352fb 100644 --- a/packages/notifications/src/locales/pt-BR.json +++ b/packages/notifications/src/locales/pt-BR.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Sua instância tem uma atualização", "scheduledAction": "Uma ação agendada tem uma atualização", "lowBalance": "Seu saldo precisa de atenção", - "securityFinding": "Um achado de segurança precisa de atenção" + "securityFinding": "Um achado de segurança precisa de atenção", + "activeAgentsGlanceable": "Os agentes ativos têm uma atualização" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/pt.json b/packages/notifications/src/locales/pt.json index f0ee98916b..688ad7703b 100644 --- a/packages/notifications/src/locales/pt.json +++ b/packages/notifications/src/locales/pt.json @@ -7,7 +7,8 @@ "instanceLifecycle": "A sua instância tem uma atualização", "scheduledAction": "Uma ação agendada tem uma atualização", "lowBalance": "O seu saldo requer atenção", - "securityFinding": "Uma descoberta de segurança requer atenção" + "securityFinding": "Uma descoberta de segurança requer atenção", + "activeAgentsGlanceable": "Os agentes ativos têm uma atualização" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ro.json b/packages/notifications/src/locales/ro.json index df6b519c6e..0c8292ae83 100644 --- a/packages/notifications/src/locales/ro.json +++ b/packages/notifications/src/locales/ro.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Instanța ta are o actualizare", "scheduledAction": "O acțiune programată are o actualizare", "lowBalance": "Soldul tău necesită atenție", - "securityFinding": "O constatare de securitate necesită atenție" + "securityFinding": "O constatare de securitate necesită atenție", + "activeAgentsGlanceable": "Agenții activi au o actualizare" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ru.json b/packages/notifications/src/locales/ru.json index 9662acdb17..4bc6ebaca5 100644 --- a/packages/notifications/src/locales/ru.json +++ b/packages/notifications/src/locales/ru.json @@ -7,7 +7,8 @@ "instanceLifecycle": "В вашем инстансе есть обновление", "scheduledAction": "В запланированном действии есть обновление", "lowBalance": "Ваш баланс требует внимания", - "securityFinding": "Результат безопасности требует внимания" + "securityFinding": "Результат безопасности требует внимания", + "activeAgentsGlanceable": "У активных агентов есть обновление" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/si.json b/packages/notifications/src/locales/si.json index a394efd4b8..04467edba7 100644 --- a/packages/notifications/src/locales/si.json +++ b/packages/notifications/src/locales/si.json @@ -7,7 +7,8 @@ "instanceLifecycle": "ඔබගේ අවස්ථාවට යාවත්කාලීනයක් ඇත", "scheduledAction": "සැලසුම්ගත ක්‍රියාවකට යාවත්කාලීනයක් ඇත", "lowBalance": "ඔබගේ ශේෂයට අවධානය අවශ්‍යයි", - "securityFinding": "ආරක්ෂක සොයා ගැනීමකට අවධානය අවශ්‍යයි" + "securityFinding": "ආරක්ෂක සොයා ගැනීමකට අවධානය අවශ්‍යයි", + "activeAgentsGlanceable": "සක්‍රිය නියෝජිතයන්ට යාවත්කාලීනයක් ඇත" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/sk.json b/packages/notifications/src/locales/sk.json index 68c1a3494d..0eea6e373b 100644 --- a/packages/notifications/src/locales/sk.json +++ b/packages/notifications/src/locales/sk.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Vaša inštancia má aktualizáciu", "scheduledAction": "Naplánovaná akcia má aktualizáciu", "lowBalance": "Váš zostatok si vyžaduje pozornosť", - "securityFinding": "Nájdený bezpečnostný problém si vyžaduje pozornosť" + "securityFinding": "Nájdený bezpečnostný problém si vyžaduje pozornosť", + "activeAgentsGlanceable": "Aktívni agenti majú aktualizáciu" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/sl.json b/packages/notifications/src/locales/sl.json index f7d1edb29b..b31effd12f 100644 --- a/packages/notifications/src/locales/sl.json +++ b/packages/notifications/src/locales/sl.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Tvoja instanca ima posodobitev", "scheduledAction": "Načrtovano dejanje ima posodobitev", "lowBalance": "Tvoje stanje potrebuje pozornost", - "securityFinding": "Varnostna najdba potrebuje pozornost" + "securityFinding": "Varnostna najdba potrebuje pozornost", + "activeAgentsGlanceable": "Aktivni agenti imajo posodobitev" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/so.json b/packages/notifications/src/locales/so.json index 27badd6cd1..1a60e69d29 100644 --- a/packages/notifications/src/locales/so.json +++ b/packages/notifications/src/locales/so.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Instance-kaagu wuxuu leeyahay cusboonaysiin", "scheduledAction": "Fal la qorsheeyay ayaa leh cusboonaysiin", "lowBalance": "Dheelitirkaagu wuxuu u baahan yahay feejignaan", - "securityFinding": "Natiijo amnigu wuxuu u baahan yahay feejignaan" + "securityFinding": "Natiijo amnigu wuxuu u baahan yahay feejignaan", + "activeAgentsGlanceable": "Wakiillada firfircoon waxay leeyihiin cusboonaysiin" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/sq.json b/packages/notifications/src/locales/sq.json index 552719ca75..48e2f90bc7 100644 --- a/packages/notifications/src/locales/sq.json +++ b/packages/notifications/src/locales/sq.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Instanca juaj ka një përditësim", "scheduledAction": "Një veprim i planifikuar ka një përditësim", "lowBalance": "Bilanci juaj ka nevojë për vëmendje", - "securityFinding": "Një gjetje sigurie ka nevojë për vëmendje" + "securityFinding": "Një gjetje sigurie ka nevojë për vëmendje", + "activeAgentsGlanceable": "Agjentët aktivë kanë një përditësim" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/sr.json b/packages/notifications/src/locales/sr.json index 7eeac4c36a..f8144b0cbc 100644 --- a/packages/notifications/src/locales/sr.json +++ b/packages/notifications/src/locales/sr.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Vaša instanca ima ažuriranje", "scheduledAction": "Zakazana radnja ima ažuriranje", "lowBalance": "Vaš saldo zahteva pažnju", - "securityFinding": "Bezbednosni nalaz zahteva pažnju" + "securityFinding": "Bezbednosni nalaz zahteva pažnju", + "activeAgentsGlanceable": "Aktivni agenti imaju ažuriranje" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/sv.json b/packages/notifications/src/locales/sv.json index bd517d0e7a..7f093c6778 100644 --- a/packages/notifications/src/locales/sv.json +++ b/packages/notifications/src/locales/sv.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Din instans har en uppdatering", "scheduledAction": "En schemalagd åtgärd har en uppdatering", "lowBalance": "Ditt saldo kräver uppmärksamhet", - "securityFinding": "En säkerhetsupptäckt kräver uppmärksamhet" + "securityFinding": "En säkerhetsupptäckt kräver uppmärksamhet", + "activeAgentsGlanceable": "Aktiva agenter har en uppdatering" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/sw.json b/packages/notifications/src/locales/sw.json index ee6809d01e..69ee343976 100644 --- a/packages/notifications/src/locales/sw.json +++ b/packages/notifications/src/locales/sw.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Mfano wako umepokea sasisho", "scheduledAction": "Kitendo kilichopangwa kimepokea sasisho", "lowBalance": "Usawa wako unahitaji umakini", - "securityFinding": "Tokeo la usalama linahitaji umakini" + "securityFinding": "Tokeo la usalama linahitaji umakini", + "activeAgentsGlanceable": "Mawakala wanaofanya kazi wana sasisho" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ta.json b/packages/notifications/src/locales/ta.json index b82185e3e7..f2804d7a06 100644 --- a/packages/notifications/src/locales/ta.json +++ b/packages/notifications/src/locales/ta.json @@ -7,7 +7,8 @@ "instanceLifecycle": "உங்கள் நிகழ்வில் ஒரு புதுப்பிப்பு உள்ளது", "scheduledAction": "திட்டமிடப்பட்ட செயலில் ஒரு புதுப்பிப்பு உள்ளது", "lowBalance": "உங்கள் இருப்புக்கு கவனம் தேவை", - "securityFinding": "ஒரு பாதுகாப்பு கண்டுபிடிப்புக்கு கவனம் தேவை" + "securityFinding": "ஒரு பாதுகாப்பு கண்டுபிடிப்புக்கு கவனம் தேவை", + "activeAgentsGlanceable": "செயலில் உள்ள முகவர்களில் ஒரு புதுப்பிப்பு உள்ளது" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/te.json b/packages/notifications/src/locales/te.json index ec78ef9048..c296dffcd4 100644 --- a/packages/notifications/src/locales/te.json +++ b/packages/notifications/src/locales/te.json @@ -7,7 +7,8 @@ "instanceLifecycle": "మీ ఇన్స్టాన్స్కు నవీకరణ ఉంది", "scheduledAction": "షెడ్యూల్ చేసిన చర్యకు నవీకరణ ఉంది", "lowBalance": "మీ బ్యాలెన్స్కు శ్రద్ధ అవసరం", - "securityFinding": "భద్రతా ఫైండింగ్కు శ్రద్ధ అవసరం" + "securityFinding": "భద్రతా ఫైండింగ్కు శ్రద్ధ అవసరం", + "activeAgentsGlanceable": "చురుకైన ఏజెంట్లకు నవీకరణ ఉంది" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/th.json b/packages/notifications/src/locales/th.json index 32ecfe0f1d..2cdd514c1f 100644 --- a/packages/notifications/src/locales/th.json +++ b/packages/notifications/src/locales/th.json @@ -7,7 +7,8 @@ "instanceLifecycle": "อินสแตนซ์ของคุณมีการอัปเดต", "scheduledAction": "การดำเนินการที่กำหนดไว้มีการอัปเดต", "lowBalance": "ยอดคงเหลือของคุณต้องได้รับการดูแล", - "securityFinding": "พบปัญหาความปลอดภัยที่ต้องได้รับการดูแล" + "securityFinding": "พบปัญหาความปลอดภัยที่ต้องได้รับการดูแล", + "activeAgentsGlanceable": "เอเจนต์ที่กำลังทำงานมีการอัปเดต" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/tr.json b/packages/notifications/src/locales/tr.json index 4259b744ee..5272414563 100644 --- a/packages/notifications/src/locales/tr.json +++ b/packages/notifications/src/locales/tr.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Örneğinizde bir güncelleme var", "scheduledAction": "Zamanlanmış bir eylemde güncelleme var", "lowBalance": "Bakiyeniz dikkat gerektiriyor", - "securityFinding": "Bir güvenlik bulgusu dikkat gerektiriyor" + "securityFinding": "Bir güvenlik bulgusu dikkat gerektiriyor", + "activeAgentsGlanceable": "Etkin ajanlarda bir güncelleme var" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/uk.json b/packages/notifications/src/locales/uk.json index b9770fd5b7..77e26763d2 100644 --- a/packages/notifications/src/locales/uk.json +++ b/packages/notifications/src/locales/uk.json @@ -7,7 +7,8 @@ "instanceLifecycle": "У вашому інстансі є оновлення", "scheduledAction": "У запланованій дії є оновлення", "lowBalance": "Ваш баланс потребує уваги", - "securityFinding": "Результат безпеки потребує уваги" + "securityFinding": "Результат безпеки потребує уваги", + "activeAgentsGlanceable": "В активних агентів є оновлення" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/ur.json b/packages/notifications/src/locales/ur.json index 1d491e907f..a39e1ee5c7 100644 --- a/packages/notifications/src/locales/ur.json +++ b/packages/notifications/src/locales/ur.json @@ -7,7 +7,8 @@ "instanceLifecycle": "آپ کی مثال میں اپڈیٹ ہے", "scheduledAction": "شیڈول شدہ عمل میں اپڈیٹ ہے", "lowBalance": "آپ کے بیلنس پر توجہ کی ضرورت ہے", - "securityFinding": "سیکیورٹی کے معاملے پر توجہ کی ضرورت ہے" + "securityFinding": "سیکیورٹی کے معاملے پر توجہ کی ضرورت ہے", + "activeAgentsGlanceable": "فعال ایجنٹس میں اپڈیٹ ہے" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/uz.json b/packages/notifications/src/locales/uz.json index c974b79d3a..1a4109a5f6 100644 --- a/packages/notifications/src/locales/uz.json +++ b/packages/notifications/src/locales/uz.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Instansiyangizda yangilanish bor", "scheduledAction": "Rejalashtirilgan harakatda yangilanish bor", "lowBalance": "Balansingiz e'tibor talab qiladi", - "securityFinding": "Xavfsizlik xulosasi e'tibor talab qiladi" + "securityFinding": "Xavfsizlik xulosasi e'tibor talab qiladi", + "activeAgentsGlanceable": "Faol agentlarda yangilanish bor" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/vi.json b/packages/notifications/src/locales/vi.json index 06a4638d73..3e7e24471b 100644 --- a/packages/notifications/src/locales/vi.json +++ b/packages/notifications/src/locales/vi.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Phiên bản của bạn có bản cập nhật", "scheduledAction": "Một hành động đã lên lịch có bản cập nhật", "lowBalance": "Số dư của bạn cần được chú ý", - "securityFinding": "Một phát hiện bảo mật cần được chú ý" + "securityFinding": "Một phát hiện bảo mật cần được chú ý", + "activeAgentsGlanceable": "Các tác nhân đang hoạt động có bản cập nhật" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/yo.json b/packages/notifications/src/locales/yo.json index e04aea2b96..567c291fd8 100644 --- a/packages/notifications/src/locales/yo.json +++ b/packages/notifications/src/locales/yo.json @@ -7,7 +7,8 @@ "instanceLifecycle": "Apeere rẹ ni imudojuiwọn", "scheduledAction": "Iṣe ti a ṣeto ni imudojuiwọn", "lowBalance": "Iwọntunwọnsi rẹ nilo akiyesi", - "securityFinding": "Wiwa aabo nilo akiyesi" + "securityFinding": "Wiwa aabo nilo akiyesi", + "activeAgentsGlanceable": "Awọn aṣoju to n ṣiṣẹ ni imudojuiwọn" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/zh-Hans.json b/packages/notifications/src/locales/zh-Hans.json index 25f39d8656..6fb699a78f 100644 --- a/packages/notifications/src/locales/zh-Hans.json +++ b/packages/notifications/src/locales/zh-Hans.json @@ -7,7 +7,8 @@ "instanceLifecycle": "您的实例有更新", "scheduledAction": "计划的操作有更新", "lowBalance": "您的余额需要关注", - "securityFinding": "安全发现需要关注" + "securityFinding": "安全发现需要关注", + "activeAgentsGlanceable": "活动代理有更新" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/zh-Hant.json b/packages/notifications/src/locales/zh-Hant.json index 57f1ac32d8..1b29db3dfa 100644 --- a/packages/notifications/src/locales/zh-Hant.json +++ b/packages/notifications/src/locales/zh-Hant.json @@ -7,7 +7,8 @@ "instanceLifecycle": "您的執行個體有更新", "scheduledAction": "排定的操作有更新", "lowBalance": "您的餘額需要留意", - "securityFinding": "安全發現需要留意" + "securityFinding": "安全發現需要留意", + "activeAgentsGlanceable": "使用中的代理有更新" } }, "cloudAgentSession": { diff --git a/packages/notifications/src/locales/zu.json b/packages/notifications/src/locales/zu.json index 5c03752138..43d41fead8 100644 --- a/packages/notifications/src/locales/zu.json +++ b/packages/notifications/src/locales/zu.json @@ -7,7 +7,8 @@ "instanceLifecycle": "I-instance yakho inokubuyekezwa", "scheduledAction": "Isenzo esihleliwe sinokubuyekezwa", "lowBalance": "Ibhalansi yakho idinga ukunakwa", - "securityFinding": "Okutholakele kokuphepha kudinga ukunakwa" + "securityFinding": "Okutholakele kokuphepha kudinga ukunakwa", + "activeAgentsGlanceable": "Ama-agent asebenzayo anokubuyekezwa" } }, "cloudAgentSession": { From 461ee6e4e3809d8f8060153fefde46fc27e78d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 12:30:40 +0200 Subject: [PATCH 18/43] fix(mobile): fence delayed glanceable publication and recovery --- .../(tabs)/(2_agents)/index.mounted.test.tsx | 190 +++++++++- .../src/app/(app)/(tabs)/(2_agents)/index.tsx | 16 +- .../glanceable/activity-kit-prompt.test.ts | 227 +++++++++--- .../src/lib/glanceable/activity-kit-prompt.ts | 27 +- apps/mobile/src/lib/notifications.test.ts | 348 ++++++++++++++++-- apps/mobile/src/lib/notifications.ts | 48 ++- 6 files changed, 739 insertions(+), 117 deletions(-) diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx index 70c0165813..25595355e3 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx @@ -1,37 +1,82 @@ /* eslint-disable typescript-eslint/no-deprecated -- react-test-renderer is the DOM-free renderer used to mount React/RN trees under vitest (node env, no jsdom); its React 19 deprecation notice points to the DOM-based Testing Library, which cannot render this app's non-DOM tree, and @testing-library/react-native cannot be transformed by the current vitest pipeline (react-native ships Flow). See src/test/render-with-providers.tsx. */ +/* eslint-disable max-lines -- mounted route outcomes and Settings recovery share the native boundary harness. */ +import * as SecureStore from 'expo-secure-store'; import { createElement } from 'react'; import TestRenderer, { act } from 'react-test-renderer'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; import AgentSessionList, { buildGitHubInstallOutcomeAlert } from './index'; import { getGitHubInstallReturnOutcome, setGitHubInstallReturnOutcome, } from '@/lib/github-install-return'; +import { + _resetGlanceablePersistForTests, + _setLastGlanceableSnapshotForTests, +} from '@/lib/glanceable/persist'; +import { + type GlanceableSink, + registerGlanceableSink, + unregisterGlanceableSink, +} from '@/lib/glanceable/sink-registry'; +import { ACTIVE_USER_ID_KEY } from '@/lib/storage-keys'; const alertMock = vi.hoisted(() => vi.fn()); const platformMock = vi.hoisted(() => ({ OS: 'ios' })); const mintInstallStateMock = vi.hoisted(() => vi.fn()); const openAuthSessionMock = vi.hoisted(() => vi.fn()); const openBrowserMock = vi.hoisted(() => vi.fn()); +const focusedRoute = vi.hoisted(() => ({ focused: true })); +const appStateListeners = vi.hoisted(() => new Set<(state: string) => void>()); +const activityKit = vi.hoisted(() => ({ denied: false, available: false, settingsOpen: false })); vi.mock('react-native', () => ({ Alert: { alert: alertMock }, Platform: platformMock, + AppState: { + addEventListener: (_event: string, listener: (state: string) => void) => { + appStateListeners.add(listener); + return { + remove: () => { + appStateListeners.delete(listener); + }, + }; + }, + }, + Linking: { + openSettings: () => { + activityKit.settingsOpen = true; + }, + }, })); vi.mock('expo-router', async () => { const { useEffect } = await import('react'); return { - useFocusEffect: (effect: () => void) => { - useEffect(effect, [effect]); + useFocusEffect: (effect: Parameters[0]) => { + const focused = focusedRoute.focused; + useEffect(() => (focused ? effect() : undefined), [effect, focused]); }, }; }); -vi.mock('@/lib/glanceable/activity-kit-prompt', () => ({ - showActivityKitDisabledAlertOnce: vi.fn(), - recoverGlanceableActivityKit: vi.fn().mockResolvedValue(undefined), +vi.mock('expo-secure-store', () => ({ + getItemAsync: vi.fn((key: string) => (key === ACTIVE_USER_ID_KEY ? 'u1' : null)), +})); + +vi.mock('@/glanceable-ios/ios-sink', () => ({ + getActivityKitDenied: () => activityKit.denied, + clearActivityKitDeniedIfAvailable: () => { + if (!activityKit.denied || !activityKit.available) { + return false; + } + activityKit.denied = false; + return true; + }, })); vi.mock('expo-web-browser', () => ({ @@ -311,3 +356,136 @@ describe('Agents tab return-outcome rendering', () => { }); }); }); + +describe('Agents ActivityKit Settings recovery', () => { + const surface: { activity: GlanceableAgentsSnapshot | null } = { activity: null }; + const sink: GlanceableSink = { + publish: () => undefined, + endImmediate() { + surface.activity = null; + }, + startOrUpdate(snapshot) { + surface.activity = snapshot; + }, + }; + const snapshot = buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }, { status: 'question' }], + userId: 'u1', + organizationId: null, + now: 1_750_000_000_000, + }); + + beforeEach(() => { + alertMock.mockClear(); + platformMock.OS = 'ios'; + focusedRoute.focused = true; + activityKit.denied = false; + activityKit.available = false; + activityKit.settingsOpen = false; + surface.activity = null; + appStateListeners.clear(); + setGitHubInstallReturnOutcome(null); + _resetGlanceablePersistForTests(); + _setLastGlanceableSnapshotForTests(snapshot); + registerGlanceableSink(sink); + }); + + afterEach(() => { + unregisterGlanceableSink(sink); + }); + + function changeAppState(state: string) { + act(() => { + for (const listener of appStateListeners) { + listener(state); + } + }); + } + + it('recovers on direct Settings return without refocusing or repeating the alert', async () => { + activityKit.denied = true; + const renderer = mountRoute(); + await flushMicrotasks(); + expect(surface.activity).toBeNull(); + expect(lastAlertButtons()).toEqual([ + { text: 'Cancel', style: 'cancel' }, + { text: 'Open Settings', onPress: expect.any(Function) }, + ]); + + act(() => { + lastAlertButtons() + ?.find(button => button.text === 'Open Settings') + ?.onPress?.(); + }); + expect(activityKit.settingsOpen).toBe(true); + changeAppState('background'); + changeAppState('active'); + await flushMicrotasks(); + expect(surface.activity).toBeNull(); + expect(alertMock.mock.calls).toHaveLength(1); + + changeAppState('background'); + activityKit.available = true; + changeAppState('inactive'); + await flushMicrotasks(); + expect(surface.activity).toBeNull(); + changeAppState('active'); + await flushMicrotasks(); + + expect(surface.activity).toEqual(snapshot); + expect(alertMock.mock.calls).toHaveLength(1); + act(() => { + renderer.unmount(); + }); + }); + + it('retries the same snapshot after a failed Settings-return identity read', async () => { + activityKit.denied = true; + const renderer = mountRoute(); + await flushMicrotasks(); + alertMock.mockClear(); + + changeAppState('background'); + activityKit.available = true; + vi.mocked(SecureStore.getItemAsync).mockRejectedValueOnce(new Error('storage unavailable')); + changeAppState('active'); + await flushMicrotasks(); + expect(surface.activity).toBeNull(); + + changeAppState('background'); + changeAppState('active'); + await flushMicrotasks(); + + expect(surface.activity).toEqual(snapshot); + expect(alertMock.mock.calls).toHaveLength(0); + act(() => { + renderer.unmount(); + }); + }); + + it.each(['blur', 'unmount'])('stops foreground recovery after route %s', async transition => { + const renderer = mountRoute(); + if (transition === 'blur') { + focusedRoute.focused = false; + act(() => { + renderer.update(createElement(AgentSessionList)); + }); + } else { + act(() => { + renderer.unmount(); + }); + } + activityKit.denied = true; + activityKit.available = true; + changeAppState('active'); + await flushMicrotasks(); + + expect(surface.activity).toBeNull(); + expect(appStateListeners.size).toBe(0); + if (transition === 'blur') { + act(() => { + renderer.unmount(); + }); + } + }); +}); diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx index 4ee3c63f2d..9dcf628b12 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; import { useFocusEffect } from 'expo-router'; -import { Alert, Platform } from 'react-native'; +import { Alert, AppState, Platform } from 'react-native'; import { i18n } from '@/i18n'; import { AgentSessionListScreen } from '@/components/agents/session-list-screen'; @@ -140,14 +140,20 @@ export default function AgentSessionList() { return subscribeToGitHubInstallReturnOutcome(consumeReturnOutcome); }, [consumeReturnOutcome]); - // Show the one-time "turn on Live Activities" alert when the Agents tab - // regains focus and ActivityKit is unavailable, and recover the surface when - // it became available again. Never auto-alerts from the publisher; this tab - // focus is the single prompt and recovery site. + // Only tab focus can show the one-time alert. Settings can return without + // changing route focus, so also retry recovery when the app becomes active. useFocusEffect( useCallback(() => { showActivityKitDisabledAlertOnce(); void recoverGlanceableActivityKit(); + const subscription = AppState.addEventListener('change', state => { + if (state === 'active') { + void recoverGlanceableActivityKit(); + } + }); + return () => { + subscription.remove(); + }; }, []) ); diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts index c758bd5a84..4a8c290907 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts @@ -5,11 +5,18 @@ import { type GlanceableAgentsSnapshot, } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { bumpAuthEpoch } from '@/lib/auth/auth-epoch'; +import { writePrivacySnapshotAndEnd, writeSignedOutSnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests, } from '@/lib/glanceable/persist'; -import { registerGlanceableSink, unregisterGlanceableSink } from '@/lib/glanceable/sink-registry'; +import { + type GlanceableSink, + type GlanceableSinkContext, + registerGlanceableSink, + unregisterGlanceableSink, +} from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { recoverGlanceableActivityKit } from './activity-kit-prompt'; @@ -44,11 +51,11 @@ vi.mock('@/i18n', () => ({ const NOW = 1_750_000_000_000; -function eligibleSnapshot(): GlanceableAgentsSnapshot { +function eligibleSnapshot(organizationId: string | null = null): GlanceableAgentsSnapshot { return buildGlanceableSnapshot({ sessions: [{ status: 'busy' }], userId: 'u1', - organizationId: null, + organizationId, now: NOW, }); } @@ -62,88 +69,206 @@ function emptySnapshot(): GlanceableAgentsSnapshot { }); } -function makeFakeSink() { - return { - publish: vi.fn(), - endImmediate: vi.fn(), - startOrUpdate: vi.fn(), - }; +const surface: { + widget: GlanceableAgentsSnapshot | null; + activity: GlanceableAgentsSnapshot | null; + context: GlanceableSinkContext | null; +} = { widget: null, activity: null, context: null }; + +const sink: GlanceableSink = { + publish(snapshot) { + surface.widget = snapshot; + }, + endImmediate() { + surface.activity = null; + }, + startOrUpdate(snapshot, context) { + surface.activity = snapshot; + surface.context = context; + }, +}; + +function deferred() { + let release: (() => void) | undefined = undefined; + const promise = new Promise(resolve => { + release = resolve; + }); + return { promise, resolve: () => release?.() }; +} + +function delayIdentityRead(delayedKey: string) { + const started = deferred(); + const gate = deferred(); + mocks.getItemAsync.mockImplementation(async (key: string) => { + const value = key === ACTIVE_USER_ID_KEY ? 'u1' : null; + if (key === delayedKey) { + started.resolve(); + await gate.promise; + } + return value; + }); + return { started: started.promise, resolve: gate.resolve }; } beforeEach(() => { vi.clearAllMocks(); _resetGlanceablePersistForTests(); + _setLastGlanceableSnapshotForTests(eligibleSnapshot()); + surface.widget = null; + surface.activity = null; + surface.context = null; + registerGlanceableSink(sink); mocks.platform.OS = 'ios'; - mocks.getItemAsync.mockResolvedValue(null); - mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(false); + mocks.getItemAsync.mockImplementation((key: string) => + key === ACTIVE_USER_ID_KEY ? 'u1' : null + ); + mocks.getActivityKitDenied.mockReturnValue(true); + mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(true); }); afterEach(() => { - vi.clearAllMocks(); + unregisterGlanceableSink(sink); }); describe('recoverGlanceableActivityKit', () => { it('does nothing when the denied latch was not cleared', async () => { mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(false); - _setLastGlanceableSnapshotForTests(eligibleSnapshot()); - const sink = makeFakeSink(); - registerGlanceableSink(sink); await recoverGlanceableActivityKit(); - expect(sink.startOrUpdate).not.toHaveBeenCalled(); - unregisterGlanceableSink(sink); + expect(surface.activity).toBeNull(); }); - it('does not re-emit when the persisted snapshot has no eligible work', async () => { - mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(true); - _setLastGlanceableSnapshotForTests(emptySnapshot()); - const sink = makeFakeSink(); - registerGlanceableSink(sink); + it.each([null, emptySnapshot()])( + 'does not start absent or ineligible work: %s', + async snapshot => { + _setLastGlanceableSnapshotForTests(snapshot); + + await recoverGlanceableActivityKit(); + + expect(surface.activity).toBeNull(); + } + ); + + it.each([null, 'org-9'])('recovers authorized work in scope %s', async organizationId => { + const snapshot = eligibleSnapshot(organizationId); + _setLastGlanceableSnapshotForTests(snapshot); + mocks.getItemAsync.mockImplementation((key: string) => + key === ACTIVE_USER_ID_KEY ? 'u1' : organizationId + ); await recoverGlanceableActivityKit(); - expect(sink.startOrUpdate).not.toHaveBeenCalled(); - unregisterGlanceableSink(sink); + expect(surface.activity).toEqual(snapshot); + expect(surface.context).toEqual({ userId: 'u1', organizationId }); }); - it('re-emits the persisted eligible snapshot with the SecureStore identity', async () => { - mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(true); - const snapshot = eligibleSnapshot(); - _setLastGlanceableSnapshotForTests(snapshot); - mocks.getItemAsync.mockImplementation(async (key: string) => { - await Promise.resolve(); - if (key === ACTIVE_USER_ID_KEY) { - return 'u1'; - } - if (key === ORGANIZATION_STORAGE_KEY) { - return 'org-9'; + it.each([null, 'u2'])('rejects the unavailable or mismatched user hint %s', async userId => { + mocks.getItemAsync.mockImplementation((key: string) => + key === ACTIVE_USER_ID_KEY ? userId : null + ); + + await recoverGlanceableActivityKit(); + + expect(surface.activity).toBeNull(); + }); + + it.each([null, 'org-10'])('rejects the mismatched organization hint %s', async organizationId => { + _setLastGlanceableSnapshotForTests(eligibleSnapshot('org-9')); + mocks.getItemAsync.mockImplementation((key: string) => + key === ACTIVE_USER_ID_KEY ? 'u1' : organizationId + ); + + await recoverGlanceableActivityKit(); + + expect(surface.activity).toBeNull(); + }); + + it.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])('rejects a failed %s read', async key => { + _setLastGlanceableSnapshotForTests(eligibleSnapshot('org-9')); + mocks.getItemAsync.mockImplementation((requestedKey: string) => { + if (requestedKey === key) { + throw new Error('storage unavailable'); } - return null; + return requestedKey === ACTIVE_USER_ID_KEY ? 'u1' : 'org-9'; }); - const sink = makeFakeSink(); - registerGlanceableSink(sink); await recoverGlanceableActivityKit(); - expect(sink.startOrUpdate).toHaveBeenCalledWith(snapshot, { - userId: 'u1', - organizationId: 'org-9', - }); - unregisterGlanceableSink(sink); + expect(surface.activity).toBeNull(); }); - it('does not re-emit on a non-iOS platform', async () => { + it('does not recover on a non-iOS platform', async () => { mocks.platform.OS = 'android'; - mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(true); - _setLastGlanceableSnapshotForTests(eligibleSnapshot()); - const sink = makeFakeSink(); - registerGlanceableSink(sink); await recoverGlanceableActivityKit(); - expect(mocks.clearActivityKitDeniedIfAvailable).not.toHaveBeenCalled(); - expect(sink.startOrUpdate).not.toHaveBeenCalled(); - unregisterGlanceableSink(sink); + expect(surface.activity).toBeNull(); }); }); + +describe.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])( + 'ActivityKit recovery while %s is pending', + delayedKey => { + it.each([ + ['logout', writeSignedOutSnapshotAndEnd], + ['account switch', bumpAuthEpoch], + ['organization switch', writePrivacySnapshotAndEnd], + ] as const)('does not restore counts after %s', async (_label, invalidate) => { + const read = delayIdentityRead(delayedKey); + const recovering = recoverGlanceableActivityKit(); + await read.started; + + invalidate(); + read.resolve(); + await recovering; + + expect(surface.activity).toBeNull(); + expect(surface.context).toBeNull(); + }); + + it('does not recover a captured snapshot after the scope changes', async () => { + const read = delayIdentityRead(delayedKey); + const recovering = recoverGlanceableActivityKit(); + await read.started; + + _setLastGlanceableSnapshotForTests(eligibleSnapshot('org-10')); + read.resolve(); + await recovering; + + expect(surface.activity).toBeNull(); + }); + + it.each([0, 7])( + 'preserves newer counts (%s) instead of recovering captured work', + async running => { + const read = delayIdentityRead(delayedKey); + const recovering = recoverGlanceableActivityKit(); + await read.started; + + const latest = { ...eligibleSnapshot(), running, revision: 2 }; + _setLastGlanceableSnapshotForTests(latest); + sink.publish(latest); + if (running > 0) { + sink.startOrUpdate(latest, { userId: 'u1', organizationId: null }); + } + read.resolve(); + await recovering; + + expect(surface.widget).toEqual(latest); + expect(surface.activity).toEqual(running > 0 ? latest : null); + } + ); + + it('still recovers a current authorized snapshot after a delayed read', async () => { + const read = delayIdentityRead(delayedKey); + const recovering = recoverGlanceableActivityKit(); + await read.started; + read.resolve(); + await recovering; + + expect(surface.activity).toEqual(eligibleSnapshot()); + expect(surface.context).toEqual({ userId: 'u1', organizationId: null }); + }); + } +); diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts index 3d62b8db85..bdecc0f89b 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -1,10 +1,15 @@ import * as SecureStore from 'expo-secure-store'; import { Alert, Linking, Platform } from 'react-native'; -import { isEligibleGlanceableWork } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { + buildOpaqueScopeKey, + isEligibleGlanceableWork, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; import { clearActivityKitDeniedIfAvailable, getActivityKitDenied } from '@/glanceable-ios/ios-sink'; -import { getLastGlanceableSnapshot } from '@/lib/glanceable/persist'; +import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; +import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; +import { getLastGlanceableSnapshot, getLocalScopeKey } from '@/lib/glanceable/persist'; import { getGlanceableSinks } from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { i18n } from '@/i18n'; @@ -51,17 +56,31 @@ async function readSecureStoreValue(key: string): Promise { * was not cleared, or the persisted snapshot has no eligible work. */ export async function recoverGlanceableActivityKit(): Promise { - if (Platform.OS !== 'ios' || !clearActivityKitDeniedIfAvailable()) { + if (Platform.OS !== 'ios' || !getActivityKitDenied()) { return; } + const authEpoch = currentAuthEpoch(); + const blankEpoch = getTerminalBlankEpoch(); + const scopeKey = getLocalScopeKey(); const snapshot = getLastGlanceableSnapshot(); - if (snapshot === null || !isEligibleGlanceableWork(snapshot)) { + if (snapshot === null || snapshot.scopeKey !== scopeKey || !isEligibleGlanceableWork(snapshot)) { return; } const [userId, organizationId] = await Promise.all([ readSecureStoreValue(ACTIVE_USER_ID_KEY), readSecureStoreValue(ORGANIZATION_STORAGE_KEY), ]); + if ( + currentAuthEpoch() !== authEpoch || + getTerminalBlankEpoch() !== blankEpoch || + getLocalScopeKey() !== scopeKey || + getLastGlanceableSnapshot() !== snapshot || + userId === null || + buildOpaqueScopeKey({ userId, organizationId }) !== scopeKey || + !clearActivityKitDeniedIfAvailable() + ) { + return; + } for (const sink of getGlanceableSinks()) { sink.startOrUpdate(snapshot, { userId, organizationId }); } diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index c19efb26f7..7b7cfedca1 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -1,13 +1,25 @@ /* eslint-disable max-lines -- one cohesive notification suite sharing the glanceable sink and native module mock harness. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { + buildOpaqueScopeKey, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { bumpAuthEpoch, currentAuthEpoch } from '@/lib/auth/auth-epoch'; +import { writePrivacySnapshotAndEnd, writeSignedOutSnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests, _setSecureStoreForTests, + getLastGlanceableSnapshot, + getLocalScopeKey, + persistGlanceableSink, } from '@/lib/glanceable/persist'; -import { registerGlanceableSink, unregisterGlanceableSink } from '@/lib/glanceable/sink-registry'; +import { + type GlanceableSinkContext, + registerGlanceableSink, + unregisterGlanceableSink, +} from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { _setGlanceableSinksLoaderForTests, @@ -77,7 +89,10 @@ vi.mock('@kilocode/notifications', () => ({ ANDROID_NOTIFICATION_CHANNELS: [ { id: 'agent', name: 'Agent sessions', importance: 'high' }, { id: 'chat', name: 'Chat messages', importance: 'high' }, + { id: 'kiloclaw', name: 'KiloClaw activity', importance: 'default' }, { id: 'balance', name: 'Balance alerts', importance: 'default' }, + { id: 'security', name: 'Security findings', importance: 'high' }, + { id: 'active-agents', name: 'Active agents', importance: 'default' }, ], pushDataSchema: { safeParse: mocks.safeParse }, })); @@ -142,24 +157,40 @@ describe('ensureAndroidNotificationChannels', () => { expect(mocks.setNotificationChannelAsync).not.toHaveBeenCalled(); }); - it('creates every channel on Android with the mapped importance', async () => { + it('silences the aggregate channel on first creation without changing other channels', async () => { const { ensureAndroidNotificationChannels } = await loadNotifications(); await ensureAndroidNotificationChannels(); - expect(mocks.setNotificationChannelAsync).toHaveBeenCalledTimes(3); - expect(mocks.setNotificationChannelAsync).toHaveBeenCalledWith('agent', { - name: 'Agent sessions', - importance: 4, - }); - expect(mocks.setNotificationChannelAsync).toHaveBeenCalledWith('chat', { - name: 'Chat messages', - importance: 4, - }); - expect(mocks.setNotificationChannelAsync).toHaveBeenCalledWith('balance', { - name: 'Balance alerts', - importance: 3, - }); + expect(mocks.setNotificationChannelAsync.mock.calls).toEqual([ + ['agent', { name: 'Agent sessions', importance: 4 }], + ['chat', { name: 'Chat messages', importance: 4 }], + ['kiloclaw', { name: 'KiloClaw activity', importance: 3 }], + ['balance', { name: 'Balance alerts', importance: 3 }], + ['security', { name: 'Security findings', importance: 4 }], + [ + 'active-agents', + { name: 'Active agents', importance: 3, sound: null, enableVibrate: false }, + ], + ]); + }); + + it('also silences first creation through channel renaming without changing other options', async () => { + const { renameAndroidNotificationChannels } = await loadNotifications(); + + await renameAndroidNotificationChannels(); + + expect(mocks.setNotificationChannelAsync.mock.calls).toEqual([ + ['agent', { name: expect.any(String), importance: 4 }], + ['chat', { name: expect.any(String), importance: 4 }], + ['kiloclaw', { name: expect.any(String), importance: 3 }], + ['balance', { name: expect.any(String), importance: 3 }], + ['security', { name: expect.any(String), importance: 4 }], + [ + 'active-agents', + { name: expect.any(String), importance: 3, sound: null, enableVibrate: false }, + ], + ]); }); it('single-flights concurrent callers to one creation pass', async () => { @@ -170,7 +201,7 @@ describe('ensureAndroidNotificationChannels', () => { expect(first).toBe(second); await Promise.all([first, second]); - expect(mocks.setNotificationChannelAsync).toHaveBeenCalledTimes(3); + expect(mocks.setNotificationChannelAsync).toHaveBeenCalledTimes(6); }); it('swallows a per-channel failure and still creates the remaining channels', async () => { @@ -179,7 +210,7 @@ describe('ensureAndroidNotificationChannels', () => { await expect(ensureAndroidNotificationChannels()).resolves.toBeUndefined(); - expect(mocks.setNotificationChannelAsync).toHaveBeenCalledTimes(3); + expect(mocks.setNotificationChannelAsync).toHaveBeenCalledTimes(6); expect(mocks.captureException).toHaveBeenCalledWith(expect.any(Error), { tags: { 'error.subsystem': 'notifications', @@ -274,6 +305,8 @@ describe('setupNotificationResponseHandler', () => { }); }); +const SCOPE_KEY = buildOpaqueScopeKey({ userId: 'u1', organizationId: 'org-9' }); + function glanceableSnapshot( overrides: Partial = {} ): GlanceableAgentsSnapshot { @@ -282,8 +315,8 @@ function glanceableSnapshot( revision: 1, updatedAt: '2026-01-01T00:00:00.000Z', expiresAt: '2026-01-01T08:00:00.000Z', - scopeKey: 'scope-1', - organizationBound: false, + scopeKey: SCOPE_KEY, + organizationBound: true, status: 'happy', running: 1, needsInput: 0, @@ -305,10 +338,24 @@ function activeGlanceablePush( } function makeFakeSink() { + const surface: { + widget: GlanceableAgentsSnapshot | null; + activity: GlanceableAgentsSnapshot | null; + context: GlanceableSinkContext | null; + } = { widget: null, activity: null, context: null }; return { - publish: vi.fn(), - endImmediate: vi.fn(), - startOrUpdate: vi.fn(), + surface, + publish: vi.fn((snapshot: GlanceableAgentsSnapshot) => { + surface.widget = snapshot; + }), + endImmediate: vi.fn(() => { + surface.activity = null; + surface.context = null; + }), + startOrUpdate: vi.fn((snapshot: GlanceableAgentsSnapshot, context: GlanceableSinkContext) => { + surface.activity = snapshot; + surface.context = context; + }), }; } @@ -327,6 +374,22 @@ function mockSecureStoreKeys() { }); } +function delayIdentityRead(delayedKey: string) { + const started = deferred(); + const gate = deferred(); + let pending = true; + mocks.getItemAsync.mockImplementation(async (key: string) => { + const value = key === ACTIVE_USER_ID_KEY ? 'u1' : 'org-9'; + if (key === delayedKey && pending) { + pending = false; + started.resolve(); + await gate.promise; + } + return value; + }); + return { started: started.promise, resolve: gate.resolve }; +} + // Map-backed SecureStore surface for the persist module's restore path. The // persist module lazy-`require`s `expo-secure-store` (a native module), which // cannot load in the pure-vitest suite, so the restore tests inject this store @@ -356,7 +419,7 @@ describe('applyGlanceablePushData', () => { it('discards a remote snapshot that is not newer than the last applied snapshot', async () => { _setLastGlanceableSnapshotForTests( glanceableSnapshot({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, revision: 3, updatedAt: '2026-01-02T00:00:00.000Z', }) @@ -365,7 +428,7 @@ describe('applyGlanceablePushData', () => { registerGlanceableSink(sink); const result = await applyGlanceablePushData( - activeGlanceablePush({ scopeKey: 'scope-1', updatedAt: '2026-01-01T00:00:00.000Z' }) + activeGlanceablePush({ scopeKey: SCOPE_KEY, updatedAt: '2026-01-01T00:00:00.000Z' }) ); expect(result).toBe(false); @@ -378,7 +441,7 @@ describe('applyGlanceablePushData', () => { it('applies a newer remote snapshot and re-registers under the selected organization', async () => { _setLastGlanceableSnapshotForTests( glanceableSnapshot({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, revision: 3, updatedAt: '2026-01-01T00:00:00.000Z', }) @@ -388,7 +451,7 @@ describe('applyGlanceablePushData', () => { const result = await applyGlanceablePushData( activeGlanceablePush({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, updatedAt: '2026-01-02T00:00:00.000Z', organizationBound: true, }) @@ -409,7 +472,7 @@ describe('applyGlanceablePushData', () => { vi.useFakeTimers(); _setLastGlanceableSnapshotForTests( glanceableSnapshot({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, revision: 3, updatedAt: '2026-01-01T00:00:00.000Z', }) @@ -419,7 +482,7 @@ describe('applyGlanceablePushData', () => { const result = await applyGlanceablePushData( activeGlanceablePush({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, updatedAt: '2026-01-02T00:00:00.000Z', status: 'empty', running: 0, @@ -440,7 +503,7 @@ describe('applyGlanceablePushData', () => { // the fire-time eligibility guard sees non-eligible work and ends it. _setLastGlanceableSnapshotForTests( glanceableSnapshot({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, revision: 4, updatedAt: '2026-01-02T00:00:00.000Z', status: 'empty', @@ -461,7 +524,7 @@ describe('applyGlanceablePushData', () => { vi.useFakeTimers(); _setLastGlanceableSnapshotForTests( glanceableSnapshot({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, revision: 3, updatedAt: '2026-01-01T00:00:00.000Z', }) @@ -471,7 +534,7 @@ describe('applyGlanceablePushData', () => { await applyGlanceablePushData( activeGlanceablePush({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, updatedAt: '2026-01-02T00:00:00.000Z', status: 'empty', running: 0, @@ -482,7 +545,7 @@ describe('applyGlanceablePushData', () => { ); await applyGlanceablePushData( activeGlanceablePush({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, updatedAt: '2026-01-03T00:00:00.000Z', organizationBound: true, }) @@ -497,6 +560,219 @@ describe('applyGlanceablePushData', () => { }); }); +describe('glanceable publication storage fences', () => { + const sink = makeFakeSink(); + + beforeEach(() => { + vi.useFakeTimers(); + _resetGlanceablePersistForTests(); + _setSecureStoreForTests(secureStoreMock); + _setLastGlanceableSnapshotForTests(glanceableSnapshot({ revision: 3 })); + secureStore.clear(); + mockSecureStoreKeys(); + sink.surface.widget = null; + sink.surface.activity = null; + sink.surface.context = null; + registerGlanceableSink(persistGlanceableSink); + registerGlanceableSink(sink); + }); + + afterEach(() => { + unregisterGlanceableSink(sink); + unregisterGlanceableSink(persistGlanceableSink); + _resetGlanceablePersistForTests(); + vi.useRealTimers(); + }); + + it('publishes authorized personal work without an organization hint', async () => { + const scopeKey = buildOpaqueScopeKey({ userId: 'u1', organizationId: null }); + _setLastGlanceableSnapshotForTests( + glanceableSnapshot({ scopeKey, organizationBound: false, revision: 3 }) + ); + mocks.getItemAsync.mockImplementation((key: string) => + key === ACTIVE_USER_ID_KEY ? 'u1' : null + ); + + const applied = await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey, + organizationBound: false, + updatedAt: '2026-01-02T00:00:00.000Z', + running: 9, + }) + ); + + expect(applied).toBe(true); + expect(sink.surface.widget).toMatchObject({ scopeKey, revision: 4, running: 9 }); + expect(sink.surface.activity).toEqual(sink.surface.widget); + expect(sink.surface.context).toEqual({ userId: 'u1', organizationId: null }); + }); + + it.each([ + [null, 'org-9'], + ['u2', 'org-9'], + ['u1', null], + ['u1', 'org-10'], + ])( + 'rejects identity hints %s / %s outside the persisted scope', + async (userId, organizationId) => { + mocks.getItemAsync.mockImplementation((key: string) => + key === ACTIVE_USER_ID_KEY ? userId : organizationId + ); + const current = getLastGlanceableSnapshot(); + + const applied = await applyGlanceablePushData( + activeGlanceablePush({ updatedAt: '2026-01-02T00:00:00.000Z', running: 9 }) + ); + + expect(applied).toBe(false); + expect(getLastGlanceableSnapshot()).toBe(current); + expect(sink.surface.widget).toBeNull(); + expect(sink.surface.activity).toBeNull(); + } + ); + + it.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])('rejects a failed %s read', async key => { + mocks.getItemAsync.mockImplementation((requestedKey: string) => { + if (requestedKey === key) { + throw new Error('storage unavailable'); + } + return requestedKey === ACTIVE_USER_ID_KEY ? 'u1' : 'org-9'; + }); + + const applied = await applyGlanceablePushData(activeGlanceablePush()); + + expect(applied).toBe(false); + expect(sink.surface.widget).toBeNull(); + expect(sink.surface.activity).toBeNull(); + }); + + describe.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])( + 'while %s is pending', + delayedKey => { + it.each([ + ['logout', writeSignedOutSnapshotAndEnd], + ['account switch', bumpAuthEpoch], + ['organization switch', writePrivacySnapshotAndEnd], + ] as const)('does not restore counts after %s', async (_label, invalidate) => { + const read = delayIdentityRead(delayedKey); + const applying = applyGlanceablePushData( + activeGlanceablePush({ updatedAt: '2026-01-02T00:00:00.000Z', running: 9 }) + ); + await read.started; + + invalidate(); + const current = getLastGlanceableSnapshot(); + const widget = sink.surface.widget; + const scopeKey = getLocalScopeKey(); + read.resolve(); + + expect(await applying).toBe(false); + expect(getLastGlanceableSnapshot()).toBe(current); + expect(getLocalScopeKey()).toBe(scopeKey); + expect(sink.surface.widget).toBe(widget); + expect(sink.surface.activity).toBeNull(); + }); + + it('rejects captured work even when the blanked scope becomes current again', async () => { + const read = delayIdentityRead(delayedKey); + const applying = applyGlanceablePushData( + activeGlanceablePush({ updatedAt: '2026-01-02T00:00:00.000Z', running: 9 }) + ); + await read.started; + writePrivacySnapshotAndEnd(); + const authorized = glanceableSnapshot({ running: 2, revision: 5 }); + persistGlanceableSink.publish(authorized); + sink.publish(authorized); + sink.startOrUpdate(authorized, { userId: 'u1', organizationId: 'org-9' }); + read.resolve(); + + expect(await applying).toBe(false); + expect(getLastGlanceableSnapshot()).toEqual(authorized); + expect(sink.surface.widget).toEqual(authorized); + expect(sink.surface.activity).toEqual(authorized); + }); + + it('keeps a replacement scope when storage still returns the old identity', async () => { + const read = delayIdentityRead(delayedKey); + const applying = applyGlanceablePushData( + activeGlanceablePush({ updatedAt: '2026-01-02T00:00:00.000Z', running: 9 }) + ); + await read.started; + const replacement = glanceableSnapshot({ + scopeKey: buildOpaqueScopeKey({ userId: 'u1', organizationId: 'org-10' }), + running: 7, + }); + persistGlanceableSink.publish(replacement); + sink.publish(replacement); + read.resolve(); + + expect(await applying).toBe(false); + expect(getLastGlanceableSnapshot()).toEqual(replacement); + expect(sink.surface.widget).toEqual(replacement); + expect(sink.surface.activity).toBeNull(); + }); + + it.each([ + [0, '2026-01-02T00:00:00.000Z'], + [9, '2026-01-02T00:00:00.000Z'], + [0, '2026-01-03T00:00:00.000Z'], + [9, '2026-01-03T00:00:00.000Z'], + ] as const)( + 'discards captured counts (%s) after publication at %s', + async (running, updatedAt) => { + const read = delayIdentityRead(delayedKey); + const applying = applyGlanceablePushData( + activeGlanceablePush({ + updatedAt: '2026-01-02T00:00:00.000Z', + running, + status: running === 0 ? 'empty' : 'happy', + eligibleStartedAt: running === 0 ? null : '2026-01-01T00:00:00.000Z', + }) + ); + await read.started; + expect( + await applyGlanceablePushData(activeGlanceablePush({ updatedAt, running: 7 })) + ).toBe(true); + const latest = getLastGlanceableSnapshot(); + read.resolve(); + + expect(await applying).toBe(false); + vi.advanceTimersByTime(8000); + expect(getLastGlanceableSnapshot()).toBe(latest); + expect(sink.surface.widget).toEqual(latest); + expect(sink.surface.activity).toEqual(latest); + expect(sink.surface.activity?.running).toBe(7); + } + ); + + it('rebases a current remote snapshot above an intervening publication', async () => { + const read = delayIdentityRead(delayedKey); + const applying = applyGlanceablePushData( + activeGlanceablePush({ updatedAt: '2026-01-03T00:00:00.000Z', running: 9 }) + ); + await read.started; + expect( + await applyGlanceablePushData( + activeGlanceablePush({ updatedAt: '2026-01-02T00:00:00.000Z', running: 2 }) + ) + ).toBe(true); + read.resolve(); + + expect(await applying).toBe(true); + expect(sink.surface.widget).toMatchObject({ + running: 9, + revision: 5, + accountEpoch: currentAuthEpoch(), + }); + expect(sink.surface.activity).toEqual(sink.surface.widget); + expect(getLastGlanceableSnapshot()).toEqual(sink.surface.widget); + expect(sink.surface.context).toEqual({ userId: 'u1', organizationId: 'org-9' }); + }); + } + ); +}); + describe('setupNotificationBackgroundHandler', () => { type HeadlessExecutor = (body: { data: unknown; @@ -532,12 +808,12 @@ describe('setupNotificationBackgroundHandler', () => { // `restorePersistedGlanceable` before applying; without it the scope-key // fence discards the push and the sink never publishes. const persisted = glanceableSnapshot({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, revision: 1, updatedAt: '2026-01-01T00:00:00.000Z', }); secureStore.set('glanceable-snapshot', JSON.stringify(persisted)); - secureStore.set('glanceable-scope-key', 'scope-1'); + secureStore.set('glanceable-scope-key', SCOPE_KEY); mocks.safeParse.mockImplementation((data: unknown) => ({ success: true, data })); _setGlanceableSinksLoaderForTests(() => undefined); @@ -562,7 +838,7 @@ describe('setupNotificationBackgroundHandler', () => { data: { dataString: JSON.stringify( activeGlanceablePush({ - scopeKey: 'scope-1', + scopeKey: SCOPE_KEY, updatedAt: '2026-01-02T00:00:00.000Z', organizationBound: true, }) diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index dc27a726b7..3f19d71dd1 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -18,6 +18,7 @@ import { NOTIFICATION_TOKEN_UPDATED_EVENT, } from '@kilocode/app-shared/analytics'; import { + buildOpaqueScopeKey, GLANCEABLE_TERMINAL_MS, type GlanceableAgentsSnapshot, isEligibleGlanceableWork, @@ -124,26 +125,44 @@ function scheduleGlanceableTerminalEnd(): void { export async function applyGlanceablePushData( data: Extract ): Promise { - if (data.scopeKey !== getLocalScopeKey()) { + const authEpoch = currentAuthEpoch(); + const blankEpoch = getTerminalBlankEpoch(); + const scopeKey = getLocalScopeKey(); + const capturedSnapshot = getLastGlanceableSnapshot(); + if (data.scopeKey !== scopeKey) { + return false; + } + + const organizationId = await getSelectedOrganizationId(); + const userId = await getActiveUserId(); + if ( + currentAuthEpoch() !== authEpoch || + getTerminalBlankEpoch() !== blankEpoch || + getLocalScopeKey() !== scopeKey || + userId === null || + buildOpaqueScopeKey({ userId, organizationId }) !== scopeKey + ) { return false; } + // Fence and rebase against the latest publication after storage reads. + // A publication during the reads also wins a timestamp tie. const { type: _type, ...fields } = data; const current = getLastGlanceableSnapshot(); - - if (current !== null && fields.updatedAt < current.updatedAt) { + if ( + current !== null && + (fields.updatedAt < current.updatedAt || + (current !== capturedSnapshot && fields.updatedAt === current.updatedAt)) + ) { return false; } const snapshot: GlanceableAgentsSnapshot = { ...fields, revision: current === null ? fields.revision : current.revision + 1, - accountEpoch: currentAuthEpoch(), + accountEpoch: authEpoch, }; - const organizationId = await getSelectedOrganizationId(); - const userId = await getActiveUserId(); - const ctx = { userId, organizationId }; if (isEligibleGlanceableWork(snapshot)) { cancelGlanceableTerminalEnd(); @@ -164,10 +183,8 @@ export async function applyGlanceablePushData( } /** - * Read the selected organization id from SecureStore. The scope-key fence above - * already proved the incoming snapshot belongs to the current scope, so this id - * (a string for an org scope, null for personal) keeps org-scoped APNs token - * lookups finding the token when `startOrUpdate` re-registers it. + * Read the selected organization id for scope validation and token registration. + * A missing hint only matches a personal scope; it cannot revive an org scope. */ async function getSelectedOrganizationId(): Promise { try { @@ -178,10 +195,9 @@ async function getSelectedOrganizationId(): Promise { } /** - * Read the active-user id hint from SecureStore. Null when the hint is - * unavailable (headless background apply before the identity resolves, or a - * failed read). It only feeds logout reconciliation ordering, never the - * snapshot. + * Read the active-user id for scope validation and logout reconciliation. + * An unavailable hint drops the push rather than reviving a persisted scope. + * The raw id never enters the snapshot. */ async function getActiveUserId(): Promise { try { @@ -390,6 +406,7 @@ async function createAndroidNotificationChannels(): Promise { channel.importance === 'high' ? Notifications.AndroidImportance.HIGH : Notifications.AndroidImportance.DEFAULT, + ...(channel.id === 'active-agents' ? { sound: null, enableVibrate: false } : {}), }); } catch (error) { Sentry.captureException(error, { @@ -445,6 +462,7 @@ export async function renameAndroidNotificationChannels(): Promise { channel.importance === 'high' ? Notifications.AndroidImportance.HIGH : Notifications.AndroidImportance.DEFAULT, + ...(channel.id === 'active-agents' ? { sound: null, enableVibrate: false } : {}), }); } catch (error) { Sentry.captureException(error, { From 14e749d3428c46661f77bc7c4d2aee7f0dedecec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 13:43:08 +0200 Subject: [PATCH 19/43] fix(i18n): add ActivityKit prompt keys with their consumer --- apps/mobile/src/i18n/locales/af.json | 4 +++- apps/mobile/src/i18n/locales/am.json | 4 +++- apps/mobile/src/i18n/locales/ar.json | 4 +++- apps/mobile/src/i18n/locales/az.json | 4 +++- apps/mobile/src/i18n/locales/be.json | 4 +++- apps/mobile/src/i18n/locales/bg.json | 4 +++- apps/mobile/src/i18n/locales/bn.json | 4 +++- apps/mobile/src/i18n/locales/bs.json | 4 +++- apps/mobile/src/i18n/locales/ca.json | 4 +++- apps/mobile/src/i18n/locales/ckb.json | 4 +++- apps/mobile/src/i18n/locales/cs.json | 4 +++- apps/mobile/src/i18n/locales/cy.json | 4 +++- apps/mobile/src/i18n/locales/da.json | 4 +++- apps/mobile/src/i18n/locales/de.json | 4 +++- apps/mobile/src/i18n/locales/el.json | 4 +++- apps/mobile/src/i18n/locales/en.json | 4 +++- apps/mobile/src/i18n/locales/es.json | 4 +++- apps/mobile/src/i18n/locales/et.json | 4 +++- apps/mobile/src/i18n/locales/eu.json | 4 +++- apps/mobile/src/i18n/locales/fa.json | 4 +++- apps/mobile/src/i18n/locales/fi.json | 4 +++- apps/mobile/src/i18n/locales/fil.json | 4 +++- apps/mobile/src/i18n/locales/fr.json | 4 +++- apps/mobile/src/i18n/locales/ga.json | 4 +++- apps/mobile/src/i18n/locales/gl.json | 4 +++- apps/mobile/src/i18n/locales/gu.json | 4 +++- apps/mobile/src/i18n/locales/ha.json | 4 +++- apps/mobile/src/i18n/locales/he.json | 4 +++- apps/mobile/src/i18n/locales/hi.json | 4 +++- apps/mobile/src/i18n/locales/hr.json | 4 +++- apps/mobile/src/i18n/locales/ht.json | 4 +++- apps/mobile/src/i18n/locales/hu.json | 4 +++- apps/mobile/src/i18n/locales/hy.json | 4 +++- apps/mobile/src/i18n/locales/id.json | 4 +++- apps/mobile/src/i18n/locales/ig.json | 4 +++- apps/mobile/src/i18n/locales/is.json | 4 +++- apps/mobile/src/i18n/locales/it.json | 4 +++- apps/mobile/src/i18n/locales/ja.json | 4 +++- apps/mobile/src/i18n/locales/ka.json | 4 +++- apps/mobile/src/i18n/locales/kk.json | 4 +++- apps/mobile/src/i18n/locales/km.json | 4 +++- apps/mobile/src/i18n/locales/kn.json | 4 +++- apps/mobile/src/i18n/locales/ko.json | 4 +++- apps/mobile/src/i18n/locales/lo.json | 4 +++- apps/mobile/src/i18n/locales/lt.json | 4 +++- apps/mobile/src/i18n/locales/lv.json | 4 +++- apps/mobile/src/i18n/locales/mg.json | 4 +++- apps/mobile/src/i18n/locales/mi.json | 4 +++- apps/mobile/src/i18n/locales/mk.json | 4 +++- apps/mobile/src/i18n/locales/ml.json | 4 +++- apps/mobile/src/i18n/locales/mn.json | 4 +++- apps/mobile/src/i18n/locales/mr.json | 4 +++- apps/mobile/src/i18n/locales/ms.json | 4 +++- apps/mobile/src/i18n/locales/mt.json | 4 +++- apps/mobile/src/i18n/locales/my.json | 4 +++- apps/mobile/src/i18n/locales/nb.json | 4 +++- apps/mobile/src/i18n/locales/ne.json | 4 +++- apps/mobile/src/i18n/locales/nl.json | 4 +++- apps/mobile/src/i18n/locales/om.json | 4 +++- apps/mobile/src/i18n/locales/or.json | 4 +++- apps/mobile/src/i18n/locales/pa.json | 4 +++- apps/mobile/src/i18n/locales/pl.json | 4 +++- apps/mobile/src/i18n/locales/ps.json | 4 +++- apps/mobile/src/i18n/locales/pt-BR.json | 4 +++- apps/mobile/src/i18n/locales/pt.json | 4 +++- apps/mobile/src/i18n/locales/ro.json | 4 +++- apps/mobile/src/i18n/locales/ru.json | 4 +++- apps/mobile/src/i18n/locales/si.json | 4 +++- apps/mobile/src/i18n/locales/sk.json | 4 +++- apps/mobile/src/i18n/locales/sl.json | 4 +++- apps/mobile/src/i18n/locales/so.json | 4 +++- apps/mobile/src/i18n/locales/sq.json | 4 +++- apps/mobile/src/i18n/locales/sr.json | 4 +++- apps/mobile/src/i18n/locales/sv.json | 4 +++- apps/mobile/src/i18n/locales/sw.json | 4 +++- apps/mobile/src/i18n/locales/ta.json | 4 +++- apps/mobile/src/i18n/locales/te.json | 4 +++- apps/mobile/src/i18n/locales/th.json | 4 +++- apps/mobile/src/i18n/locales/tr.json | 4 +++- apps/mobile/src/i18n/locales/uk.json | 4 +++- apps/mobile/src/i18n/locales/ur.json | 4 +++- apps/mobile/src/i18n/locales/uz.json | 4 +++- apps/mobile/src/i18n/locales/vi.json | 4 +++- apps/mobile/src/i18n/locales/yo.json | 4 +++- apps/mobile/src/i18n/locales/zh-Hans.json | 4 +++- apps/mobile/src/i18n/locales/zh-Hant.json | 4 +++- apps/mobile/src/i18n/locales/zu.json | 4 +++- 87 files changed, 261 insertions(+), 87 deletions(-) diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 1b4ee043ca..cf670472a7 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -3203,6 +3203,8 @@ "running": "LOOP", "needsInput": "benodig invoer", "reconnecting": "Verbind tans weer", - "channelName": "Aktiewe agente" + "channelName": "Aktiewe agente", + "activityKitDisabledTitle": "Regstreekse Aktiwiteite is af", + "activityKitDisabledBody": "Skakel Regstreekse Aktiwiteite in Instellings aan om Aktiewe Agente op die Sluitskerm te sien." } } diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 5b7ada46af..6eb270f961 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -3203,6 +3203,8 @@ "running": "በስራ ላይ", "needsInput": "ግብዓት ይፈልጋል", "reconnecting": "እንደገና በመገናኘት ላይ", - "channelName": "ንቁ ወኪሎች" + "channelName": "ንቁ ወኪሎች", + "activityKitDisabledTitle": "የቀጥታ እንቅስቃሴዎች ጠፍተዋል", + "activityKitDisabledBody": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ ለማየት በቅንብሮች ውስጥ የቀጥታ እንቅስቃሴዎችን ያብሩ።" } } diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index e8af7140d7..baaef6572f 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -3287,6 +3287,8 @@ "running": "قيد التشغيل", "needsInput": "يتطلب إدخالًا", "reconnecting": "جارٍ إعادة الاتصال", - "channelName": "الوكلاء النشطون" + "channelName": "الوكلاء النشطون", + "activityKitDisabledTitle": "الأنشطة المباشرة متوقفة", + "activityKitDisabledBody": "فعّل الأنشطة المباشرة في الإعدادات لرؤية الوكلاء النشطين على شاشة القفل." } } diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 67e9b552c7..760a5f472b 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -3203,6 +3203,8 @@ "running": "İŞLƏYİR", "needsInput": "GİRİŞ TƏLƏB OLUNUR", "reconnecting": "Yenidən qoşulur", - "channelName": "Aktiv agentlər" + "channelName": "Aktiv agentlər", + "activityKitDisabledTitle": "Canlı fəaliyyətlər söndürülüb", + "activityKitDisabledBody": "Kilid ekranında aktiv agentləri görmək üçün Parametrlərdə Canlı fəaliyyətləri aktivləşdirin." } } diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index c418ee0c38..dd9e35eea3 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -3245,6 +3245,8 @@ "running": "ПРАЦУЕ", "needsInput": "патрабуецца ўвод", "reconnecting": "Паўторнае падключэнне", - "channelName": "Актыўныя агенты" + "channelName": "Актыўныя агенты", + "activityKitDisabledTitle": "Жывыя дзеянні выключаны", + "activityKitDisabledBody": "Уключыце «Жывыя дзеянні» ў «Наладах», каб бачыць актыўных агентаў на экране блакіроўкі." } } diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 5cd8407ff4..bd69c06cff 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -3203,6 +3203,8 @@ "running": "Изпълнява се", "needsInput": "изисква въвеждане", "reconnecting": "Повторно свързване", - "channelName": "Активни агенти" + "channelName": "Активни агенти", + "activityKitDisabledTitle": "Дейностите на живо са изключени", + "activityKitDisabledBody": "Включете Дейности на живо в Настройки, за да виждате активните агенти на заключения екран." } } diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index ba4620cb81..c4987395e3 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -3203,6 +3203,8 @@ "running": "চলছে", "needsInput": "ইনপুট প্রয়োজন", "reconnecting": "পুনরায় সংযোগ করা হচ্ছে", - "channelName": "সক্রিয় এজেন্ট" + "channelName": "সক্রিয় এজেন্ট", + "activityKitDisabledTitle": "সরাসরি কার্যকলাপ বন্ধ আছে", + "activityKitDisabledBody": "লক স্ক্রিনে সক্রিয় এজেন্টগুলি দেখতে সেটিংসে সরাসরি কার্যকলাপ চালু করুন।" } } diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index 0e49ae771e..df8241f41a 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -3224,6 +3224,8 @@ "running": "RADI", "needsInput": "treba unos", "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti" + "channelName": "Aktivni agenti", + "activityKitDisabledTitle": "Aktivnosti uživo su isključene", + "activityKitDisabledBody": "Uključite aktivnosti uživo u Postavkama da biste vidjeli aktivne agente na zaključanom ekranu." } } diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 424f8144dc..8e1ae0ba1b 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -3224,6 +3224,8 @@ "running": "EN EXECUCIÓ", "needsInput": "requereix entrada", "reconnecting": "Reconnectant", - "channelName": "Agents actius" + "channelName": "Agents actius", + "activityKitDisabledTitle": "Les activitats en directe estan desactivades", + "activityKitDisabledBody": "Activa les activitats en directe a Configuració per veure els agents actius a la pantalla de bloqueig." } } diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 2c1311bd58..b162480f86 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -3203,6 +3203,8 @@ "running": "لە کاردایە", "needsInput": "پێویستی بە داخڵکردن", "reconnecting": "لە پەیوەستبوونەوەدایە", - "channelName": "ئەجێنتە چالاکەکان" + "channelName": "ئەجێنتە چالاکەکان", + "activityKitDisabledTitle": "چالاکییە ڕاستەوخۆکان ناچالاکن", + "activityKitDisabledBody": "چالاکییە ڕاستەوخۆکان لە ڕێکخستنەکان چالاک بکە بۆ بینینی ئەجێنتە چالاکەکان لە شاشەی قوفڵ." } } diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 6b1877c7ad..689e0cef35 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -3245,6 +3245,8 @@ "running": "BĚŽÍ", "needsInput": "vyžaduje vstup", "reconnecting": "Obnovování připojení", - "channelName": "Aktivní agenti" + "channelName": "Aktivní agenti", + "activityKitDisabledTitle": "Živé aktivity jsou vypnuté", + "activityKitDisabledBody": "Zapněte Živé aktivity v Nastavení, abyste viděli aktivní agenty na zamknuté obrazovce." } } diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index c89bfe339d..ae034409c3 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -3287,6 +3287,8 @@ "running": "YN RHEDEG", "needsInput": "angen mewnbwn", "reconnecting": "Yn ailgysylltu", - "channelName": "Asiantau gweithredol" + "channelName": "Asiantau gweithredol", + "activityKitDisabledTitle": "Mae Gweithgareddau Byw wedi'u diffodd", + "activityKitDisabledBody": "Trowch Weithgareddau Byw ymlaen yn Gosodiadau i weld Asiantau gweithredol ar y Sgrin Glo." } } diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index df2630b2f1..e4e3101fc3 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -3203,6 +3203,8 @@ "running": "KØRER", "needsInput": "kræver input", "reconnecting": "Genopretter forbindelsen", - "channelName": "Aktive agenter" + "channelName": "Aktive agenter", + "activityKitDisabledTitle": "Liveaktiviteter er slået fra", + "activityKitDisabledBody": "Slå Liveaktiviteter til i Indstillinger for at se aktive agenter på låseskærmen." } } diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index c2091c7fd8..0d65fb86b4 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -3203,6 +3203,8 @@ "running": "LÄUFT", "needsInput": "Eingabe erforderlich", "reconnecting": "Verbindung wird wiederhergestellt", - "channelName": "Aktive Agenten" + "channelName": "Aktive Agenten", + "activityKitDisabledTitle": "Live-Aktivitäten sind deaktiviert", + "activityKitDisabledBody": "Aktiviere Live-Aktivitäten in den Einstellungen, um aktive Agenten auf dem Sperrbildschirm zu sehen." } } diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index 2fc7506b84..afc450b50f 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -3203,6 +3203,8 @@ "running": "Σε εξέλιξη", "needsInput": "χρειάζεται είσοδο", "reconnecting": "Επανασύνδεση", - "channelName": "Ενεργοί πράκτορες" + "channelName": "Ενεργοί πράκτορες", + "activityKitDisabledTitle": "Οι Ζωντανές δραστηριότητες είναι απενεργοποιημένες", + "activityKitDisabledBody": "Ενεργοποιήστε τις Ζωντανές δραστηριότητες στις Ρυθμίσεις για να δείτε τους ενεργούς πράκτορες στην Οθόνη κλειδώματος." } } diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 74b46f2668..725ca45c84 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -3203,6 +3203,8 @@ "running": "Running", "needsInput": "Needs input", "reconnecting": "Reconnecting", - "channelName": "Active agents" + "channelName": "Active agents", + "activityKitDisabledTitle": "Live Activities are off", + "activityKitDisabledBody": "Turn on Live Activities in Settings to see Active Agents on the Lock Screen." } } diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 15e9b24f29..8eeb9eaa2d 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -3224,6 +3224,8 @@ "running": "EN EJECUCIÓN", "needsInput": "requiere entrada", "reconnecting": "Reconectando", - "channelName": "Agentes activos" + "channelName": "Agentes activos", + "activityKitDisabledTitle": "Las actividades en directo están desactivadas", + "activityKitDisabledBody": "Activa las actividades en directo en Ajustes para ver los agentes activos en la pantalla de bloqueo." } } diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index efedf0e21f..bcc271ae98 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -3203,6 +3203,8 @@ "running": "TÖÖTAB", "needsInput": "vajab sisendit", "reconnecting": "Ühenduse taastamine", - "channelName": "Aktiivsed agendid" + "channelName": "Aktiivsed agendid", + "activityKitDisabledTitle": "Reaalajas tegevused on välja lülitatud", + "activityKitDisabledBody": "Lülitage seadetes reaalajas tegevused sisse, et näha aktiivseid agente lukustuskuval." } } diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 007513a714..69401ac1c6 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -3203,6 +3203,8 @@ "running": "Exekutatzen", "needsInput": "sarreraren zain", "reconnecting": "Berriro konektatzen", - "channelName": "Agente aktiboak" + "channelName": "Agente aktiboak", + "activityKitDisabledTitle": "Zuzeneko jarduerak desaktibatuta daude", + "activityKitDisabledBody": "Aktibatu Zuzeneko jarduerak Ezarpenetan, Agente aktiboak Blokeo-pantailan ikusteko." } } diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index c5686f38c6..4742b0a89b 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -3203,6 +3203,8 @@ "running": "در حال اجرا", "needsInput": "نیاز به ورودی", "reconnecting": "در حال اتصال مجدد", - "channelName": "عامل‌های فعال" + "channelName": "عامل‌های فعال", + "activityKitDisabledTitle": "فعالیت‌های زنده خاموش هستند", + "activityKitDisabledBody": "برای دیدن عامل‌های فعال در صفحه قفل، فعالیت‌های زنده را در تنظیمات روشن کنید." } } diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index 63c2436f4e..bec672d01b 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -3203,6 +3203,8 @@ "running": "KÄYNNISSÄ", "needsInput": "vaatii syötettä", "reconnecting": "Yhdistetään uudelleen", - "channelName": "Aktiiviset agentit" + "channelName": "Aktiiviset agentit", + "activityKitDisabledTitle": "Live-aktiviteetit ovat pois päältä", + "activityKitDisabledBody": "Ota live-aktiviteetit käyttöön Asetuksissa, niin näet aktiiviset agentit lukitulla näytöllä." } } diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index c8c7507ac2..0aa3f3643a 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -3203,6 +3203,8 @@ "running": "TUMATAKBO", "needsInput": "kailangan ng input", "reconnecting": "Muling kumokonekta", - "channelName": "Mga aktibong agent" + "channelName": "Mga aktibong agent", + "activityKitDisabledTitle": "Naka-off ang Mga Live na Aktibidad", + "activityKitDisabledBody": "I-on ang Mga Live na Aktibidad sa Mga setting para makita ang Mga aktibong agent sa Naka-lock na Screen." } } diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index ec83f5440e..a062104a15 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -3224,6 +3224,8 @@ "running": "EN COURS", "needsInput": "saisie requise", "reconnecting": "Reconnexion en cours", - "channelName": "Agents actifs" + "channelName": "Agents actifs", + "activityKitDisabledTitle": "Les activités en direct sont désactivées", + "activityKitDisabledBody": "Activez les activités en direct dans Réglages pour voir les agents actifs sur l'écran verrouillé." } } diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index 5e4c91f5d2..0e54003877 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -3266,6 +3266,8 @@ "running": "AG RITH", "needsInput": "teastaíonn ionchur", "reconnecting": "Ag athcheangal", - "channelName": "Gníomhairí gníomhacha" + "channelName": "Gníomhairí gníomhacha", + "activityKitDisabledTitle": "Tá Gníomhaíochtaí Beo as", + "activityKitDisabledBody": "Cumasaigh Gníomhaíochtaí Beo sna Socruithe chun Gníomhairí gníomhacha a fheiceáil ar an Scáileán Glasála." } } diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index f3a70de821..e59937cb68 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -3203,6 +3203,8 @@ "running": "Executando", "needsInput": "precisa entrada", "reconnecting": "Reconectando", - "channelName": "Axentes activos" + "channelName": "Axentes activos", + "activityKitDisabledTitle": "As actividades en directo están desactivadas", + "activityKitDisabledBody": "Activa as actividades en directo en Configuración para ver os axentes activos na pantalla de bloqueo." } } diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index c271998e8f..b23be114ea 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -3203,6 +3203,8 @@ "running": "ચાલી રહ્યું છે", "needsInput": "ઇનપુટ જરૂરી", "reconnecting": "ફરી કનેક્ટ થઈ રહ્યું છે", - "channelName": "સક્રિય એજન્ટો" + "channelName": "સક્રિય એજન્ટો", + "activityKitDisabledTitle": "લાઇવ પ્રવૃત્તિઓ બંધ છે", + "activityKitDisabledBody": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો જોવા માટે સેટિંગ્સમાં લાઇવ પ્રવૃત્તિઓ ચાલુ કરો." } } diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 183f2b80f6..0707d0bb16 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -3203,6 +3203,8 @@ "running": "Ana gudana", "needsInput": "yana buƙatar bayani", "reconnecting": "Ana sake haɗawa", - "channelName": "Wakilai da ke aiki" + "channelName": "Wakilai da ke aiki", + "activityKitDisabledTitle": "Ayyukan Kai Tsaye suna a kashe", + "activityKitDisabledBody": "Kunna Ayyukan Kai Tsaye a cikin Saituna don ganin Wakilai da ke Aiki a kan Allon Kulle." } } diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index d18236d0a7..f673155d73 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -3224,6 +3224,8 @@ "running": "רץ", "needsInput": "נדרש קלט", "reconnecting": "מתחבר מחדש", - "channelName": "סוכנים פעילים" + "channelName": "סוכנים פעילים", + "activityKitDisabledTitle": "פעילויות בזמן אמת כבויות", + "activityKitDisabledBody": "הפעל פעילויות בזמן אמת בהגדרות כדי לראות סוכנים פעילים במסך הנעילה." } } diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 4ae7b35d52..63e95174cc 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -3203,6 +3203,8 @@ "running": "चालू", "needsInput": "इनपुट आवश्यक", "reconnecting": "फिर से कनेक्ट हो रहा है", - "channelName": "सक्रिय एजेंट" + "channelName": "सक्रिय एजेंट", + "activityKitDisabledTitle": "लाइव ऐक्टिविटी बंद हैं", + "activityKitDisabledBody": "लॉक स्क्रीन पर सक्रिय एजेंट देखने के लिए सेटिंग में लाइव ऐक्टिविटी चालू करें।" } } diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 7046d3715b..7cb949d93a 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -3224,6 +3224,8 @@ "running": "RADI", "needsInput": "treba unos", "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti" + "channelName": "Aktivni agenti", + "activityKitDisabledTitle": "Aktivnosti uživo su isključene", + "activityKitDisabledBody": "Uključite Aktivnosti uživo u Postavkama kako biste vidjeli aktivne agente na zaključanom zaslonu." } } diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 0f458c784b..92272deb3c 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -3203,6 +3203,8 @@ "running": "AP KOURI", "needsInput": "bezwen input", "reconnecting": "Ap rekonekte", - "channelName": "Ajans aktif yo" + "channelName": "Ajans aktif yo", + "activityKitDisabledTitle": "Aktivite an dirèk yo fèmen", + "activityKitDisabledBody": "Aktive Aktivite an dirèk nan Paramèt pou wè Ajans aktif yo sou Ekran bloke a." } } diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index 3009fd6a05..72be12dc20 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -3203,6 +3203,8 @@ "running": "Folyamatban", "needsInput": "bemenetet igényel", "reconnecting": "Újracsatlakozás", - "channelName": "Aktív ügynökök" + "channelName": "Aktív ügynökök", + "activityKitDisabledTitle": "Az Élő tevékenységek ki vannak kapcsolva", + "activityKitDisabledBody": "Kapcsolja be az Élő tevékenységeket a Beállításokban, hogy az aktív ügynökök megjelenjenek a zárolási képernyőn." } } diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index 7b6d4c9435..fd86d5065b 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -3203,6 +3203,8 @@ "running": "Ընթացքի մեջ է", "needsInput": "մուտքագրման կարիք ունի", "reconnecting": "Կրկին միացում", - "channelName": "Ակտիվ գործակալներ" + "channelName": "Ակտիվ գործակալներ", + "activityKitDisabledTitle": "Ուղիղ ակտիվություններն անջատված են", + "activityKitDisabledBody": "Միացրեք «Ուղիղ ակտիվություններ»-ը Կարգավորումներում՝ ակտիվ գործակալներին կողպման էկրանին տեսնելու համար։" } } diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index a3254f067b..53be7b234d 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -3203,6 +3203,8 @@ "running": "BERJALAN", "needsInput": "memerlukan input", "reconnecting": "Menghubungkan kembali", - "channelName": "Agen aktif" + "channelName": "Agen aktif", + "activityKitDisabledTitle": "Aktivitas Langsung nonaktif", + "activityKitDisabledBody": "Aktifkan Aktivitas Langsung di Pengaturan untuk melihat Agen Aktif di Layar Terkunci." } } diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 2ff50d86aa..c4aadb9e50 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -3203,6 +3203,8 @@ "running": "NA-AGBA", "needsInput": "chọrọ ntinye", "reconnecting": "Na-ejikọ ọzọ", - "channelName": "Ndị ọrụ na-arụ ọrụ" + "channelName": "Ndị ọrụ na-arụ ọrụ", + "activityKitDisabledTitle": "Agbanyụrụ Ihe Omume Dị Ndụ", + "activityKitDisabledBody": "Gbanye Ihe Omume Dị Ndụ na Ntọala iji hụ Ndị ọrụ na-arụ ọrụ na Ihuenyo Mkpọchi." } } diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index af9f5a572b..0988544e0d 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -3203,6 +3203,8 @@ "running": "Í gangi", "needsInput": "þarfnast inntaks", "reconnecting": "Tengist aftur", - "channelName": "Virk umboð" + "channelName": "Virk umboð", + "activityKitDisabledTitle": "Slökkt er á Beinni virkni", + "activityKitDisabledBody": "Kveiktu á Beinni virkni í Stillingum til að sjá Virk umboð á Lásskjánum." } } diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 8bdd22a3f1..a6dff9624e 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -3224,6 +3224,8 @@ "running": "IN ESECUZIONE", "needsInput": "richiede input", "reconnecting": "Riconnessione in corso", - "channelName": "Agenti attivi" + "channelName": "Agenti attivi", + "activityKitDisabledTitle": "Le attività in tempo reale sono disattivate", + "activityKitDisabledBody": "Attiva le attività in tempo reale in Impostazioni per vedere gli agenti attivi sulla schermata di blocco." } } diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 54e1029390..01c1057b01 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -3203,6 +3203,8 @@ "running": "実行中", "needsInput": "入力が必要", "reconnecting": "再接続中", - "channelName": "アクティブなエージェント" + "channelName": "アクティブなエージェント", + "activityKitDisabledTitle": "ライブアクティビティはオフです", + "activityKitDisabledBody": "ロック画面にアクティブなエージェントを表示するには、設定でライブアクティビティをオンにしてください。" } } diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 14450163af..fb467472e1 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -3203,6 +3203,8 @@ "running": "მუშაობს", "needsInput": "მოითხოვს შეყვანას", "reconnecting": "კავშირის აღდგენა", - "channelName": "აქტიური აგენტები" + "channelName": "აქტიური აგენტები", + "activityKitDisabledTitle": "ცოცხალი აქტივობები გამორთულია", + "activityKitDisabledBody": "ჩართეთ ცოცხალი აქტივობები პარამეტრებში, რათა დაბლოკვის ეკრანზე აქტიური აგენტები ნახოთ." } } diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 4c944a7290..f6f359ecf1 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -3203,6 +3203,8 @@ "running": "Орындалуда", "needsInput": "енгізу қажет", "reconnecting": "Қайта қосылуда", - "channelName": "Белсенді агенттер" + "channelName": "Белсенді агенттер", + "activityKitDisabledTitle": "Тікелей әрекеттер өшірулі", + "activityKitDisabledBody": "Құлыптау экранында белсенді агенттерді көру үшін Параметрлерде тікелей әрекеттерді қосыңыз." } } diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 4ae77a6a52..2831e9cf43 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -3203,6 +3203,8 @@ "running": "កំពុងដំណើរការ", "needsInput": "ត្រូវការបញ្ចូល", "reconnecting": "កំពុងភ្ជាប់ឡើងវិញ", - "channelName": "ភ្នាក់ងារសកម្ម" + "channelName": "ភ្នាក់ងារសកម្ម", + "activityKitDisabledTitle": "សកម្មភាពបន្តផ្ទាល់ត្រូវបានបិទ", + "activityKitDisabledBody": "បើកសកម្មភាពបន្តផ្ទាល់នៅក្នុងការកំណត់ ដើម្បីមើលភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ។" } } diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 47ed0eae90..c91b75e06e 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -3203,6 +3203,8 @@ "running": "ಚಾಲನೆಯಲ್ಲಿದೆ", "needsInput": "ಇನ್‌ಪುಟ್ ಅಗತ್ಯವಿದೆ", "reconnecting": "ಮರುಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ", - "channelName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು" + "channelName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು", + "activityKitDisabledTitle": "ನೇರ ಚಟುವಟಿಕೆಗಳು ಆಫ್ ಆಗಿವೆ", + "activityKitDisabledBody": "ಲಾಕ್ ಪರದೆಯಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ನೇರ ಚಟುವಟಿಕೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ." } } diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 5439a2bab3..9af46ce32a 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -3203,6 +3203,8 @@ "running": "실행 중", "needsInput": "입력 필요", "reconnecting": "다시 연결 중", - "channelName": "활성 에이전트" + "channelName": "활성 에이전트", + "activityKitDisabledTitle": "실시간 현황이 꺼져 있습니다", + "activityKitDisabledBody": "잠금 화면에서 활성 에이전트를 보려면 설정에서 실시간 현황을 켜세요." } } diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index 8d6b277352..0f5dc14831 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -3203,6 +3203,8 @@ "running": "ກຳລັງດຳເນີນການ", "needsInput": "ຕ້ອງການຂໍ້ມູນເຂົ້າ", "reconnecting": "ກຳລັງເຊື່ອມຕໍ່ຄືນ", - "channelName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ" + "channelName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ", + "activityKitDisabledTitle": "ກິດຈະກຳສົດປິດຢູ່", + "activityKitDisabledBody": "ເປີດກິດຈະກຳສົດໃນການຕັ້ງຄ່າ ເພື່ອເບິ່ງຕົວແທນທີ່ກຳລັງເຮັດວຽກໃນໜ້າຈໍລັອກ." } } diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 3bc314f349..00b614f2ab 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -3245,6 +3245,8 @@ "running": "Vykdoma", "needsInput": "reikia įvesties", "reconnecting": "Jungiamasi iš naujo", - "channelName": "Aktyvūs agentai" + "channelName": "Aktyvūs agentai", + "activityKitDisabledTitle": "Tiesioginės veiklos išjungtos", + "activityKitDisabledBody": "Nustatymuose įjunkite tiesiogines veiklas, kad užrakinimo ekrane matytumėte aktyvius agentus." } } diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index 05a7724241..e9da3f9f1a 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -3224,6 +3224,8 @@ "running": "DARBOJAS", "needsInput": "nepieciešama ievade", "reconnecting": "Atkārtoti izveido savienojumu", - "channelName": "Aktīvie aģenti" + "channelName": "Aktīvie aģenti", + "activityKitDisabledTitle": "Tiešraides aktivitātes ir izslēgtas", + "activityKitDisabledBody": "Ieslēdz tiešraides aktivitātes iestatījumos, lai bloķēšanas ekrānā redzētu aktīvos aģentus." } } diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index 1a3dc4d8ee..dbb5cdcf65 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -3203,6 +3203,8 @@ "running": "MANDEHA", "needsInput": "mila fampidirana", "reconnecting": "Mampifandray indray", - "channelName": "Agent mavitrika" + "channelName": "Agent mavitrika", + "activityKitDisabledTitle": "Tsy mandeha ny Hetsika Mivantana", + "activityKitDisabledBody": "Alefaso ao amin'ny Fikirana ny Hetsika Mivantana mba hahitana ny Agent Mavitrika eo amin'ny Efijery Fihidy." } } diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index 1dc66c66c0..48614e612d 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -3203,6 +3203,8 @@ "running": "Kei te oma", "needsInput": "e hiahia ana ki te whakaurunga", "reconnecting": "Kei te hono anō", - "channelName": "Ngā māngai hohe" + "channelName": "Ngā māngai hohe", + "activityKitDisabledTitle": "Kua whakawetohia ngā Mahi Mataora", + "activityKitDisabledBody": "Whakakāngia ngā Mahi Mataora i Ngā tautuhinga kia kite i ngā Māngai Hohe i te Mata Maukati." } } diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index e4ada1704f..ba85de30a5 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -3203,6 +3203,8 @@ "running": "Во тек", "needsInput": "бара внес", "reconnecting": "Повторно поврзување", - "channelName": "Активни агенти" + "channelName": "Активни агенти", + "activityKitDisabledTitle": "Активностите во живо се исклучени", + "activityKitDisabledBody": "Вклучете Активности во живо во Поставки за да ги видите активните агенти на заклучениот екран." } } diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index d6692e6a94..9322d2271d 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -3203,6 +3203,8 @@ "running": "പ്രവർത്തിക്കുന്നു", "needsInput": "ഇൻപുട്ട് ആവശ്യമാണ്", "reconnecting": "വീണ്ടും ബന്ധിപ്പിക്കുന്നു", - "channelName": "സജീവ ഏജന്റുകൾ" + "channelName": "സജീവ ഏജന്റുകൾ", + "activityKitDisabledTitle": "തത്സമയ പ്രവർത്തനങ്ങൾ ഓഫാണ്", + "activityKitDisabledBody": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണാൻ ക്രമീകരണങ്ങളിൽ തത്സമയ പ്രവർത്തനങ്ങൾ പ്രവർത്തനക്ഷമമാക്കുക." } } diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index 1d34ea0a0e..b81eab872a 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -3203,6 +3203,8 @@ "running": "АЖИЛЛАЖ БАЙНА", "needsInput": "оролт шаардлагатай", "reconnecting": "Дахин холбогдож байна", - "channelName": "Идэвхтэй агентууд" + "channelName": "Идэвхтэй агентууд", + "activityKitDisabledTitle": "Шууд үйл ажиллагаа унтраалттай байна", + "activityKitDisabledBody": "Түгжээтэй дэлгэц дээр Идэвхтэй агентуудыг харахын тулд Тохиргоо хэсэгт Шууд үйл ажиллагааг асаана уу." } } diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index c719d9f452..b938634c68 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -3203,6 +3203,8 @@ "running": "चालू आहे", "needsInput": "इनपुट आवश्यक", "reconnecting": "पुन्हा जोडत आहे", - "channelName": "सक्रिय एजंट्स" + "channelName": "सक्रिय एजंट्स", + "activityKitDisabledTitle": "थेट क्रियाकलाप बंद आहेत", + "activityKitDisabledBody": "लॉक स्क्रीनवर सक्रिय एजंट्स पाहण्यासाठी सेटिंग्जमध्ये थेट क्रियाकलाप सुरू करा." } } diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 37b9e24910..f2383b0678 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -3203,6 +3203,8 @@ "running": "Sedang berjalan", "needsInput": "perlu input", "reconnecting": "Menyambung semula", - "channelName": "Ejen aktif" + "channelName": "Ejen aktif", + "activityKitDisabledTitle": "Aktiviti Langsung dimatikan", + "activityKitDisabledBody": "Hidupkan Aktiviti Langsung dalam Tetapan untuk melihat Ejen Aktif pada Skrin Kunci." } } diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index cf0f0b7279..9de9bda52b 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -3266,6 +3266,8 @@ "running": "Għaddej", "needsInput": "jeħtieġ input", "reconnecting": "Qed jerġa' jaqbad", - "channelName": "Aġenti attivi" + "channelName": "Aġenti attivi", + "activityKitDisabledTitle": "L-Attivitajiet Diretti huma mitfija", + "activityKitDisabledBody": "Ixgħel l-Attivitajiet Diretti fis-Settings biex tara l-Aġenti Attivi fuq l-Iskrin Imsakkar." } } diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index 26475618a2..0acbfd71ca 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -3203,6 +3203,8 @@ "running": "လည်ပတ်နေသည်", "needsInput": "ထည့်သွင်းမှု လိုအပ်သည်", "reconnecting": "ပြန်ချိတ်ဆက်နေသည်", - "channelName": "လုပ်ဆောင်နေသော agent များ" + "channelName": "လုပ်ဆောင်နေသော agent များ", + "activityKitDisabledTitle": "တိုက်ရိုက်လှုပ်ရှားမှုများ ပိတ်ထားသည်", + "activityKitDisabledBody": "သော့ခတ်မျက်နှာပြင်တွင် လုပ်ဆောင်နေသော agent များကို ကြည့်ရန် ဆက်တင်များတွင် တိုက်ရိုက်လှုပ်ရှားမှုများကို ဖွင့်ပါ။" } } diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index ec6a72b9ba..a4298c5f36 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -3203,6 +3203,8 @@ "running": "KJØRER", "needsInput": "trenger innspill", "reconnecting": "Kobler til på nytt", - "channelName": "Aktive agenter" + "channelName": "Aktive agenter", + "activityKitDisabledTitle": "Oppdateringer i sanntid er av", + "activityKitDisabledBody": "Slå på Oppdateringer i sanntid i Innstillinger for å se Aktive agenter på låst skjerm." } } diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index 838478720b..ecf5f482fb 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -3203,6 +3203,8 @@ "running": "चलिरहेको", "needsInput": "इनपुट चाहिन्छ", "reconnecting": "पुनः जडान गर्दै", - "channelName": "सक्रिय एजेन्टहरू" + "channelName": "सक्रिय एजेन्टहरू", + "activityKitDisabledTitle": "प्रत्यक्ष गतिविधिहरू बन्द छन्", + "activityKitDisabledBody": "लक स्क्रिनमा सक्रिय एजेन्टहरू हेर्न सेटिङ्समा प्रत्यक्ष गतिविधिहरू चालू गर्नुहोस्।" } } diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 8c4666a517..11b04a90b7 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -3203,6 +3203,8 @@ "running": "Bezig", "needsInput": "heeft invoer nodig", "reconnecting": "Opnieuw verbinden", - "channelName": "Actieve agents" + "channelName": "Actieve agents", + "activityKitDisabledTitle": "Liveactiviteiten staan uit", + "activityKitDisabledBody": "Schakel liveactiviteiten in via Instellingen om actieve agents op het toegangsscherm te zien." } } diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index d5752001be..7e7c873d39 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -3203,6 +3203,8 @@ "running": "Hojii irra jira", "needsInput": "seensa barbaada", "reconnecting": "Irra deebi'ee walqabachaa jira", - "channelName": "Eejentoota hojii irra jiran" + "channelName": "Eejentoota hojii irra jiran", + "activityKitDisabledTitle": "Sochiiwwan Kallattii cufamaniiru", + "activityKitDisabledBody": "Eejentoota hojii irra jiran Iskiriinii Qulfii irratti arguuf, Qindaa'ina keessatti Sochiiwwan Kallattii banaa." } } diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 7484c06307..28dfd8f459 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -3203,6 +3203,8 @@ "running": "ଚାଲୁଛି", "needsInput": "ଇନପୁଟ୍ ଆବଶ୍ୟକ", "reconnecting": "ପୁଣି ସଂଯୋଗ ହେଉଛି", - "channelName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ" + "channelName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ", + "activityKitDisabledTitle": "ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ବନ୍ଦ ଅଛି", + "activityKitDisabledBody": "ଲକ୍ ସ୍କ୍ରିନ୍‌ରେ ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସେଟିଂସ୍‌ରେ ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ଚାଲୁ କରନ୍ତୁ।" } } diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 1f02febb3f..dc384c98b0 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -3203,6 +3203,8 @@ "running": "ਚੱਲ ਰਿਹਾ ਹੈ", "needsInput": "ਇਨਪੁੱਟ ਦੀ ਲੋੜ ਹੈ", "reconnecting": "ਮੁੜ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", - "channelName": "ਸਰਗਰਮ ਏਜੰਟ" + "channelName": "ਸਰਗਰਮ ਏਜੰਟ", + "activityKitDisabledTitle": "ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਬੰਦ ਹਨ", + "activityKitDisabledBody": "ਲਾਕ ਸਕ੍ਰੀਨ 'ਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦੇਖਣ ਲਈ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਚਾਲੂ ਕਰੋ।" } } diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index 556da10f5e..e0b439e9c0 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -3245,6 +3245,8 @@ "running": "W toku", "needsInput": "wymaga danych", "reconnecting": "Ponowne łączenie", - "channelName": "Aktywni agenci" + "channelName": "Aktywni agenci", + "activityKitDisabledTitle": "Wydarzenia na żywo są wyłączone", + "activityKitDisabledBody": "Włącz wydarzenia na żywo w Ustawieniach, aby widzieć aktywnych agentów na ekranie blokady." } } diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 7a0e195688..023a93365b 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -3203,6 +3203,8 @@ "running": "روان", "needsInput": "ورودی ته اړتیا لري", "reconnecting": "بیا نښلېږي", - "channelName": "فعال اجنټان" + "channelName": "فعال اجنټان", + "activityKitDisabledTitle": "ژوندي فعالیتونه بند دي", + "activityKitDisabledBody": "په قلف شوې پرده کې د فعالو اجنټانو د لیدلو لپاره په ترتیباتو کې ژوندي فعالیتونه فعال کړئ." } } diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index c4aa487c44..ce93386fce 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -3224,6 +3224,8 @@ "running": "Em execução", "needsInput": "requer entrada", "reconnecting": "Reconectando", - "channelName": "Agentes ativos" + "channelName": "Agentes ativos", + "activityKitDisabledTitle": "As Atividades ao Vivo estão desativadas", + "activityKitDisabledBody": "Ative as Atividades ao Vivo em Ajustes para ver os agentes ativos na Tela Bloqueada." } } diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 215759a7b1..73f9fcdd8c 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -3224,6 +3224,8 @@ "running": "EM EXECUÇÃO", "needsInput": "requer entrada", "reconnecting": "A restabelecer ligação", - "channelName": "Agentes ativos" + "channelName": "Agentes ativos", + "activityKitDisabledTitle": "As Atividades em tempo real estão desativadas", + "activityKitDisabledBody": "Ative as Atividades em tempo real nas Definições para ver os agentes ativos no Ecrã bloqueado." } } diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 8725fe53ee..f1a3aa9777 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -3224,6 +3224,8 @@ "running": "Rulează", "needsInput": "necesită introducere", "reconnecting": "Se reconectează", - "channelName": "Agenți activi" + "channelName": "Agenți activi", + "activityKitDisabledTitle": "Activitățile live sunt dezactivate", + "activityKitDisabledBody": "Activează Activități live în Setări pentru a vedea Agenții activi pe ecranul de blocare." } } diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index c8e591b265..8412bf2ac9 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -3245,6 +3245,8 @@ "running": "Выполняется", "needsInput": "требует ввода", "reconnecting": "Повторное подключение", - "channelName": "Активные агенты" + "channelName": "Активные агенты", + "activityKitDisabledTitle": "Эфир активности выключен", + "activityKitDisabledBody": "Включите Эфир активности в Настройках, чтобы видеть активных агентов на экране блокировки." } } diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index c6f64d4d94..f8bd34b3e0 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -3203,6 +3203,8 @@ "running": "ධාවනය වෙමින්", "needsInput": "ආදානය අවශ්යයි", "reconnecting": "නැවත සම්බන්ධ වෙමින්", - "channelName": "සක්‍රිය නියෝජිතයන්" + "channelName": "සක්‍රිය නියෝජිතයන්", + "activityKitDisabledTitle": "සජීවී ක්‍රියාකාරකම් අක්‍රියයි", + "activityKitDisabledBody": "අගුළු තිරයේ සක්‍රිය නියෝජිතයන් බැලීමට සැකසුම් තුළ සජීවී ක්‍රියාකාරකම් සක්‍රිය කරන්න." } } diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index 24a8d360d9..5e66e53b27 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -3245,6 +3245,8 @@ "running": "Prebieha", "needsInput": "vyžaduje vstup", "reconnecting": "Opätovné pripájanie", - "channelName": "Aktívni agenti" + "channelName": "Aktívni agenti", + "activityKitDisabledTitle": "Živé aktivity sú vypnuté", + "activityKitDisabledBody": "Zapnite živé aktivity v Nastaveniach, aby sa aktívni agenti zobrazovali na zamknutej obrazovke." } } diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index 0aad448db8..4cc7c05f56 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -3245,6 +3245,8 @@ "running": "DELUJE", "needsInput": "potrebuje vnos", "reconnecting": "Ponovno povezovanje", - "channelName": "Aktivni agenti" + "channelName": "Aktivni agenti", + "activityKitDisabledTitle": "Dejavnosti v živo so izklopljene", + "activityKitDisabledBody": "V Nastavitvah vklopite Dejavnosti v živo, da bodo Aktivni agenti prikazani na zaklenjenem zaslonu." } } diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 24bf096d20..218014c494 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -3203,6 +3203,8 @@ "running": "Socodaya", "needsInput": "u baahan wax-soo-gal", "reconnecting": "Dib u xiriirinaya", - "channelName": "Wakiillada firfircoon" + "channelName": "Wakiillada firfircoon", + "activityKitDisabledTitle": "Hawlaha Tooska ah waa daman", + "activityKitDisabledBody": "Ku daar Hawlaha Tooska ah Dejinta si aad Wakiillada firfircoon ugu aragto Shaashadda Qufulka." } } diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index b55329e94a..dacf2759e9 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -3203,6 +3203,8 @@ "running": "Në ekzekutim", "needsInput": "ka nevojë për të dhëna", "reconnecting": "Duke u rilidhur", - "channelName": "Agjentët aktivë" + "channelName": "Agjentët aktivë", + "activityKitDisabledTitle": "Aktivitetet e drejtpërdrejta janë çaktivizuar", + "activityKitDisabledBody": "Aktivizoni Aktivitetet e drejtpërdrejta te Cilësimet për të parë Agjentët aktivë në Ekranin e kyçjes." } } diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index 4132753bc9..472e29f72b 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -3224,6 +3224,8 @@ "running": "U toku", "needsInput": "zahteva unos", "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti" + "channelName": "Aktivni agenti", + "activityKitDisabledTitle": "Aktivnosti uživo su isključene", + "activityKitDisabledBody": "Uključite Aktivnosti uživo u Podešavanjima da biste videli aktivne agente na zaključanom ekranu." } } diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index 7be04ca3d9..5ddf047e59 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -3203,6 +3203,8 @@ "running": "KÖRS", "needsInput": "kräver indata", "reconnecting": "Återansluter", - "channelName": "Aktiva agenter" + "channelName": "Aktiva agenter", + "activityKitDisabledTitle": "Liveaktiviteter är avstängda", + "activityKitDisabledBody": "Aktivera liveaktiviteter i Inställningar för att se Aktiva agenter på låsskärmen." } } diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index a34f9ff458..4b57985a59 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -3203,6 +3203,8 @@ "running": "Inaendelea", "needsInput": "inahitaji mchango", "reconnecting": "Inaunganisha tena", - "channelName": "Mawakala wanaofanya kazi" + "channelName": "Mawakala wanaofanya kazi", + "activityKitDisabledTitle": "Shughuli za Moja kwa Moja zimezimwa", + "activityKitDisabledBody": "Washa Shughuli za Moja kwa Moja katika Mipangilio ili uone Mawakala wanaofanya kazi kwenye Skrini Iliyofungwa." } } diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 476ce4b157..71c0233906 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -3203,6 +3203,8 @@ "running": "இயங்குகிறது", "needsInput": "உள்ளீடு தேவை", "reconnecting": "மீண்டும் இணைக்கிறது", - "channelName": "செயலில் உள்ள முகவர்கள்" + "channelName": "செயலில் உள்ள முகவர்கள்", + "activityKitDisabledTitle": "நேரலைச் செயல்பாடுகள் முடக்கப்பட்டுள்ளன", + "activityKitDisabledBody": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காண அமைப்புகளில் நேரலைச் செயல்பாடுகளை இயக்கவும்." } } diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 78267afaa8..7c83980d8f 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -3203,6 +3203,8 @@ "running": "నడుస్తోంది", "needsInput": "ఇన్పుట్ అవసరం", "reconnecting": "మళ్లీ కనెక్ట్ అవుతోంది", - "channelName": "చురుకైన ఏజెంట్లు" + "channelName": "చురుకైన ఏజెంట్లు", + "activityKitDisabledTitle": "ప్రత్యక్ష కార్యకలాపాలు ఆఫ్‌లో ఉన్నాయి", + "activityKitDisabledBody": "లాక్ స్క్రీన్‌పై చురుకైన ఏజెంట్లను చూడటానికి సెట్టింగ్‌లలో ప్రత్యక్ష కార్యకలాపాలను ఆన్ చేయండి." } } diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index afc37c8ae7..b1cad084ff 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -3203,6 +3203,8 @@ "running": "กำลังทำงาน", "needsInput": "ต้องป้อนข้อมูล", "reconnecting": "กำลังเชื่อมต่อใหม่", - "channelName": "เอเจนต์ที่กำลังทำงาน" + "channelName": "เอเจนต์ที่กำลังทำงาน", + "activityKitDisabledTitle": "กิจกรรมสดปิดอยู่", + "activityKitDisabledBody": "เปิดกิจกรรมสดในการตั้งค่าเพื่อดูเอเจนต์ที่กำลังทำงานบนหน้าจอล็อค" } } diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index 9b4ad75bbf..ea85addb9b 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -3203,6 +3203,8 @@ "running": "Çalışıyor", "needsInput": "Girdi gerekli", "reconnecting": "Yeniden bağlanılıyor", - "channelName": "Etkin ajanlar" + "channelName": "Etkin ajanlar", + "activityKitDisabledTitle": "Canlı Etkinlikler kapalı", + "activityKitDisabledBody": "Etkin ajanları Kilit Ekranı'nda görmek için Ayarlar'dan Canlı Etkinlikler'i açın." } } diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 462e15e333..3a959178d8 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -3245,6 +3245,8 @@ "running": "Виконується", "needsInput": "потребує вводу", "reconnecting": "Повторне підключення", - "channelName": "Активні агенти" + "channelName": "Активні агенти", + "activityKitDisabledTitle": "Дії наживо вимкнено", + "activityKitDisabledBody": "Увімкніть «Дії наживо» в «Параметрах», щоб бачити активних агентів на замкненому екрані." } } diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 51a4e2f11d..7acf73692f 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -3203,6 +3203,8 @@ "running": "چل رہا ہے", "needsInput": "ان پٹ درکار", "reconnecting": "دوبارہ منسلک ہو رہا ہے", - "channelName": "فعال ایجنٹس" + "channelName": "فعال ایجنٹس", + "activityKitDisabledTitle": "لائیو سرگرمیاں بند ہیں", + "activityKitDisabledBody": "لاک اسکرین پر فعال ایجنٹس دیکھنے کے لیے ترتیبات میں لائیو سرگرمیاں فعال کریں۔" } } diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 575f1032aa..550efd65b8 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -3203,6 +3203,8 @@ "running": "Ishlamoqda", "needsInput": "kiritish kerak", "reconnecting": "Qayta ulanmoqda", - "channelName": "Faol agentlar" + "channelName": "Faol agentlar", + "activityKitDisabledTitle": "Jonli faoliyatlar o'chirilgan", + "activityKitDisabledBody": "Qulflangan ekranda Faol agentlarni ko'rish uchun Sozlamalarda Jonli faoliyatlarni yoqing." } } diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 164bdaaebf..3f06694011 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -3203,6 +3203,8 @@ "running": "ĐANG CHẠY", "needsInput": "cần nhập", "reconnecting": "Đang kết nối lại", - "channelName": "Tác nhân đang hoạt động" + "channelName": "Tác nhân đang hoạt động", + "activityKitDisabledTitle": "Hoạt động trực tiếp đang tắt", + "activityKitDisabledBody": "Bật Hoạt động trực tiếp trong Cài đặt để xem các tác nhân đang hoạt động trên Màn hình khóa." } } diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index 669d001c41..fb68588575 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -3203,6 +3203,8 @@ "running": "ǸJẸ́ ṢÍṢIṢẸ́", "needsInput": "nilo igbewọle", "reconnecting": "Ti n tun sopọ", - "channelName": "Awọn aṣoju to n ṣiṣẹ" + "channelName": "Awọn aṣoju to n ṣiṣẹ", + "activityKitDisabledTitle": "Awọn Iṣẹ Lọwọlọwọ wa ni pipa", + "activityKitDisabledBody": "Tan Awọn Iṣẹ Lọwọlọwọ ninu Eto lati ri Awọn aṣoju to n ṣiṣẹ lori Iboju Titiipa." } } diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index b3e610d7d7..6f4c98633c 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -3203,6 +3203,8 @@ "running": "运行中", "needsInput": "需要输入", "reconnecting": "正在重新连接", - "channelName": "活动代理" + "channelName": "活动代理", + "activityKitDisabledTitle": "实时活动已关闭", + "activityKitDisabledBody": "请在“设置”中开启“实时活动”,以在锁定屏幕上查看活动代理。" } } diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index d66c9e3e3e..0ac6b71722 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -3203,6 +3203,8 @@ "running": "執行中", "needsInput": "需要輸入", "reconnecting": "正在重新連線", - "channelName": "使用中的代理" + "channelName": "使用中的代理", + "activityKitDisabledTitle": "即時動態已關閉", + "activityKitDisabledBody": "請在「設定」中開啟「即時動態」,以在鎖定畫面上查看使用中的代理。" } } diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 8e866a5cbc..1fbf891bb2 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -3203,6 +3203,8 @@ "running": "IYASEBENZA", "needsInput": "idinga okokufaka", "reconnecting": "Ixhuma kabusha", - "channelName": "Ama-agent asebenzayo" + "channelName": "Ama-agent asebenzayo", + "activityKitDisabledTitle": "Imisebenzi Ebukhoma ivaliwe", + "activityKitDisabledBody": "Vula Imisebenzi Ebukhoma ku-Izilungiselelo ukuze ubone Ama-agent asebenzayo Esikrinini Esikhiyiwe." } } From da2854ac3dadf525e894559696987b1052e1afe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 16:36:12 +0200 Subject: [PATCH 20/43] fix(mobile): recover ActivityKit capability while agents are idle --- .../glanceable/activity-kit-prompt.test.ts | 178 +++++++++++++++--- .../src/lib/glanceable/activity-kit-prompt.ts | 36 ++-- 2 files changed, 168 insertions(+), 46 deletions(-) diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts index 4a8c290907..79f1500606 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts @@ -1,16 +1,20 @@ +/* eslint-disable max-lines -- recovery and privacy regressions share the native ActivityKit harness */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { buildGlanceableSnapshot, type GlanceableAgentsSnapshot, } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; +import { _resetIosSinkForTests, getActivityKitDenied, iosSink } from '@/glanceable-ios/ios-sink'; import { bumpAuthEpoch } from '@/lib/auth/auth-epoch'; import { writePrivacySnapshotAndEnd, writeSignedOutSnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests, } from '@/lib/glanceable/persist'; +import { GlanceablePublisher } from '@/lib/glanceable/publisher'; import { type GlanceableSink, type GlanceableSinkContext, @@ -26,8 +30,8 @@ const mocks = vi.hoisted(() => ({ alert: vi.fn(), openSettings: vi.fn(), getItemAsync: vi.fn(), - clearActivityKitDeniedIfAvailable: vi.fn(), - getActivityKitDenied: vi.fn(), + instancesError: null as Error | null, + nativeActivity: null as Partial | null, })); vi.mock('react-native', () => ({ @@ -40,9 +44,32 @@ vi.mock('expo-secure-store', () => ({ getItemAsync: mocks.getItemAsync, })); -vi.mock('@/glanceable-ios/ios-sink', () => ({ - clearActivityKitDeniedIfAvailable: mocks.clearActivityKitDeniedIfAvailable, - getActivityKitDenied: mocks.getActivityKitDenied, +vi.mock('@/glanceable-ios/active-agents-live-activity', () => ({ + ActiveAgentsLiveActivity: { + getInstances() { + if (mocks.instancesError !== null) { + throw mocks.instancesError; + } + return []; + }, + start(props: Partial) { + mocks.nativeActivity = props; + return { + async update(next: Partial) { + mocks.nativeActivity = next; + await Promise.resolve(); + }, + async end() { + mocks.nativeActivity = null; + await Promise.resolve(); + }, + }; + }, + }, +})); + +vi.mock('@/glanceable-ios/active-agents-widget', () => ({ + ActiveAgentsWidget: { updateSnapshot: vi.fn(), updateTimeline: vi.fn() }, })); vi.mock('@/i18n', () => ({ @@ -60,11 +87,11 @@ function eligibleSnapshot(organizationId: string | null = null): GlanceableAgent }); } -function emptySnapshot(): GlanceableAgentsSnapshot { +function emptySnapshot(organizationId: string | null = null): GlanceableAgentsSnapshot { return buildGlanceableSnapshot({ sessions: [], userId: 'u1', - organizationId: null, + organizationId, now: NOW, }); } @@ -113,30 +140,43 @@ function delayIdentityRead(delayedKey: string) { beforeEach(() => { vi.clearAllMocks(); _resetGlanceablePersistForTests(); + _resetIosSinkForTests(); _setLastGlanceableSnapshotForTests(eligibleSnapshot()); surface.widget = null; surface.activity = null; surface.context = null; + mocks.nativeActivity = null; registerGlanceableSink(sink); + registerGlanceableSink(iosSink); mocks.platform.OS = 'ios'; mocks.getItemAsync.mockImplementation((key: string) => key === ACTIVE_USER_ID_KEY ? 'u1' : null ); - mocks.getActivityKitDenied.mockReturnValue(true); - mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(true); + mocks.instancesError = Object.assign(new Error('ActivityKit unavailable'), { + code: 'ERR_LIVE_ACTIVITIES_NOT_SUPPORTED', + }); + iosSink.startOrUpdate(eligibleSnapshot(), { userId: 'u1', organizationId: null }); + mocks.instancesError = null; }); afterEach(() => { unregisterGlanceableSink(sink); + unregisterGlanceableSink(iosSink); }); describe('recoverGlanceableActivityKit', () => { - it('does nothing when the denied latch was not cleared', async () => { - mocks.clearActivityKitDeniedIfAvailable.mockReturnValue(false); + it('keeps recovery available after a failed capability probe', async () => { + mocks.instancesError = new Error('ActivityKit unavailable'); await recoverGlanceableActivityKit(); expect(surface.activity).toBeNull(); + expect(getActivityKitDenied()).toBe(true); + + mocks.instancesError = null; + await recoverGlanceableActivityKit(); + + expect(surface.activity).toEqual(eligibleSnapshot()); }); it.each([null, emptySnapshot()])( @@ -147,6 +187,39 @@ describe('recoverGlanceableActivityKit', () => { await recoverGlanceableActivityKit(); expect(surface.activity).toBeNull(); + expect(surface.widget).toBeNull(); + expect(mocks.nativeActivity).toBeNull(); + } + ); + + it.each([null, 'org-9'])( + 'starts new work after idle recovery in scope %s', + async organizationId => { + const snapshot = emptySnapshot(organizationId); + _setLastGlanceableSnapshotForTests(snapshot); + mocks.getItemAsync.mockImplementation((key: string) => + key === ACTIVE_USER_ID_KEY ? 'u1' : organizationId + ); + + await recoverGlanceableActivityKit(); + + expect(surface).toEqual({ widget: null, activity: null, context: null }); + expect(mocks.nativeActivity).toBeNull(); + + const publisher = new GlanceablePublisher({ + sinks: [iosSink], + initial: snapshot, + now: () => NOW, + }); + publisher.handleSessions([{ status: 'question' }], { userId: 'u1', organizationId }); + + expect(mocks.nativeActivity).toMatchObject({ + status: 'happy', + running: 0, + needsInput: 1, + reconnecting: 0, + }); + publisher.dispose(); } ); @@ -171,6 +244,7 @@ describe('recoverGlanceableActivityKit', () => { await recoverGlanceableActivityKit(); expect(surface.activity).toBeNull(); + expect(getActivityKitDenied()).toBe(true); }); it.each([null, 'org-10'])('rejects the mismatched organization hint %s', async organizationId => { @@ -182,20 +256,46 @@ describe('recoverGlanceableActivityKit', () => { await recoverGlanceableActivityKit(); expect(surface.activity).toBeNull(); + expect(getActivityKitDenied()).toBe(true); }); - it.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])('rejects a failed %s read', async key => { - _setLastGlanceableSnapshotForTests(eligibleSnapshot('org-9')); - mocks.getItemAsync.mockImplementation((requestedKey: string) => { - if (requestedKey === key) { - throw new Error('storage unavailable'); - } - return requestedKey === ACTIVE_USER_ID_KEY ? 'u1' : 'org-9'; - }); - - await recoverGlanceableActivityKit(); + describe.each([ + ['eligible', eligibleSnapshot], + ['idle', emptySnapshot], + ] as const)('%s recovery after storage failures', (_label, snapshotFor) => { + it.each([ + [null, ACTIVE_USER_ID_KEY], + [null, ORGANIZATION_STORAGE_KEY], + ['org-9', ACTIVE_USER_ID_KEY], + ['org-9', ORGANIZATION_STORAGE_KEY], + ] as const)( + 'keeps recovery in scope %s after a failed %s read', + async (organizationId, key) => { + _setLastGlanceableSnapshotForTests(snapshotFor(organizationId)); + mocks.getItemAsync.mockImplementation(async (requestedKey: string) => { + await Promise.resolve(); + if (requestedKey === key) { + throw new Error('storage unavailable'); + } + return requestedKey === ACTIVE_USER_ID_KEY ? 'u1' : organizationId; + }); + + await recoverGlanceableActivityKit(); + + expect(surface.activity).toBeNull(); + expect(getActivityKitDenied()).toBe(true); + + const latest = eligibleSnapshot(organizationId); + _setLastGlanceableSnapshotForTests(latest); + mocks.getItemAsync.mockImplementation((requestedKey: string) => + requestedKey === ACTIVE_USER_ID_KEY ? 'u1' : organizationId + ); + await recoverGlanceableActivityKit(); - expect(surface.activity).toBeNull(); + expect(surface.activity).toEqual(latest); + expect(surface.context).toEqual({ userId: 'u1', organizationId }); + } + ); }); it('does not recover on a non-iOS platform', async () => { @@ -211,10 +311,14 @@ describe.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])( 'ActivityKit recovery while %s is pending', delayedKey => { it.each([ - ['logout', writeSignedOutSnapshotAndEnd], - ['account switch', bumpAuthEpoch], - ['organization switch', writePrivacySnapshotAndEnd], - ] as const)('does not restore counts after %s', async (_label, invalidate) => { + ['logout', writeSignedOutSnapshotAndEnd, eligibleSnapshot], + ['account switch', bumpAuthEpoch, eligibleSnapshot], + ['organization switch', writePrivacySnapshotAndEnd, eligibleSnapshot], + ['idle logout', writeSignedOutSnapshotAndEnd, emptySnapshot], + ['idle account switch', bumpAuthEpoch, emptySnapshot], + ['idle organization switch', writePrivacySnapshotAndEnd, emptySnapshot], + ] as const)('does not restore counts after %s', async (_label, invalidate, snapshotFor) => { + _setLastGlanceableSnapshotForTests(snapshotFor()); const read = delayIdentityRead(delayedKey); const recovering = recoverGlanceableActivityKit(); await read.started; @@ -225,18 +329,29 @@ describe.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])( expect(surface.activity).toBeNull(); expect(surface.context).toBeNull(); + expect(getActivityKitDenied()).toBe(true); }); - it('does not recover a captured snapshot after the scope changes', async () => { + it('keeps recovery available for the new scope after a delayed read', async () => { const read = delayIdentityRead(delayedKey); const recovering = recoverGlanceableActivityKit(); await read.started; - _setLastGlanceableSnapshotForTests(eligibleSnapshot('org-10')); + const latest = eligibleSnapshot('org-10'); + _setLastGlanceableSnapshotForTests(latest); read.resolve(); await recovering; expect(surface.activity).toBeNull(); + expect(getActivityKitDenied()).toBe(true); + + mocks.getItemAsync.mockImplementation((key: string) => + key === ACTIVE_USER_ID_KEY ? 'u1' : 'org-10' + ); + await recoverGlanceableActivityKit(); + + expect(surface.activity).toEqual(latest); + expect(surface.context).toEqual({ userId: 'u1', organizationId: 'org-10' }); }); it.each([0, 7])( @@ -257,6 +372,13 @@ describe.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])( expect(surface.widget).toEqual(latest); expect(surface.activity).toEqual(running > 0 ? latest : null); + expect(getActivityKitDenied()).toBe(true); + + await recoverGlanceableActivityKit(); + + expect(getActivityKitDenied()).toBe(false); + expect(surface.widget).toEqual(latest); + expect(surface.activity).toEqual(running > 0 ? latest : null); } ); diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts index bdecc0f89b..75260977dc 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -40,20 +40,10 @@ export function showActivityKitDisabledAlertOnce(): void { ); } -async function readSecureStoreValue(key: string): Promise { - try { - return await SecureStore.getItemAsync(key); - } catch { - return null; - } -} - /** - * Re-emit the last eligible snapshot after a once-denied ActivityKit surface - * became available again. Reads the active-user and selected-organization hints - * from SecureStore exactly as `notifications.ts` does, so the re-emitted token - * registration keeps the right scope. No-op when the latch was never denied, - * was not cleared, or the persisted snapshot has no eligible work. + * Recover a once-denied ActivityKit surface after verifying the stored identity + * and current snapshot. Clear denial even while idle so later work can start + * without another focus event; replay only eligible work. */ export async function recoverGlanceableActivityKit(): Promise { if (Platform.OS !== 'ios' || !getActivityKitDenied()) { @@ -63,13 +53,20 @@ export async function recoverGlanceableActivityKit(): Promise { const blankEpoch = getTerminalBlankEpoch(); const scopeKey = getLocalScopeKey(); const snapshot = getLastGlanceableSnapshot(); - if (snapshot === null || snapshot.scopeKey !== scopeKey || !isEligibleGlanceableWork(snapshot)) { + if (snapshot === null || snapshot.scopeKey !== scopeKey) { + return; + } + let userId: string | null = null; + let organizationId: string | null = null; + try { + [userId, organizationId] = await Promise.all([ + SecureStore.getItemAsync(ACTIVE_USER_ID_KEY), + SecureStore.getItemAsync(ORGANIZATION_STORAGE_KEY), + ]); + } catch { + // A failed organization read must not be treated as the personal scope. return; } - const [userId, organizationId] = await Promise.all([ - readSecureStoreValue(ACTIVE_USER_ID_KEY), - readSecureStoreValue(ORGANIZATION_STORAGE_KEY), - ]); if ( currentAuthEpoch() !== authEpoch || getTerminalBlankEpoch() !== blankEpoch || @@ -81,6 +78,9 @@ export async function recoverGlanceableActivityKit(): Promise { ) { return; } + if (!isEligibleGlanceableWork(snapshot)) { + return; + } for (const sink of getGlanceableSinks()) { sink.startOrUpdate(snapshot, { userId, organizationId }); } From 649c79be9d5a03d6ae0fbc31f7747ced34238d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 18:08:24 +0200 Subject: [PATCH 21/43] fix(mobile): retain scope delivery and register rotated tokens --- .../glanceable-android/android-sink.test.ts | 46 +- .../src/glanceable-android/android-sink.ts | 2 +- .../src/glanceable-ios/ios-sink.test.ts | 96 +++- apps/mobile/src/glanceable-ios/ios-sink.ts | 22 +- .../src/lib/auth/logout-cleanup.test.ts | 47 ++ apps/mobile/src/lib/auth/logout-cleanup.ts | 23 +- apps/mobile/src/lib/glanceable/cleanup.ts | 3 +- .../glanceable/delivery-registration.test.ts | 341 ++++++++++++++ .../lib/glanceable/delivery-registration.ts | 443 ++++++++++-------- apps/mobile/src/lib/glanceable/publisher.ts | 11 +- .../src/lib/glanceable/sink-registry.ts | 20 +- apps/mobile/src/lib/notifications.test.ts | 287 +++++++++++- patches/expo-widgets@57.0.11.patch | 14 + pnpm-lock.yaml | 7 +- pnpm-workspace.yaml | 1 + 15 files changed, 1121 insertions(+), 242 deletions(-) create mode 100644 patches/expo-widgets@57.0.11.patch diff --git a/apps/mobile/src/glanceable-android/android-sink.test.ts b/apps/mobile/src/glanceable-android/android-sink.test.ts index e6f586790f..c6e0924870 100644 --- a/apps/mobile/src/glanceable-android/android-sink.test.ts +++ b/apps/mobile/src/glanceable-android/android-sink.test.ts @@ -5,6 +5,7 @@ import { } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { GlanceablePublisher } from '@/lib/glanceable/publisher'; import { setGlanceableDelivery } from '@/lib/glanceable/sink-registry'; import { i18n } from '@/i18n'; @@ -88,9 +89,20 @@ vi.mock('react-native-android-widget', () => ({ const NOW = 1_750_000_000_000; const CTX = { organizationId: null, userId: 'u1' }; +const subscriptions = new Set(); const delivery = { - registerTokens: vi.fn(), - unregisterTokens: vi.fn().mockResolvedValue({ ok: true, tokens: [] }), + registerScopeTokens: vi.fn(() => subscriptions.add('scope')), + registerTokens: vi.fn(() => subscriptions.add('scope')), + cleanupTokens: vi.fn((lifetime: 'scope' | 'activity') => { + if (lifetime === 'scope') { + subscriptions.clear(); + } + }), + unregisterTokens: vi.fn().mockImplementation(async () => { + await Promise.resolve(); + subscriptions.clear(); + return { ok: true, tokens: [] }; + }), }; function snapshotFor( @@ -147,7 +159,10 @@ beforeEach(() => { // eslint-disable-next-line promise-function-async, prefer-await-to-then -- tension between lint rules _setPermissionReaderForTests(() => Promise.resolve('granted')); setGlanceableDelivery(delivery); + subscriptions.clear(); + delivery.registerScopeTokens.mockClear(); delivery.registerTokens.mockClear(); + delivery.cleanupTokens.mockClear(); delivery.unregisterTokens.mockClear(); mocks.native.isPromotionCapable.mockReturnValue(true); mocks.native.end(); @@ -164,6 +179,25 @@ afterEach(() => { }); describe('androidSink start and update', () => { + it('keeps idle delivery available before and after work arrives in the background', async () => { + const publisher = new GlanceablePublisher({ sinks: [androidSink], now: () => NOW }); + publisher.handleSessions([{ status: 'idle' }], CTX); + await flushAsync(); + expect(mocks.getNotification()).toBeNull(); + expect(getCurrentWidgetProps()?.statusLine).toBe('No work in progress'); + expect(subscriptions).toEqual(new Set(['scope'])); + + publisher.applySnapshot(snapshotFor([{ status: 'busy' }], 1), CTX); + await flushAsync(); + expect(mocks.getNotification()?.text).toBe('1 Running'); + + publisher.handleSessions([{ status: 'idle' }], CTX); + await vi.advanceTimersByTimeAsync(8000); + expect(mocks.getNotification()).toBeNull(); + expect(subscriptions).toEqual(new Set(['scope'])); + publisher.dispose(); + }); + it('forwards the ranked compact number and all counts on start and update', async () => { androidSink.startOrUpdate(MIXED, CTX); await flushAsync(); @@ -426,13 +460,15 @@ describe('androidSink widget publish and end', () => { expect(getCurrentWidgetProps()?.primaryCount).toBe(1); }); - it('unregisters the token on endImmediate', async () => { + it('ends the ongoing notification without removing widget delivery', async () => { androidSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); await flushAsync(); - expect(delivery.registerTokens).toHaveBeenCalledTimes(1); + expect(mocks.getNotification()).not.toBeNull(); androidSink.endImmediate(); - expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + await flushAsync(); + expect(mocks.getNotification()).toBeNull(); + expect(subscriptions).toEqual(new Set(['scope'])); }); it.each(['happy', 'stale'] as const)( diff --git a/apps/mobile/src/glanceable-android/android-sink.ts b/apps/mobile/src/glanceable-android/android-sink.ts index b7193b1d1c..ab86e8e66a 100644 --- a/apps/mobile/src/glanceable-android/android-sink.ts +++ b/apps/mobile/src/glanceable-android/android-sink.ts @@ -213,8 +213,8 @@ export const androidSink: GlanceableSink = { }, endImmediate() { + // The scope subscription also delivers widget updates while no work is active. endNotification(); - void getGlanceableDelivery().unregisterTokens(); }, }; diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index f7e55b35b1..e578646128 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -67,6 +67,7 @@ vi.mock('expo-widgets', () => ({ const state = { props, url, ended: false }; mockState.started.push(state); const instance = { + getPushToken: vi.fn().mockResolvedValue(null), update: async (next: unknown) => { mockState.updated.push(next); if (mockState.updatePromise !== null) { @@ -109,9 +110,24 @@ vi.mock('expo-widgets', () => ({ const NOW = 1_750_000_000_000; const CTX = { userId: 'u1', organizationId: null }; +const subscriptions = new Set(); const delivery = { - registerTokens: vi.fn(), - unregisterTokens: vi.fn().mockResolvedValue({ ok: true, tokens: [] }), + registerScopeTokens: vi.fn(() => subscriptions.add('scope')), + registerTokens: vi.fn(() => { + subscriptions.add('scope'); + subscriptions.add('activity'); + }), + cleanupTokens: vi.fn((lifetime: 'scope' | 'activity') => { + subscriptions.delete('activity'); + if (lifetime === 'scope') { + subscriptions.delete('scope'); + } + }), + unregisterTokens: vi.fn().mockImplementation(async () => { + await Promise.resolve(); + subscriptions.clear(); + return { ok: true, tokens: [] }; + }), }; function snapshotFor( @@ -131,6 +147,7 @@ function snapshotFor( beforeEach(() => { _resetIosSinkForTests(); + subscriptions.clear(); mockState.startError = null; mockState.instancesError = null; mockState.instances = []; @@ -151,17 +168,31 @@ afterEach(() => { }); describe('iosSink start and update', () => { + it('registers an idle scope without a Live Activity and accepts later background work', () => { + const publisher = new GlanceablePublisher({ sinks: [iosSink], now: () => NOW }); + publisher.handleSessions([{ status: 'idle' }], CTX); + + expect(mockState.started).toEqual([]); + expect(mockState.snapshots.at(-1)).toMatchObject({ statusLine: 'No work in progress' }); + expect(subscriptions).toEqual(new Set(['scope'])); + + publisher.applySnapshot(snapshotFor([{ status: 'busy' }], 1), CTX); + expect(mockState.started).toMatchObject([{ ended: false, props: { running: 1 } }]); + expect(subscriptions).toEqual(new Set(['scope', 'activity'])); + publisher.dispose(); + }); + it('starts once and updates the same activity on a newer revision', () => { iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 1), CTX); expect(mockState.started.length).toBe(1); expect(mockState.updated.length).toBe(1); - expect(delivery.registerTokens).toHaveBeenCalledTimes(1); + expect(subscriptions).toEqual(new Set(['scope', 'activity'])); expect(delivery.unregisterTokens).not.toHaveBeenCalled(); }); - it('discards an older revision without overwriting the newest props', () => { + it('discards an older revision without overwriting the newest props', async () => { const newer = snapshotFor([{ status: 'busy' }], 1); const older = { ...snapshotFor([{ status: 'busy' }, { status: 'busy' }], 0), @@ -174,7 +205,9 @@ describe('iosSink start and update', () => { expect(mockState.updated.length).toBe(0); iosSink.endImmediate(); - expect(mockState.ended.length).toBe(1); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); expect(mockState.ended[0]?.contentDate).toBeInstanceOf(Date); expect( (mockState.ended[0]?.props as GlanceableLiveActivityContentState | undefined)?.running @@ -237,28 +270,33 @@ describe('iosSink start and update', () => { }); describe('iosSink end', () => { - it('ends with a contentDate not older than the last native write', () => { + it('ends with a contentDate not older than the last native write', async () => { const writeTime = NOW + 120_000; vi.useFakeTimers(); vi.setSystemTime(new Date(writeTime)); iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); iosSink.endImmediate(); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); const contentDate = mockState.ended[0]?.contentDate as Date | undefined; expect(contentDate).toBeInstanceOf(Date); // The snapshot's updatedAt (NOW) is older than the write wall-clock; an end // carrying NOW instead would be discarded by ActivityKit. - expect(contentDate?.getTime()).toBe(writeTime); + expect(contentDate?.getTime()).toBeGreaterThanOrEqual(writeTime); }); - it('unregisters tokens even when no activity handle exists', () => { + it('preserves scope delivery when no activity handle exists', async () => { mockState.instances = []; + delivery.registerScopeTokens(); iosSink.endImmediate(); + await Promise.resolve(); expect(mockState.ended.length).toBe(0); - expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + expect(subscriptions).toEqual(new Set(['scope'])); }); it('ends immediately on signed-out with a wall-clock contentDate', async () => { @@ -280,7 +318,7 @@ describe('iosSink end', () => { expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBeGreaterThanOrEqual( terminalTime ); - expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + expect(subscriptions.size).toBe(0); }); it('ends with the wall-clock of the last publish, not the eligible start', async () => { @@ -429,9 +467,10 @@ describe('iosSink end', () => { ); }); - it('adopts and ends a leftover activity when the handle is null after restart', () => { + it('adopts and ends a leftover activity when the handle is null after restart', async () => { mockState.instances = [ { + getPushToken: vi.fn().mockResolvedValue(null), update: (next: unknown) => { mockState.updated.push(next); }, @@ -442,9 +481,30 @@ describe('iosSink end', () => { iosSink.endImmediate(); - expect(mockState.ended.length).toBe(1); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); + expect(mockState.ended[0]?.policy).toBe('immediate'); + expect(subscriptions.has('activity')).toBe(false); + }); + + it('ends the native activity even when its token lookup rejects', async () => { + mockState.instances = [ + { + getPushToken: vi.fn().mockRejectedValue(new Error('native token unavailable')), + end: (policy: unknown, props?: unknown, contentDate?: unknown) => + mockState.ended.push({ policy, props, contentDate }), + }, + ]; + delivery.registerScopeTokens(); + + iosSink.endImmediate(); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); + expect(mockState.ended[0]?.policy).toBe('immediate'); - expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + expect(subscriptions).toEqual(new Set(['scope'])); }); it('ends after the 8s terminal window when work becomes empty', async () => { @@ -463,6 +523,7 @@ describe('iosSink end', () => { expect(mockState.ended.length).toBe(1); }); expect(mockState.ended[0]?.policy).toBe('immediate'); + expect(subscriptions).toEqual(new Set(['scope'])); publisher.dispose(); }); }); @@ -601,9 +662,10 @@ describe('iosSink Live Activity content-state', () => { expect(delivery.registerTokens).not.toHaveBeenCalled(); }); - it('ends an adopted leftover activity when publish receives ineligible work', () => { + it('ends an adopted leftover activity when publish receives ineligible work', async () => { mockState.instances = [ { + getPushToken: vi.fn().mockResolvedValue(null), update: (next: unknown) => mockState.updated.push(next), end: (policy: unknown, props?: unknown, contentDate?: unknown) => mockState.ended.push({ policy, props, contentDate }), @@ -612,10 +674,12 @@ describe('iosSink Live Activity content-state', () => { iosSink.publish(snapshotFor([], 1, 'empty')); - expect(mockState.ended.length).toBe(1); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); expect(mockState.ended[0]?.policy).toBe('immediate'); expect(mockState.updated.length).toBe(0); - expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + expect(subscriptions.has('activity')).toBe(false); }); }); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index 2d33d4cde2..fc90c07360 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -83,14 +83,22 @@ function buildExpiredProps(snapshot: GlanceableAgentsSnapshot): Partial { + try { + return await instance.getPushToken(); + } catch { + // Recorded tokens still need cleanup when the native lookup fails. + return null; + } +} + async function endNow(): Promise { - // Unregister push-to-start tokens even when no Live Activity handle exists: - // an account or org switch with no live handle must not keep the prior - // scope's token registered. - void getGlanceableDelivery().unregisterTokens(); // A process restart leaves the JS handle null while ActivityKit still // holds the activity; adopt it so the end actually clears the Lock Screen. activity ??= adoptExistingActivity(); + const endingToken = activity === null ? undefined : readEndingToken(activity); + // Capture before end removes native discovery, without waiting for the network. + getGlanceableDelivery().cleanupTokens('activity', endingToken); if (activity === null) { return; } @@ -116,6 +124,7 @@ async function endNow(): Promise { } } try { + await endingToken; await endingActivity.end('immediate', endingProps ?? undefined, new Date()); } finally { pendingEnds -= 1; @@ -227,13 +236,16 @@ export const iosSink: GlanceableSink = { lastProps = contentState; revision = snapshot.revision; - getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId); + getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId, activity); if (adopted) { inFlightUpdate = activity.update(contentState); } return; } + // publish can adopt an activity before this method sees it. Bind its token + // listener here too; delivery deduplicates the sink's stable native handle. + getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId, activity); // The publisher coalesces and guards revisions, but keep the sink monotonic // so a late or replayed emit can never move the surface backwards. if (snapshot.revision <= revision) { diff --git a/apps/mobile/src/lib/auth/logout-cleanup.test.ts b/apps/mobile/src/lib/auth/logout-cleanup.test.ts index 501cfb2d80..ff8a7ed799 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.test.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.test.ts @@ -371,6 +371,53 @@ describe('unregisterActivityTokensAndTombstone', () => { }); }); + it('retains scope delivery and tombstones only the failed ended activity', async () => { + const rows = new Set(['scope-token', 'ended-token']); + deliveryMock.unregisterTokens.mockImplementation(async (lifetime: 'scope' | 'activity') => { + await Promise.resolve(); + if (lifetime !== 'activity') { + rows.delete('scope-token'); + } + return { ok: false, tokens: ['ended-token'] }; + }); + + await unregisterActivityTokensAndTombstone('activity'); + + expect(rows).toEqual(new Set(['scope-token', 'ended-token'])); + expect(await readLogoutCleanupTombstone()).toMatchObject({ + userId: 'u1', + needsActivityUnregister: true, + activityTokens: ['ended-token'], + }); + }); + + it('finishes an earlier activity tombstone write before successful logout clears it', async () => { + const { setItemAsync } = await import('expo-secure-store'); + const writing = Promise.withResolvers(); + const writeGate = Promise.withResolvers(); + vi.mocked(setItemAsync).mockImplementationOnce(async (key, value) => { + writing.resolve(undefined); + await writeGate.promise; + store.set(key, value); + }); + deliveryMock.unregisterTokens + .mockResolvedValueOnce({ ok: false, tokens: ['ended-token'] }) + .mockResolvedValue({ ok: true, tokens: [] }); + pushOutcome('none'); + trpcMock.revokeCurrentDeviceSession.mutate.mockResolvedValue({ outcome: 'revoked' }); + + const activityEnd = unregisterActivityTokensAndTombstone('activity'); + await writing.promise; + const logout = runLogoutCleanup(); + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + writeGate.resolve(undefined); + await Promise.all([activityEnd, logout]); + + expect(await readLogoutCleanupTombstone()).toBeNull(); + }); + it('leaves an existing tombstone untouched on success so a pending push unregister survives', async () => { store.set( LOGOUT_CLEANUP_TOMBSTONE_KEY, diff --git a/apps/mobile/src/lib/auth/logout-cleanup.ts b/apps/mobile/src/lib/auth/logout-cleanup.ts index 66d1b88dc3..463d5ee4b9 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.ts @@ -96,6 +96,9 @@ export async function writeLogoutCleanupTombstone( export async function runLogoutCleanup(): Promise { try { const userId = readCachedUserId(queryClient); + // Terminal blanking can already be retiring tokens. Finish its tombstone + // write before full logout decides whether to replace or delete that record. + await awaitActivityCleanupSettled(); // Push token outcome: 'none' → nothing to unregister; 'lookup-failed' → // a server row may exist, so reconciliation re-reads the stable device @@ -183,7 +186,8 @@ export async function awaitActivityCleanupSettled(): Promise { * tombstone a failure, WITHOUT revoking the device session or unregistering * the Expo push token (those are logout-only). Never throws by contract. * - * Called on account switch (`signIn`) and org switch (`setOrganizationId`), + * Ordinary activity ends pass `activity` to preserve scope delivery. Account + * switch (`signIn`) and org switch (`setOrganizationId`) retire the whole scope, * where the prior scope's activity tokens must stop receiving APNs before the * new scope registers its own. The cached user id is read before any switch * clears it, so a failed unregister tombstones the prior account's identity — @@ -191,10 +195,13 @@ export async function awaitActivityCleanupSettled(): Promise { * leaves any existing tombstone untouched. A failure merges the same owner's * pending push cleanup and failed activity tokens so both survive a switch. */ -export async function unregisterActivityTokensAndTombstone(): Promise { +export async function unregisterActivityTokensAndTombstone( + lifetime: 'scope' | 'activity' = 'scope', + activityToken?: Promise +): Promise { const previous = activityCleanupInFlight; const cleanup = (async () => { - await Promise.all([previous, runActivityCleanup(previous)]); + await Promise.all([previous, runActivityCleanup(previous, lifetime, activityToken)]); })(); activityCleanupInFlight = cleanup; try { @@ -206,12 +213,16 @@ export async function unregisterActivityTokensAndTombstone(): Promise { } } -async function runActivityCleanup(previous: Promise | null): Promise { +async function runActivityCleanup( + previous: Promise | null, + lifetime: 'scope' | 'activity', + activityToken?: Promise +): Promise { try { const userId = readCachedUserId(queryClient); // Start the unregister now to fence stale registration intent. Serialize - // only the tombstone merge behind earlier scope cleanup writes. - const result = await getGlanceableDelivery().unregisterTokens(); + // only the tombstone merge behind earlier cleanup writes. + const result = await getGlanceableDelivery().unregisterTokens(lifetime, activityToken); await previous; if (result.ok) { return; diff --git a/apps/mobile/src/lib/glanceable/cleanup.ts b/apps/mobile/src/lib/glanceable/cleanup.ts index ded58ef91b..d57c0ddb42 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.ts @@ -6,7 +6,7 @@ import { import { getLastGlanceableSnapshot } from './persist'; import { withStatus } from './publisher'; -import { getGlanceableSinks } from './sink-registry'; +import { getGlanceableDelivery, getGlanceableSinks } from './sink-registry'; // Monotonic epoch bumped on every terminal blank (signed-out or privacy). The // publisher captures it at construction and refuses to emit once it advances, @@ -77,6 +77,7 @@ function writeTerminalAndEnd(status: 'signed_out' | 'privacy'): void { // Arm the publisher gate before any sink writes, so a cache success that // lands during this window can never emit for the torn-down session. terminalBlankEpoch += 1; + getGlanceableDelivery().cleanupTokens('scope'); const snapshot = buildTerminalSnapshot(status); const sinks = getGlanceableSinks(); // Write the snapshot first, then end: the surface shows the terminal copy diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts index ee3a4faa84..0b44de2d0e 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.test.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts @@ -11,6 +11,7 @@ const logoutMock = vi.hoisted(() => ({ const expoWidgetsMock = vi.hoisted(() => ({ pushToStartListener: null as ((event: { activityPushToStartToken: string }) => void) | null, + listeners: new Set<(event: { activityPushToStartToken: string }) => void>(), })); const trpcMock = vi.hoisted(() => ({ @@ -20,12 +21,19 @@ const trpcMock = vi.hoisted(() => ({ const activityMock = vi.hoisted(() => ({ getPushToken: vi.fn(), + addPushTokenListener: vi.fn(), + listeners: new Set<(event: { activityId: string; pushToken: string }) => void>(), })); const platformMock = vi.hoisted(() => ({ OS: 'ios' as string })); /* eslint-disable import/first */ vi.mock('@/lib/auth/logout-reconciliation', () => logoutMock); +vi.mock('@/lib/auth/logout-cleanup', () => ({ + unregisterActivityTokensAndTombstone: async (lifetime: 'scope' | 'activity') => { + await getGlanceableDelivery().unregisterTokens(lifetime); + }, +})); vi.mock('@/lib/trpc', () => ({ trpcClient: { user: { @@ -39,6 +47,8 @@ vi.mock('expo-widgets', () => ({ listener: (event: { activityPushToStartToken: string }) => void ) => { expoWidgetsMock.pushToStartListener = listener; + expoWidgetsMock.listeners.add(listener); + return { remove: () => expoWidgetsMock.listeners.delete(listener) }; }, })); vi.mock('@/glanceable-ios/active-agents-live-activity', () => ({ @@ -50,6 +60,14 @@ vi.mock('react-native', () => ({ Platform: platformMock, })); +import { bumpAuthEpoch } from '@/lib/auth/auth-epoch'; + +import { + getTerminalBlankEpoch, + writePrivacySnapshotAndEnd, + writeSignedOutSnapshotAndEnd, +} from './cleanup'; +import { GlanceablePublisher } from './publisher'; import { getGlanceableDelivery } from './sink-registry'; // Import side effect: registers the real delivery under the mocks above. import { @@ -93,6 +111,13 @@ function snapshot() { }); } +function emitActivityToken(pushToken: string): void { + activityMock.getPushToken.mockResolvedValue(pushToken); + for (const listener of activityMock.listeners) { + listener({ activityId: 'activity-1', pushToken }); + } +} + describe('delivery registerTokens', () => { beforeEach(() => { vi.clearAllMocks(); @@ -100,6 +125,13 @@ describe('delivery registerTokens', () => { _setGetDevicePushTokenForTests(null); _resetDeliveryRegistrationForTests(); activityMock.getPushToken.mockResolvedValue('token-1'); + activityMock.listeners.clear(); + activityMock.addPushTokenListener.mockImplementation( + (listener: (event: { activityId: string; pushToken: string }) => void) => { + activityMock.listeners.add(listener); + return { remove: () => activityMock.listeners.delete(listener) }; + } + ); trpcMock.registerActivityToken.mutate.mockResolvedValue({ success: true }); trpcMock.unregisterActivityToken.mutate.mockResolvedValue({ success: true }); logoutMock.attemptLogoutReconciliation.mockResolvedValue({ kind: 'no-tombstone' }); @@ -514,6 +546,315 @@ describe('delivery registerTokens', () => { } ); + it.each(['ios', 'android'])( + 'registers an initially idle %s scope without observing an activity', + async platform => { + platformMock.OS = platform; + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + _setGetDevicePushTokenForTests(async () => { + await Promise.resolve(); + return 'scope-token'; + }); + const publisher = new GlanceablePublisher({ sinks: [], now: () => NOW }); + + publisher.handleSessions([{ status: 'idle' }], { organizationId: 'org-1', userId: 'u1' }); + await flushRegistration(); + publisher.dispose(); + + expect(rows).toEqual(new Map([['scope-token', 'org-1']])); + expect(activityMock.listeners.size).toBe(0); + } + ); + + it.each(['ios', 'android'])( + 'keeps %s scope delivery after an ordinary activity end', + async platform => { + platformMock.OS = platform; + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + _setGetDevicePushTokenForTests(async () => { + await Promise.resolve(); + return 'scope-token'; + }); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await flushRegistration(); + + await getGlanceableDelivery().unregisterTokens('activity'); + expect(rows).toEqual(new Map([['scope-token', 'org-1']])); + + // Background delivery can still find the scope after the visible surface ends. + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'late-scope-token' }); + await flushRegistration(); + expect(rows.get(platform === 'ios' ? 'late-scope-token' : 'scope-token')).toBe('org-1'); + await getGlanceableDelivery().unregisterTokens(); + expect(rows.size).toBe(0); + } + ); + + it('registers late and rotated iOS tokens and removes every recorded version on scope cleanup', async () => { + const rows = trackRemoteTokens(); + activityMock.getPushToken.mockResolvedValue(null); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await flushRegistration(); + expect(rows.size).toBe(0); + + for (const suffix of ['first', 'rotated']) { + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: `start-${suffix}` }); + emitActivityToken(`activity-${suffix}`); + // eslint-disable-next-line no-await-in-loop -- each rotation must settle before the next native event + await flushRegistration(); + expect(rows.get(`start-${suffix}`)).toBe('org-1'); + expect(rows.get(`activity-${suffix}`)).toBe('org-1'); + } + + await getGlanceableDelivery().unregisterTokens(); + expect(rows.size).toBe(0); + expect(activityMock.listeners.size).toBe(0); + }); + + it('holds late token events behind pending cleanup and registers them after it clears', async () => { + const rows = trackRemoteTokens(); + logoutMock.hasPendingActivityUnregister.mockResolvedValue(true); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + emitActivityToken('activity-token'); + await flushRegistration(); + expect(rows.size).toBe(0); + + logoutMock.hasPendingActivityUnregister.mockResolvedValue(false); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + emitActivityToken('activity-token'); + await flushRegistration(); + expect(rows).toEqual( + new Map([ + ['scope-token', 'org-1'], + ['activity-token', 'org-1'], + ]) + ); + }); + + it('cleans up an uncertain upsert even after its token rotates', async () => { + const rows = trackRemoteTokens(); + trpcMock.registerActivityToken.mutate.mockImplementationOnce( + async (input: { token: string; organizationId: string | null }) => { + await Promise.resolve(); + rows.set(input.token, input.organizationId); + throw new Error('registration response lost'); + } + ); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await flushRegistration(); + emitActivityToken('rotated-token'); + await flushRegistration(); + expect(rows.get('rotated-token')).toBe('org-1'); + + await getGlanceableDelivery().unregisterTokens(); + expect(rows.size).toBe(0); + }); + + it('does not overwrite a token event with an older initial token read', async () => { + const rows = trackRemoteTokens(); + const initialToken = Promise.withResolvers(); + activityMock.getPushToken.mockReturnValueOnce(initialToken.promise); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + + emitActivityToken('current-token'); + await flushRegistration(); + initialToken.resolve('outdated-token'); + await flushRegistration(); + + expect(rows).toEqual(new Map([['current-token', 'org-1']])); + }); + + it('replaces the activity listener and rejects delayed events from the ended activity', async () => { + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await flushRegistration(); + const oldListener = [...activityMock.listeners][0]; + const replacement = { + getPushToken: async () => { + await Promise.resolve(); + return 'replacement-token'; + }, + addPushTokenListener: () => ({ remove: () => undefined }), + }; + + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1', replacement); + oldListener?.({ activityId: 'old-activity', pushToken: 'stale-event-token' }); + await flushRegistration(); + + expect(rows).toEqual( + new Map([ + ['scope-token', 'org-1'], + ['replacement-token', 'org-1'], + ]) + ); + expect(activityMock.listeners.size).toBe(0); + }); + + it('fences old scope listeners while cleanup waits and after a replacement scope registers', async () => { + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + getGlanceableDelivery().registerTokens(snapshot(), 'old-org', 'u1'); + await flushRegistration(); + const oldStartListener = expoWidgetsMock.pushToStartListener; + const oldActivityListener = [...activityMock.listeners][0]; + const deleting = Promise.withResolvers(); + const deleteGate = Promise.withResolvers(); + trpcMock.unregisterActivityToken.mutate.mockImplementationOnce( + async (input: { token: string }) => { + deleting.resolve(undefined); + await deleteGate.promise; + rows.delete(input.token); + return { success: true }; + } + ); + + const cleanup = getGlanceableDelivery().unregisterTokens(); + await deleting.promise; + oldStartListener?.({ activityPushToStartToken: 'stale-start-during-cleanup' }); + oldActivityListener?.({ + activityId: 'old-activity', + pushToken: 'stale-activity-during-cleanup', + }); + getGlanceableDelivery().registerTokens(snapshot(), 'new-org', 'u1'); + deleteGate.resolve(undefined); + await cleanup; + await flushRegistration(); + oldStartListener?.({ activityPushToStartToken: 'stale-start-after-cleanup' }); + oldActivityListener?.({ + activityId: 'old-activity', + pushToken: 'stale-activity-after-cleanup', + }); + await flushRegistration(); + + expect(rows).toEqual( + new Map([ + ['scope-token', 'new-org'], + ['token-1', 'new-org'], + ]) + ); + expect(expoWidgetsMock.listeners.size).toBe(1); + expect(activityMock.listeners.size).toBe(1); + }); + + it.each(['signed_out', 'privacy'] as const)( + 'invalidates listeners and the publisher on %s without losing cleanup', + async status => { + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await flushRegistration(); + const oldStartListener = expoWidgetsMock.pushToStartListener; + const oldActivityListener = [...activityMock.listeners][0]; + const publisher = new GlanceablePublisher({ + sinks: [], + terminalBlankEpoch: getTerminalBlankEpoch, + }); + + if (status === 'signed_out') { + writeSignedOutSnapshotAndEnd(); + } else { + writePrivacySnapshotAndEnd(); + } + oldStartListener?.({ activityPushToStartToken: 'late-start' }); + oldActivityListener?.({ activityId: 'old-activity', pushToken: 'late-activity' }); + publisher.handleSessions([{ status: 'busy' }], { organizationId: 'org-1', userId: 'u1' }); + await flushRegistration(); + publisher.dispose(); + + expect(rows.size).toBe(0); + expect(activityMock.listeners.size).toBe(0); + } + ); + + it('rejects token events when the authentication epoch changes', async () => { + const rows = trackRemoteTokens(); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await flushRegistration(); + + bumpAuthEpoch(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'wrong-auth-start' }); + emitActivityToken('wrong-auth-activity'); + await flushRegistration(); + + expect(rows).toEqual(new Map([['token-1', 'org-1']])); + }); + + it('waits for an in-flight activity upsert before removing only activity tokens', async () => { + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + getGlanceableDelivery().registerScopeTokens('org-1', 'u1'); + await flushRegistration(); + const registering = Promise.withResolvers(); + const registerGate = Promise.withResolvers(); + trpcMock.registerActivityToken.mutate.mockImplementationOnce( + async (input: { token: string; organizationId: string | null }) => { + registering.resolve(undefined); + await registerGate.promise; + rows.set(input.token, input.organizationId); + return { success: true }; + } + ); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await registering.promise; + + const ending = getGlanceableDelivery().unregisterTokens('activity'); + registerGate.resolve(undefined); + await ending; + + expect(rows).toEqual(new Map([['scope-token', 'org-1']])); + }); + + it('retains only failed retired activity tokens for cleanup without deleting the scope', async () => { + const rows = trackRemoteTokens(); + expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'scope-token' }); + getGlanceableDelivery().registerTokens(snapshot(), 'org-1', 'u1'); + await flushRegistration(); + emitActivityToken('rotated-activity'); + await flushRegistration(); + trpcMock.unregisterActivityToken.mutate.mockRejectedValueOnce(new Error('network')); + + const result = await getGlanceableDelivery().unregisterTokens('activity'); + + expect(result).toEqual({ ok: false, tokens: ['token-1'] }); + expect(rows).toEqual( + new Map([ + ['scope-token', 'org-1'], + ['token-1', 'org-1'], + ]) + ); + await getGlanceableDelivery().unregisterTokens(); + expect(rows.size).toBe(0); + }); + + it('discovers a cold activity without a scope registration and retains a failed token after native end', async () => { + const rows = trackRemoteTokens(); + rows.set('scope-token', 'org-1'); + rows.set('token-1', 'org-1'); + trpcMock.unregisterActivityToken.mutate.mockRejectedValueOnce(new Error('network')); + + expect(await getGlanceableDelivery().unregisterTokens('activity')).toEqual({ + ok: false, + tokens: ['token-1'], + }); + expect(rows).toEqual( + new Map([ + ['scope-token', 'org-1'], + ['token-1', 'org-1'], + ]) + ); + + activityMock.getPushToken.mockResolvedValue(null); + expect(await getGlanceableDelivery().unregisterTokens('activity')).toEqual({ + ok: true, + tokens: [], + }); + expect(rows).toEqual(new Map([['scope-token', 'org-1']])); + }); + it('returns only the failed iOS tokens on a partial unregister failure', async () => { expoWidgetsMock.pushToStartListener?.({ activityPushToStartToken: 'ptt-token' }); activityMock.getPushToken.mockResolvedValue('activity-token-1'); diff --git a/apps/mobile/src/lib/glanceable/delivery-registration.ts b/apps/mobile/src/lib/glanceable/delivery-registration.ts index 5ac971a0dc..0cf652f612 100644 --- a/apps/mobile/src/lib/glanceable/delivery-registration.ts +++ b/apps/mobile/src/lib/glanceable/delivery-registration.ts @@ -3,6 +3,8 @@ import { Platform } from 'react-native'; import { addPushToStartTokenListener } from 'expo-widgets'; import { ActiveAgentsLiveActivity } from '@/glanceable-ios/active-agents-live-activity'; +import { currentAuthEpoch, isCurrentAuthEpoch } from '@/lib/auth/auth-epoch'; +import { unregisterActivityTokensAndTombstone } from '@/lib/auth/logout-cleanup'; import { attemptLogoutReconciliation, awaitLogoutReconciliationSettled, @@ -10,31 +12,38 @@ import { } from '@/lib/auth/logout-reconciliation'; import { trpcClient } from '@/lib/trpc'; -import { type GlanceableDelivery, setGlanceableDelivery } from './sink-registry'; +import { + type GlanceableActivity, + type GlanceableDelivery, + type GlanceableSinkContext, + setGlanceableDelivery, +} from './sink-registry'; -/** - * Activity-token registrar. Wires the glanceable publisher's delivery hooks to - * `user.registerActivityToken`/`user.unregisterActivityToken` so the server can - * reach this device's surface token: on iOS the Live Activity and push-to-start - * token via APNs, on Android the per-device Expo push token (`android_ongoing`). - */ +/** Scope delivery survives idle work; only the activity registration ends with its surface. */ +type Registration = GlanceableSinkContext & { + kind: 'ios_push_to_start' | 'ios_activity' | 'android_ongoing'; + epoch: number; + authEpoch: number; + token: string | null; + registeredToken: string | null; + tokens: Set; +}; let pushToStartToken: string | null = null; - -/** The last Android device token registered, so end/cleanup can unregister it. */ -let androidOngoingToken: string | null = null; - -/** Epoch bumped on every unregister/end. A register that started before - * the bump must abort instead of recreating the row after end/logout. */ +let scopeRegistration: Registration | null = null; +let activityRegistration: Registration | null = null; +let observedActivity: GlanceableActivity | null = null; +let startSubscription: ReturnType | null = null; +let activitySubscription: ReturnType | null = null; +// Keep every attempted token until its delete succeeds, including tokens rotated away by native. +const scopeTokens = new Set(); +const activityTokens = new Set(); let registerEpoch = 0; -/** FIFO chain serializing activity-token mutations so the last client intent - * wins: an upsert and a delete must not race for stable iOS or Android tokens. */ +/** FIFO chain: an upsert and delete of a stable token must never race. */ let mutationTail: Promise | null = null; - const NOOP = (): void => undefined; -/** Serialize one mutation; a rejected prior mutation never blocks the next. */ async function enqueueTokenMutation(op: () => Promise): Promise { const previous = mutationTail; let release: () => void = NOOP; @@ -42,13 +51,8 @@ async function enqueueTokenMutation(op: () => Promise): Promise { release = resolve; }); mutationTail = gate; - if (previous !== null) { - try { - await previous; - } catch { - // A prior mutation failure must not block the next one. - } - } + // The tail is a release gate, not the mutation promise; it always resolves. + await previous; try { return await op(); } finally { @@ -56,10 +60,8 @@ async function enqueueTokenMutation(op: () => Promise): Promise { } } -// Test-only override so pure suites never load @/lib/notifications -// (→ expo-notifications → expo-modules-core → RN). +// Pure suites must not load the native notification graph. let getDevicePushTokenForTests: (() => Promise) | null = null; - function getDevicePushTokenLazy(): () => Promise { if (getDevicePushTokenForTests !== null) { return getDevicePushTokenForTests; @@ -71,229 +73,270 @@ function getDevicePushTokenLazy(): () => Promise { return getDevicePushToken; } -async function register(input: { - token: string; - kind: 'ios_push_to_start' | 'ios_activity' | 'android_ongoing'; - platform: 'ios' | 'android'; - organizationId: string | null; -}): Promise { - try { - await trpcClient.user.registerActivityToken.mutate(input); - } catch { - // Best effort: a failed registration is retried on the next start. +function isCurrent(target: Registration): boolean { + return ( + target.epoch === registerEpoch && + isCurrentAuthEpoch(target.authEpoch) && + (target === scopeRegistration || target === activityRegistration) + ); +} + +async function canRegister(target: Registration): Promise { + if (!isCurrent(target)) { + return false; } + // Never wait for cleanup inside the mutation queue: cleanup needs that queue itself. + if (target.userId !== null) { + void attemptLogoutReconciliation(target.userId); + } + await awaitLogoutReconciliationSettled(); + return ( + isCurrent(target) && !(await hasPendingActivityUnregister(target.userId)) && isCurrent(target) + ); } -async function unregister(token: string): Promise { +async function registerToken(target: Registration, token: string): Promise { + if (!isCurrent(target) || !token) { + return; + } + target.token = token; + if (target.registeredToken === token) { + return; + } try { - await trpcClient.user.unregisterActivityToken.mutate({ token }); - return true; + if (!(await canRegister(target))) { + return; + } + await enqueueTokenMutation(async () => { + if (!isCurrent(target) || target.token !== token || target.registeredToken === token) { + return; + } + target.tokens.add(token); + await trpcClient.user.registerActivityToken.mutate({ + token, + kind: target.kind, + platform: target.kind === 'android_ongoing' ? 'android' : 'ios', + organizationId: target.organizationId, + }); + target.registeredToken = token; + }); } catch { - // The caller aggregates success and tombstones the token on failure. - return false; + // Retry on the next token event or scope refresh; an uncertain upsert still needs cleanup. } } -if (Platform.OS === 'ios') { - // Push-to-start token events are emitted whenever the system rotates the - // token; cache the latest and register on the next activity start. - addPushToStartTokenListener(({ activityPushToStartToken }) => { +function observePushToStart(target: Registration | null): void { + startSubscription?.remove(); + const epoch = registerEpoch; + startSubscription = addPushToStartTokenListener(({ activityPushToStartToken }) => { + if (epoch !== registerEpoch || (target !== null && !isCurrent(target))) { + return; + } pushToStartToken = activityPushToStartToken; + if (target !== null) { + void registerToken(target, activityPushToStartToken); + } }); } -/** - * Android: register the device Expo push token as the `android_ongoing` - * activity token. The device-token lookup happens outside the mutation chain - * (the chain must never await logout reconciliation or it can deadlock against - * `runLogoutCleanup`); only the server mutation and the slot write are - * serialized, so the last client intent wins even for a stable token. - */ -async function registerAndroidOngoingToken( - organizationId: string | null, - userId: string | null -): Promise { - // Capture the epoch before the first await so an unregister/end that lands - // during reconciliation or the token lookup aborts this stale register. - const epoch = registerEpoch; - if (userId !== null) { - void attemptLogoutReconciliation(userId); +function detachActivity(): void { + activityRegistration = null; + observedActivity = null; + activitySubscription?.remove(); + activitySubscription = null; +} + +function getScope(organizationId: string | null, userId: string | null): Registration { + if ( + scopeRegistration === null || + scopeRegistration.organizationId !== organizationId || + scopeRegistration.userId !== userId || + !isCurrent(scopeRegistration) + ) { + registerEpoch += 1; + detachActivity(); + scopeRegistration = { + organizationId, + userId, + kind: Platform.OS === 'android' ? 'android_ongoing' : 'ios_push_to_start', + epoch: registerEpoch, + authEpoch: currentAuthEpoch(), + token: null, + registeredToken: null, + tokens: scopeTokens, + }; + if (Platform.OS === 'ios') { + observePushToStart(scopeRegistration); + } } + return scopeRegistration; +} + +async function registerAndroidToken(target: Registration): Promise { try { - await awaitLogoutReconciliationSettled(); - if (epoch !== registerEpoch) { - return; - } - if (await hasPendingActivityUnregister(userId)) { - // A pending retry owns the recorded activity tokens; re-registering this - // device token now would only be deleted by the next attempt. + if (target.registeredToken !== null || !(await canRegister(target))) { return; } const token = await getDevicePushTokenLazy()(); - if (token === null) { - return; + if (token !== null) { + await registerToken(target, token); } - if (epoch !== registerEpoch) { + } catch { + // A failed lookup retries on the next authorized scope refresh. + } +} + +async function observeActivity(target: Registration, instance: GlanceableActivity): Promise { + try { + if (observedActivity === instance && activityRegistration !== null) { + if (activityRegistration.token !== null) { + await registerToken(activityRegistration, activityRegistration.token); + } return; } - await enqueueTokenMutation(async () => { - if (epoch !== registerEpoch) { - return; - } - await register({ token, kind: 'android_ongoing', platform: 'android', organizationId }); - androidOngoingToken = token; + if (activityRegistration !== null) { + delivery.cleanupTokens('activity'); + } + const registration: Registration = { + ...target, + kind: 'ios_activity', + token: null, + registeredToken: null, + tokens: activityTokens, + }; + activityRegistration = registration; + observedActivity = instance; + activitySubscription = instance.addPushTokenListener(({ pushToken }) => { + void registerToken(registration, pushToken); }); + const token = await instance.getPushToken(); + // A token event is newer than the initial asynchronous read. + if (token !== null && registration.token === null) { + await registerToken(registration, token); + } } catch { - // Best effort: a failed lookup is retried on the next start. + // Unsupported or transient native reads must not discard the scope subscription. } } -/** Android: unregister the recorded device token, tombstoning it on failure. - * Bumps the epoch (invalidating in-flight registers) and serializes the delete - * against register so a delete never races an upsert of the same token: the - * FIFO order decides the final state. */ -async function unregisterAndroidOngoingToken(): Promise<{ ok: boolean; tokens: string[] }> { - registerEpoch += 1; - const result = enqueueTokenMutation(async () => { - const token = androidOngoingToken; - if (token === null) { - return { ok: true, tokens: [] as string[] }; - } - const ok = await unregister(token); - if (ok) { - androidOngoingToken = null; - } - return { ok, tokens: [token] }; - }); - await result; - return result; +async function collectActivityToken( + instance: GlanceableActivity | null, + activityToken?: Promise +): Promise { + try { + return (await (activityToken ?? instance?.getPushToken())) ?? null; + } catch { + // Recorded tokens still need deletion when a native read fails. + return null; + } } const delivery: GlanceableDelivery = { - registerTokens(_snapshot, organizationId, userId) { - if (Platform.OS === 'android') { - void registerAndroidOngoingToken(organizationId, userId); + registerScopeTokens(organizationId, userId) { + if (Platform.OS !== 'ios' && Platform.OS !== 'android') { return; } - if (Platform.OS !== 'ios') { + const target = getScope(organizationId, userId); + if (Platform.OS === 'android') { + void registerAndroidToken(target); + } else if (pushToStartToken !== null) { + void registerToken(target, pushToStartToken); + } + }, + + // eslint-disable-next-line max-params -- preserve the existing delivery arguments and pass the sink's stable native handle + registerTokens(_snapshot, organizationId, userId, instance) { + delivery.registerScopeTokens(organizationId, userId); + if (Platform.OS !== 'ios' || scopeRegistration === null) { return; } - void (async () => { - const epoch = registerEpoch; - // Keep reconciliation and scope-cleanup waits outside the mutation queue: - // cleanup itself needs that queue to finish its unregister. - if (userId !== null) { - void attemptLogoutReconciliation(userId); - } - await awaitLogoutReconciliationSettled(); - if ((await hasPendingActivityUnregister(userId)) || epoch !== registerEpoch) { - return; - } - const startToken = pushToStartToken; - if (startToken !== null) { - await enqueueTokenMutation(async () => { - if (epoch !== registerEpoch) { - return; - } - await register({ - token: startToken, - kind: 'ios_push_to_start', - platform: 'ios', - organizationId, - }); - }); - } - if (epoch !== registerEpoch) { - return; + try { + const current = instance ?? ActiveAgentsLiveActivity.getInstances().at(-1); + if (current) { + void observeActivity(scopeRegistration, current); } + } catch { + // getInstances can throw on unsupported surfaces; the sink owns retry. + } + }, + + cleanupTokens(lifetime, activityToken) { + void unregisterActivityTokensAndTombstone(lifetime, activityToken); + }, + + async unregisterTokens(lifetime, activityToken) { + const includeScope = lifetime !== 'activity'; + let instance = observedActivity; + if (Platform.OS === 'ios' && instance === null && activityToken === undefined) { try { - const activity = ActiveAgentsLiveActivity.getInstances().at(-1); - if (activity) { - const token = await activity.getPushToken(); - if (token) { - await enqueueTokenMutation(async () => { - if (epoch !== registerEpoch) { - return; - } - await register({ token, kind: 'ios_activity', platform: 'ios', organizationId }); - }); - } - } + instance = ActiveAgentsLiveActivity.getInstances().at(-1) ?? null; } catch { - // getInstances can throw on unsupported surfaces; the sink owns retry. + // Recorded tokens remain available when native discovery fails. } - })(); - }, - - async unregisterTokens() { - if (Platform.OS === 'android') { - return unregisterAndroidOngoingToken(); } - if (Platform.OS !== 'ios') { - return { ok: true, tokens: [] }; + if (includeScope) { + registerEpoch += 1; + scopeRegistration = null; + if (Platform.OS === 'ios') { + if (pushToStartToken !== null) { + scopeTokens.add(pushToStartToken); + } + observePushToStart(null); + } } - registerEpoch += 1; - const tokens = collectIosActivityTokens(); - const result = await enqueueTokenMutation(async () => unregisterActivityTokens(await tokens)); + detachActivity(); + const nativeToken = collectActivityToken(instance, activityToken); + const result = await enqueueTokenMutation(async () => { + const capturedToken = await nativeToken; + if (capturedToken) { + activityTokens.add(capturedToken); + } + // Read the sets inside the FIFO so an already-running upsert is included. + const tokens = [ + ...new Set(includeScope ? [...scopeTokens, ...activityTokens] : activityTokens), + ]; + const results = await Promise.all( + tokens.map(async token => { + try { + await trpcClient.user.unregisterActivityToken.mutate({ token }); + scopeTokens.delete(token); + activityTokens.delete(token); + return true; + } catch { + return false; + } + }) + ); + const failed = tokens.filter((_token, index) => !results[index]); + // Keep Android's existing successful-cleanup result contract. + return { + ok: failed.length === 0, + tokens: Platform.OS === 'android' && failed.length === 0 ? tokens : failed, + }; + }); return result; }, }; -/** Capture the current iOS tokens before a later scope replaces the native instance. */ -async function collectIosActivityTokens(): Promise { - const tokens: string[] = []; - if (pushToStartToken !== null) { - tokens.push(pushToStartToken); - } - try { - const activity = ActiveAgentsLiveActivity.getInstances().at(-1); - if (activity) { - const token = await activity.getPushToken(); - if (token) { - tokens.push(token); - } - } - } catch { - // Nothing to unregister when no activity survives. - } - return tokens; -} - -/** - * Unregister the captured iOS tokens in parallel and report only failures. - * The caller tombstones those tokens, so a retry never re-deletes a token - * that already succeeded and can belong to the new session. - */ -async function unregisterActivityTokens( - tokens: string[] -): Promise<{ ok: boolean; tokens: string[] }> { - if (tokens.length === 0) { - return { ok: true, tokens }; - } - const results = await Promise.allSettled( - tokens.map(async token => { - const ok = await unregister(token); - return ok; - }) - ); - const failedTokens = tokens.filter((_token, index) => { - const result = results[index]; - return result === undefined || result.status === 'rejected' || !result.value; - }); - return { ok: failedTokens.length === 0, tokens: failedTokens }; +if (Platform.OS === 'ios') { + // Cache early tokens without registering an unauthenticated scope. + observePushToStart(null); } - setGlanceableDelivery(delivery); -// ── Test-only helpers ────────────────────────────────────────────────────── - export function _setGetDevicePushTokenForTests(fn: (() => Promise) | null): void { getDevicePushTokenForTests = fn; } export function _resetDeliveryRegistrationForTests(): void { - androidOngoingToken = null; - pushToStartToken = null; registerEpoch += 1; + detachActivity(); + scopeRegistration = null; + scopeTokens.clear(); + activityTokens.clear(); + pushToStartToken = null; mutationTail = null; + if (Platform.OS === 'ios') { + observePushToStart(null); + } } diff --git a/apps/mobile/src/lib/glanceable/publisher.ts b/apps/mobile/src/lib/glanceable/publisher.ts index e74f241e72..8ac8476465 100644 --- a/apps/mobile/src/lib/glanceable/publisher.ts +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -9,7 +9,11 @@ import { shouldDiscardGlanceableRevision, } from '@kilocode/app-shared/glanceable-agents-snapshot'; -import { type GlanceableSink, type GlanceableSinkContext } from './sink-registry'; +import { + getGlanceableDelivery, + type GlanceableSink, + type GlanceableSinkContext, +} from './sink-registry'; /** * Framework-agnostic publisher state machine. Derives one versioned snapshot @@ -100,6 +104,7 @@ export class GlanceablePublisher { if (this.isGated()) { return; } + getGlanceableDelivery().registerScopeTokens(ctx.organizationId, ctx.userId); const now = this.now(); this.applyExpiry(now, ctx); @@ -138,6 +143,7 @@ export class GlanceablePublisher { if (this.isGated()) { return; } + getGlanceableDelivery().registerScopeTokens(ctx.organizationId, ctx.userId); if (this.current !== null) { return; } @@ -183,6 +189,9 @@ export class GlanceablePublisher { if (this.current !== null && shouldDiscardGlanceableRevision(incoming, this.current)) { return; } + if (incoming.status !== 'signed_out' && incoming.status !== 'privacy') { + getGlanceableDelivery().registerScopeTokens(ctx.organizationId, ctx.userId); + } // A late background delivery supersedes a pending coalesced emit and any // pending 8 s terminal, so neither can fire after the newer snapshot. this.cancelCoalesce(); diff --git a/apps/mobile/src/lib/glanceable/sink-registry.ts b/apps/mobile/src/lib/glanceable/sink-registry.ts index e518ff1da5..dd350abf29 100644 --- a/apps/mobile/src/lib/glanceable/sink-registry.ts +++ b/apps/mobile/src/lib/glanceable/sink-registry.ts @@ -1,4 +1,5 @@ import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { type LiveActivity } from 'expo-widgets'; /** * One sink consumes the glanceable snapshot for one native surface (persist, @@ -39,19 +40,34 @@ export function getGlanceableSinks(): readonly GlanceableSink[] { * `unregisterTokens` reports only the tokens whose unregister failed, so * logout can tombstone the failed tokens and retry them later. */ +export type GlanceableActivity = Pick; + export type GlanceableDelivery = { + registerScopeTokens(organizationId: string | null, userId: string | null): void; registerTokens( snapshot: GlanceableAgentsSnapshot, organizationId: string | null, - userId: string | null + userId: string | null, + activity?: GlanceableActivity ): void; - unregisterTokens(): Promise<{ ok: boolean; tokens: string[] }>; + /** Retire a lifetime and tombstone failures. The optional lookup starts before native end. */ + cleanupTokens(lifetime: 'scope' | 'activity', activityToken?: Promise): void; + unregisterTokens( + lifetime?: 'scope' | 'activity', + activityToken?: Promise + ): Promise<{ ok: boolean; tokens: string[] }>; }; const noopDelivery: GlanceableDelivery = { + registerScopeTokens() { + // No-op until a token slice registers a delivery. + }, registerTokens() { // No-op until a token slice registers a delivery. }, + cleanupTokens() { + // No-op until a token slice registers a delivery. + }, async unregisterTokens() { // No-op until a token slice registers a delivery. await Promise.resolve(); diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index 7b7cfedca1..bea5943484 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -42,6 +42,12 @@ const mocks = vi.hoisted(() => { setPendingDeepLink: vi.fn(), safeParse: vi.fn(), getItemAsync: vi.fn(), + setItemAsync: vi.fn(), + deleteItemAsync: vi.fn(), + nativeInstances: vi.fn(), + startTokenListeners: new Set<(event: { activityPushToStartToken: string }) => void>(), + registerActivityToken: vi.fn(), + unregisterActivityToken: vi.fn(), defineTask: vi.fn(), registerTaskAsync: vi.fn(), captureEvent: vi.fn(), @@ -81,10 +87,51 @@ vi.mock('expo-constants', () => ({ vi.mock('expo-secure-store', () => ({ getItemAsync: mocks.getItemAsync, - setItemAsync: vi.fn(), - deleteItemAsync: vi.fn(), + setItemAsync: mocks.setItemAsync, + deleteItemAsync: mocks.deleteItemAsync, })); +vi.mock('expo-widgets', () => ({ + addPushToStartTokenListener: ( + listener: (event: { activityPushToStartToken: string }) => void + ) => { + mocks.startTokenListeners.add(listener); + return { remove: () => mocks.startTokenListeners.delete(listener) }; + }, +})); + +vi.mock('expo-widgets/src/ExpoWidgets', () => ({ + default: { + LiveActivityFactory: class { + getInstances = mocks.nativeInstances; + start = vi.fn(() => { + throw new Error('A remote activity must be adopted, not started locally'); + }); + }, + }, +})); + +vi.mock('@/glanceable-ios/active-agents-live-activity', async () => { + // Keep the real expo-widgets wrapper between the sink and the native handles. + const { LiveActivityFactory } = await import('expo-widgets/src/Widgets'); + return { + ActiveAgentsLiveActivity: new LiveActivityFactory('ActiveAgentsLiveActivity', () => null), + }; +}); +vi.mock('@/glanceable-ios/active-agents-widget', () => ({ + ActiveAgentsWidget: { updateSnapshot: vi.fn(), updateTimeline: vi.fn() }, +})); +vi.mock('@/lib/trpc', () => ({ + trpcClient: { + user: { + registerActivityToken: { mutate: mocks.registerActivityToken }, + unregisterActivityToken: { mutate: mocks.unregisterActivityToken }, + }, + }, +})); +vi.mock('@/lib/query-client', () => ({ queryClient: {} })); +vi.mock('@/lib/persist/read-cache', () => ({ readCachedUserId: () => null })); + vi.mock('@kilocode/notifications', () => ({ ANDROID_NOTIFICATION_CHANNELS: [ { id: 'agent', name: 'Agent sessions', importance: 'high' }, @@ -888,6 +935,242 @@ describe('setupNotificationBackgroundHandler', () => { }); }); +describe('cold iOS background delivery', () => { + const rows = new Map(); + const native = { + exists: true, + token: null as string | null, + tokenRead: null as Promise | null, + observers: new Set<(token: string) => void>(), + }; + + function emitNativeToken(token: string): void { + native.token = token; + for (const observer of native.observers) { + observer(token); + } + } + + async function loadColdBackground() { + vi.resetModules(); + await import('@/lib/glanceable/delivery-registration'); + const [notifications, registry, persist, sink, cleanup] = await Promise.all([ + import('./notifications'), + import('@/lib/glanceable/sink-registry'), + import('@/lib/glanceable/persist'), + import('@/glanceable-ios/ios-sink'), + import('@/lib/auth/logout-cleanup'), + ]); + persist._setSecureStoreForTests(secureStoreMock); + for (const listener of mocks.startTokenListeners) { + listener({ activityPushToStartToken: 'scope-token' }); + } + notifications._setGlanceableSinksLoaderForTests(() => { + registry.registerGlanceableSink(persist.persistGlanceableSink); + registry.registerGlanceableSink(sink.iosSink); + }); + notifications.setupNotificationBackgroundHandler(); + const executor = mocks.defineTask.mock.calls[0]?.[1] as (body: { + data: { notification: null; data: { dataString: string } }; + error: null; + executionInfo: { eventId: string; taskName: string }; + }) => Promise; + return { + cleanup, + deliver: async (overrides: Partial) => { + const result = await executor({ + data: { + notification: null, + data: { + dataString: JSON.stringify( + activeGlanceablePush({ updatedAt: '2026-01-02T00:00:00.000Z', ...overrides }) + ), + }, + }, + error: null, + executionInfo: { eventId: 'cold', taskName: 'active-agents-glanceable-background-task' }, + }); + return result; + }, + }; + } + + beforeEach(() => { + vi.useFakeTimers(); + mocks.platform.OS = 'ios'; + mocks.startTokenListeners.clear(); + mocks.defineTask.mockReset(); + mocks.registerTaskAsync.mockResolvedValue(null); + mocks.safeParse.mockImplementation((data: unknown) => ({ success: true, data })); + secureStore.clear(); + secureStore.set('glanceable-snapshot', JSON.stringify(glanceableSnapshot())); + secureStore.set('glanceable-scope-key', SCOPE_KEY); + secureStore.set(ACTIVE_USER_ID_KEY, 'u1'); + secureStore.set(ORGANIZATION_STORAGE_KEY, 'org-9'); + mocks.getItemAsync.mockImplementation(secureStoreMock.getItemAsync); + mocks.setItemAsync.mockImplementation(secureStoreMock.setItemAsync); + mocks.deleteItemAsync.mockImplementation(async (key: string) => { + await Promise.resolve(); + secureStore.delete(key); + }); + rows.clear(); + rows.set('scope-token', { kind: 'ios_push_to_start', organizationId: 'org-9' }); + mocks.registerActivityToken.mockImplementation( + async ({ + token, + kind, + organizationId, + }: { + token: string; + kind: string; + organizationId: string | null; + }) => { + await Promise.resolve(); + rows.set(token, { kind, organizationId }); + return { success: true }; + } + ); + mocks.unregisterActivityToken.mockImplementation(async ({ token }: { token: string }) => { + await Promise.resolve(); + rows.delete(token); + return { success: true }; + }); + native.exists = true; + native.token = null; + native.tokenRead = null; + native.observers.clear(); + mocks.nativeInstances.mockImplementation(() => { + if (!native.exists) { + return []; + } + const listeners = new Set<(event: { activityId: string; pushToken: string }) => void>(); + // Model the patched native factory: adoption starts observation on this handle. + native.observers.add(token => { + for (const listener of listeners) { + listener({ activityId: 'remote-activity', pushToken: token }); + } + }); + return [ + { + getPushToken: async () => { + await native.tokenRead; + if (!native.exists) { + throw new Error('Activity no longer exists'); + } + return native.token; + }, + addListener: ( + name: string, + listener: (event: { activityId: string; pushToken: string }) => void + ) => { + if (name !== 'onExpoWidgetsTokenReceived') { + throw new Error('Unknown native event'); + } + listeners.add(listener); + return { remove: () => listeners.delete(listener) }; + }, + update: async () => { + await Promise.resolve(); + }, + end: async () => { + native.exists = false; + native.observers.clear(); + await Promise.resolve(); + }, + }, + ]; + }); + }); + + afterEach(() => { + native.observers.clear(); + vi.clearAllTimers(); + vi.useRealTimers(); + }); + + it('registers late and rotated tokens from an adopted native handle through the real widget wrapper', async () => { + const background = await loadColdBackground(); + expect(await background.deliver({})).toBe(0); + await vi.advanceTimersByTimeAsync(0); + expect(rows.size).toBe(1); + + emitNativeToken('late-activity-token'); + await vi.advanceTimersByTimeAsync(0); + expect(rows.get('late-activity-token')).toEqual({ + kind: 'ios_activity', + organizationId: 'org-9', + }); + emitNativeToken('rotated-activity-token'); + await vi.advanceTimersByTimeAsync(0); + expect(rows.get('rotated-activity-token')).toEqual({ + kind: 'ios_activity', + organizationId: 'org-9', + }); + + await background.cleanup.unregisterActivityTokensAndTombstone(); + emitNativeToken('after-cleanup'); + await vi.advanceTimersByTimeAsync(0); + expect(rows.size).toBe(0); + }); + + it('captures the cold idle token before native end and preserves scope delivery without awaiting the network', async () => { + native.token = 'ended-activity-token'; + rows.set(native.token, { kind: 'ios_activity', organizationId: 'org-9' }); + const read = deferred(); + const deletion = deferred(); + native.tokenRead = read.promise; + mocks.unregisterActivityToken.mockImplementation(async ({ token }: { token: string }) => { + await deletion.promise; + rows.delete(token); + return { success: true }; + }); + const background = await loadColdBackground(); + expect(await background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null })).toBe( + 0 + ); + expect(native.exists).toBe(true); + read.resolve(); + await vi.advanceTimersByTimeAsync(0); + expect(native.exists).toBe(false); + expect(rows.has('ended-activity-token')).toBe(true); + + deletion.resolve(); + await background.cleanup.awaitActivityCleanupSettled(); + expect(rows).toEqual( + new Map([['scope-token', { kind: 'ios_push_to_start', organizationId: 'org-9' }]]) + ); + expect(await background.cleanup.readLogoutCleanupTombstone()).toBeNull(); + }); + + it('tombstones only the failed cold idle token after native discovery disappears', async () => { + native.token = 'failed-activity-token'; + rows.set(native.token, { kind: 'ios_activity', organizationId: 'org-9' }); + mocks.unregisterActivityToken.mockRejectedValueOnce(new Error('network unavailable')); + const background = await loadColdBackground(); + expect(await background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null })).toBe( + 0 + ); + await background.cleanup.awaitActivityCleanupSettled(); + await vi.advanceTimersByTimeAsync(0); + + expect(native.exists).toBe(false); + expect(rows.has('scope-token')).toBe(true); + expect(rows.has('failed-activity-token')).toBe(true); + expect(await background.cleanup.readLogoutCleanupTombstone()).toMatchObject({ + userId: null, + needsPushUnregister: false, + needsActivityUnregister: true, + activityTokens: ['failed-activity-token'], + }); + const { attemptLogoutReconciliation } = await import('@/lib/auth/logout-reconciliation'); + await attemptLogoutReconciliation('u1'); + expect(rows).toEqual( + new Map([['scope-token', { kind: 'ios_push_to_start', organizationId: 'org-9' }]]) + ); + expect(await background.cleanup.readLogoutCleanupTombstone()).toBeNull(); + }); +}); + describe('notification permission and token events', () => { it('emits granted when a live permission request is granted', async () => { mocks.getPermissionsAsync.mockResolvedValue({ status: 'denied' }); diff --git a/patches/expo-widgets@57.0.11.patch b/patches/expo-widgets@57.0.11.patch new file mode 100644 index 0000000000..752679ef2e --- /dev/null +++ b/patches/expo-widgets@57.0.11.patch @@ -0,0 +1,14 @@ +diff --git a/ios/LiveActivityFactory.swift b/ios/LiveActivityFactory.swift +--- a/ios/LiveActivityFactory.swift ++++ b/ios/LiveActivityFactory.swift +@@ -43,7 +43,9 @@ final class LiveActivityFactory: SharedObject { + guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() } + + return Activity.activities.map { activity in +- LiveActivity(id: activity.id, name: name) ++ let instance = LiveActivity(id: activity.id, name: name) ++ instance.observePushTokenUpdates(for: activity, pushNotificationsEnabled: LiveActivityFactory.pushNotificationsEnabled) ++ return instance + } + } + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14b1f316ef..a7cdc5b2cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,7 @@ packageExtensionsChecksum: sha256-1pgKZxx87NNMe1poF5N5u5kZB/qlEyILBQPxofM1shE= patchedDependencies: expo-router@57.0.10: 616f8a79932a3ebc7d31343b53b3fc425a14bfb89bb86cdafa614cefa4e603ed expo-server-sdk: 7850520582b5b394397b35d1ea195192fe78589d8a6a748fe15177b818c4ed0b + expo-widgets@57.0.11: 3e90bdda241862937ae562137f60f4bc916a8820b827ebf0af8e61a365b96f2e react-native-appsflyer@6.18.0: 82df99378c830e774b0f01796d8be595da114d1d13393d85ddd47d565c5c2aab importers: @@ -541,7 +542,7 @@ importers: version: 57.0.2(expo@57.0.10)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-widgets: specifier: 57.0.11 - version: 57.0.11(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 57.0.11(patch_hash=3e90bdda241862937ae562137f60f4bc916a8820b827ebf0af8e61a365b96f2e)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) i18next: specifier: ^26.3.6 version: 26.3.6(typescript@6.0.3) @@ -28193,7 +28194,7 @@ snapshots: optionalDependencies: '@babel/runtime': 7.29.7 expo: 57.0.10(@babel/core@7.29.7)(@expo/metro-runtime@57.0.8)(bufferutil@4.1.0)(expo-router@57.0.10)(expo-widgets@57.0.11)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-widgets: 57.0.11(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-widgets: 57.0.11(patch_hash=3e90bdda241862937ae562137f60f4bc916a8820b827ebf0af8e61a365b96f2e)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -30529,7 +30530,7 @@ snapshots: expo: 57.0.10(@babel/core@7.29.7)(@expo/metro-runtime@57.0.8)(bufferutil@4.1.0)(expo-router@57.0.10)(expo-widgets@57.0.11)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react-native: 0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-widgets@57.0.11(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-widgets@57.0.11(patch_hash=3e90bdda241862937ae562137f60f4bc916a8820b827ebf0af8e61a365b96f2e)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/plist': 0.8.1 '@expo/ui': 57.0.12(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 896105b9a3..7fdbbde61c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -160,6 +160,7 @@ packageExtensions: patchedDependencies: expo-router@57.0.10: patches/expo-router@57.0.10.patch expo-server-sdk: patches/expo-server-sdk.patch + expo-widgets@57.0.11: patches/expo-widgets@57.0.11.patch react-native-appsflyer@6.18.0: patches/react-native-appsflyer@6.18.0.patch publicHoistPattern: - '@types/*' From 835e9e057492603ff74bcb7b7f2936d71912b2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 20:19:56 +0200 Subject: [PATCH 22/43] fix(glanceable): order refreshes and persist eligible intervals --- .../notifications/src/rpc-schemas.test.ts | 33 + packages/notifications/src/rpc-schemas.ts | 7 + .../src/dos/NotificationChannelDO.ts | 9 + services/notifications/src/index.ts | 224 +----- .../src/lib/apns-live-activity.test.ts | 56 ++ .../src/lib/apns-live-activity.ts | 17 +- .../notifications/src/lib/expo-push.test.ts | 54 ++ services/notifications/src/lib/expo-push.ts | 10 +- .../src/lib/glanceable-delivery-deps.ts | 162 ++++ .../src/lib/glanceable-delivery.test.ts | 733 +++++++++++++++++- .../src/lib/glanceable-delivery.ts | 30 +- .../src/lib/glanceable-refresh.ts | 84 ++ .../src/dos/UserConnectionDO.test.ts | 254 +++++- .../src/dos/UserConnectionDO.ts | 33 + .../src/ingest/metadata.test.ts | 215 ++++- .../session-ingest/src/ingest/metadata.ts | 10 + .../src/notifications-binding.ts | 2 + .../src/remote-session-notifications.ts | 16 + 18 files changed, 1724 insertions(+), 225 deletions(-) create mode 100644 packages/notifications/src/rpc-schemas.test.ts create mode 100644 services/notifications/src/lib/glanceable-delivery-deps.ts create mode 100644 services/notifications/src/lib/glanceable-refresh.ts diff --git a/packages/notifications/src/rpc-schemas.test.ts b/packages/notifications/src/rpc-schemas.test.ts new file mode 100644 index 0000000000..3477f7a73c --- /dev/null +++ b/packages/notifications/src/rpc-schemas.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { refreshGlanceableSessionsInputSchema } from './rpc-schemas'; + +describe('refreshGlanceableSessionsInputSchema', () => { + it.each([ + { userId: '', cliSessionIds: ['ses_1'] }, + { userId: 'usr_1', cliSessionIds: [] }, + { userId: 'usr_1', cliSessionIds: [''] }, + { userId: 'usr_1', cliSessionIds: [42] }, + ])('rejects invalid refresh identity: %j', input => { + expect(refreshGlanceableSessionsInputSchema.safeParse(input).success).toBe(false); + }); + + it('accepts OAuth user IDs without imposing UUID validation', () => { + expect( + refreshGlanceableSessionsInputSchema.safeParse({ + userId: 'oauth/github/123', + cliSessionIds: ['ses_1', 'ses_2'], + }).success + ).toBe(true); + }); + + it('does not forward caller-supplied counts or organization scope', () => { + const parsed = refreshGlanceableSessionsInputSchema.parse({ + userId: 'usr_1', + cliSessionIds: ['ses_1'], + organizationId: 'org_foreign', + running: 100, + }); + expect(parsed).not.toHaveProperty('organizationId'); + expect(parsed).not.toHaveProperty('running'); + }); +}); diff --git a/packages/notifications/src/rpc-schemas.ts b/packages/notifications/src/rpc-schemas.ts index 2bbf2c092a..dc8f4072e4 100644 --- a/packages/notifications/src/rpc-schemas.ts +++ b/packages/notifications/src/rpc-schemas.ts @@ -168,6 +168,13 @@ export type SendCloudAgentSessionNotificationResult = z.infer< typeof sendCloudAgentSessionNotificationOutputSchema >; +// Aggregate refreshes carry identity only; the server reads current status and scope. +export const refreshGlanceableSessionsInputSchema = z.object({ + userId: z.string().min(1), + cliSessionIds: z.array(z.string().min(1)).min(1), +}); +export type RefreshGlanceableSessionsParams = z.infer; + // ── sendSessionReadyNotification ──────────────────────────────────── export const sendSessionReadyNotificationInputSchema = z.object({ diff --git a/services/notifications/src/dos/NotificationChannelDO.ts b/services/notifications/src/dos/NotificationChannelDO.ts index aeb12071c6..b1efc094d7 100644 --- a/services/notifications/src/dos/NotificationChannelDO.ts +++ b/services/notifications/src/dos/NotificationChannelDO.ts @@ -14,6 +14,8 @@ import { eq, inArray } from 'drizzle-orm'; import { isPushSinkEnabled } from '../lib/push-sink'; import type { ExpoPushMessage, SendResult, TicketTokenPair } from '../lib/expo-push'; import { sendPushNotifications } from '../lib/expo-push'; +import { glanceableDeliveryDeps } from '../lib/glanceable-delivery-deps'; +import { refreshGlanceableSnapshot } from '../lib/glanceable-refresh'; type ReceiptCheckMessage = { ticketTokenPairs: TicketTokenPair[] }; @@ -60,6 +62,13 @@ export class NotificationChannelDO extends DurableObject { // `pending` slot as before. private readonly inFlight = new Set(); + async refreshGlanceableSnapshot(params: { + userId: string; + organizationId: string | null; + }): Promise { + await refreshGlanceableSnapshot(params, this.ctx.storage, glanceableDeliveryDeps(this.env)); + } + async dispatchPush(input: DispatchPushInput): Promise { if (this.inFlight.has(input.idempotencyKey)) { return { kind: 'duplicate' }; diff --git a/services/notifications/src/index.ts b/services/notifications/src/index.ts index 8fbca64a01..e30d8f838f 100644 --- a/services/notifications/src/index.ts +++ b/services/notifications/src/index.ts @@ -4,11 +4,10 @@ import { getWorkerDb } from '@kilocode/db/client'; import { cli_sessions_v2, organization_memberships, - user_activity_tokens, user_notification_preferences, user_push_tokens, } from '@kilocode/db/schema'; -import { and, eq, inArray, isNotNull, isNull } from 'drizzle-orm'; +import { and, eq, inArray } from 'drizzle-orm'; import { Hono } from 'hono'; import type { MiddlewareHandler } from 'hono'; import { cors } from 'hono/cors'; @@ -19,7 +18,8 @@ import { badgeBucketForConversation, internalDispatchRequestSchema, markBadgeReadInputSchema, - pushDataSchema, + refreshGlanceableSessionsInputSchema, + type RefreshGlanceableSessionsParams, type ClearBadgeBucketForUserInput, type ClearBadgeBucketForUserOutput, type DispatchPushInput, @@ -44,7 +44,6 @@ import { dispatchAgentSessionNotificationPush, type DispatchAgentSessionNotificationPushDeps, } from './lib/agent-session-notification-push'; -import { sendLiveActivityApns, type ApnsCredentials } from './lib/apns-live-activity'; import { dispatchCloudAgentSessionPush, dispatchSessionReadyPush, @@ -53,11 +52,6 @@ import { } from './lib/cloud-agent-session-push'; import type { TicketTokenPair } from './lib/expo-push'; import { sendPushNotifications } from './lib/expo-push'; -import { - deliverGlanceableSnapshot, - type GlanceableDeliveryDeps, - type IosActivityToken, -} from './lib/glanceable-delivery'; import { dispatchInstanceLifecyclePush } from './lib/instance-lifecycle-push'; import { dispatchInternalPushCore } from './lib/internal-dispatch-push'; import { @@ -336,192 +330,44 @@ export class NotificationsService extends WorkerEntrypoint { async sendCloudAgentSessionNotification( params: SendCloudAgentSessionNotificationParams ): Promise { - const deps = this.cloudAgentSessionPushDeps(); - const result = await dispatchCloudAgentSessionPush(params, deps); - // Best-effort aggregate glanceable delivery (§psh). Runs after the push - // result is terminal so a failure here never changes the RPC outcome. - this.ctx.waitUntil( - this.deliverGlanceableAfterSessionPush(params.userId, params.cliSessionId, deps.getSession) - ); - return result; + return dispatchCloudAgentSessionPush(params, this.cloudAgentSessionPushDeps()); } - /** - * Resolve the session's organization (null means personal) and deliver the - * fresh glanceable snapshot to iOS activity tokens and Android Expo tokens. - * A session whose row cannot be resolved skips delivery (there is nothing to - * scope the snapshot to). All failures are best-effort and logged without - * device tokens or private content. - */ - private async deliverGlanceableAfterSessionPush( - userId: string, - cliSessionId: string, - getSession: DispatchCloudAgentSessionPushDeps['getSession'] - ): Promise { - try { - const session = await getSession(userId, cliSessionId); - if (!session) { - return; - } - await deliverGlanceableSnapshot( - { userId, organizationId: session.organizationId }, - this.glanceableDeliveryDeps() - ); - } catch (error) { - console.warn('Glanceable aggregate delivery failed', { - error: error instanceof Error ? error.message : String(error), - }); + /** Refresh each affected scope without notification preferences or viewer-presence gates. */ + async refreshGlanceableSessions(params: RefreshGlanceableSessionsParams): Promise { + const { userId, cliSessionIds } = refreshGlanceableSessionsInputSchema.parse(params); + const db = getWorkerDb(this.env.HYPERDRIVE.connectionString); + // Read ownership too: an absent row is personal, but a foreign row is not authorized. + const rows = await db + .select({ + sessionId: cli_sessions_v2.session_id, + userId: cli_sessions_v2.kilo_user_id, + organizationId: cli_sessions_v2.organization_id, + }) + .from(cli_sessions_v2) + .where(inArray(cli_sessions_v2.session_id, cliSessionIds)); + const byId = new Map(rows.map(row => [row.sessionId, row])); + const scopes = new Set(); + for (const sessionId of cliSessionIds) { + const row = byId.get(sessionId); + if (!row) scopes.add(null); + else if (row.userId === userId) scopes.add(row.organizationId); } - } - - private glanceableDeliveryDeps(): GlanceableDeliveryDeps { - let db: ReturnType | undefined; - const getDbForCall = () => (db ??= getWorkerDb(this.env.HYPERDRIVE.connectionString)); - return { - buildSnapshot: async (userId, organizationId) => { - const baseUrl = this.env.KILO_WEB_API_BASE_URL; - if (!baseUrl) { - console.warn('KILO_WEB_API_BASE_URL missing; skipping glanceable aggregate delivery'); - return null; - } - let internalApiSecret: string | undefined; - try { - internalApiSecret = await this.env.INTERNAL_API_SECRET.get(); - } catch { - internalApiSecret = undefined; - } - if (!internalApiSecret) { - console.warn('INTERNAL_API_SECRET missing; skipping glanceable aggregate delivery'); - return null; - } - - const response = await fetch(`${baseUrl}/api/internal/glanceable-agents-snapshot`, { - method: 'POST', - headers: { - 'content-type': 'application/json', - 'x-internal-secret': internalApiSecret, - }, - body: JSON.stringify({ userId, organizationId }), - }); - if (!response.ok) { - console.warn('Glanceable snapshot route failed', { status: response.status }); - return null; - } - const raw: unknown = await response.json().catch(() => null); - const candidate = { - type: 'active_agents_glanceable', - ...(typeof raw === 'object' && raw !== null ? raw : {}), - }; - const parsed = pushDataSchema.safeParse(candidate); - if (!parsed.success || parsed.data.type !== 'active_agents_glanceable') { - console.warn('Glanceable snapshot route returned an invalid snapshot'); - return null; - } - return parsed.data; - }, - listIosActivityTokens: async (userId, organizationId) => { - const orgPredicate = - organizationId === null - ? isNull(user_activity_tokens.organization_id) - : eq(user_activity_tokens.organization_id, organizationId); - const rows = await getDbForCall() - .select({ token: user_activity_tokens.token, kind: user_activity_tokens.kind }) - .from(user_activity_tokens) - .where( - and( - eq(user_activity_tokens.user_id, userId), - orgPredicate, - inArray(user_activity_tokens.kind, ['ios_activity', 'ios_push_to_start']) - ) - ); - return rows.map(row => ({ - token: row.token, - kind: row.kind as IosActivityToken['kind'], - })); - }, - sendIosLiveActivity: async (tokens, contentState) => { - const credentials = await this.readApnsCredentials(); - if (credentials === null) { - return; - } - const result = await sendLiveActivityApns({ - credentials, - tokens, - contentState, - nowSeconds: Math.floor(Date.now() / 1000), + // Every entrypoint uses the same user DO. The snapshot route still rechecks membership. + const stub = this.env.NOTIFICATION_CHANNEL_DO.get( + this.env.NOTIFICATION_CHANNEL_DO.idFromName(userId) + ); + const results = await Promise.allSettled( + [...scopes].map(organizationId => stub.refreshGlanceableSnapshot({ userId, organizationId })) + ); + for (const result of results) { + if (result.status === 'rejected') { + console.warn('Glanceable aggregate delivery failed', { + error: result.reason instanceof Error ? result.reason.message : String(result.reason), }); - if (result.failed > 0) { - console.warn('Some Live Activity APNs sends failed', { - attempted: result.attempted, - failed: result.failed, - }); - } - }, - listIosExpoTokens: async userId => { - const rows = await getDbForCall() - .select({ token: user_push_tokens.token, locale: user_push_tokens.locale }) - .from(user_push_tokens) - .where(and(eq(user_push_tokens.user_id, userId), eq(user_push_tokens.platform, 'ios'))); - return rows.map(row => ({ token: row.token, locale: row.locale })); - }, - listAndroidExpoTokens: async userId => { - const rows = await getDbForCall() - .select({ token: user_push_tokens.token, locale: user_push_tokens.locale }) - .from(user_push_tokens) - .where( - and( - eq(user_push_tokens.user_id, userId), - eq(user_push_tokens.platform, 'android'), - isNotNull(user_push_tokens.app_version) - ) - ); - return rows.map(row => ({ token: row.token, locale: row.locale })); - }, - hasAndroidOngoingToken: async (userId, organizationId) => { - const orgPredicate = - organizationId === null - ? isNull(user_activity_tokens.organization_id) - : eq(user_activity_tokens.organization_id, organizationId); - const [row] = await getDbForCall() - .select({ id: user_activity_tokens.id }) - .from(user_activity_tokens) - .where( - and( - eq(user_activity_tokens.user_id, userId), - orgPredicate, - eq(user_activity_tokens.kind, 'android_ongoing') - ) - ) - .limit(1); - return row !== undefined; - }, - sendExpoPush: async messages => { - const accessToken = await this.env.EXPO_ACCESS_TOKEN.get(); - await sendPushNotifications(messages, accessToken); - }, - }; - } - - private async readApnsCredentials(): Promise { - const { APNS_TEAM_ID: teamId, APNS_KEY_ID: keyId, APNS_TOPIC: topic } = this.env; - const privateKeyBinding = this.env.APNS_PRIVATE_KEY; - if (!teamId || !keyId || !topic || !privateKeyBinding) { - console.warn('APNs Live Activity credentials missing; skipping Live Activity delivery'); - return null; - } - let privateKeyPem: string; - try { - privateKeyPem = await privateKeyBinding.get(); - } catch { - console.warn('APNs Live Activity private key read failed; skipping Live Activity delivery'); - return null; - } - if (!privateKeyPem) { - console.warn('APNs Live Activity private key empty; skipping Live Activity delivery'); - return null; + } } - return { teamId, keyId, topic, privateKeyPem }; } /** diff --git a/services/notifications/src/lib/apns-live-activity.test.ts b/services/notifications/src/lib/apns-live-activity.test.ts index 75ce06807e..de733a8dae 100644 --- a/services/notifications/src/lib/apns-live-activity.test.ts +++ b/services/notifications/src/lib/apns-live-activity.test.ts @@ -152,6 +152,62 @@ describe('sendLiveActivityApns', () => { expect(body.aps['content-state']).toEqual({ revision: 1, running: 1 }); }); + it('keeps the snapshot timestamp when signing occurs after a delayed read', async () => { + const privateKeyPem = await generateTestPrivateKeyPem(); + const requests: Array<{ timestamp: number; issuedAt: number }> = []; + await sendLiveActivityApns({ + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem, topic: TOPIC }, + tokens: [{ token: 'token-delayed', event: 'update' }], + contentState: { running: 1 }, + nowSeconds: 1_750_000_100, + timestampSeconds: 1_750_000_000, + fetchFn: async (_url, init) => { + if (typeof init?.body !== 'string') throw new Error('Expected a JSON body'); + const body = JSON.parse(init.body) as { aps: { timestamp: number } }; + const authorization = new Headers(init.headers).get('authorization'); + if (!authorization) throw new Error('Missing provider token'); + const claimsPart = authorization.split('.')[1]; + const claims = JSON.parse(atob(claimsPart.replace(/-/g, '+').replace(/_/g, '/'))) as { + iat: number; + }; + requests.push({ timestamp: body.aps.timestamp, issuedAt: claims.iat }); + return new Response(null, { status: 200 }); + }, + }); + expect(requests).toEqual([{ timestamp: 1_750_000_000, issuedAt: 1_750_000_100 }]); + }); + + it('checks each token after signing and excludes superseded sends from the result', async () => { + const privateKeyPem = await generateTestPrivateKeyPem(); + const secondCheck = Promise.withResolvers(); + let first = true; + const delivered: string[] = []; + + const result = await sendLiveActivityApns({ + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem, topic: TOPIC }, + tokens: [ + { token: 'token-current', event: 'start' }, + { token: 'token-superseded', event: 'start' }, + ], + contentState: { running: 1 }, + nowSeconds: 1_750_000_000, + isCurrent: async () => { + if (!first) return secondCheck.promise; + first = false; + return true; + }, + fetchFn: async url => { + if (typeof url !== 'string') throw new Error('Expected a string URL'); + delivered.push(url); + secondCheck.resolve(false); + return new Response(null, { status: 200 }); + }, + }); + + expect(delivered).toEqual(['https://api.push.apple.com/3/device/token-current']); + expect(result).toEqual({ attempted: 1, ok: 1, failed: 0 }); + }); + it('counts rejected pushes as failures', async () => { const privateKeyPem = await generateTestPrivateKeyPem(); const fetchFn = vi diff --git a/services/notifications/src/lib/apns-live-activity.ts b/services/notifications/src/lib/apns-live-activity.ts index 0e0f1c8647..dd849f847b 100644 --- a/services/notifications/src/lib/apns-live-activity.ts +++ b/services/notifications/src/lib/apns-live-activity.ts @@ -120,6 +120,10 @@ export async function sendLiveActivityApns(params: { tokens: readonly { token: string; event: LiveActivityEvent }[]; contentState: Record; nowSeconds: number; + /** Snapshot ordering time, independent of the provider token's signing time. */ + timestampSeconds?: number; + /** Recheck the durable generation after signing, before each request. */ + isCurrent?: () => Promise; fetchFn?: typeof fetch; }): Promise { if (params.tokens.length === 0) { @@ -137,8 +141,9 @@ export async function sendLiveActivityApns(params: { contentState: params.contentState, credentials: params.credentials, authorizationJwt, - timestampSeconds: params.nowSeconds, + timestampSeconds: params.timestampSeconds ?? params.nowSeconds, }); + if (params.isCurrent && !(await params.isCurrent())) return false; const response = await fetchFn(request.url, { method: 'POST', headers: request.headers, @@ -147,13 +152,11 @@ export async function sendLiveActivityApns(params: { if (!response.ok) { throw new Error(`APNs rejected the push with status ${response.status}`); } + return true; }) ); - const ok = results.filter(result => result.status === 'fulfilled').length; - return { - attempted: params.tokens.length, - ok, - failed: params.tokens.length - ok, - }; + const ok = results.filter(result => result.status === 'fulfilled' && result.value).length; + const failed = results.filter(result => result.status === 'rejected').length; + return { attempted: ok + failed, ok, failed }; } diff --git a/services/notifications/src/lib/expo-push.test.ts b/services/notifications/src/lib/expo-push.test.ts index 87399641f4..c0eabcd01a 100644 --- a/services/notifications/src/lib/expo-push.test.ts +++ b/services/notifications/src/lib/expo-push.test.ts @@ -58,6 +58,60 @@ describe('sendPushNotifications', () => { }); }); + it('stops remaining chunks when the refresh loses ownership', async () => { + const nextMessage: ExpoPushMessage = { ...message, to: 'ExponentPushToken[token-2]' }; + chunkPushNotifications.mockReturnValue([[message], [nextMessage]]); + let current = true; + const delivered: ExpoPushMessage[] = []; + sendPushNotificationsAsync.mockImplementation(async chunk => { + delivered.push(...chunk); + current = false; + return [{ status: 'ok', id: 'ticket-1' }]; + }); + + const result = await sendPushNotifications( + [message, nextMessage], + 'access-token', + async () => current + ); + + expect(delivered).toEqual([message]); + expect(result).toEqual({ + ticketTokenPairs: [{ ticketId: 'ticket-1', token: 'ExponentPushToken[token-1]' }], + staleTokens: [], + ticketErrors: [], + }); + }); + + it.each(['transport', 'ticket'] as const)( + 'stops a superseded retry after a %s failure', + async failure => { + let current = true; + const delivered: ExpoPushMessage[] = []; + sendPushNotificationsAsync + .mockImplementationOnce(async () => { + current = false; + if (failure === 'transport') throw new Error('network timeout'); + return [ + { + status: 'error', + message: 'Rate exceeded', + details: { error: 'MessageRateExceeded' }, + }, + ]; + }) + .mockImplementation(async chunk => { + delivered.push(...chunk); + return [{ status: 'ok', id: 'stale-ticket' }]; + }); + + const result = await sendPushNotifications([message], 'access-token', async () => current); + + expect(delivered).toEqual([]); + expect(result).toEqual({ ticketTokenPairs: [], staleTokens: [], ticketErrors: [] }); + } + ); + it('does not retry permanent stale-token ticket failures', async () => { sendPushNotificationsAsync.mockResolvedValueOnce([ { diff --git a/services/notifications/src/lib/expo-push.ts b/services/notifications/src/lib/expo-push.ts index 81802adf28..ad8c4ce5b9 100644 --- a/services/notifications/src/lib/expo-push.ts +++ b/services/notifications/src/lib/expo-push.ts @@ -49,9 +49,12 @@ function sleep(ms: number): Promise { async function sendChunkWithTransientRetry( expo: ExpoClient, - chunk: ExpoPushChunk + chunk: ExpoPushChunk, + isCurrent?: () => Promise ): Promise { for (let attempt = 0; ; attempt++) { + // A newer refresh can supersede this chunk during a retry delay. + if (isCurrent && !(await isCurrent())) return []; try { return await expo.sendPushNotificationsAsync(chunk); } catch (err) { @@ -71,7 +74,8 @@ function isRetryableTicketError(errorCode: string | undefined): boolean { export async function sendPushNotifications( messages: ExpoPushMessage[], - accessToken: string + accessToken: string, + isCurrent?: () => Promise ): Promise { if (messages.length === 0) return { ticketTokenPairs: [], staleTokens: [], ticketErrors: [] }; @@ -86,7 +90,7 @@ export async function sendPushNotifications( let pendingChunk = chunk; for (let attempt = 0; ; attempt++) { - const tickets = await sendChunkWithTransientRetry(expo, pendingChunk); + const tickets = await sendChunkWithTransientRetry(expo, pendingChunk, isCurrent); const retryChunk: ExpoPushMessage[] = []; for (let i = 0; i < tickets.length; i++) { diff --git a/services/notifications/src/lib/glanceable-delivery-deps.ts b/services/notifications/src/lib/glanceable-delivery-deps.ts new file mode 100644 index 0000000000..4bc9a4710d --- /dev/null +++ b/services/notifications/src/lib/glanceable-delivery-deps.ts @@ -0,0 +1,162 @@ +import { getWorkerDb } from '@kilocode/db/client'; +import { user_activity_tokens, user_push_tokens } from '@kilocode/db/schema'; +import { pushDataSchema } from '@kilocode/notifications'; +import { and, eq, inArray, isNotNull, isNull } from 'drizzle-orm'; + +import { sendLiveActivityApns, type ApnsCredentials } from './apns-live-activity'; +import { sendPushNotifications } from './expo-push'; +import type { GlanceableDeliveryDeps, IosActivityToken } from './glanceable-delivery'; + +/** Per-refresh I/O dependencies, shared by every entrypoint through the user DO. */ +export function glanceableDeliveryDeps(env: Env): GlanceableDeliveryDeps { + let db: ReturnType | undefined; + const getDbForCall = () => (db ??= getWorkerDb(env.HYPERDRIVE.connectionString)); + + return { + buildSnapshot: async (userId, organizationId) => { + const baseUrl = env.KILO_WEB_API_BASE_URL; + if (!baseUrl) { + console.warn('KILO_WEB_API_BASE_URL missing; skipping glanceable aggregate delivery'); + return null; + } + let internalApiSecret: string | undefined; + try { + internalApiSecret = await env.INTERNAL_API_SECRET.get(); + } catch { + internalApiSecret = undefined; + } + if (!internalApiSecret) { + console.warn('INTERNAL_API_SECRET missing; skipping glanceable aggregate delivery'); + return null; + } + + const response = await fetch(`${baseUrl}/api/internal/glanceable-agents-snapshot`, { + method: 'POST', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + 'x-internal-secret': internalApiSecret, + }, + body: JSON.stringify({ userId, organizationId }), + }); + if (!response.ok) { + console.warn('Glanceable snapshot route failed', { status: response.status }); + return null; + } + if (response.headers.get('content-type')?.split(';')[0].trim() !== 'application/json') { + console.warn('Glanceable snapshot route returned a non-JSON response'); + return null; + } + const raw: unknown = await response.json().catch(() => null); + const candidate = { + type: 'active_agents_glanceable', + ...(typeof raw === 'object' && raw !== null ? raw : {}), + }; + const parsed = pushDataSchema.safeParse(candidate); + if (!parsed.success || parsed.data.type !== 'active_agents_glanceable') { + console.warn('Glanceable snapshot route returned an invalid snapshot'); + return null; + } + return parsed.data; + }, + listIosActivityTokens: async (userId, organizationId) => { + const orgPredicate = + organizationId === null + ? isNull(user_activity_tokens.organization_id) + : eq(user_activity_tokens.organization_id, organizationId); + const rows = await getDbForCall() + .select({ token: user_activity_tokens.token, kind: user_activity_tokens.kind }) + .from(user_activity_tokens) + .where( + and( + eq(user_activity_tokens.user_id, userId), + orgPredicate, + inArray(user_activity_tokens.kind, ['ios_activity', 'ios_push_to_start']) + ) + ); + return rows.map(row => ({ token: row.token, kind: row.kind as IosActivityToken['kind'] })); + }, + sendIosLiveActivity: async (tokens, contentState, timestampSeconds, isCurrent) => { + const credentials = await readApnsCredentials(env); + if (credentials === null || (isCurrent && !(await isCurrent()))) return; + const result = await sendLiveActivityApns({ + credentials, + tokens, + contentState, + nowSeconds: Math.floor(Date.now() / 1000), + timestampSeconds, + isCurrent, + }); + if (result.failed > 0) { + console.warn('Some Live Activity APNs sends failed', { + attempted: result.attempted, + failed: result.failed, + }); + } + }, + listIosExpoTokens: async userId => { + const rows = await getDbForCall() + .select({ token: user_push_tokens.token, locale: user_push_tokens.locale }) + .from(user_push_tokens) + .where(and(eq(user_push_tokens.user_id, userId), eq(user_push_tokens.platform, 'ios'))); + return rows.map(row => ({ token: row.token, locale: row.locale })); + }, + listAndroidExpoTokens: async userId => { + const rows = await getDbForCall() + .select({ token: user_push_tokens.token, locale: user_push_tokens.locale }) + .from(user_push_tokens) + .where( + and( + eq(user_push_tokens.user_id, userId), + eq(user_push_tokens.platform, 'android'), + isNotNull(user_push_tokens.app_version) + ) + ); + return rows.map(row => ({ token: row.token, locale: row.locale })); + }, + hasAndroidOngoingToken: async (userId, organizationId) => { + const orgPredicate = + organizationId === null + ? isNull(user_activity_tokens.organization_id) + : eq(user_activity_tokens.organization_id, organizationId); + const [row] = await getDbForCall() + .select({ id: user_activity_tokens.id }) + .from(user_activity_tokens) + .where( + and( + eq(user_activity_tokens.user_id, userId), + orgPredicate, + eq(user_activity_tokens.kind, 'android_ongoing') + ) + ) + .limit(1); + return row !== undefined; + }, + sendExpoPush: async (messages, isCurrent) => { + const accessToken = await env.EXPO_ACCESS_TOKEN.get(); + if (isCurrent && !(await isCurrent())) return; + await sendPushNotifications(messages, accessToken, isCurrent); + }, + }; +} + +async function readApnsCredentials(env: Env): Promise { + const { APNS_TEAM_ID: teamId, APNS_KEY_ID: keyId, APNS_TOPIC: topic } = env; + const privateKeyBinding = env.APNS_PRIVATE_KEY; + if (!teamId || !keyId || !topic || !privateKeyBinding) { + console.warn('APNs Live Activity credentials missing; skipping Live Activity delivery'); + return null; + } + let privateKeyPem: string; + try { + privateKeyPem = await privateKeyBinding.get(); + } catch { + console.warn('APNs Live Activity private key read failed; skipping Live Activity delivery'); + return null; + } + if (!privateKeyPem) { + console.warn('APNs Live Activity private key empty; skipping Live Activity delivery'); + return null; + } + return { teamId, keyId, topic, privateKeyPem }; +} diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts index b05f9ef1a9..b7fa563ffa 100644 --- a/services/notifications/src/lib/glanceable-delivery.test.ts +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -1,6 +1,22 @@ -import { describe, expect, it, vi } from 'vitest'; - -import type { ExpoPushMessage } from './expo-push'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + createExecutionContext, + env, + runInDurableObject, + waitOnExecutionContext, +} from 'cloudflare:test'; +import { getWorkerDb } from '@kilocode/db/client'; +import type { DispatchPushInput } from '@kilocode/notifications'; +import { drizzle } from 'drizzle-orm/pg-proxy'; +import { NotificationChannelDO, NotificationsService } from '../index'; +import { sendPushNotifications, type ExpoPushMessage } from './expo-push'; +import type * as ExpoPushModule from './expo-push'; + +vi.mock('@kilocode/db/client', () => ({ getWorkerDb: vi.fn() })); +vi.mock('./expo-push', async importOriginal => ({ + ...(await importOriginal()), + sendPushNotifications: vi.fn(), +})); import { apnsSendsForTokens, buildGlanceableExpoMessages, @@ -51,6 +67,717 @@ function fakeDeps(overrides: Partial = {}): { return { deps, calls }; } +describe('NotificationsService.refreshGlanceableSessions', () => { + beforeEach(() => { + vi.mocked(getWorkerDb).mockReset(); + vi.mocked(sendPushNotifications).mockReset(); + vi.spyOn(Date, 'now').mockReturnValue(Date.parse(snapshot.updatedAt)); + }); + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + type Scope = { userId: string; organizationId: string | null }; + type ApnsPayload = { + aps: { event: string; timestamp: number; 'content-state': GlanceableApnsContentState }; + }; + + function setupService( + options: { + deniedOrganizationId?: string; + failedOrganizationId?: string; + response?: (scope: Scope) => Response | Promise; + beforeIosTokens?: () => Promise; + iosTokenKind?: IosActivityToken['kind']; + privateKey?: () => Promise; + beforeApnsResponse?: () => Promise; + expoAccessToken?: () => Promise; + } = {} + ) { + const messages: ExpoPushMessage[] = []; + const apns: ApnsPayload[] = []; + const queries: Array<{ sql: string; params: unknown[] }> = []; + const requestedScopes: Scope[] = []; + const sessions = [ + { id: 'personal', userId: 'usr_1', organizationId: null }, + { id: 'org-a', userId: 'usr_1', organizationId: 'org-1' }, + { id: 'org-b', userId: 'usr_1', organizationId: 'org-1' }, + { id: 'org-c', userId: 'usr_1', organizationId: 'org-2' }, + { id: 'other-personal', userId: 'usr_2', organizationId: null }, + { id: 'foreign', userId: 'usr_2', organizationId: 'org-2' }, + ]; + const db = drizzle(async (sql, params) => { + queries.push({ sql, params }); + if (sql.includes('from "cli_sessions_v2"')) { + return { + rows: sessions + .filter(session => params.includes(session.id)) + .map(session => [session.id, session.userId, session.organizationId]), + }; + } + if (sql.includes('from "user_push_tokens"')) { + return { + rows: [ + [ + params.includes('android') ? 'ExponentPushToken[android]' : 'ExponentPushToken[ios]', + null, + ], + ], + }; + } + if (sql.includes('from "user_activity_tokens"')) { + if (params.includes('android_ongoing')) return { rows: [['subscription']] }; + await options.beforeIosTokens?.(); + return { + rows: options.privateKey + ? [['activity-token', options.iosTokenKind ?? 'ios_activity']] + : [], + }; + } + if (sql.includes('from "user_notification_preferences"')) { + return { rows: [[false, false, false, false, false, false, false]] }; + } + throw new Error(`Unexpected query: ${sql}`); + }); + vi.mocked(getWorkerDb).mockReturnValue(db as never); + vi.mocked(sendPushNotifications).mockImplementation(async incoming => { + messages.push(...incoming); + return { ticketTokenPairs: [], staleTokens: [], ticketErrors: [] }; + }); + vi.stubGlobal('fetch', async (url: string, init: RequestInit) => { + if (typeof init.body !== 'string') throw new Error('Expected a JSON request body'); + if (url === 'https://api.push.apple.com/3/device/activity-token') { + await options.beforeApnsResponse?.(); + apns.push(JSON.parse(init.body) as ApnsPayload); + return new Response(null, { status: 200 }); + } + expect(url).toBe('https://snapshot.test/api/internal/glanceable-agents-snapshot'); + expect(new Headers(init.headers).get('accept')).toBe('application/json'); + const scope = JSON.parse(init.body) as Scope; + requestedScopes.push(scope); + if (scope.organizationId === options.deniedOrganizationId) + return new Response(null, { status: 403 }); + if (scope.organizationId === options.failedOrganizationId) + throw new Error('snapshot unavailable'); + return ( + options.response?.(scope) ?? + Response.json({ + ...snapshot, + scopeKey: scope.organizationId ?? 'personal', + organizationBound: scope.organizationId !== null, + running: scope.organizationId === null ? 2 : 7, + }) + ); + }); + const objectPrefix = crypto.randomUUID(); + const serviceEnv = { + HYPERDRIVE: { connectionString: 'postgres://unused' }, + KILO_WEB_API_BASE_URL: 'https://snapshot.test', + INTERNAL_API_SECRET: { get: async () => 'test-internal-secret' }, + EXPO_ACCESS_TOKEN: { get: options.expoAccessToken ?? (async () => 'test-expo-token') }, + APNS_TEAM_ID: 'test-team', + APNS_KEY_ID: 'test-key', + APNS_TOPIC: 'test.topic', + APNS_PRIVATE_KEY: { get: options.privateKey }, + NOTIFICATION_CHANNEL_DO: { + idFromName: (userId: string) => + env.NOTIFICATION_CHANNEL_DO.idFromName(`${objectPrefix}:${userId}`), + get: (id: DurableObjectId) => ({ + refreshGlanceableSnapshot: (scope: Scope) => + runInDurableObject(env.NOTIFICATION_CHANNEL_DO.get(id), async (_instance, state) => { + // Reconstruct the real class on real durable storage on every call. + await new NotificationChannelDO(state, serviceEnv as never).refreshGlanceableSnapshot( + scope + ); + }), + }), + }, + }; + const createService = () => + new NotificationsService(createExecutionContext(), serviceEnv as never); + return { service: createService(), createService, messages, apns, queries, requestedScopes }; + } + + function freshSnapshot(overrides: Partial = {}): ActiveAgentsGlanceable { + return { + ...snapshot, + needsInput: 0, + updatedAt: new Date(Date.now()).toISOString(), + expiresAt: new Date(Date.now() + 28_800_000).toISOString(), + eligibleStartedAt: new Date(Date.now()).toISOString(), + ...overrides, + }; + } + + const personalRefresh = { userId: 'usr_1', cliSessionIds: ['personal'] }; + + it('fences a deferred busy read after idle from another entrypoint and reconstructed DO', async () => { + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let current = freshSnapshot(); + let first = true; + const { service, createService, messages } = setupService({ + response: async () => { + const captured = current; + if (first) { + first = false; + started.resolve(); + await release.promise; + } + return Response.json({ ...captured, updatedAt: new Date(Date.now()).toISOString() }); + }, + }); + const busy = service.refreshGlanceableSessions(personalRefresh); + await started.promise; + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + await createService().refreshGlanceableSessions(personalRefresh); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:01:00.000Z')); + release.resolve(); + await busy; + expect(messages.map(message => message.data)).toMatchObject([ + { + status: 'empty', + running: 0, + eligibleStartedAt: null, + updatedAt: '2026-08-27T10:00:01.000Z', + }, + { + status: 'empty', + running: 0, + eligibleStartedAt: null, + updatedAt: '2026-08-27T10:00:01.000Z', + }, + ]); + }); + + it('anchors freshness before a delayed snapshot read instead of extending it at completion', async () => { + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + const { service, messages } = setupService({ + response: async () => { + started.resolve(); + await release.promise; + return Response.json(freshSnapshot()); + }, + }); + const pending = service.refreshGlanceableSessions(personalRefresh); + await started.promise; + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); + release.resolve(); + await pending; + expect(messages.map(message => message.data)).toMatchObject([ + { updatedAt: '2026-08-27T10:00:00.000Z', expiresAt: '2026-08-27T18:00:00.000Z' }, + { updatedAt: '2026-08-27T10:00:00.000Z', expiresAt: '2026-08-27T18:00:00.000Z' }, + ]); + }); + + it('fences a busy delivery delayed during token lookup after a newer idle delivery', async () => { + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + let current = freshSnapshot(); + const { service, createService, messages } = setupService({ + response: () => Response.json(current), + beforeIosTokens: async () => { + if (!first) return; + first = false; + started.resolve(); + await release.promise; + }, + }); + const busy = service.refreshGlanceableSessions(personalRefresh); + await started.promise; + current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + await createService().refreshGlanceableSessions(personalRefresh); + release.resolve(); + await busy; + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'empty', running: 0, eligibleStartedAt: null }, + { status: 'empty', running: 0, eligibleStartedAt: null }, + ]); + }); + + it('retains the eligible start through retry and reconstructed worker and DO instances', async () => { + let current = freshSnapshot(); + const { service, createService, messages } = setupService({ + response: () => Response.json(current), + }); + await service.refreshGlanceableSessions(personalRefresh); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); + current = freshSnapshot({ running: 0, reconnecting: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:20:00.000Z')); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[ios]') + .map(message => message.data) + ).toMatchObject([ + { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 1 }, + { reconnecting: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 2 }, + { needsInput: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 3 }, + ]); + }); + + it('clears on authoritative empty and prevents an older empty read resetting the new interval', async () => { + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let current = freshSnapshot(); + let deferNext = false; + const { createService, messages } = setupService({ + response: async () => { + const captured = current; + if (deferNext) { + deferNext = false; + started.resolve(); + await release.promise; + } + return Response.json(captured); + }, + }); + await createService().refreshGlanceableSessions(personalRefresh); + current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + deferNext = true; + const oldIdle = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + await createService().refreshGlanceableSessions(personalRefresh); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); + current = freshSnapshot(); + await createService().refreshGlanceableSessions(personalRefresh); + release.resolve(); + await oldIdle; + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:20:00.000Z')); + current = freshSnapshot({ running: 0, reconnecting: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[ios]') + .map(message => message.data) + ).toMatchObject([ + { status: 'happy', eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { status: 'empty', eligibleStartedAt: null }, + { status: 'happy', eligibleStartedAt: '2026-08-27T10:10:00.000Z' }, + { reconnecting: 1, eligibleStartedAt: '2026-08-27T10:10:00.000Z' }, + ]); + }); + + it('keeps user and organization intervals separate while another scope has a deferred read', async () => { + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + const { createService, messages } = setupService({ + response: async scope => { + const captured = freshSnapshot({ + scopeKey: `${scope.userId}:${scope.organizationId ?? 'personal'}`, + organizationBound: scope.organizationId !== null, + }); + if (first) { + first = false; + started.resolve(); + await release.promise; + } + return Response.json(captured); + }, + }); + const personal = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + for (const [userId, cliSessionId, time] of [ + ['usr_1', 'org-a', '2026-08-27T10:01:00.000Z'], + ['usr_1', 'org-c', '2026-08-27T10:02:00.000Z'], + ['usr_2', 'other-personal', '2026-08-27T10:03:00.000Z'], + ]) { + vi.mocked(Date.now).mockReturnValue(Date.parse(time)); + await createService().refreshGlanceableSessions({ userId, cliSessionIds: [cliSessionId] }); + } + release.resolve(); + await personal; + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:04:00.000Z')); + await createService().refreshGlanceableSessions(personalRefresh); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[ios]') + .map(message => message.data) + ).toMatchObject([ + { scopeKey: 'usr_1:org-1', eligibleStartedAt: '2026-08-27T10:01:00.000Z' }, + { scopeKey: 'usr_1:org-2', eligibleStartedAt: '2026-08-27T10:02:00.000Z' }, + { scopeKey: 'usr_2:personal', eligibleStartedAt: '2026-08-27T10:03:00.000Z' }, + { scopeKey: 'usr_1:personal', eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { scopeKey: 'usr_1:personal', eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + ]); + }); + + it('preserves the interval after snapshot and delivery failures instead of clearing or replacing it', async () => { + let current = freshSnapshot(); + let unavailable = false; + const { createService, messages } = setupService({ + response: () => (unavailable ? new Response(null, { status: 503 }) : Response.json(current)), + }); + await createService().refreshGlanceableSessions(personalRefresh); + unavailable = true; + await createService().refreshGlanceableSessions(personalRefresh); + unavailable = false; + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); + current = freshSnapshot({ running: 0, reconnecting: 1 }); + vi.mocked(sendPushNotifications).mockRejectedValueOnce(new Error('Expo unavailable')); + await createService().refreshGlanceableSessions(personalRefresh); + await createService().refreshGlanceableSessions(personalRefresh); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[ios]') + .map(message => message.data) + ).toMatchObject([ + { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { reconnecting: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + ]); + }); + + it('does not clear an interval from a non-authoritative zero-count response', async () => { + let current = freshSnapshot(); + const { createService, messages } = setupService({ response: () => Response.json(current) }); + await createService().refreshGlanceableSessions(personalRefresh); + current = freshSnapshot({ status: 'stale', running: 0, eligibleStartedAt: null }); + await createService().refreshGlanceableSessions(personalRefresh); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); + current = freshSnapshot({ running: 0, reconnecting: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[ios]') + .map(message => message.data) + ).toMatchObject([ + { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { reconnecting: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + ]); + }); + + async function generateTestPrivateKeyPem(): Promise { + const pair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [ + 'sign', + 'verify', + ])) as CryptoKeyPair; + const der = new Uint8Array(await crypto.subtle.exportKey('pkcs8', pair.privateKey)); + return `-----BEGIN PRIVATE KEY-----\n${btoa(String.fromCharCode(...der))}\n-----END PRIVATE KEY-----`; + } + + it('keeps an in-flight update timestamp below idle when the older request finishes last', async () => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + let current = freshSnapshot(); + const { createService, messages, apns } = setupService({ + response: () => Response.json(current), + privateKey: async () => pem, + beforeApnsResponse: async () => { + if (first) { + first = false; + started.resolve(); + await release.promise; + } + }, + }); + const busy = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + await createService().refreshGlanceableSessions(personalRefresh); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); + release.resolve(); + await busy; + expect(apns.map(request => request.aps.event)).toEqual(['update', 'update']); + expect(apns.map(request => JSON.parse(request.aps['content-state'].props))).toMatchObject([ + { status: 'empty', running: 0, eligibleStartedAt: null }, + { status: 'happy', running: 2 }, + ]); + expect(apns[1].aps.timestamp).toBeLessThan(apns[0].aps.timestamp); + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'empty', running: 0 }, + { status: 'empty', running: 0 }, + ]); + }); + + it.each([ + ['ios_push_to_start', 'credentials', 'start'], + ['ios_push_to_start', 'signing', 'start'], + ['ios_activity', 'credentials', 'update'], + ['ios_activity', 'signing', 'update'], + ] as const)('fences superseded %s delivery after delayed %s', async (kind, delayed, event) => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + if (delayed === 'signing') { + const sign = crypto.subtle.sign.bind(crypto.subtle); + vi.spyOn(crypto.subtle, 'sign').mockImplementationOnce(async (...args) => { + started.resolve(); + await release.promise; + return sign(...args); + }); + } + let first = true; + let current = freshSnapshot(); + const { createService, messages, apns } = setupService({ + response: () => Response.json(current), + iosTokenKind: kind, + privateKey: async () => { + if (delayed === 'credentials' && first) { + first = false; + started.resolve(); + await release.promise; + } + return pem; + }, + }); + const busy = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + await createService().refreshGlanceableSessions(personalRefresh); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); + release.resolve(); + await busy; + + expect( + apns.map(request => ({ + event: request.aps.event, + props: JSON.parse(request.aps['content-state'].props), + })) + ).toEqual([ + { + event, + props: { + status: 'empty', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }, + }, + ]); + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'empty', running: 0, eligibleStartedAt: null }, + { status: 'empty', running: 0, eligibleStartedAt: null }, + ]); + }); + + it.each([ + ['ios', 1], + ['android', 2], + ] as const)( + 'fences superseded %s delivery after delayed Expo credentials', + async (platform, delayedRead) => { + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let reads = 0; + let current = freshSnapshot(); + const { createService, messages } = setupService({ + response: () => Response.json(current), + expoAccessToken: async () => { + reads += 1; + if (reads === delayedRead) { + started.resolve(); + await release.promise; + } + return 'test-expo-token'; + }, + }); + const busy = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + await createService().refreshGlanceableSessions(personalRefresh); + release.resolve(); + await busy; + + expect( + messages + .filter(message => message.to === `ExponentPushToken[${platform}]`) + .map(message => message.data) + ).toMatchObject([{ status: 'empty', running: 0, eligibleStartedAt: null }]); + } + ); + + it('delivers distinct personal and organization scopes without attention preferences or presence', async () => { + const { service, messages, queries, requestedScopes } = setupService(); + await service.refreshGlanceableSessions({ + userId: 'usr_1', + cliSessionIds: ['personal', 'org-a', 'org-b', 'foreign'], + }); + expect(requestedScopes).toEqual([ + { userId: 'usr_1', organizationId: null }, + { userId: 'usr_1', organizationId: 'org-1' }, + ]); + expect(queries[0].sql).toContain('select "session_id", "kilo_user_id", "organization_id"'); + expect(queries[0].sql).toContain('"cli_sessions_v2"."session_id" in'); + expect(messages).toHaveLength(4); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[android]') + .map(message => message.data) + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ scopeKey: 'personal', organizationBound: false, running: 2 }), + expect.objectContaining({ scopeKey: 'org-1', organizationBound: true, running: 7 }), + ]) + ); + expect( + messages.every( + message => message._contentAvailable && message.sound === null && !message.body + ) + ).toBe(true); + }); + + it('delivers the personal aggregate for a rowless session without adopting a foreign scope', async () => { + const { service, messages, requestedScopes } = setupService(); + await service.refreshGlanceableSessions({ + userId: 'usr_1', + cliSessionIds: ['missing', 'foreign'], + }); + expect(requestedScopes).toEqual([{ userId: 'usr_1', organizationId: null }]); + expect(messages.map(message => message.data)).toMatchObject([ + { scopeKey: 'personal', organizationBound: false, running: 2 }, + { scopeKey: 'personal', organizationBound: false, running: 2 }, + ]); + }); + + it('does not treat a foreign-owned row as rowless personal work', async () => { + const { service, messages, requestedScopes } = setupService(); + await service.refreshGlanceableSessions({ userId: 'usr_1', cliSessionIds: ['foreign'] }); + expect(requestedScopes).toEqual([]); + expect(messages).toEqual([]); + }); + + it('delivers no private counts when the snapshot route rejects revoked organization access', async () => { + const { service, messages } = setupService({ deniedOrganizationId: 'org-1' }); + await service.refreshGlanceableSessions({ + userId: 'usr_1', + cliSessionIds: ['personal', 'org-a'], + }); + expect(messages).toHaveLength(2); + expect(messages.map(message => message.data)).toEqual([ + expect.objectContaining({ scopeKey: 'personal', organizationBound: false }), + expect.objectContaining({ scopeKey: 'personal', organizationBound: false }), + ]); + }); + + it('does not let a failed scope suppress a successful scope', async () => { + const { service, messages } = setupService({ failedOrganizationId: 'org-1' }); + await service.refreshGlanceableSessions({ + userId: 'usr_1', + cliSessionIds: ['org-a', 'personal'], + }); + expect(messages).toHaveLength(2); + expect(messages.every(message => message.data?.scopeKey === 'personal')).toBe(true); + }); + + it('keeps a transient snapshot failure best-effort and permits the next refresh', async () => { + let failed = true; + const { service, messages } = setupService({ + response: () => (failed ? new Response(null, { status: 503 }) : Response.json(snapshot)), + }); + await service.refreshGlanceableSessions({ userId: 'usr_1', cliSessionIds: ['personal'] }); + expect(messages).toEqual([]); + failed = false; + await service.refreshGlanceableSessions({ userId: 'usr_1', cliSessionIds: ['personal'] }); + const expected = { + ...snapshot, + revision: 2, + updatedAt: '2026-08-27T10:00:00.001Z', + expiresAt: '2026-08-27T18:00:00.001Z', + }; + expect(messages.map(message => message.data)).toEqual([expected, expected]); + }); + + it.each([ + () => new Response(JSON.stringify(snapshot), { headers: { 'content-type': 'text/html' } }), + () => Response.json({ ...snapshot, running: -1 }), + () => Response.json({ ...snapshot, updatedAt: 'invalid-date' }), + () => Response.json({ ...snapshot, expiresAt: 'invalid-date' }), + () => Response.json({ ...snapshot, eligibleStartedAt: 'invalid-date' }), + ])('rejects an unusable snapshot without poisoning the next refresh', async response => { + let currentResponse = response; + const { service, createService, messages } = setupService({ + response: () => currentResponse(), + }); + await service.refreshGlanceableSessions(personalRefresh); + expect(messages).toEqual([]); + currentResponse = () => Response.json(snapshot); + await createService().refreshGlanceableSessions(personalRefresh); + expect(messages.map(message => message.data)).toMatchObject([ + { running: 2, eligibleStartedAt: '2026-08-27T09:00:00.000Z' }, + { running: 2, eligibleStartedAt: '2026-08-27T09:00:00.000Z' }, + ]); + }); + + it.each([true, false])( + 'preserves ordinary attention dispatch without an early aggregate (preference: %s)', + async enabled => { + const { messages, requestedScopes } = setupService(); + const attention: DispatchPushInput[] = []; + vi.mocked(getWorkerDb).mockReturnValue( + drizzle(async sql => ({ + rows: sql.includes('from "user_notification_preferences"') + ? [[enabled, enabled, enabled, enabled, enabled, enabled, enabled]] + : [['Attention session', null]], + })) as never + ); + const ctx = createExecutionContext(); + const service = new NotificationsService(ctx, { + HYPERDRIVE: { connectionString: 'postgres://unused' }, + KILO_WEB_API_BASE_URL: 'https://snapshot.test', + INTERNAL_API_SECRET: { get: async () => 'test-internal-secret' }, + EXPO_ACCESS_TOKEN: { get: async () => 'test-expo-token' }, + NOTIFICATION_CHANNEL_DO: { + idFromName: (userId: string) => userId, + get: () => ({ + dispatchPush: async (input: DispatchPushInput) => { + attention.push(input); + return { kind: 'delivered', tokenCount: 1 }; + }, + }), + }, + } as never); + const result = await service.sendCloudAgentSessionNotification({ + userId: 'usr_1', + cliSessionId: 'personal', + executionId: 'exec-1', + status: 'completed', + category: 'attention', + body: 'Needs input', + suppressIfViewingSession: true, + }); + await waitOnExecutionContext(ctx); + expect(result).toEqual( + enabled ? { dispatched: true } : { dispatched: false, reason: 'suppressed_preference' } + ); + expect(attention).toMatchObject( + enabled + ? [ + { + presenceContext: '/presence/cli-session/personal', + push: { + title: 'Attention session', + body: 'Needs input', + data: { type: 'cloud_agent_session', category: 'attention' }, + }, + }, + ] + : [] + ); + expect(requestedScopes).toEqual([]); + expect(messages).toEqual([]); + } + ); + + it('rejects invalid RPC identity before any database or delivery work', async () => { + const { service, messages, queries } = setupService(); + await expect( + service.refreshGlanceableSessions({ userId: '', cliSessionIds: ['personal'] }) + ).rejects.toThrow(); + expect(queries).toEqual([]); + expect(messages).toEqual([]); + }); +}); + describe('apnsSendsForTokens', () => { it('sends update only to the activity tokens when one exists, never start to push-to-start', () => { expect( diff --git a/services/notifications/src/lib/glanceable-delivery.ts b/services/notifications/src/lib/glanceable-delivery.ts index 62f922dd6f..405817730a 100644 --- a/services/notifications/src/lib/glanceable-delivery.ts +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -1,8 +1,8 @@ /** * Aggregate glanceable snapshot delivery for the Active Agents Live Activity, - * widgets, and Android ongoing notification. Runs after a cloud-agent session - * notification send: it fetches the fresh snapshot from the web internal route, - * then pushes it to the registered iOS activity tokens over APNs and to the + * widgets, and Android ongoing notification. Committed metadata and live-session + * transitions trigger a fresh snapshot fetch from the web internal route, + * which is then pushed to the registered iOS activity tokens over APNs and to the * user's Expo tokens on iOS and Android. Pure orchestrator — all IO is injected * via `deps` so tests substitute in-memory fakes. */ @@ -106,15 +106,21 @@ export type GlanceableDeliveryDeps = { ) => Promise; sendIosLiveActivity: ( tokens: readonly { token: string; event: LiveActivityEvent }[], - contentState: GlanceableApnsContentState + contentState: GlanceableApnsContentState, + timestampSeconds: number, + isCurrent?: () => Promise ) => Promise; + /** Reserved before reading; do not assign a new timestamp after a delayed send. */ + apnsTimestampSeconds?: number; + /** Durable generation fence, also checked by adapters after awaits and before outbound sends. */ + isCurrent?: () => Promise; listIosExpoTokens: (userId: string, organizationId: string | null) => Promise; listAndroidExpoTokens: ( userId: string, organizationId: string | null ) => Promise; hasAndroidOngoingToken: (userId: string, organizationId: string | null) => Promise; - sendExpoPush: (messages: ExpoPushMessage[]) => Promise; + sendExpoPush: (messages: ExpoPushMessage[], isCurrent?: () => Promise) => Promise; }; export async function deliverGlanceableSnapshot( @@ -128,22 +134,30 @@ export async function deliverGlanceableSnapshot( const contentState = toGlanceableContentState(snapshot); const iosTokens = await deps.listIosActivityTokens(params.userId, params.organizationId); + if (deps.isCurrent && !(await deps.isCurrent())) return; const iosSends = apnsSendsForTokens(iosTokens); if (iosSends.length > 0) { - await deps.sendIosLiveActivity(iosSends, contentState); + await deps.sendIosLiveActivity( + iosSends, + contentState, + deps.apnsTimestampSeconds ?? Math.floor(Date.parse(snapshot.updatedAt) / 1000), + deps.isCurrent + ); } // iOS Expo tokens always need the data-only wake: it drives the widget // timeline through the background task while the app is not foregrounded. const iosExpoTokens = await deps.listIosExpoTokens(params.userId, params.organizationId); + if (deps.isCurrent && !(await deps.isCurrent())) return; if (iosExpoTokens.length > 0) { - await deps.sendExpoPush(buildGlanceableExpoMessages(iosExpoTokens, snapshot)); + await deps.sendExpoPush(buildGlanceableExpoMessages(iosExpoTokens, snapshot), deps.isCurrent); } if (await deps.hasAndroidOngoingToken(params.userId, params.organizationId)) { const expoTokens = await deps.listAndroidExpoTokens(params.userId, params.organizationId); + if (deps.isCurrent && !(await deps.isCurrent())) return; if (expoTokens.length > 0) { - await deps.sendExpoPush(buildGlanceableExpoMessages(expoTokens, snapshot)); + await deps.sendExpoPush(buildGlanceableExpoMessages(expoTokens, snapshot), deps.isCurrent); } } } diff --git a/services/notifications/src/lib/glanceable-refresh.ts b/services/notifications/src/lib/glanceable-refresh.ts new file mode 100644 index 0000000000..ea5ba29841 --- /dev/null +++ b/services/notifications/src/lib/glanceable-refresh.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; + +import { deliverGlanceableSnapshot, type GlanceableDeliveryDeps } from './glanceable-delivery'; + +const scopeSchema = z.object({ + userId: z.string().min(1), + organizationId: z.string().min(1).nullable(), +}); + +const refreshStateSchema = z.object({ + revision: z.number().int().positive(), + updatedAt: z.string().datetime(), + apnsTimestampSeconds: z.number().int().nonnegative(), + eligibleStartedAt: z.string().datetime().nullable(), +}); +const snapshotTimestampsSchema = refreshStateSchema + .pick({ updatedAt: true, eligibleStartedAt: true }) + .extend({ expiresAt: z.string().datetime() }); + +/** The user DO owns these records; no ordering or interval state lives in a Worker instance. */ +export async function refreshGlanceableSnapshot( + params: { userId: string; organizationId: string | null }, + storage: DurableObjectStorage, + deps: GlanceableDeliveryDeps +): Promise { + const scope = scopeSchema.parse(params); + const key = `glanceable:${JSON.stringify([scope.userId, scope.organizationId])}`; + const request = await storage.transaction(async tx => { + const previous = refreshStateSchema.optional().parse(await tx.get(key)); + const now = Date.now(); + const next = { + revision: (previous?.revision ?? 0) + 1, + updatedAt: new Date( + Math.max(now, previous ? Date.parse(previous.updatedAt) + 1 : now) + ).toISOString(), + // APNs orders by whole seconds. Reserve a strict order even for same-second refreshes. + apnsTimestampSeconds: Math.max( + Math.floor(now / 1000), + (previous?.apnsTimestampSeconds ?? 0) + 1 + ), + eligibleStartedAt: previous?.eligibleStartedAt ?? null, + }; + await tx.put(key, next); + return next; + }); + + const snapshot = await deps.buildSnapshot(scope.userId, scope.organizationId); + // Only the authoritative happy/empty result can change an eligible interval. + if (snapshot === null || (snapshot.status !== 'happy' && snapshot.status !== 'empty')) return; + // The shared wire schema accepts strings; validate dates before persisting the interval. + snapshotTimestampsSchema.parse(snapshot); + + const committed = await storage.transaction(async tx => { + const current = refreshStateSchema.parse(await tx.get(key)); + if (current.revision !== request.revision) return null; + const eligibleStartedAt = + snapshot.running + snapshot.needsInput + snapshot.reconnecting > 0 + ? (current.eligibleStartedAt ?? snapshot.eligibleStartedAt ?? request.updatedAt) + : null; + await tx.put(key, { ...current, eligibleStartedAt }); + return { + ...snapshot, + revision: request.revision, + updatedAt: request.updatedAt, + expiresAt: new Date( + Date.parse(request.updatedAt) + + Date.parse(snapshot.expiresAt) - + Date.parse(snapshot.updatedAt) + ).toISOString(), + eligibleStartedAt, + }; + }); + if (committed === null) return; + + await deliverGlanceableSnapshot(scope, { + ...deps, + buildSnapshot: async () => committed, + apnsTimestampSeconds: request.apnsTimestampSeconds, + isCurrent: async () => { + const current = refreshStateSchema.parse(await storage.get(key)); + return current.revision === request.revision; + }, + }); +} diff --git a/services/session-ingest/src/dos/UserConnectionDO.test.ts b/services/session-ingest/src/dos/UserConnectionDO.test.ts index 1e055d80e8..5bfff51fff 100644 --- a/services/session-ingest/src/dos/UserConnectionDO.test.ts +++ b/services/session-ingest/src/dos/UserConnectionDO.test.ts @@ -1,15 +1,34 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; - -// Mock cloudflare:workers before importing UserConnectionDO -vi.mock('cloudflare:workers', () => ({ - DurableObject: class { +import { + buildGlanceableSnapshot, + buildOpaqueScopeKey, +} from '../../../../packages/app-shared/src/glanceable-agents-snapshot'; +import { getWorkerDb } from '@kilocode/db/client'; +import { drizzle } from 'drizzle-orm/pg-proxy'; +import { NotificationChannelDO, NotificationsService } from '../../../notifications/src/index'; +import { + sendPushNotifications, + type ExpoPushMessage, +} from '../../../notifications/src/lib/expo-push'; +import type * as ExpoPushModule from '../../../notifications/src/lib/expo-push'; +import type { Env } from '../env'; + +// Mock only the runtime base classes; the producers, coordinator, and delivery adapter stay real. +vi.mock('cloudflare:workers', () => { + class WorkerBase { ctx: unknown; env: unknown; constructor(ctx: unknown, env: unknown) { this.ctx = ctx; this.env = env; } - }, + } + return { DurableObject: WorkerBase, WorkerEntrypoint: WorkerBase }; +}); +vi.mock('@kilocode/db/client', () => ({ getWorkerDb: vi.fn() })); +vi.mock('../../../notifications/src/lib/expo-push', async importOriginal => ({ + ...(await importOriginal()), + sendPushNotifications: vi.fn(), })); const sessionIngestMocks = vi.hoisted(() => ({ @@ -191,13 +210,69 @@ function getCorrelationId(cliWs: MockWS, callIndex = 0): string { } /** Instantiate a fresh DO with a mock context. Returns the DO and helpers. */ -function setup() { +function setup(env: Partial = {}) { const mockCtx = createMockCtx(); const ctx = mockCtx.build(); - const doInstance = new UserConnectionDO(ctx as never, {} as never); + const doInstance = new UserConnectionDO(ctx as never, env as Env); return { doInstance, ctx, mockCtx }; } +function setupGlanceableDelivery(foreignSessionIds: string[] = []) { + const messages: ExpoPushMessage[] = []; + vi.mocked(getWorkerDb).mockReturnValue( + drizzle(async (sql, params) => { + if (sql.includes('from "cli_sessions_v2"')) { + return { + rows: foreignSessionIds.filter(id => params.includes(id)).map(id => [id, 'usr_2', null]), + }; + } + if (sql.includes('from "user_activity_tokens"')) return { rows: [] }; + if (sql.includes('from "user_push_tokens"')) + return { rows: [['ExponentPushToken[ios]', null]] }; + throw new Error(`Unexpected query: ${sql}`); + }) as never + ); + vi.mocked(sendPushNotifications).mockImplementation(async incoming => { + messages.push(...incoming); + return { ticketTokenPairs: [], staleTokens: [], ticketErrors: [] }; + }); + const storage = makeStorageFake(); + const notificationEnv = { + HYPERDRIVE: { connectionString: 'postgres://unused' }, + KILO_WEB_API_BASE_URL: 'https://snapshot.test', + INTERNAL_API_SECRET: { get: async () => 'test-internal-secret' }, + EXPO_ACCESS_TOKEN: { get: async () => 'test-expo-token' }, + NOTIFICATION_CHANNEL_DO: { + idFromName: (userId: string) => userId, + get: () => channel, + }, + }; + const channel = new NotificationChannelDO( + { + storage: { + ...storage, + transaction: async (fn: (tx: typeof storage) => Promise) => fn(storage), + }, + } as never, + notificationEnv as never + ); + const service = new NotificationsService({} as never, notificationEnv as never); + const env: Partial = { NOTIFICATIONS: service as never }; + const result = setup(env); + vi.stubGlobal('fetch', async (_url: string, init: RequestInit) => { + if (typeof init.body !== 'string') throw new Error('Expected a JSON request body'); + const scope = JSON.parse(init.body) as { userId: string; organizationId: string | null }; + return Response.json( + buildGlanceableSnapshot({ + ...scope, + sessions: result.doInstance.getActiveSessions(), + now: Date.now(), + }) + ); + }); + return { ...result, env, messages }; +} + function connectWebSocket(doInstance: UserConnectionDO, connectionId: string): MockWS { const client = createMockWs(); const server = createMockWs(); @@ -505,6 +580,171 @@ describe('UserConnectionDO', () => { // Heartbeat processing // ------------------------------------------------------------------------- + describe('glanceable aggregate transitions', () => { + it('delivers rowless personal busy, retry, attention-clear, and idle heartbeats through the real coordinator', async () => { + const { doInstance, mockCtx, messages } = setupGlanceableDelivery(); + const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + for (const status of ['busy', 'retry', 'question', 'busy', 'idle']) { + sendHeartbeat(doInstance, cliWs, [makeSession('s1', status)]); + await flushAsync(); + } + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'happy', running: 1, needsInput: 0, reconnecting: 0 }, + { status: 'happy', running: 0, needsInput: 0, reconnecting: 1 }, + { status: 'happy', running: 0, needsInput: 1, reconnecting: 0 }, + { status: 'happy', running: 1, needsInput: 0, reconnecting: 0 }, + { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + ]); + expect(messages.every(message => message._contentAvailable && !message.body)).toBe(true); + expect( + messages.every( + message => + message.data?.scopeKey === + buildOpaqueScopeKey({ userId: 'usr_1', organizationId: null }) + ) + ).toBe(true); + expect(messages.every(message => message.data?.organizationBound === false)).toBe(true); + }); + + it('does not authorize a foreign-owned row from a real authenticated heartbeat', async () => { + const { doInstance, mockCtx, messages } = setupGlanceableDelivery(['foreign']); + const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + sendHeartbeat(doInstance, cliWs, [makeSession('foreign')]); + await flushAsync(); + expect(messages).toEqual([]); + expect(allSent(cliWs)).toContainEqual({ type: 'heartbeat_ack' }); + }); + + it('does not resend unchanged roots after reordered heartbeats or child-only status changes', async () => { + const { doInstance, mockCtx, messages } = setupGlanceableDelivery(); + const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + sendHeartbeat(doInstance, cliWs, [makeSession('s1'), makeSession('s2', 'retry')]); + await flushAsync(); + sendHeartbeat(doInstance, cliWs, [ + makeSession('s2', 'retry', 'Renamed'), + makeSession('child', 'question', 'Child', 's1'), + makeSession('s1'), + ]); + await flushAsync(); + sendHeartbeat(doInstance, cliWs, [ + makeSession('s1'), + makeSession('s2', 'retry'), + makeSession('child', 'busy', 'Child', 's1'), + ]); + await flushAsync(); + expect(messages.map(message => message.data)).toMatchObject([ + { running: 1, needsInput: 0, reconnecting: 1 }, + ]); + }); + + it('ignores child-only heartbeats and disconnects', async () => { + const { doInstance, mockCtx, messages } = setupGlanceableDelivery(); + const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + sendHeartbeat(doInstance, cliWs, [makeSession('child', 'busy', 'Child', 'parent')]); + await flushAsync(); + await disconnectCli(doInstance, cliWs); + await flushAsync(); + expect(messages).toEqual([]); + }); + + it('uses the persisted heartbeat attachment before delivery and after hibernation', async () => { + const { doInstance, mockCtx, ctx, env, messages } = setupGlanceableDelivery(); + const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + sendHeartbeat(doInstance, cliWs, [makeSession('s1', 'retry')]); + await flushAsync(); + const restored = new UserConnectionDO(ctx as never, env as Env); + expect(restored.getActiveSessions()).toMatchObject([{ id: 's1', status: 'retry' }]); + sendHeartbeat(restored, cliWs, [makeSession('s1', 'retry')]); + await flushAsync(); + expect(messages.map(message => message.data)).toMatchObject([ + { running: 0, reconnecting: 1 }, + ]); + }); + + it('delivers an empty aggregate when a root disappears from the heartbeat', async () => { + const { doInstance, mockCtx, messages } = setupGlanceableDelivery(); + const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + sendHeartbeat(doInstance, cliWs, [makeSession('s1')]); + await flushAsync(); + sendHeartbeat(doInstance, cliWs, []); + await flushAsync(); + expect(messages.map(message => message.data)).toMatchObject([ + { running: 1 }, + { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + ]); + }); + + it.each([true, false])( + 'delivers disconnect only after attention reset (socket still listed: %s)', + async listed => { + const { doInstance, mockCtx, messages } = setupGlanceableDelivery(); + const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + sendHeartbeat(doInstance, cliWs, [makeSession('s1', 'question')]); + await flushAsync(); + messages.length = 0; + const reset = Promise.withResolvers(); + sessionIngestMocks.resetAttentionStatusOnCliDisconnect.mockImplementation( + () => reset.promise + ); + if (!listed) mockCtx.removeSocket(cliWs); + const disconnect = disconnectCli(doInstance, cliWs); + await flushAsync(); + expect(messages).toEqual([]); + reset.resolve(); + await disconnect; + await flushAsync(); + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + ]); + } + ); + + it.each(['cli-1', 'cli-2'])( + 'does not send a stale close after replacement by %s', + async replacementId => { + const { doInstance, mockCtx, messages } = setupGlanceableDelivery(); + const oldCli = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + sendHeartbeat(doInstance, oldCli, [makeSession('s1')]); + await flushAsync(); + const nextCli = addCliSocket(mockCtx, replacementId, [], undefined, 'usr_1'); + sendHeartbeat(doInstance, nextCli, [makeSession('s1')]); + await flushAsync(); + mockCtx.removeSocket(oldCli); + await disconnectCli(doInstance, oldCli); + await flushAsync(); + expect(messages.map(message => message.data)).toMatchObject([{ running: 1 }]); + expect(doInstance.getActiveSessions()).toMatchObject([ + { id: 's1', connectionId: replacementId }, + ]); + } + ); + + it('never infers user identity for legacy sockets', async () => { + const { doInstance, mockCtx, messages } = setupGlanceableDelivery(); + const cliWs = addCliSocket(mockCtx, 'cli-1'); + sendHeartbeat(doInstance, cliWs, [makeSession('s1')]); + await flushAsync(); + await disconnectCli(doInstance, cliWs); + await flushAsync(); + expect(messages).toEqual([]); + }); + + it('keeps heartbeat state and acknowledgement when aggregate transport fails', async () => { + const { doInstance, mockCtx } = setup({ + NOTIFICATIONS: { + refreshGlanceableSessions: async () => { + throw new Error('transport unavailable'); + }, + } as Env['NOTIFICATIONS'], + }); + const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); + sendHeartbeat(doInstance, cliWs, [makeSession('s1', 'retry')]); + await flushAsync(); + expect(doInstance.getActiveSessions()).toMatchObject([{ id: 's1', status: 'retry' }]); + expect(allSent(cliWs)).toContainEqual({ type: 'heartbeat_ack' }); + }); + }); + describe('heartbeat processing', () => { it('updates session ownership and persists attachment', async () => { const { doInstance, mockCtx } = setup(); diff --git a/services/session-ingest/src/dos/UserConnectionDO.ts b/services/session-ingest/src/dos/UserConnectionDO.ts index 1801331b08..f62844d406 100644 --- a/services/session-ingest/src/dos/UserConnectionDO.ts +++ b/services/session-ingest/src/dos/UserConnectionDO.ts @@ -3,6 +3,7 @@ import { DurableObject } from 'cloudflare:workers'; import type { Env } from '../env'; import { getSessionIngestDO } from './SessionIngestDO'; import { resolveAccessibleKiloSession } from '../services/session-access'; +import { refreshGlanceableSessions } from '../remote-session-notifications'; import { CLIOutboundMessageSchema, type CLIInboundMessage, @@ -590,6 +591,9 @@ export class UserConnectionDO extends DurableObject { instance: Instance | undefined ): void { const { connectionId } = attachment; + const previousStatuses = new Map( + this.aggregateSessions().map(session => [session.id, session.status]) + ); const now = Date.now(); this.lastHeartbeatAt.set(connectionId, now); this.connectionProtocolVersion.set(connectionId, protocolVersion); @@ -671,6 +675,23 @@ export class UserConnectionDO extends DurableObject { }; ws.serializeAttachment(updatedAttachment); + if (attachment.kiloUserId) { + const changedSessionIds = new Set(); + for (const session of this.aggregateSessions()) { + if (previousStatuses.get(session.id) !== session.status) changedSessionIds.add(session.id); + previousStatuses.delete(session.id); + } + for (const sessionId of previousStatuses.keys()) changedSessionIds.add(sessionId); + if (changedSessionIds.size > 0) { + this.ctx.waitUntil( + refreshGlanceableSessions(this.env, { + userId: attachment.kiloUserId, + cliSessionIds: [...changedSessionIds], + }) + ); + } + } + // Broadcast the heartbeat to every one of the user's web sockets. Subscribers // and non-subscribers both receive it: a removed session id is detectable // from its absence in the payload, so no subscriber special-case is needed. @@ -1793,6 +1814,18 @@ export class UserConnectionDO extends DurableObject { // without it we cannot safely target rows and must no-op. await this.resetOwnedSessionAttentionOnDisconnect(attachment.kiloUserId, ownedSessions); + const rootSessionIds = sessions + .filter(session => !session.parentSessionId && ownedSessions.has(session.id)) + .map(session => session.id); + if (attachment.kiloUserId && rootSessionIds.length > 0) { + this.ctx.waitUntil( + refreshGlanceableSessions(this.env, { + userId: attachment.kiloUserId, + cliSessionIds: rootSessionIds, + }) + ); + } + this.broadcastToWeb({ type: 'system', event: 'cli.disconnected', diff --git a/services/session-ingest/src/ingest/metadata.test.ts b/services/session-ingest/src/ingest/metadata.test.ts index 182aa1ff07..40877799bf 100644 --- a/services/session-ingest/src/ingest/metadata.test.ts +++ b/services/session-ingest/src/ingest/metadata.test.ts @@ -1,4 +1,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + buildGlanceableSnapshot, + buildOpaqueScopeKey, +} from '../../../../packages/app-shared/src/glanceable-agents-snapshot'; +import { deliverGlanceableSnapshot } from '../../../notifications/src/lib/glanceable-delivery'; +import type { ExpoPushMessage } from '../../../notifications/src/lib/expo-push'; +import type { RefreshGlanceableSessionsParams } from '@kilocode/notifications'; +import type { SessionEventDbRow } from '../session-events'; vi.mock('cloudflare:workers', () => ({ DurableObject: class { @@ -20,10 +28,12 @@ vi.mock('../dos/SessionAccessCacheDO', () => ({ })); vi.mock('../session-events', () => ({ - mapSessionEventRow: vi.fn((row: { session_id: string; status: string | null }) => ({ + mapSessionEventRow: vi.fn((row: SessionEventDbRow) => ({ source: 'v2' as const, sessionId: row.session_id, status: row.status, + organizationId: row.organization_id, + parentSessionId: row.parent_session_id, statusUpdatedAt: '2026-07-25T00:00:00.000Z', updatedAt: '2026-07-25T00:00:00.000Z', })), @@ -124,6 +134,8 @@ type ApplyMetadataDbOptions = { initialTitle?: string | null; /** git_url stored on the row before applyMetadataChanges runs. Defaults to NULL. */ initialGitUrl?: string | null; + initialOrganizationId?: string | null; + beforeCommit?: () => Promise; }; /** @@ -162,20 +174,21 @@ function createApplyMetadataDb(options: ApplyMetadataDbOptions = {}) { }; } - function persistedSessionRow() { - return { + function persistedSessionRow(): SessionEventDbRow { + const row: SessionEventDbRow = { session_id: 'ses_1', created_at: '2026-01-01T00:00:00.000Z', updated_at: '2026-01-01T00:00:01.000Z', title: 'T', created_on_platform: 'cli', - organization_id: null, + organization_id: options.initialOrganizationId ?? null, git_url: options.initialGitUrl ?? null, git_branch: null, - parent_session_id: null, + parent_session_id: options.parentSessionId ?? null, status: options.initialStatus ?? 'idle', status_updated_at: '2026-07-25T00:00:00.000Z', }; + return Object.assign(row, ...updateSets); } function sessionLimitResult() { @@ -237,11 +250,16 @@ function createApplyMetadataDb(options: ApplyMetadataDbOptions = {}) { const execute = vi.fn(async () => ({ rows: [{ creates_cycle: options.createsCycle ?? false }], })); - const transaction = vi.fn(async (fn: (tx: unknown) => Promise) => - fn({ select, update: applyUpdate, execute }) - ); + let committedSession = persistedSessionRow(); + const transaction = vi.fn(async (fn: (tx: unknown) => Promise) => { + const result = await fn({ select, update: applyUpdate, execute }); + await options.beforeCommit?.(); + committedSession = persistedSessionRow(); + return result; + }); return { + readCommittedSession: () => committedSession, transaction, select, applyUpdate, @@ -254,6 +272,53 @@ function createApplyMetadataDb(options: ApplyMetadataDbOptions = {}) { }; } +function metadataDelivery(db: ReturnType) { + const messages: ExpoPushMessage[] = []; + const tasks: Promise[] = []; + const env = { + HYPERDRIVE: { connectionString: 'postgres://unused' }, + NOTIFICATIONS: { + async refreshGlanceableSessions(params: RefreshGlanceableSessionsParams) { + if (params.userId !== 'usr_1' || !params.cliSessionIds.includes('ses_1')) return; + const row = db.readCommittedSession(); + await deliverGlanceableSnapshot( + { userId: params.userId, organizationId: row.organization_id }, + { + buildSnapshot: async (userId, organizationId) => ({ + type: 'active_agents_glanceable', + ...buildGlanceableSnapshot({ + userId, + organizationId, + sessions: + row.parent_session_id === null && row.status ? [{ status: row.status }] : [], + now: Date.now(), + }), + }), + listIosActivityTokens: async () => [], + sendIosLiveActivity: async () => undefined, + listIosExpoTokens: async () => [{ token: 'ExponentPushToken[ios]', locale: null }], + hasAndroidOngoingToken: async () => false, + listAndroidExpoTokens: async () => [], + sendExpoPush: async incoming => { + messages.push(...incoming); + }, + } + ); + }, + }, + }; + return { + env, + messages, + tasks, + ctx: { + waitUntil: (task: Promise) => { + tasks.push(task); + }, + }, + }; +} + describe('resetAttentionStatusOnCliDisconnect', () => { beforeEach(() => { vi.mocked(getWorkerDb).mockReset(); @@ -373,6 +438,140 @@ describe('applyMetadataChanges', () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); }); + describe('glanceable aggregate refresh', () => { + it.each([ + ['idle', 'busy', { status: 'happy', running: 1, needsInput: 0, reconnecting: 0 }], + ['busy', 'retry', { status: 'happy', running: 0, needsInput: 0, reconnecting: 1 }], + ['question', 'busy', { status: 'happy', running: 1, needsInput: 0, reconnecting: 0 }], + ['permission', 'idle', { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }], + ['busy', 'idle', { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }], + ] as const)( + 'delivers persisted cloud status %s → %s without attention or stream clients', + async (initialStatus, status, expected) => { + const db = createApplyMetadataDb({ initialStatus, cloudAgentSessionId: 'cloud-1' }); + vi.mocked(getWorkerDb).mockReturnValue(db as never); + const delivery = metadataDelivery(db); + await applyMetadataChanges( + delivery.env as never, + 'usr_1', + 'ses_1', + new Map([['status', status]]), + delivery.ctx + ); + await Promise.all(delivery.tasks); + expect(delivery.messages.map(message => message.data)).toMatchObject([expected]); + expect(db.readCommittedSession().status).toBe(status); + } + ); + + it('does not deliver the old snapshot while the transaction still awaits commit', async () => { + const commit = Promise.withResolvers(); + const db = createApplyMetadataDb({ + initialStatus: 'idle', + beforeCommit: () => commit.promise, + }); + vi.mocked(getWorkerDb).mockReturnValue(db as never); + const delivery = metadataDelivery(db); + const applying = applyMetadataChanges( + delivery.env as never, + 'usr_1', + 'ses_1', + new Map([['status', 'busy']]), + delivery.ctx + ); + await vi.waitFor(() => expect(db.queryLog).toContain('read-back')); + expect(db.readCommittedSession().status).toBe('idle'); + expect(delivery.messages).toEqual([]); + commit.resolve(); + await applying; + await Promise.all(delivery.tasks); + expect(delivery.messages.map(message => message.data)).toMatchObject([ + { running: 1, status: 'happy' }, + ]); + }); + + it('does not deliver a transaction that fails to commit', async () => { + const db = createApplyMetadataDb({ + beforeCommit: async () => { + throw new Error('commit failed'); + }, + }); + vi.mocked(getWorkerDb).mockReturnValue(db as never); + const delivery = metadataDelivery(db); + await expect( + applyMetadataChanges( + delivery.env as never, + 'usr_1', + 'ses_1', + new Map([['status', 'busy']]), + delivery.ctx + ) + ).rejects.toThrow('commit failed'); + await Promise.all(delivery.tasks); + expect(db.readCommittedSession().status).toBe('idle'); + expect(delivery.messages).toEqual([]); + }); + + it.each([{ initialStatus: 'busy' }, { rowMissing: true }, { parentSessionId: 'root' }])( + 'skips unchanged, inaccessible, and child rows: %j', + async options => { + const db = createApplyMetadataDb(options); + vi.mocked(getWorkerDb).mockReturnValue(db as never); + const delivery = metadataDelivery(db); + await applyMetadataChanges( + delivery.env as never, + 'usr_1', + 'ses_1', + new Map([['status', 'busy']]) + ); + expect(delivery.messages).toEqual([]); + } + ); + + it.each([null, 'org_live'])( + 'uses the persisted scope %s instead of an unauthorized org claim', + async organizationId => { + const db = createApplyMetadataDb({ + initialOrganizationId: organizationId, + membershipRows: 0, + }); + vi.mocked(getWorkerDb).mockReturnValue(db as never); + const delivery = metadataDelivery(db); + await applyMetadataChanges( + delivery.env as never, + 'usr_1', + 'ses_1', + new Map([ + ['status', 'busy'], + ['orgId', 'org_foreign'], + ]) + ); + expect(db.readCommittedSession().organization_id).toBe(organizationId); + expect(delivery.messages.map(message => message.data)).toMatchObject([ + { + running: 1, + scopeKey: buildOpaqueScopeKey({ userId: 'usr_1', organizationId }), + organizationBound: organizationId !== null, + }, + ]); + } + ); + + it('keeps committed ingestion successful when aggregate transport fails', async () => { + const db = createApplyMetadataDb(); + vi.mocked(getWorkerDb).mockReturnValue(db as never); + const delivery = metadataDelivery(db); + delivery.env.NOTIFICATIONS.refreshGlanceableSessions = async () => { + throw new Error('transport unavailable'); + }; + await expect( + applyMetadataChanges(delivery.env as never, 'usr_1', 'ses_1', new Map([['status', 'busy']])) + ).resolves.toBeUndefined(); + expect(db.readCommittedSession().status).toBe('busy'); + expect(delivery.messages).toEqual([]); + }); + }); + it('persists organization_id and invalidates access cache when the user is a member', async () => { const db = createApplyMetadataDb({ membershipRows: 1 }); vi.mocked(getWorkerDb).mockReturnValue(db as never); diff --git a/services/session-ingest/src/ingest/metadata.ts b/services/session-ingest/src/ingest/metadata.ts index abc4240e55..dd970121ea 100644 --- a/services/session-ingest/src/ingest/metadata.ts +++ b/services/session-ingest/src/ingest/metadata.ts @@ -7,6 +7,7 @@ import type { Env } from '../env'; import { getSessionAccessCacheDO } from '../dos/SessionAccessCacheDO'; import { isNeedsInputStatus } from '../dos/session-ingest-attention'; import { mapSessionEventRow, notifyUserSessionEvent } from '../session-events'; +import { refreshGlanceableSessions } from '../remote-session-notifications'; import { SessionStatusSchema } from '../types/user-connection-protocol'; import { isDefaultSessionTitle } from './default-session-title'; @@ -427,6 +428,15 @@ export async function applyMetadataChanges( }, ctx ); + if (notification.session.parentSessionId === null) { + // The transaction has committed, so the snapshot route reads the new status. + const delivery = refreshGlanceableSessions(env, { + userId: kiloUserId, + cliSessionIds: [sessionId], + }); + if (ctx) ctx.waitUntil(delivery); + else await delivery; + } } } diff --git a/services/session-ingest/src/notifications-binding.ts b/services/session-ingest/src/notifications-binding.ts index 184a340240..b256f8b28c 100644 --- a/services/session-ingest/src/notifications-binding.ts +++ b/services/session-ingest/src/notifications-binding.ts @@ -7,6 +7,7 @@ */ import type { + RefreshGlanceableSessionsParams, SendAgentSessionNotificationParams, SendAgentSessionNotificationResult, SendCloudAgentSessionNotificationParams, @@ -16,6 +17,7 @@ import type { } from '@kilocode/notifications'; export type NotificationsBinding = Fetcher & { + refreshGlanceableSessions(params: RefreshGlanceableSessionsParams): Promise; sendCloudAgentSessionNotification( params: SendCloudAgentSessionNotificationParams ): Promise; diff --git a/services/session-ingest/src/remote-session-notifications.ts b/services/session-ingest/src/remote-session-notifications.ts index 9eb7f9d3f5..663428950b 100644 --- a/services/session-ingest/src/remote-session-notifications.ts +++ b/services/session-ingest/src/remote-session-notifications.ts @@ -1,10 +1,26 @@ import type { + RefreshGlanceableSessionsParams, SendAgentSessionNotificationParams, SendAgentSessionNotificationResult, SendCloudAgentSessionNotificationParams, SendCloudAgentSessionNotificationResult, } from '@kilocode/notifications'; import type { AttentionSignal } from './dos/session-ingest-attention'; +import type { Env } from './env'; + +/** Call only after the snapshot source reflects the transition. Never gate on attention pushes. */ +export async function refreshGlanceableSessions( + env: Pick, + params: RefreshGlanceableSessionsParams +): Promise { + try { + await env.NOTIFICATIONS.refreshGlanceableSessions(params); + } catch (error) { + console.warn('Glanceable aggregate refresh failed (non-fatal)', { + error: error instanceof Error ? error.message : String(error), + }); + } +} export type RemoteSessionInfo = { parentSessionId: string | null; From c22890380d28d9525d655d3037f51175156cedcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 20:43:52 +0200 Subject: [PATCH 23/43] fix(glanceable): refresh counts after cloud run reports --- .../src/notifications-binding.ts | 2 + .../report-consumer.glanceable.test.ts | 466 ++++++++++++++++++ .../src/telemetry/report-consumer.ts | 33 +- 3 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts diff --git a/services/cloud-agent-next/src/notifications-binding.ts b/services/cloud-agent-next/src/notifications-binding.ts index 6db84e7776..23da3b8bea 100644 --- a/services/cloud-agent-next/src/notifications-binding.ts +++ b/services/cloud-agent-next/src/notifications-binding.ts @@ -7,6 +7,7 @@ */ import type { + RefreshGlanceableSessionsParams, SendCloudAgentSessionNotificationParams, SendCloudAgentSessionNotificationResult, } from '@kilocode/notifications'; @@ -18,6 +19,7 @@ export type { } from '@kilocode/notifications'; export type NotificationsBinding = Fetcher & { + refreshGlanceableSessions(params: RefreshGlanceableSessionsParams): Promise; sendCloudAgentSessionNotification( params: SendCloudAgentSessionNotificationParams ): Promise; diff --git a/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts b/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts new file mode 100644 index 0000000000..acdbbc73c9 --- /dev/null +++ b/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts @@ -0,0 +1,466 @@ +import { DatabaseSync } from 'node:sqlite'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { and, eq, getTableColumns, getTableName, inArray } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/pg-proxy'; +import type { WorkerDb } from '@kilocode/db/client'; +import { + cli_sessions_v2, + cloud_agent_session_runs, + cloud_agent_sessions, + github_branch_pull_requests, +} from '@kilocode/db/schema'; +import type { RefreshGlanceableSessionsParams } from '@kilocode/notifications'; +import type { CloudAgentQueueReport } from '@kilocode/worker-utils/cloud-agent-queue-report'; +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '../../../../packages/app-shared/src/glanceable-agents-snapshot'; +import { deliverGlanceableSnapshot } from '../../../notifications/src/lib/glanceable-delivery'; +import type { ExpoPushMessage } from '../../../notifications/src/lib/expo-push'; + +const database = vi.hoisted(() => ({ current: undefined as WorkerDb | undefined })); +vi.mock('../db/pg.js', () => ({ getPgDb: () => database.current })); +vi.mock('../../../../apps/web/node_modules/server-only/index.js', () => ({})); +vi.mock('@/lib/config.server', () => ({ SESSION_INGEST_WORKER_URL: undefined })); +vi.mock('@/lib/tokens', () => ({ generateInternalServiceToken: vi.fn() })); +vi.mock('@/lib/drizzle', () => ({ + get db() { + return database.current; + }, +})); +vi.mock('@/routers/cli-sessions-v2-router', async () => { + const { sql } = await import('drizzle-orm'); + const { z } = await import('zod'); + return { + associatedPrSchema: z.unknown(), + formatAssociatedPr: () => null, + sessionPrJoinPredicate: sql`false`, + }; +}); + +import { consumeCloudAgentReportBatch } from './report-consumer.js'; + +// Load the real web query without adding the web app's alias graph to the service typecheck. +const { listActiveSessions } = await vi.importActual<{ + listActiveSessions: (input: { + userId: string; + organizationId: string | null; + includeCloudAgentSessions: boolean; + }) => Promise<{ sessions: { id: string; status: string }[] }>; +}>('../../../../apps/web/src/lib/active-sessions-list'); + +const cloudAgentSessionId = 'agent_12345678-1234-4234-8234-123456789abc'; +const cliSessionId = 'ses_12345678901234567890123456'; +const userId = 'oauth/cloud-eligibility'; +const occurredAt = '2026-08-28T10:00:00.000Z'; +const report: CloudAgentQueueReport = { + version: 1, + type: 'run.state', + occurredAt, + session: { cloudAgentSessionId }, + run: { messageId: 'msg_1', status: 'accepted', dispatchAcceptedAt: occurredAt }, +}; + +function messageFor(body: unknown) { + return { + body, + outcome: 'pending', + ack() { + this.outcome = 'ack'; + }, + retry() { + this.outcome = 'retry'; + }, + }; +} + +function setup(options: { beforeCommit?: () => Promise; refreshError?: Error } = {}) { + const sqlite = new DatabaseSync(':memory:'); + for (const table of [ + cli_sessions_v2, + cloud_agent_sessions, + cloud_agent_session_runs, + github_branch_pull_requests, + ]) { + const columns = Object.values(getTableColumns(table)).map(column => `"${column.name}"`); + sqlite.exec(`CREATE TABLE "${getTableName(table)}" (${columns.join(', ')})`); + } + + // Run the real Drizzle queries, including the web list's EXISTS/root/scope predicates. + // SQLite needs positional placeholders, explicit null defaults, and a test-clock idle cutoff. + const db = drizzle(async (query, params) => { + if (query.includes('pg_advisory_xact_lock')) return { rows: [] }; + const statement = sqlite.prepare( + query + .replace(/\$\d+/g, '?') + .replace(/\bdefault\b/gi, 'null') + .replace( + "now() - interval '15 minutes'", + `'${new Date(Date.now() - 15 * 60_000).toISOString()}'` + ) + ); + statement.setReturnArrays(true); + return { rows: statement.all(...params) }; + }); + db.transaction = async operation => { + sqlite.exec('BEGIN'); + try { + const result = await operation(db as never); + await options.beforeCommit?.(); + sqlite.exec('COMMIT'); + return result; + } catch (error) { + sqlite.exec('ROLLBACK'); + throw error; + } + }; + database.current = db as unknown as WorkerDb; + + const messages: ExpoPushMessage[] = []; + const previous = new Map(); + const env = { + NOTIFICATIONS: { + // The aggregate path must not use the attention transport or its preference/presence gates. + sendCloudAgentSessionNotification() { + throw new Error('Attention notifications are disabled and the session has a viewer'); + }, + async refreshGlanceableSessions(params: RefreshGlanceableSessionsParams) { + if (options.refreshError) throw options.refreshError; + expect(Object.keys(params).sort()).toEqual(['cliSessionIds', 'userId']); + const rows = await db + .select({ organizationId: cli_sessions_v2.organization_id }) + .from(cli_sessions_v2) + .where( + and( + eq(cli_sessions_v2.kilo_user_id, params.userId), + inArray(cli_sessions_v2.session_id, params.cliSessionIds) + ) + ); + for (const organizationId of new Set(rows.map(row => row.organizationId))) { + await deliverGlanceableSnapshot( + { userId: params.userId, organizationId }, + { + buildSnapshot: async (owner, organizationId) => { + const { sessions } = await listActiveSessions({ + userId: owner, + organizationId, + includeCloudAgentSessions: true, + }); + const prior = previous.get(organizationId); + const snapshot = buildGlanceableSnapshot({ + userId: owner, + organizationId, + sessions, + now: Date.now(), + previousRevision: prior?.revision, + previousEligibleStartedAt: prior?.eligibleStartedAt, + }); + previous.set(organizationId, snapshot); + return { type: 'active_agents_glanceable', ...snapshot }; + }, + listIosActivityTokens: async () => [], + sendIosLiveActivity: async () => undefined, + listIosExpoTokens: async () => [{ token: 'ExponentPushToken[test]', locale: null }], + hasAndroidOngoingToken: async () => false, + listAndroidExpoTokens: async () => [], + sendExpoPush: async incoming => { + messages.push(...incoming); + }, + } + ); + } + }, + }, + }; + async function seed(status = 'busy', organizationId: string | null = null) { + await db.insert(cloud_agent_sessions).values({ + cloud_agent_session_id: cloudAgentSessionId, + kilo_session_id: cliSessionId, + initial_message_id: report.run.messageId, + created_at: occurredAt, + }); + await db.insert(cli_sessions_v2).values({ + session_id: cliSessionId, + kilo_user_id: userId, + cloud_agent_session_id: cloudAgentSessionId, + status, + organization_id: organizationId, + created_at: occurredAt, + updated_at: occurredAt, + status_updated_at: occurredAt, + }); + } + async function consume(body: unknown = report) { + const message = messageFor(body); + await consumeCloudAgentReportBatch({ messages: [message] } as never, env as never); + return message.outcome; + } + return { db, sqlite, env, seed, consume, messages }; +} + +let fixture: ReturnType; +beforeEach(() => { + vi.useFakeTimers({ toFake: ['Date'] }); + vi.setSystemTime(new Date(occurredAt)); + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + vi.spyOn(console, 'error').mockImplementation(() => undefined); +}); +afterEach(() => { + fixture?.sqlite.close(); + database.current = undefined; + vi.restoreAllMocks(); + vi.useRealTimers(); +}); + +async function activeIds(organizationId: string | null = null) { + const { sessions } = await listActiveSessions({ + userId, + organizationId, + includeCloudAgentSessions: true, + }); + return sessions.map(session => session.id).sort(); +} + +describe('committed cloud eligibility refresh', () => { + it.each([null, '11111111-1111-4111-8111-111111111111'])( + 'refreshes delayed run insertion in scope %s without attention delivery', + async organizationId => { + fixture = setup(); + await fixture.seed('busy', organizationId); + expect(await activeIds(organizationId)).toEqual([]); + + expect(await fixture.consume()).toBe('ack'); + + expect(await activeIds(organizationId)).toEqual([cliSessionId]); + expect(fixture.messages.map(message => message.data)).toMatchObject([ + { + status: 'happy', + running: 1, + reconnecting: 0, + organizationBound: organizationId !== null, + }, + ]); + } + ); + + it.each(['busy', 'retry'])( + 'removes a terminal run while the stored session remains %s', + async status => { + fixture = setup(); + await fixture.seed(status); + await fixture.consume(); + const terminalAt = '2026-08-28T10:04:00.000Z'; + expect( + await fixture.consume({ + ...report, + run: { + messageId: report.run.messageId, + status: 'failed', + terminalAt, + failureStage: 'unknown', + failureCode: 'unclassified', + }, + }) + ).toBe('ack'); + + expect(await activeIds()).toEqual([]); + expect( + await fixture.db.select({ status: cli_sessions_v2.status }).from(cli_sessions_v2) + ).toEqual([{ status }]); + expect(fixture.messages.map(message => message.data)).toMatchObject([ + { + status: 'happy', + running: status === 'busy' ? 1 : 0, + reconnecting: status === 'retry' ? 1 : 0, + }, + { status: 'empty', running: 0, needsInput: 0, reconnecting: 0, eligibleStartedAt: null }, + ]); + } + ); + + it('retains the eligible interval when nonterminal retry work refreshes', async () => { + fixture = setup(); + await fixture.seed(); + await fixture.consume(); + vi.setSystemTime(new Date('2026-08-28T10:02:00.000Z')); + await fixture.db + .update(cli_sessions_v2) + .set({ status: 'retry', updated_at: new Date().toISOString() }) + .where(eq(cli_sessions_v2.session_id, cliSessionId)); + await fixture.consume(); + expect(fixture.messages.map(message => message.data)).toMatchObject([ + { running: 1, eligibleStartedAt: occurredAt }, + { running: 0, reconnecting: 1, eligibleStartedAt: occurredAt }, + ]); + }); + + it('does not refresh before the report transaction commits', async () => { + const started = Promise.withResolvers(); + const commit = Promise.withResolvers(); + fixture = setup({ + beforeCommit: async () => { + started.resolve(); + await commit.promise; + }, + }); + await fixture.seed(); + const consuming = fixture.consume(); + await started.promise; + expect(fixture.messages).toEqual([]); + commit.resolve(); + expect(await consuming).toBe('ack'); + expect(fixture.messages.map(message => message.data)).toMatchObject([{ running: 1 }]); + }); + + it('retries a failed commit without publishing uncommitted work', async () => { + fixture = setup({ + beforeCommit: async () => { + throw new Error('commit failed'); + }, + }); + await fixture.seed(); + expect(await fixture.consume()).toBe('retry'); + expect(await fixture.db.select().from(cloud_agent_session_runs)).toEqual([]); + expect(fixture.messages).toEqual([]); + }); + + it('keeps a committed report acknowledged when refresh fails without logging credentials', async () => { + fixture = setup({ + refreshError: new Error('upstream-error-body-must-not-be-logged'), + }); + await fixture.seed(); + expect(await fixture.consume()).toBe('ack'); + expect(await activeIds()).toEqual([cliSessionId]); + expect(fixture.messages).toEqual([]); + expect(vi.mocked(console.warn).mock.calls).toEqual([ + ['Cloud Agent glanceable refresh failed', { cloudAgentSessionId }], + ]); + expect(vi.mocked(console.error).mock.calls).toEqual([]); + }); + + it('acknowledges duplicate and out-of-order reports without reviving terminal work', async () => { + fixture = setup(); + await fixture.seed(); + const completed = { + ...report, + run: { messageId: report.run.messageId, status: 'completed', terminalAt: occurredAt }, + }; + const outcomes = []; + for (const body of [report, report, completed, completed, report]) { + outcomes.push(await fixture.consume(body)); + } + expect(outcomes).toEqual(['ack', 'ack', 'ack', 'ack', 'ack']); + expect( + await fixture.db + .select({ + status: cloud_agent_session_runs.status, + terminalAt: cloud_agent_session_runs.terminal_at, + }) + .from(cloud_agent_session_runs) + ).toEqual([{ status: 'completed', terminalAt: occurredAt }]); + expect(fixture.messages.map(message => message.data)).toMatchObject([ + { running: 1 }, + { running: 1 }, + { running: 0 }, + { running: 0 }, + { running: 0 }, + ]); + }); + + it('keeps the session counted until its last nonterminal run ends', async () => { + fixture = setup(); + await fixture.seed(); + for (const run of [ + report.run, + { messageId: 'msg_2', status: 'queued', queuedAt: occurredAt }, + { messageId: report.run.messageId, status: 'completed', terminalAt: occurredAt }, + { messageId: 'msg_2', status: 'interrupted', terminalAt: occurredAt }, + ]) { + expect(await fixture.consume({ ...report, run })).toBe('ack'); + } + expect(fixture.messages.map(message => message.data)).toMatchObject([ + { running: 1 }, + { running: 1 }, + { running: 1 }, + { status: 'empty', running: 0 }, + ]); + expect(await activeIds()).toEqual([]); + }); + + it.each(['expired', 'missing_parent'])( + 'does not publish an unapplied %s report', + async outcome => { + fixture = setup(); + await fixture.seed(); + const parent = eq(cloud_agent_sessions.cloud_agent_session_id, cloudAgentSessionId); + if (outcome === 'expired') { + await fixture.db + .update(cloud_agent_sessions) + .set({ created_at: '2026-05-01T00:00:00.000Z' }) + .where(parent); + } else { + await fixture.db.delete(cloud_agent_sessions).where(parent); + } + expect(await fixture.consume()).toBe('ack'); + expect(await fixture.db.select().from(cloud_agent_session_runs)).toEqual([]); + expect(fixture.messages).toEqual([]); + } + ); + + it('does not invent a recipient when CLI session metadata has not arrived', async () => { + fixture = setup(); + await fixture.seed(); + await fixture.db.delete(cli_sessions_v2).where(eq(cli_sessions_v2.session_id, cliSessionId)); + expect(await fixture.consume()).toBe('ack'); + expect( + await fixture.db + .select({ status: cloud_agent_session_runs.status }) + .from(cloud_agent_session_runs) + ).toEqual([{ status: 'accepted' }]); + expect(fixture.messages).toEqual([]); + }); + + it('uses the real cloud predicate for roots, run liveness, warm idle, and scope', async () => { + fixture = setup(); + await fixture.seed(); + for (const [id, status, parent, owner, organization, cloudId, updated] of [ + ['no-run', 'retry', null, userId, null, 'no-run-cloud', occurredAt], + ['terminal', 'busy', null, userId, null, 'terminal-cloud', occurredAt], + ['warm-idle', 'idle', null, userId, null, 'terminal-cloud', occurredAt], + ['cold-idle', 'idle', null, userId, null, 'terminal-cloud', '2026-08-28T09:40:00.000Z'], + ['child', 'busy', cliSessionId, userId, null, cloudAgentSessionId, occurredAt], + ['other-user', 'busy', null, 'oauth/other', null, cloudAgentSessionId, occurredAt], + ['other-org', 'busy', null, userId, 'another-org', cloudAgentSessionId, occurredAt], + ['not-cloud', 'busy', null, userId, null, null, occurredAt], + ]) { + await fixture.db.insert(cli_sessions_v2).values({ + session_id: id!, + status, + parent_session_id: parent, + kilo_user_id: owner!, + organization_id: organization, + cloud_agent_session_id: cloudId, + status_updated_at: updated, + created_at: occurredAt, + updated_at: occurredAt, + }); + } + await fixture.db.insert(cloud_agent_session_runs).values({ + cloud_agent_session_id: 'terminal-cloud', + message_id: 'terminal-msg', + status: 'completed', + terminal_at: occurredAt, + }); + expect(await activeIds()).toEqual(['warm-idle']); + await fixture.consume(); + expect(await activeIds()).toEqual([cliSessionId, 'warm-idle']); + expect( + fixture.messages + .filter( + message => + message.data?.type === 'active_agents_glanceable' && !message.data.organizationBound + ) + .map(message => message.data) + ).toMatchObject([{ running: 1, needsInput: 0, reconnecting: 0 }]); + }); +}); diff --git a/services/cloud-agent-next/src/telemetry/report-consumer.ts b/services/cloud-agent-next/src/telemetry/report-consumer.ts index 677223630b..4fe248317c 100644 --- a/services/cloud-agent-next/src/telemetry/report-consumer.ts +++ b/services/cloud-agent-next/src/telemetry/report-consumer.ts @@ -1,4 +1,6 @@ +import { cli_sessions_v2, cloud_agent_sessions } from '@kilocode/db/schema'; import { CloudAgentQueueReportSchema } from '@kilocode/worker-utils/cloud-agent-queue-report'; +import { eq } from 'drizzle-orm'; import { getPgDb } from '../db/pg.js'; import type { Env } from '../types.js'; @@ -41,7 +43,8 @@ export async function consumeCloudAgentReportBatch( batch: MessageBatch, env: Env ): Promise { - const reportStore = createCloudAgentReportStore(getPgDb(env)); + const db = getPgDb(env); + const reportStore = createCloudAgentReportStore(db); for (const message of batch.messages) { const parsed = parseReportWithoutInvalidDiagnostic(message.body); @@ -54,6 +57,34 @@ export async function consumeCloudAgentReportBatch( } try { const result = await reportStore.saveReport(parsed.data); + if (result.outcome === 'applied' && env.NOTIFICATIONS) { + // Run liveness can change independently of session status. Refresh only after commit. + try { + const cloudAgentSessionId = parsed.data.session.cloudAgentSessionId; + const [session] = await db + .select({ + userId: cli_sessions_v2.kilo_user_id, + cliSessionId: cli_sessions_v2.session_id, + }) + .from(cloud_agent_sessions) + .innerJoin( + cli_sessions_v2, + eq(cli_sessions_v2.session_id, cloud_agent_sessions.kilo_session_id) + ) + .where(eq(cloud_agent_sessions.cloud_agent_session_id, cloudAgentSessionId)) + .limit(1); + if (session) { + await env.NOTIFICATIONS.refreshGlanceableSessions({ + userId: session.userId, + cliSessionIds: [session.cliSessionId], + }); + } + } catch { + console.warn('Cloud Agent glanceable refresh failed', { + cloudAgentSessionId: parsed.data.session.cloudAgentSessionId, + }); + } + } if (result.outcome === 'missing_parent') { console.warn('Dropping Cloud Agent run report without a session anchor', { cloudAgentSessionId: parsed.data.session.cloudAgentSessionId, From cdcd9db295dff18260c49a9a3de7f90050098ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 23:06:51 +0200 Subject: [PATCH 24/43] fix(glanceable): retire terminal APNs targets before fresh work --- .../src/lib/apns-live-activity.test.ts | 82 ++ .../src/lib/apns-live-activity.ts | 46 +- .../src/lib/glanceable-delivery-deps.ts | 43 +- .../src/lib/glanceable-delivery.test.ts | 920 +++++++++++++++++- .../src/lib/glanceable-delivery.ts | 40 +- .../src/lib/glanceable-refresh.ts | 31 + 6 files changed, 1083 insertions(+), 79 deletions(-) diff --git a/services/notifications/src/lib/apns-live-activity.test.ts b/services/notifications/src/lib/apns-live-activity.test.ts index de733a8dae..a7e0000524 100644 --- a/services/notifications/src/lib/apns-live-activity.test.ts +++ b/services/notifications/src/lib/apns-live-activity.test.ts @@ -90,6 +90,68 @@ describe('buildLiveActivityApnsRequest', () => { }); }); +describe('APNs terminal contract', () => { + it('encodes final content and a native dismissal date without start attributes', () => { + const request = buildLiveActivityApnsRequest({ + token: 'ending-activity', + event: 'end', + contentState: { name: 'ActiveAgentsLiveActivity', props: '{"status":"empty","running":0}' }, + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem: 'pem', topic: TOPIC }, + authorizationJwt: 'header.payload.sig', + timestampSeconds: 1_750_000_000, + dismissalDateSeconds: 1_750_000_108, + }); + expect(JSON.parse(request.body)).toEqual({ + aps: { + timestamp: 1_750_000_000, + event: 'end', + 'dismissal-date': 1_750_000_108, + 'content-state': { + name: 'ActiveAgentsLiveActivity', + props: '{"status":"empty","running":0}', + }, + }, + }); + }); + + it('anchors the terminal window at the send boundary without changing snapshot order', async () => { + const privateKeyPem = await generateTestPrivateKeyPem(); + const clock = vi.spyOn(Date, 'now').mockReturnValue(1_750_000_000_000); + const bodies: unknown[] = []; + try { + await sendLiveActivityApns({ + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem, topic: TOPIC }, + tokens: [{ token: 'ending-activity', event: 'end' }], + contentState: { running: 0 }, + nowSeconds: 1_750_000_000, + timestampSeconds: 1_750_000_001, + isCurrent: async () => true, + beforeEnd: async () => { + clock.mockReturnValue(1_750_000_100_000); + return true; + }, + fetchFn: async (_url, init) => { + if (typeof init?.body !== 'string') throw new Error('Expected a JSON body'); + bodies.push(JSON.parse(init.body)); + return new Response(null, { status: 200 }); + }, + }); + expect(bodies).toEqual([ + { + aps: { + event: 'end', + timestamp: 1_750_000_001, + 'dismissal-date': 1_750_000_108, + 'content-state': { running: 0 }, + }, + }, + ]); + } finally { + clock.mockRestore(); + } + }); +}); + describe('signApnsJwt', () => { it('signs a JWT whose header carries alg/kid and claims carry iss/iat', async () => { const privateKeyPem = await generateTestPrivateKeyPem(); @@ -208,6 +270,26 @@ describe('sendLiveActivityApns', () => { expect(result).toEqual({ attempted: 1, ok: 1, failed: 0 }); }); + it('skips an end when the durable intent loses its generation before the request', async () => { + const privateKeyPem = await generateTestPrivateKeyPem(); + const delivered: string[] = []; + const result = await sendLiveActivityApns({ + credentials: { teamId: TEAM_ID, keyId: KEY_ID, privateKeyPem, topic: TOPIC }, + tokens: [{ token: 'ending-activity', event: 'end' }], + contentState: { running: 0 }, + nowSeconds: 1_750_000_000, + isCurrent: async () => true, + beforeEnd: async () => false, + fetchFn: async url => { + if (typeof url !== 'string') throw new Error('Expected a string URL'); + delivered.push(url); + return new Response(null, { status: 200 }); + }, + }); + expect(delivered).toEqual([]); + expect(result).toEqual({ attempted: 0, ok: 0, failed: 0 }); + }); + it('counts rejected pushes as failures', async () => { const privateKeyPem = await generateTestPrivateKeyPem(); const fetchFn = vi diff --git a/services/notifications/src/lib/apns-live-activity.ts b/services/notifications/src/lib/apns-live-activity.ts index dd849f847b..e0179dc505 100644 --- a/services/notifications/src/lib/apns-live-activity.ts +++ b/services/notifications/src/lib/apns-live-activity.ts @@ -1,5 +1,5 @@ /** - * Token-based APNs client for Live Activity pushes (push-to-start and update). + * Token-based APNs client for Live Activity start, update, and end pushes. * Pure: every network hop goes through the injected `fetchFn` so unit tests * substitute a fake. Never logs a device token or the private key. */ @@ -13,7 +13,10 @@ export type ApnsCredentials = { topic: string; }; -export type LiveActivityEvent = 'start' | 'update'; +export type LiveActivityEvent = 'start' | 'update' | 'end'; + +// ActivityKit controls Lock Screen dismissal, not Dynamic Island retention. +const TERMINAL_SECONDS = 8; const APNS_BASE_URL = 'https://api.push.apple.com'; const APNS_KEY_PREFIX = '-----BEGIN PRIVATE KEY-----'; @@ -75,14 +78,15 @@ export async function signApnsJwt( * `.push-type.liveactivity` topic suffix. The `timestamp` (Unix seconds) is * what lets iOS discard an older revision that arrives late. */ -export function buildLiveActivityApnsRequest(params: { - token: string; - event: LiveActivityEvent; - contentState: Record; - credentials: ApnsCredentials; - authorizationJwt: string; - timestampSeconds: number; -}): { url: string; headers: Record; body: string } { +export function buildLiveActivityApnsRequest( + params: { + token: string; + contentState: Record; + credentials: ApnsCredentials; + authorizationJwt: string; + timestampSeconds: number; + } & ({ event: 'start' | 'update' } | { event: 'end'; dismissalDateSeconds: number }) +): { url: string; headers: Record; body: string } { return { url: `${APNS_BASE_URL}/3/device/${params.token}`, headers: { @@ -103,6 +107,7 @@ export function buildLiveActivityApnsRequest(params: { ? { 'attributes-type': LIVE_ACTIVITY_ATTRIBUTES_TYPE, attributes: {} } : {}), 'content-state': params.contentState, + ...(params.event === 'end' ? { 'dismissal-date': params.dismissalDateSeconds } : {}), }, }), }; @@ -124,6 +129,12 @@ export async function sendLiveActivityApns(params: { timestampSeconds?: number; /** Recheck the durable generation after signing, before each request. */ isCurrent?: () => Promise; + /** Persist terminal intent after signing, before the end can reach ActivityKit. */ + beforeEnd?: (token: string) => Promise; + /** Retire successful ends by registration identity, even after a newer generation. */ + onEnded?: (token: string) => Promise; + /** Release only an explicitly rejected end; a lost response leaves delivery uncertain. */ + onEndRejected?: (token: string) => Promise; fetchFn?: typeof fetch; }): Promise { if (params.tokens.length === 0) { @@ -135,23 +146,34 @@ export async function sendLiveActivityApns(params: { const results = await Promise.allSettled( params.tokens.map(async ({ token, event }) => { + if (params.isCurrent && !(await params.isCurrent())) return false; + if (event === 'end' && params.beforeEnd && !(await params.beforeEnd(token))) return false; const request = buildLiveActivityApnsRequest({ token, - event, + ...(event === 'end' + ? { event, dismissalDateSeconds: Math.floor(Date.now() / 1000) + TERMINAL_SECONDS } + : { event }), contentState: params.contentState, credentials: params.credentials, authorizationJwt, timestampSeconds: params.timestampSeconds ?? params.nowSeconds, }); - if (params.isCurrent && !(await params.isCurrent())) return false; const response = await fetchFn(request.url, { method: 'POST', headers: request.headers, body: request.body, }); if (!response.ok) { + if (event === 'end') { + // A 410 confirms an inactive target, not a live activity that can recover. + if (response.status === 410) await params.onEnded?.(token); + else await params.onEndRejected?.(token); + } throw new Error(`APNs rejected the push with status ${response.status}`); } + if (event === 'end') { + await params.onEnded?.(token); + } return true; }) ); diff --git a/services/notifications/src/lib/glanceable-delivery-deps.ts b/services/notifications/src/lib/glanceable-delivery-deps.ts index 4bc9a4710d..71ab042c03 100644 --- a/services/notifications/src/lib/glanceable-delivery-deps.ts +++ b/services/notifications/src/lib/glanceable-delivery-deps.ts @@ -11,6 +11,10 @@ import type { GlanceableDeliveryDeps, IosActivityToken } from './glanceable-deli export function glanceableDeliveryDeps(env: Env): GlanceableDeliveryDeps { let db: ReturnType | undefined; const getDbForCall = () => (db ??= getWorkerDb(env.HYPERDRIVE.connectionString)); + const iosTargets = new Map< + string, + Pick + >(); return { buildSnapshot: async (userId, organizationId) => { @@ -65,7 +69,12 @@ export function glanceableDeliveryDeps(env: Env): GlanceableDeliveryDeps { ? isNull(user_activity_tokens.organization_id) : eq(user_activity_tokens.organization_id, organizationId); const rows = await getDbForCall() - .select({ token: user_activity_tokens.token, kind: user_activity_tokens.kind }) + .select({ + token: user_activity_tokens.token, + kind: user_activity_tokens.kind, + id: user_activity_tokens.id, + updated_at: user_activity_tokens.updated_at, + }) .from(user_activity_tokens) .where( and( @@ -74,9 +83,19 @@ export function glanceableDeliveryDeps(env: Env): GlanceableDeliveryDeps { inArray(user_activity_tokens.kind, ['ios_activity', 'ios_push_to_start']) ) ); - return rows.map(row => ({ token: row.token, kind: row.kind as IosActivityToken['kind'] })); + for (const row of rows) { + if (row.kind === 'ios_activity') iosTargets.set(row.token, row); + } + return rows.map(row => ({ ...row, kind: row.kind as IosActivityToken['kind'] })); }, - sendIosLiveActivity: async (tokens, contentState, timestampSeconds, isCurrent) => { + sendIosLiveActivity: async ( + tokens, + contentState, + timestampSeconds, + isCurrent, + beforeEnd, + onEndRejected + ) => { const credentials = await readApnsCredentials(env); if (credentials === null || (isCurrent && !(await isCurrent()))) return; const result = await sendLiveActivityApns({ @@ -86,6 +105,24 @@ export function glanceableDeliveryDeps(env: Env): GlanceableDeliveryDeps { nowSeconds: Math.floor(Date.now() / 1000), timestampSeconds, isCurrent, + beforeEnd, + onEndRejected, + onEnded: async token => { + const target = iosTargets.get(token); + if (!target) return; + // A delayed end must not delete a scope subscription, another activity, + // or a registration refreshed since this delivery selected its target. + await getDbForCall() + .delete(user_activity_tokens) + .where( + and( + eq(user_activity_tokens.id, target.id), + eq(user_activity_tokens.token, token), + eq(user_activity_tokens.kind, 'ios_activity'), + eq(user_activity_tokens.updated_at, target.updated_at) + ) + ); + }, }); if (result.failed > 0) { console.warn('Some Live Activity APNs sends failed', { diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts index b7fa563ffa..5e91ce270f 100644 --- a/services/notifications/src/lib/glanceable-delivery.test.ts +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -51,7 +51,7 @@ function fakeDeps(overrides: Partial = {}): { const deps: GlanceableDeliveryDeps = { buildSnapshot: vi.fn(async () => snapshot), - listIosActivityTokens: vi.fn(async () => [] as IosActivityToken[]), + listIosActivityTokens: vi.fn(async () => []), sendIosLiveActivity: vi.fn(async (_tokens, _contentState) => { calls.iosSends.push([_tokens, _contentState]); }), @@ -80,7 +80,13 @@ describe('NotificationsService.refreshGlanceableSessions', () => { type Scope = { userId: string; organizationId: string | null }; type ApnsPayload = { - aps: { event: string; timestamp: number; 'content-state': GlanceableApnsContentState }; + token: string; + aps: { + event: string; + timestamp: number; + 'dismissal-date'?: number; + 'content-state': GlanceableApnsContentState; + }; }; function setupService( @@ -90,8 +96,11 @@ describe('NotificationsService.refreshGlanceableSessions', () => { response?: (scope: Scope) => Response | Promise; beforeIosTokens?: () => Promise; iosTokenKind?: IosActivityToken['kind']; + iosTokens?: Array>; privateKey?: () => Promise; - beforeApnsResponse?: () => Promise; + beforeApnsDelivery?: (token: string) => Promise; + beforeApnsResponse?: (token: string) => Promise; + apnsStatus?: (token: string) => number; expoAccessToken?: () => Promise; } = {} ) { @@ -99,6 +108,33 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const apns: ApnsPayload[] = []; const queries: Array<{ sql: string; params: unknown[] }> = []; const requestedScopes: Scope[] = []; + const activityRows = new Map< + string, + Partial & { id: string; kind: IosActivityToken['kind']; updated_at: string } + >( + ( + options.iosTokens ?? + (options.privateKey + ? [{ token: 'activity-token', kind: options.iosTokenKind ?? 'ios_activity' }] + : []) + ).map(({ token, kind, ...scope }, index) => [ + token, + { + ...scope, + id: `row-${index}`, + kind, + updated_at: '2026-08-27 10:00:00+00', + }, + ]) + ); + const activities = new Map( + [...activityRows] + .filter(([, row]) => row.kind === 'ios_activity') + .map(([token]) => [ + token, + { ended: false, timestamp: 0, contentState: toGlanceableContentState(snapshot) }, + ]) + ); const sessions = [ { id: 'personal', userId: 'usr_1', organizationId: null }, { id: 'org-a', userId: 'usr_1', organizationId: 'org-1' }, @@ -126,13 +162,37 @@ describe('NotificationsService.refreshGlanceableSessions', () => { ], }; } + // Honor the emitted predicates, including absent guards, rather than + // making the fake protect rows that the real query would expose or delete. + const matches = (column: string, value: string | null) => { + if (sql.includes(`"${column}" is null`)) return value === null; + const predicate = sql.match(new RegExp(`"${column}" = \\$(\\d+)`)); + return predicate === null || params[Number(predicate[1]) - 1] === value; + }; + if (sql.startsWith('delete from "user_activity_tokens"')) { + for (const [key, row] of activityRows) { + if ( + matches('id', row.id) && + matches('token', key) && + matches('kind', row.kind) && + matches('updated_at', row.updated_at) + ) { + activityRows.delete(key); + } + } + return { rows: [] }; + } if (sql.includes('from "user_activity_tokens"')) { if (params.includes('android_ongoing')) return { rows: [['subscription']] }; await options.beforeIosTokens?.(); return { - rows: options.privateKey - ? [['activity-token', options.iosTokenKind ?? 'ios_activity']] - : [], + rows: [...activityRows] + .filter( + ([, row]) => + matches('user_id', row.userId ?? 'usr_1') && + matches('organization_id', row.organizationId ?? null) + ) + .map(([token, row]) => [token, row.kind, row.id, row.updated_at]), }; } if (sql.includes('from "user_notification_preferences"')) { @@ -147,10 +207,32 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); vi.stubGlobal('fetch', async (url: string, init: RequestInit) => { if (typeof init.body !== 'string') throw new Error('Expected a JSON request body'); - if (url === 'https://api.push.apple.com/3/device/activity-token') { - await options.beforeApnsResponse?.(); - apns.push(JSON.parse(init.body) as ApnsPayload); - return new Response(null, { status: 200 }); + const apnsPrefix = 'https://api.push.apple.com/3/device/'; + if (url.startsWith(apnsPrefix)) { + const token = url.slice(apnsPrefix.length); + const request = { ...(JSON.parse(init.body) as ApnsPayload), token }; + const status = options.apnsStatus?.(token) ?? 200; + await options.beforeApnsDelivery?.(token); + apns.push(request); + if (status === 200) { + if (request.aps.event === 'start') { + activities.set(`started-${apns.length}`, { + ended: false, + timestamp: request.aps.timestamp, + contentState: request.aps['content-state'], + }); + } else { + const activity = activities.get(token); + // ActivityKit never revives an ended activity, even with a newer timestamp. + if (activity && !activity.ended && request.aps.timestamp > activity.timestamp) { + activity.ended = request.aps.event === 'end'; + activity.timestamp = request.aps.timestamp; + activity.contentState = request.aps['content-state']; + } + } + } + await options.beforeApnsResponse?.(token); + return new Response(null, { status }); } expect(url).toBe('https://snapshot.test/api/internal/glanceable-agents-snapshot'); expect(new Headers(init.headers).get('accept')).toBe('application/json'); @@ -196,7 +278,20 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }; const createService = () => new NotificationsService(createExecutionContext(), serviceEnv as never); - return { service: createService(), createService, messages, apns, queries, requestedScopes }; + return { + service: createService(), + createService, + messages, + apns, + queries, + requestedScopes, + activityRows, + activities, + liveActivityProps: () => + [...activities.values()] + .filter(activity => !activity.ended) + .map(activity => JSON.parse(activity.contentState.props) as Record), + }; } function freshSnapshot(overrides: Partial = {}): ActiveAgentsGlanceable { @@ -462,6 +557,700 @@ describe('NotificationsService.refreshGlanceableSessions', () => { return `-----BEGIN PRIVATE KEY-----\n${btoa(String.fromCharCode(...der))}\n-----END PRIVATE KEY-----`; } + it('ends empty work and starts later eligible work without mobile token cleanup', async () => { + const pem = await generateTestPrivateKeyPem(); + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'old-activity', kind: 'ios_activity' }, + ], + response: () => Response.json(current), + }); + await createService().refreshGlanceableSessions(personalRefresh); + expect([...activityRows.keys()]).toEqual(['scope-token']); + expect(apns[0]).toMatchObject({ + token: 'old-activity', + aps: { event: 'end', 'dismissal-date': Date.parse('2026-08-27T10:00:08.000Z') / 1000 }, + }); + expect(JSON.parse(apns[0].aps['content-state'].props)).toEqual({ + status: 'empty', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }); + + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + current = freshSnapshot({ running: 0, reconnecting: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['old-activity', 'end'], + ['scope-token', 'start'], + ]); + expect(JSON.parse(apns[1].aps['content-state'].props)).toMatchObject({ + reconnecting: 1, + eligibleStartedAt: '2026-08-27T10:00:01.000Z', + }); + expect([...activityRows.keys()]).toEqual(['scope-token']); + }); + + it('retires only successful ends and preserves failed targets and scope subscriptions', async () => { + const pem = await generateTestPrivateKeyPem(); + const { service, activityRows } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'ended-token', kind: 'ios_activity' }, + { token: 'failed-token', kind: 'ios_activity' }, + ], + apnsStatus: token => (token === 'failed-token' ? 503 : 200), + response: () => + Response.json(freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null })), + }); + await service.refreshGlanceableSessions(personalRefresh); + expect([...activityRows.keys()]).toEqual(['scope-token', 'failed-token']); + }); + + it.each(['identity', 'version'] as const)( + 'preserves a renewed registration %s and unrelated targets after delayed cleanup', + async renewal => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, activityRows, activities, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'activity-token', kind: 'ios_activity' }, + ], + beforeApnsDelivery: async () => { + if (!first) return; + first = false; + started.resolve(); + await release.promise; + }, + response: () => Response.json(current), + }); + const ending = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + try { + activityRows.set('activity-token', { + id: renewal === 'identity' ? 'renewed-row' : 'row-1', + kind: 'ios_activity', + updated_at: renewal === 'version' ? '2026-08-27 10:00:01+00' : '2026-08-27 10:00:00+00', + }); + activityRows.set('new-activity', { + id: 'new-row', + kind: 'ios_activity', + updated_at: '2026-08-27 10:00:01+00', + }); + activities.set('new-activity', { + ended: false, + timestamp: 0, + contentState: toGlanceableContentState(snapshot), + }); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + } finally { + release.resolve(); + await ending; + } + expect([...activityRows.keys()]).toEqual(['scope-token', 'activity-token', 'new-activity']); + expect(activities.get('activity-token')?.ended).toBe(true); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + } + ); + + it.each([ + ['identity', true], + ['version', true], + ['reregistration', true], + ['identity', false], + ['version', false], + ['reregistration', false], + ] as const)( + 'keeps the same native token retired after %s and an end-first delayed response (push-to-start: %s)', + async (renewal, withPushToStart) => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const renewedRow = { + id: renewal === 'version' ? `row-${withPushToStart ? 1 : 0}` : 'renewed-row', + kind: 'ios_activity' as const, + updated_at: renewal === 'identity' ? '2026-08-27 10:00:00+00' : '2026-08-27 10:00:01+00', + }; + const iosTokens: IosActivityToken[] = []; + if (withPushToStart) iosTokens.push({ token: 'scope-token', kind: 'ios_push_to_start' }); + iosTokens.push({ token: 'old-activity', kind: 'ios_activity' }); + const { createService, apns, activityRows, activities, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens, + response: () => Response.json(current), + beforeApnsDelivery: async token => { + // The same native token renews after selection, before ActivityKit ends it. + if (token === 'old-activity') activityRows.set(token, renewedRow); + }, + beforeApnsResponse: async token => { + if (token !== 'old-activity' || !first) return; + first = false; + started.resolve(); + await release.promise; + }, + }); + const ending = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + try { + expect(liveActivityProps()).toEqual([]); + if (renewal === 'reregistration') { + activityRows.delete('old-activity'); + await createService().refreshGlanceableSessions(personalRefresh); + activityRows.set('old-activity', renewedRow); + } + if (!withPushToStart) { + activityRows.set('live-activity', { ...renewedRow, id: 'live-row' }); + activities.set('live-activity', { + ended: false, + timestamp: 0, + contentState: toGlanceableContentState(snapshot), + }); + } + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + for (const [token, activity] of activities) { + if (!activity.ended) { + activityRows.set(token, { ...renewedRow, id: 'live-row' }); + } + } + } finally { + release.resolve(); + await ending; + } + expect(activityRows.get('old-activity')).toEqual(renewedRow); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:02.000Z')); + current = freshSnapshot({ running: 0, reconnecting: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([ + { + running: 0, + needsInput: 0, + reconnecting: 1, + eligibleStartedAt: '2026-08-27T10:00:01.000Z', + }, + ]); + const liveToken = withPushToStart ? 'started-2' : 'live-activity'; + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['old-activity', 'end'], + withPushToStart ? ['scope-token', 'start'] : [liveToken, 'update'], + [liveToken, 'update'], + ]); + expect([...activityRows.keys()]).toEqual( + withPushToStart ? ['scope-token', 'old-activity', liveToken] : ['old-activity', liveToken] + ); + } + ); + + it.each(['end-first', 'start-first'] as const)( + 'keeps fresh work on a live activity with %s delivery and a delayed end response', + async arrivalOrder => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let delayed = false; + const delayEnd = async (token: string) => { + if (token !== 'old-activity' || delayed) return; + delayed = true; + started.resolve(); + await release.promise; + }; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows, liveActivityProps, messages } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'old-activity', kind: 'ios_activity' }, + ], + response: () => Response.json(current), + beforeApnsDelivery: arrivalOrder === 'start-first' ? delayEnd : undefined, + beforeApnsResponse: arrivalOrder === 'end-first' ? delayEnd : undefined, + }); + const ending = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + try { + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + } finally { + release.resolve(); + await ending; + } + expect(liveActivityProps()).toEqual([ + { + status: 'happy', + running: 0, + needsInput: 1, + reconnecting: 0, + eligibleStartedAt: '2026-08-27T10:00:01.000Z', + }, + ]); + expect([...activityRows.keys()]).toEqual(['scope-token']); + expect(apns.map(request => request.aps.event)).toEqual( + arrivalOrder === 'end-first' ? ['end', 'start'] : ['start', 'end'] + ); + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'happy', needsInput: 1 }, + { status: 'happy', needsInput: 1 }, + ]); + } + ); + + it('keeps other users and organizations live while a personal end response is delayed', async () => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'old-activity', kind: 'ios_activity' }, + { token: 'org-scope', kind: 'ios_push_to_start', organizationId: 'org-1' }, + { token: 'org-activity', kind: 'ios_activity', organizationId: 'org-1' }, + { token: 'other-scope', kind: 'ios_push_to_start', userId: 'usr_2' }, + { token: 'other-activity', kind: 'ios_activity', userId: 'usr_2' }, + ], + response: scope => + Response.json( + scope.userId === 'usr_2' + ? freshSnapshot({ running: 3 }) + : scope.organizationId === 'org-1' + ? freshSnapshot({ running: 7, organizationBound: true }) + : current + ), + beforeApnsResponse: async token => { + if (token !== 'old-activity' || !first) return; + first = false; + started.resolve(); + await release.promise; + }, + }); + const ending = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + try { + await createService().refreshGlanceableSessions({ + userId: 'usr_1', + cliSessionIds: ['org-a'], + }); + await createService().refreshGlanceableSessions({ + userId: 'usr_2', + cliSessionIds: ['other-personal'], + }); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + } finally { + release.resolve(); + await ending; + } + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['old-activity', 'end'], + ['org-activity', 'update'], + ['other-activity', 'update'], + ['scope-token', 'start'], + ]); + expect([...activityRows.keys()]).toEqual([ + 'scope-token', + 'org-scope', + 'org-activity', + 'other-scope', + 'other-activity', + ]); + expect(liveActivityProps().map(props => [props.running, props.needsInput])).toEqual([ + [7, 0], + [3, 0], + [0, 1], + ]); + }); + + it.each(['', 'invalid-key'])( + 'leaves a live target usable when end credentials are unusable (%s)', + async unusableKey => { + const pem = await generateTestPrivateKeyPem(); + let configured = false; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows, liveActivityProps } = setupService({ + privateKey: async () => (configured ? pem : unusableKey), + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'activity-token', kind: 'ios_activity' }, + ], + response: () => Response.json(current), + }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(apns).toEqual([]); + expect([...activityRows.keys()]).toEqual(['scope-token', 'activity-token']); + configured = true; + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['activity-token', 'update'], + ]); + } + ); + + it('recovers after a delivered end loses its HTTP response across coordinator reconstruction', async () => { + const pem = await generateTestPrivateKeyPem(); + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows, activities, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'old-activity', kind: 'ios_activity' }, + ], + response: () => Response.json(current), + beforeApnsResponse: async token => { + if (token === 'old-activity') throw new Error('Connection lost after delivery'); + }, + }); + await createService().refreshGlanceableSessions(personalRefresh); + expect([...activityRows.keys()]).toEqual(['scope-token', 'old-activity']); + expect(liveActivityProps()).toEqual([]); + + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + // Simulate the new activity's token registration, not cleanup of the dead token. + for (const [token, activity] of activities) { + if (activity.ended) continue; + activityRows.set(token, { + id: 'new-row', + kind: 'ios_activity', + updated_at: '2026-08-27 10:00:01+00', + }); + } + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:02.000Z')); + current = freshSnapshot({ running: 0, reconnecting: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([ + { running: 0, needsInput: 0, reconnecting: 1, eligibleStartedAt: '2026-08-27T10:00:01.000Z' }, + ]); + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['old-activity', 'end'], + ['scope-token', 'start'], + ['started-2', 'update'], + ]); + }); + + it.each([false, true])( + 'updates the live target directly after a rejected end (push-to-start: %s)', + async withPushToStart => { + const pem = await generateTestPrivateKeyPem(); + let rejected = true; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const iosTokens: IosActivityToken[] = [{ token: 'old-activity', kind: 'ios_activity' }]; + if (withPushToStart) iosTokens.push({ token: 'scope-token', kind: 'ios_push_to_start' }); + const { createService, apns, activityRows, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens, + response: () => Response.json(current), + apnsStatus: () => (rejected ? 503 : 200), + }); + await createService().refreshGlanceableSessions(personalRefresh); + rejected = false; + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([ + { running: 0, needsInput: 1, eligibleStartedAt: '2026-08-27T10:00:01.000Z' }, + ]); + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['old-activity', 'end'], + ['old-activity', 'update'], + ]); + expect([...activityRows.keys()]).toEqual(iosTokens.map(({ token }) => token)); + } + ); + + it('starts fresh work after an unregistered end target across reconstruction', async () => { + const pem = await generateTestPrivateKeyPem(); + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows, activities, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'old-activity', kind: 'ios_activity' }, + ], + response: () => Response.json(current), + apnsStatus: token => (token === 'old-activity' ? 410 : 200), + }); + const oldActivity = activities.get('old-activity'); + if (!oldActivity) throw new Error('Missing native activity fixture'); + oldActivity.ended = true; + await createService().refreshGlanceableSessions(personalRefresh); + + expect([...activityRows.keys()]).toEqual(['scope-token']); + // A delayed registration retry cannot make the same inactive native token live. + activityRows.set('old-activity', { + id: 'renewed-row', + kind: 'ios_activity', + updated_at: '2026-08-27 10:00:01+00', + }); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['old-activity', 'end'], + ['scope-token', 'start'], + ]); + }); + + it.each([ + ['older', 'lost'], + ['older', 'accepted'], + ['newer', 'lost'], + ['newer', 'accepted'], + ] as const)( + 'keeps the other end obligation when the %s attempt rejects (%s response)', + async (rejectedAttempt, otherResponse) => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + const rejectedIndex = rejectedAttempt === 'older' ? 1 : 2; + let requests = 0; + let responses = 0; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows, activities, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'old-activity', kind: 'ios_activity' }, + ], + response: () => Response.json(current), + apnsStatus: token => { + if (token !== 'old-activity') return 200; + requests += 1; + return requests === rejectedIndex ? 503 : 200; + }, + beforeApnsResponse: async token => { + if (token !== 'old-activity') return; + const response = ++responses; + if (response === 1) { + started.resolve(); + await release.promise; + } + if (response !== rejectedIndex) { + if (otherResponse === 'lost') throw new Error('Connection lost after delivery'); + // Keep the same token registered after successful version-guarded cleanup. + activityRows.set(token, { + id: 'renewed-row', + kind: 'ios_activity', + updated_at: '2026-08-27 10:00:01+00', + }); + } + }, + }); + const firstEnd = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + try { + await createService().refreshGlanceableSessions(personalRefresh); + if (rejectedAttempt === 'older') { + release.resolve(); + await firstEnd; + } + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + } finally { + release.resolve(); + await firstEnd; + } + expect(activityRows.has('old-activity')).toBe(true); + for (const [token, activity] of activities) { + if (!activity.ended) { + activityRows.set(token, { + id: 'live-row', + kind: 'ios_activity', + updated_at: '2026-08-27 10:00:01+00', + }); + } + } + current = freshSnapshot({ running: 0, reconnecting: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 0, reconnecting: 1 }]); + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['old-activity', 'end'], + ['old-activity', 'end'], + ['scope-token', 'start'], + ['started-3', 'update'], + ]); + } + ); + + it('releases both rejected end attempts when the older response completes last', async () => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + let rejected = true; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'old-activity', kind: 'ios_activity' }, + ], + response: () => Response.json(current), + apnsStatus: () => (rejected ? 503 : 200), + beforeApnsResponse: async () => { + if (!first) return; + first = false; + started.resolve(); + await release.promise; + }, + }); + const firstEnd = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + try { + await createService().refreshGlanceableSessions(personalRefresh); + } finally { + release.resolve(); + await firstEnd; + } + rejected = false; + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + expect(apns.map(request => request.aps.event)).toEqual(['end', 'end', 'update']); + }); + + it('keeps a native end obligation across scope renewal and a rejected attempt', async () => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + let rejected = false; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'old-activity', kind: 'ios_activity' }, + { token: 'org-scope', kind: 'ios_push_to_start', organizationId: 'org-1' }, + ], + response: scope => + Response.json({ + ...current, + scopeKey: scope.organizationId ?? 'personal', + organizationBound: scope.organizationId !== null, + }), + apnsStatus: () => (rejected ? 503 : 200), + beforeApnsResponse: async () => { + if (!first) return; + first = false; + started.resolve(); + await release.promise; + }, + }); + const firstEnd = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + try { + activityRows.set('old-activity', { + id: 'row-0', + kind: 'ios_activity', + organizationId: 'org-1', + updated_at: '2026-08-27 10:00:01+00', + }); + rejected = true; + // Both scopes use revision 1. Rejection in one must not release the other's end. + await createService().refreshGlanceableSessions({ + userId: 'usr_1', + cliSessionIds: ['org-a'], + }); + rejected = false; + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions({ + userId: 'usr_1', + cliSessionIds: ['org-a'], + }); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + } finally { + release.resolve(); + await firstEnd; + } + expect([...activityRows.keys()]).toEqual(['old-activity', 'org-scope']); + expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ + ['old-activity', 'end'], + ['old-activity', 'end'], + ['org-scope', 'start'], + ]); + }); + + it('retries a rejected end without starting an empty activity', async () => { + const pem = await generateTestPrivateKeyPem(); + let rejected = true; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows, liveActivityProps } = setupService({ + privateKey: async () => pem, + iosTokens: [ + { token: 'scope-token', kind: 'ios_push_to_start' }, + { token: 'old-activity', kind: 'ios_activity' }, + ], + response: () => Response.json(current), + apnsStatus: () => (rejected ? 503 : 200), + }); + await createService().refreshGlanceableSessions(personalRefresh); + expect([...activityRows.keys()]).toEqual(['scope-token', 'old-activity']); + rejected = false; + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toEqual([]); + expect([...activityRows.keys()]).toEqual(['scope-token']); + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 1 }]); + expect(apns.map(request => request.aps.event)).toEqual(['end', 'end', 'start']); + }); + + it('fences a terminal send delayed during credentials after fresh work arrives', async () => { + const pem = await generateTestPrivateKeyPem(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + let first = true; + let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + const { createService, apns, activityRows } = setupService({ + response: () => Response.json(current), + privateKey: async () => { + if (first) { + first = false; + started.resolve(); + await release.promise; + } + return pem; + }, + }); + const ending = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + current = freshSnapshot({ running: 0, needsInput: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + release.resolve(); + await ending; + expect(apns.map(request => request.aps.event)).toEqual(['update']); + expect(JSON.parse(apns[0].aps['content-state'].props)).toMatchObject({ + status: 'happy', + needsInput: 1, + }); + expect([...activityRows.keys()]).toEqual(['activity-token']); + }); + it('keeps an in-flight update timestamp below idle when the older request finishes last', async () => { const pem = await generateTestPrivateKeyPem(); const started = Promise.withResolvers(); @@ -471,7 +1260,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const { createService, messages, apns } = setupService({ response: () => Response.json(current), privateKey: async () => pem, - beforeApnsResponse: async () => { + beforeApnsDelivery: async () => { if (first) { first = false; started.resolve(); @@ -486,7 +1275,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); release.resolve(); await busy; - expect(apns.map(request => request.aps.event)).toEqual(['update', 'update']); + expect(apns.map(request => request.aps.event)).toEqual(['end', 'update']); expect(apns.map(request => JSON.parse(request.aps['content-state'].props))).toMatchObject([ { status: 'empty', running: 0, eligibleStartedAt: null }, { status: 'happy', running: 2 }, @@ -499,10 +1288,10 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); it.each([ - ['ios_push_to_start', 'credentials', 'start'], - ['ios_push_to_start', 'signing', 'start'], - ['ios_activity', 'credentials', 'update'], - ['ios_activity', 'signing', 'update'], + ['ios_push_to_start', 'credentials', null], + ['ios_push_to_start', 'signing', null], + ['ios_activity', 'credentials', 'end'], + ['ios_activity', 'signing', 'end'], ] as const)('fences superseded %s delivery after delayed %s', async (kind, delayed, event) => { const pem = await generateTestPrivateKeyPem(); const started = Promise.withResolvers(); @@ -542,18 +1331,22 @@ describe('NotificationsService.refreshGlanceableSessions', () => { event: request.aps.event, props: JSON.parse(request.aps['content-state'].props), })) - ).toEqual([ - { - event, - props: { - status: 'empty', - running: 0, - needsInput: 0, - reconnecting: 0, - eligibleStartedAt: null, - }, - }, - ]); + ).toEqual( + event === null + ? [] + : [ + { + event, + props: { + status: 'empty', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }, + }, + ] + ); expect(messages.map(message => message.data)).toMatchObject([ { status: 'empty', running: 0, eligibleStartedAt: null }, { status: 'empty', running: 0, eligibleStartedAt: null }, @@ -781,34 +1574,49 @@ describe('NotificationsService.refreshGlanceableSessions', () => { describe('apnsSendsForTokens', () => { it('sends update only to the activity tokens when one exists, never start to push-to-start', () => { expect( - apnsSendsForTokens([ - { token: 'ptt-token', kind: 'ios_push_to_start' }, - { token: 'activity-token', kind: 'ios_activity' }, - ]) + apnsSendsForTokens( + [ + { token: 'ptt-token', kind: 'ios_push_to_start' }, + { token: 'activity-token', kind: 'ios_activity' }, + ], + true + ) ).toEqual([{ token: 'activity-token', event: 'update' }]); }); it('sends start to the push-to-start token when no activity token exists', () => { - expect(apnsSendsForTokens([{ token: 'ptt-token', kind: 'ios_push_to_start' }])).toEqual([ + expect(apnsSendsForTokens([{ token: 'ptt-token', kind: 'ios_push_to_start' }], true)).toEqual([ { token: 'ptt-token', event: 'start' }, ]); }); - it('sends update to every activity token when several are registered', () => { - expect( - apnsSendsForTokens([ - { token: 'ptt-token', kind: 'ios_push_to_start' }, - { token: 'activity-token-1', kind: 'ios_activity' }, - { token: 'activity-token-2', kind: 'ios_activity' }, - ]) - ).toEqual([ - { token: 'activity-token-1', event: 'update' }, - { token: 'activity-token-2', event: 'update' }, - ]); - }); + it.each([ + [true, 'update'], + [false, 'end'], + ] as const)( + 'sends the eligible=%s event to every activity without starting another', + (eligible, event) => { + expect( + apnsSendsForTokens( + [ + { token: 'ptt-token', kind: 'ios_push_to_start' }, + { token: 'activity-token-1', kind: 'ios_activity' }, + { token: 'activity-token-2', kind: 'ios_activity' }, + ], + eligible + ) + ).toEqual([ + { token: 'activity-token-1', event }, + { token: 'activity-token-2', event }, + ]); + } + ); - it('sends nothing when no iOS token exists', () => { - expect(apnsSendsForTokens([])).toEqual([]); + it('does not start an activity for empty work', () => { + expect(apnsSendsForTokens([{ token: 'ptt-token', kind: 'ios_push_to_start' }], false)).toEqual( + [] + ); + expect(apnsSendsForTokens([], true)).toEqual([]); }); }); @@ -885,7 +1693,13 @@ describe('deliverGlanceableSnapshot', () => { { token: 'activity-token', kind: 'ios_activity' }, ]; const { deps, calls } = fakeDeps({ - listIosActivityTokens: vi.fn(async () => iosTokens), + listIosActivityTokens: vi.fn(async () => + iosTokens.map((token, index) => ({ + ...token, + id: `row-${index}`, + updated_at: snapshot.updatedAt, + })) + ), }); await deliverGlanceableSnapshot({ userId: 'u1', organizationId: 'org-1' }, deps); @@ -911,7 +1725,13 @@ describe('deliverGlanceableSnapshot', () => { it('sends start to the push-to-start token when no activity token exists', async () => { const iosTokens: IosActivityToken[] = [{ token: 'ptt-token', kind: 'ios_push_to_start' }]; const { deps, calls } = fakeDeps({ - listIosActivityTokens: vi.fn(async () => iosTokens), + listIosActivityTokens: vi.fn(async () => + iosTokens.map((token, index) => ({ + ...token, + id: `row-${index}`, + updated_at: snapshot.updatedAt, + })) + ), }); await deliverGlanceableSnapshot({ userId: 'u1', organizationId: 'org-1' }, deps); diff --git a/services/notifications/src/lib/glanceable-delivery.ts b/services/notifications/src/lib/glanceable-delivery.ts index 405817730a..280a256880 100644 --- a/services/notifications/src/lib/glanceable-delivery.ts +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -31,22 +31,23 @@ export type IosActivityToken = { token: string; kind: 'ios_activity' | 'ios_push export type ExpoPushToken = { token: string; locale: string | null }; /** - * Maps the registered iOS activity tokens to the APNs sends for one delivery. - * When an `ios_activity` token exists, that Live Activity is already on screen, - * so send `update` to those tokens only — never `start`, which would stack a - * second activity. Only when no `ios_activity` token exists does the - * still-registered push-to-start token get `start` to create one. + * Update eligible activities or end zero-count activities. Never start empty work. + * A push-to-start token is used only when no activity target remains, avoiding + * duplicate activities while allowing fresh work after terminal target retirement. */ export function apnsSendsForTokens( - tokens: readonly IosActivityToken[] + tokens: readonly IosActivityToken[], + eligible: boolean ): { token: string; event: LiveActivityEvent }[] { const activityTokens = tokens.filter(token => token.kind === 'ios_activity'); if (activityTokens.length > 0) { - return activityTokens.map(({ token }) => ({ token, event: 'update' })); + return activityTokens.map(({ token }) => ({ token, event: eligible ? 'update' : 'end' })); } - return tokens - .filter(token => token.kind === 'ios_push_to_start') - .map(({ token }) => ({ token, event: 'start' })); + return eligible + ? tokens + .filter(token => token.kind === 'ios_push_to_start') + .map(({ token }) => ({ token, event: 'start' })) + : []; } export function toGlanceableContentState( @@ -90,6 +91,8 @@ export function buildGlanceableExpoMessages( ); } +type IosActivityRegistration = IosActivityToken & { id: string; updated_at: string }; + export type GlanceableDeliveryDeps = { /** * Build the fresh snapshot via the web internal route. `null` means the @@ -103,17 +106,23 @@ export type GlanceableDeliveryDeps = { listIosActivityTokens: ( userId: string, organizationId: string | null - ) => Promise; + ) => Promise; sendIosLiveActivity: ( tokens: readonly { token: string; event: LiveActivityEvent }[], contentState: GlanceableApnsContentState, timestampSeconds: number, - isCurrent?: () => Promise + isCurrent?: () => Promise, + beforeEnd?: (token: string) => Promise, + onEndRejected?: (token: string) => Promise ) => Promise; /** Reserved before reading; do not assign a new timestamp after a delayed send. */ apnsTimestampSeconds?: number; /** Durable generation fence, also checked by adapters after awaits and before outbound sends. */ isCurrent?: () => Promise; + /** Atomically fence and persist an end intent before the transport sends it. */ + beforeIosEnd?: (token: string) => Promise; + /** Release the current attempt only after an explicit transport rejection. */ + onIosEndRejected?: (token: string) => Promise; listIosExpoTokens: (userId: string, organizationId: string | null) => Promise; listAndroidExpoTokens: ( userId: string, @@ -135,13 +144,16 @@ export async function deliverGlanceableSnapshot( const iosTokens = await deps.listIosActivityTokens(params.userId, params.organizationId); if (deps.isCurrent && !(await deps.isCurrent())) return; - const iosSends = apnsSendsForTokens(iosTokens); + const eligible = snapshot.running + snapshot.needsInput + snapshot.reconnecting > 0; + const iosSends = apnsSendsForTokens(iosTokens, eligible); if (iosSends.length > 0) { await deps.sendIosLiveActivity( iosSends, contentState, deps.apnsTimestampSeconds ?? Math.floor(Date.parse(snapshot.updatedAt) / 1000), - deps.isCurrent + deps.isCurrent, + deps.beforeIosEnd, + deps.onIosEndRejected ); } diff --git a/services/notifications/src/lib/glanceable-refresh.ts b/services/notifications/src/lib/glanceable-refresh.ts index ea5ba29841..20bf0a1ebc 100644 --- a/services/notifications/src/lib/glanceable-refresh.ts +++ b/services/notifications/src/lib/glanceable-refresh.ts @@ -25,6 +25,8 @@ export async function refreshGlanceableSnapshot( ): Promise { const scope = scopeSchema.parse(params); const key = `glanceable:${JSON.stringify([scope.userId, scope.organizationId])}`; + // Row renewal or temporary absence cannot prove that the native token is live. + const iosEndPrefix = (token: string) => `glanceable-ios-end:${JSON.stringify(token)}:`; const request = await storage.transaction(async tx => { const previous = refreshStateSchema.optional().parse(await tx.get(key)); const now = Date.now(); @@ -72,6 +74,7 @@ export async function refreshGlanceableSnapshot( }); if (committed === null) return; + const eligible = committed.running + committed.needsInput + committed.reconnecting > 0; await deliverGlanceableSnapshot(scope, { ...deps, buildSnapshot: async () => committed, @@ -80,5 +83,33 @@ export async function refreshGlanceableSnapshot( const current = refreshStateSchema.parse(await storage.get(key)); return current.revision === request.revision; }, + listIosActivityTokens: async (userId, organizationId) => { + const tokens = await deps.listIosActivityTokens(userId, organizationId); + const current = refreshStateSchema.parse(await storage.get(key)); + if (current.revision !== request.revision) return []; + // Empty work can retry ends. Eligible work excludes every accepted or uncertain end. + if (!eligible) return tokens; + const retiring = await Promise.all( + tokens.map(async ({ token, kind }) => + kind === 'ios_activity' + ? (await storage.list({ prefix: iosEndPrefix(token), limit: 1 })).size > 0 + : false + ) + ); + return tokens.filter((_, index) => !retiring[index]); + }, + beforeIosEnd: async token => { + return storage.transaction(async tx => { + const current = refreshStateSchema.parse(await tx.get(key)); + if (current.revision !== request.revision) return false; + // Each revision sends at most one end per token. Keep its obligation separate. + await tx.put(`${iosEndPrefix(token)}${key}:${request.revision}`, true); + return true; + }); + }, + onIosEndRejected: async token => { + // A delayed rejection releases only its attempt, not another pending or accepted end. + await storage.delete(`${iosEndPrefix(token)}${key}:${request.revision}`); + }, }); } From 5b00160b5fdade2b519db602775b1e92c1888806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 2 Sep 2026 14:53:45 +0200 Subject: [PATCH 25/43] fix(glanceable): hand iOS terminal dismissal to ActivityKit Submit the Live Activity end during publish so ActivityKit owns removal after the push handler returns. Track pending native ends by ActivityKit ID, not by a JS counter, so a recreated wrapper cannot restart a dying activity. Patch expo-widgets for getInfo() and getInstances(includeEnded) to read native state and reach terminal-retained instances on privacy. --- .../glanceable-ios/ios-sink.native.test.ts | 301 ++++++++++++++++++ .../src/glanceable-ios/ios-sink.test.ts | 243 +++++++++++--- apps/mobile/src/glanceable-ios/ios-sink.ts | 257 +++++++++------ apps/mobile/src/lib/glanceable/publisher.ts | 4 +- .../src/lib/glanceable/sink-registry.ts | 2 + apps/mobile/src/lib/notifications.test.ts | 271 ++++++++++++++-- apps/mobile/src/lib/notifications.ts | 27 +- patches/expo-widgets@57.0.11.patch | 205 +++++++++++- pnpm-lock.yaml | 8 +- 9 files changed, 1145 insertions(+), 173 deletions(-) create mode 100644 apps/mobile/src/glanceable-ios/ios-sink.native.test.ts diff --git a/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts new file mode 100644 index 0000000000..8df12bb359 --- /dev/null +++ b/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts @@ -0,0 +1,301 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; + +type NativeRecord = { + id: string; + state: 'active' | 'stale' | 'ended' | 'dismissed'; + props: Partial; + dismissAt: number | null; + updateGate: Promise | null; + endGate: Promise | null; + endSubmitted: (() => void) | null; + policies: string[]; +}; + +// Model only the native boundary. The sink and expo-widgets adapter remain real. +// These records survive JS module recreation, not a native process restart. +const native = vi.hoisted(() => { + const records: NativeRecord[] = []; + const ignoredUpdates: string[] = []; + const snapshots: unknown[] = []; + const failures = { info: false }; + function add(props: string): NativeRecord { + const record: NativeRecord = { + id: `activity-${records.length}`, + state: 'active', + props: JSON.parse(props) as Partial, + dismissAt: null, + updateGate: null, + endGate: null, + endSubmitted: null, + policies: [], + }; + records.push(record); + return record; + } + function wrap(record: NativeRecord) { + return { + getInfo: () => { + if (failures.info) { + throw new Error('Native state temporarily unavailable'); + } + return { id: record.id, state: record.state }; + }, + getPushToken: async () => { + await Promise.resolve(); + return `token-${record.id}`; + }, + addListener: () => ({ remove: () => undefined }), + update: async (props: string) => { + await record.updateGate; + if (record.state === 'active' || record.state === 'stale') { + record.props = JSON.parse(props) as Partial; + } else { + ignoredUpdates.push(record.id); + } + }, + // eslint-disable-next-line max-params -- match the expo-widgets native bridge + end: async (policy: string, afterDate?: number, props?: string, _contentDate?: number) => { + record.policies.push(policy); + record.endSubmitted?.(); + await record.endGate; + // Ended content cannot update again; immediate dismissal can remove it. + if (record.state === 'active' || record.state === 'stale') { + record.props = JSON.parse(props ?? '{}') as Partial; + } + record.state = policy === 'immediate' ? 'dismissed' : 'ended'; + record.dismissAt = policy === 'immediate' ? Date.now() : (afterDate ?? null); + }, + }; + } + return { records, ignoredUpdates, snapshots, failures, add, wrap }; +}); + +vi.mock('expo-widgets', async () => { + const { after } = await import('expo-widgets/src/Widgets'); + return { after }; +}); +vi.mock('expo-widgets/src/ExpoWidgets', () => ({ + default: { + LiveActivityFactory: function LiveActivityFactory() { + return { + start: (props: string) => native.wrap(native.add(props)), + getInstances: (includeEnded = false) => + native.records + .filter(record => + includeEnded + ? record.state !== 'dismissed' + : record.state === 'active' || record.state === 'stale' + ) + .toReversed() + .map(record => native.wrap(record)), + }; + }, + }, +})); +vi.mock('./active-agents-live-activity', async () => { + const { LiveActivityFactory } = await import('expo-widgets/src/Widgets'); + return { + ActiveAgentsLiveActivity: new LiveActivityFactory('ActiveAgentsLiveActivity', () => ({ + banner: null, + })), + }; +}); +vi.mock('./active-agents-widget', () => ({ + ActiveAgentsWidget: { + updateSnapshot: (props: unknown) => native.snapshots.push(props), + updateTimeline: () => undefined, + }, +})); + +const NOW = Date.parse('2026-01-02T00:00:00Z'); +const CTX = { userId: 'u1', organizationId: null }; + +function snapshot(sessions: { status: string }[], revision = 0): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + ...CTX, + sessions, + now: NOW + revision, + previousRevision: revision, + previousEligibleStartedAt: new Date(NOW - 60_000).toISOString(), + }); +} + +async function loadSink() { + const { iosSink } = await import('./ios-sink'); + const { registerGlanceableSink } = await import('@/lib/glanceable/sink-registry'); + registerGlanceableSink(iosSink); + return iosSink; +} + +function firstActivity(): NativeRecord { + const record = native.records[0]; + if (!record) { + throw new Error('The native activity was not created'); + } + return record; +} + +function remoteEnd(record: NativeRecord): void { + record.state = 'ended'; + record.props = { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }; + record.dismissAt = Date.now() + 8000; +} + +beforeEach(() => { + vi.resetModules(); + vi.useFakeTimers(); + vi.setSystemTime(NOW); + native.records.length = 0; + native.ignoredUpdates.length = 0; + native.snapshots.length = 0; + native.failures.info = false; +}); +afterEach(() => vi.useRealTimers()); + +describe('native adapter recovery', () => { + it.each(['publish then start', 'start only'])( + 'recovers a remotely ended cached handle through %s without an empty Expo publication', + async path => { + const sink = await loadSink(); + sink.startOrUpdate(snapshot([{ status: 'busy' }]), CTX); + remoteEnd(firstActivity()); + const fresh = snapshot([{ status: 'busy' }, { status: 'retry' }], 2); + if (path === 'publish then start') { + sink.publish(fresh); + } + sink.startOrUpdate(fresh, CTX); + await Promise.resolve(); + + expect(native.records.filter(record => record.state === 'active')).toMatchObject([ + { + props: { + running: 1, + reconnecting: 1, + eligibleStartedAt: new Date(NOW - 60_000).toISOString(), + }, + }, + ]); + expect(native.records).toHaveLength(2); + expect(native.ignoredUpdates).toEqual([]); + expect(firstActivity().props.running).toBe(0); + } + ); + + it('adopts fresh native work instead of updating the remotely ended cached handle', async () => { + const sink = await loadSink(); + sink.startOrUpdate(snapshot([{ status: 'busy' }]), CTX); + remoteEnd(firstActivity()); + const adopted = native.add(JSON.stringify({ running: 9 })); + const fresh = snapshot([{ status: 'question' }], 2); + sink.publish(fresh); + sink.startOrUpdate(fresh, CTX); + await Promise.resolve(); + + expect(native.records).toHaveLength(2); + expect(adopted).toMatchObject({ state: 'active', props: { needsInput: 1, running: 0 } }); + expect(native.ignoredUpdates).toEqual([]); + }); + + it('excludes only the pending native ID when discovery recreates wrappers', async () => { + const sink = await loadSink(); + sink.startOrUpdate(snapshot([{ status: 'busy' }]), CTX); + const update = Promise.withResolvers(); + firstActivity().updateGate = update.promise; + sink.publish(snapshot([{ status: 'busy' }], 1)); + sink.publish(snapshot([], 2)); + const adopted = native.add(JSON.stringify({ running: 9 })); + const fresh = snapshot([{ status: 'question' }], 3); + sink.publish(fresh); + sink.startOrUpdate(fresh, CTX); + update.resolve(undefined); + await sink.waitForNativeTerminal?.(); + + expect(native.records).toHaveLength(2); + expect(firstActivity()).toMatchObject({ state: 'ended', dismissAt: NOW + 8000 }); + expect(adopted).toMatchObject({ state: 'active', props: { needsInput: 1, running: 0 } }); + }); + + it('retries a failed native state read without duplicating or updating an unverified handle', async () => { + const sink = await loadSink(); + sink.startOrUpdate(snapshot([{ status: 'busy' }]), CTX); + native.failures.info = true; + sink.startOrUpdate(snapshot([{ status: 'question' }], 1), CTX); + expect(firstActivity().props).toMatchObject({ running: 1, needsInput: 0 }); + native.failures.info = false; + sink.startOrUpdate(snapshot([{ status: 'question' }], 2), CTX); + await Promise.resolve(); + + expect(native.records).toHaveLength(1); + expect(firstActivity().props).toMatchObject({ running: 0, needsInput: 1 }); + }); +}); + +describe('native adapter terminal privacy', () => { + it.each([ + ['local', 'privacy'], + ['local', 'signed_out'], + ['remote', 'privacy'], + ['remote', 'signed_out'], + ] as const)('dismisses %s terminal content after JS restart for %s', async (source, status) => { + const sink = await loadSink(); + sink.startOrUpdate(snapshot([{ status: 'busy' }]), CTX); + if (source === 'local') { + sink.publish(snapshot([], 1)); + await sink.waitForNativeTerminal?.(); + } else { + remoteEnd(firstActivity()); + } + expect(firstActivity().dismissAt).toBe(NOW + 8000); + + vi.resetModules(); + const restarted = await loadSink(); + const cleanup = await import('@/lib/glanceable/cleanup'); + if (status === 'privacy') { + cleanup.writePrivacySnapshotAndEnd(); + } else { + cleanup.writeSignedOutSnapshotAndEnd(); + } + await restarted.waitForNativeTerminal?.(); + + expect(firstActivity()).toMatchObject({ state: 'dismissed', dismissAt: NOW }); + expect(native.snapshots.at(-1)).toMatchObject({ primaryCount: 0, showOpenAgents: false }); + expect(native.ignoredUpdates).toEqual([]); + }); + + it('orders privacy after an older submitted end without dismissing new-scope work', async () => { + const sink = await loadSink(); + sink.startOrUpdate(snapshot([{ status: 'busy' }]), CTX); + const end = Promise.withResolvers(); + const submitted = Promise.withResolvers(); + firstActivity().endGate = end.promise; + firstActivity().endSubmitted = () => { + submitted.resolve(undefined); + }; + sink.publish(snapshot([], 1)); + await submitted.promise; + const cleanup = await import('@/lib/glanceable/cleanup'); + cleanup.writePrivacySnapshotAndEnd(); + const ctx = { userId: 'u2', organizationId: 'new-org' }; + const fresh = buildGlanceableSnapshot({ ...ctx, sessions: [{ status: 'question' }], now: NOW }); + sink.publish(fresh); + sink.startOrUpdate(fresh, ctx); + end.resolve(undefined); + await sink.waitForNativeTerminal?.(); + + expect(firstActivity()).toMatchObject({ + state: 'dismissed', + dismissAt: NOW, + policies: ['after', 'immediate'], + }); + expect(native.records.filter(record => record.state === 'active')).toMatchObject([ + { props: { needsInput: 1, running: 0 } }, + ]); + expect(native.records).toHaveLength(2); + }); +}); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index e578646128..99525f7c49 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -46,8 +46,8 @@ vi.mock('react-native', () => ({ PlatformColor: (name: string) => name })); const mockState = vi.hoisted(() => ({ startError: null as { code: string; message: string } | null, instancesError: null as { code: string; message: string } | null, - instances: [] as unknown[], - started: [] as { props: unknown; url?: string; ended: boolean }[], + instances: [] as object[], + started: [] as { props: unknown; url?: string; ended: boolean; dismissAt: number | null }[], updated: [] as unknown[], snapshots: [] as unknown[], timeline: [] as { date: Date; props: unknown }[], @@ -64,9 +64,11 @@ vi.mock('expo-widgets', () => ({ error.code = mockState.startError.code; throw error; } - const state = { props, url, ended: false }; + const state = { props, url, ended: false, dismissAt: null as number | null }; + const id = `local-${mockState.started.length}`; mockState.started.push(state); const instance = { + getInfo: () => ({ id, state: state.ended ? 'ended' : 'active' }), getPushToken: vi.fn().mockResolvedValue(null), update: async (next: unknown) => { mockState.updated.push(next); @@ -75,23 +77,35 @@ vi.mock('expo-widgets', () => ({ } state.props = next; }, - end: (policy: unknown, finalProps?: unknown, contentDate?: unknown) => { + end: ( + policy: 'immediate' | { after: Date }, + finalProps?: unknown, + contentDate?: unknown + ) => { state.ended = true; + state.dismissAt = policy === 'immediate' ? Date.now() : policy.after.getTime(); state.props = finalProps; - mockState.instances = mockState.instances.filter(current => current !== instance); + if (policy === 'immediate') { + mockState.instances = mockState.instances.filter(current => current !== instance); + } mockState.ended.push({ policy, props: finalProps, contentDate }); }, }; mockState.instances.push(instance); return instance; }, - getInstances: () => { + getInstances: (includeEnded = false) => { if (mockState.instancesError !== null) { const error = new Error(mockState.instancesError.message) as Error & { code: string }; error.code = mockState.instancesError.code; throw error; } - return mockState.instances; + return mockState.instances + .map((instance, index) => ({ + getInfo: () => ({ id: `adopted-${index}`, state: 'active' }), + ...instance, + })) + .filter(instance => includeEnded || instance.getInfo().state === 'active'); }, }), createWidget: () => ({ @@ -347,15 +361,14 @@ describe('iosSink end', () => { // The native update does not settle at the JS publish stamp: ActivityKit // stamps its own later wall-clock at native execution. Simulate that gap. - let resolveUpdate: () => void = undefined as unknown as () => void; - mockState.updatePromise = new Promise(resolve => { - resolveUpdate = resolve; - }); - iosSink.publish(snapshotFor([], 1, 'empty')); + const update = Promise.withResolvers(); + mockState.updatePromise = update.promise; + iosSink.publish(snapshotFor([{ status: 'busy' }], 1)); + iosSink.publish(snapshotFor([], 2, 'empty')); const nativeWriteTime = NOW + 50; vi.setSystemTime(new Date(nativeWriteTime)); - resolveUpdate(); + update.resolve(undefined); iosSink.endImmediate(); await vi.waitFor(() => { @@ -376,7 +389,8 @@ describe('iosSink end', () => { const update = Promise.withResolvers(); mockState.updatePromise = update.promise; - iosSink.publish(snapshotFor([], 1, 'empty')); + iosSink.publish(snapshotFor([{ status: 'busy' }], 1)); + iosSink.publish(snapshotFor([], 2, 'empty')); iosSink.endImmediate(); iosSink.endImmediate(); @@ -407,7 +421,8 @@ describe('iosSink end', () => { iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 4), CTX); const oldUpdate = Promise.withResolvers(); mockState.updatePromise = oldUpdate.promise; - iosSink.publish(snapshotFor([], 5, 'empty')); + iosSink.publish(snapshotFor([{ status: 'busy' }], 5)); + iosSink.publish(snapshotFor([], 6, 'empty')); iosSink.endImmediate(); const newSnapshot = snapshotFor([{ status: 'question' }], 0); @@ -451,7 +466,8 @@ describe('iosSink end', () => { const update = Promise.withResolvers(); mockState.updatePromise = update.promise; - iosSink.publish(snapshotFor([], 1, 'empty')); + iosSink.publish(snapshotFor([{ status: 'busy' }], 1)); + iosSink.publish(snapshotFor([], 2, 'empty')); iosSink.endImmediate(); const failureTime = NOW + 50; @@ -507,25 +523,167 @@ describe('iosSink end', () => { expect(subscriptions).toEqual(new Set(['scope'])); }); - it('ends after the 8s terminal window when work becomes empty', async () => { + it('submits terminal content and native dismissal without running the publisher timer', async () => { vi.useFakeTimers(); + vi.setSystemTime(NOW); const publisher = new GlanceablePublisher({ sinks: [iosSink], now: () => NOW }); - publisher.handleSessions([{ status: 'busy' }], CTX); - expect(mockState.started.length).toBe(1); - expect(mockState.ended.length).toBe(0); - publisher.handleSessions([{ status: 'idle' }], CTX); - expect(mockState.ended.length).toBe(0); + await iosSink.waitForNativeTerminal?.(); - vi.advanceTimersByTime(8000); - await vi.waitFor(() => { - expect(mockState.ended.length).toBe(1); - }); - expect(mockState.ended[0]?.policy).toBe('immediate'); + expect(mockState.started).toMatchObject([ + { + ended: true, + dismissAt: NOW + 8000, + props: { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + }, + ]); expect(subscriptions).toEqual(new Set(['scope'])); publisher.dispose(); }); + + it('keeps the full native terminal window after a delayed update', async () => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }]), CTX); + const update = Promise.withResolvers(); + mockState.updatePromise = update.promise; + iosSink.publish(snapshotFor([{ status: 'busy' }], 1)); + iosSink.publish(snapshotFor([], 2)); + vi.setSystemTime(NOW + 60_000); + update.resolve(undefined); + await iosSink.waitForNativeTerminal?.(); + + expect(mockState.started[0]).toMatchObject({ + ended: true, + dismissAt: NOW + 68_000, + props: { status: 'empty', running: 0 }, + }); + expect(mockState.ended[0]?.contentDate).toEqual(new Date(NOW + 60_000)); + }); + + it('keeps fresh work after an older native dismissal and an older publisher timer', async () => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + const older = new GlanceablePublisher({ sinks: [iosSink], now: () => NOW }); + older.handleSessions([{ status: 'busy' }], CTX); + older.handleSessions([], CTX); + await iosSink.waitForNativeTerminal?.(); + const newer = new GlanceablePublisher({ sinks: [iosSink], now: () => NOW + 1 }); + newer.handleSessions([{ status: 'question' }], CTX); + await vi.advanceTimersByTimeAsync(8000); + + expect( + mockState.started.filter(state => state.dismissAt === null || state.dismissAt > Date.now()) + ).toMatchObject([{ ended: false, props: { status: 'happy', needsInput: 1, running: 0 } }]); + expect(subscriptions).toEqual(new Set(['scope', 'activity'])); + older.dispose(); + newer.dispose(); + }); + + it.each(['privacy', 'signed_out'] as const)( + 'dismisses retained terminal handles immediately for %s', + async status => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }]), CTX); + iosSink.publish(snapshotFor([], 1)); + await iosSink.waitForNativeTerminal?.(); + expect(mockState.started[0]?.dismissAt).toBe(NOW + 8000); + + iosSink.publish(snapshotFor([], 2, status)); + iosSink.endImmediate(); + await iosSink.waitForNativeTerminal?.(); + expect(mockState.started[0]).toMatchObject({ + dismissAt: NOW, + props: { status, running: 0, needsInput: 0, reconnecting: 0 }, + }); + } + ); + + it.each(['privacy', 'signed_out'] as const)( + 'removes adopted work as well as a retained terminal handle for %s', + async status => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }]), CTX); + iosSink.publish(snapshotFor([], 1)); + await iosSink.waitForNativeTerminal?.(); + + let visible = true; + let content: Partial = { status: 'happy', running: 4 }; + mockState.instances.push({ + getPushToken: vi.fn().mockResolvedValue('adopted-token'), + end: ( + policy: 'immediate' | { after: Date }, + props?: Partial + ) => { + visible = policy !== 'immediate'; + content = props ?? {}; + }, + }); + iosSink.publish(snapshotFor([], 2, status)); + await iosSink.waitForNativeTerminal?.(); + + expect(visible).toBe(false); + expect(content).toMatchObject({ status, running: 0, needsInput: 0, reconnecting: 0 }); + expect(mockState.started[0]?.dismissAt).toBe(NOW); + } + ); + + it('supersedes a pending terminal intent without ending new-scope work', async () => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }]), CTX); + const update = Promise.withResolvers(); + mockState.updatePromise = update.promise; + iosSink.publish(snapshotFor([{ status: 'busy' }], 1)); + iosSink.publish(snapshotFor([], 2)); + iosSink.publish(snapshotFor([], 3, 'privacy')); + iosSink.endImmediate(); + + mockState.updatePromise = null; + const ctx = { userId: 'u2', organizationId: 'new-org' }; + const fresh = buildGlanceableSnapshot({ + ...ctx, + sessions: [{ status: 'question' }], + now: NOW + 1, + }); + iosSink.publish(fresh); + iosSink.startOrUpdate(fresh, ctx); + update.resolve(undefined); + await iosSink.waitForNativeTerminal?.(); + + expect(mockState.ended).toMatchObject([ + { + policy: 'immediate', + props: { status: 'privacy', running: 0, needsInput: 0 }, + }, + ]); + expect(mockState.started).toMatchObject([ + { ended: true, dismissAt: NOW, props: { status: 'privacy' } }, + { ended: false, dismissAt: null, props: { status: 'happy', needsInput: 1 } }, + ]); + expect(subscriptions).toEqual(new Set(['scope', 'activity'])); + }); + + it('retains the elapsed anchor when running work becomes reconnecting', async () => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + const publisher = new GlanceablePublisher({ sinks: [iosSink], now: () => Date.now() }); + publisher.handleSessions([{ status: 'busy' }], CTX); + vi.setSystemTime(NOW + 60_000); + publisher.handleSessions([{ status: 'retry' }], CTX); + await vi.advanceTimersByTimeAsync(1000); + expect(mockState.started).toMatchObject([ + { + ended: false, + dismissAt: null, + props: { running: 0, reconnecting: 1, eligibleStartedAt: new Date(NOW).toISOString() }, + }, + ]); + publisher.dispose(); + }); }); describe('iosSink widget publish', () => { @@ -621,16 +779,17 @@ describe('iosSink widget publish', () => { }); describe('iosSink Live Activity content-state', () => { - it('mirrors the empty content-state without starting a second activity', () => { + it('ends with empty content-state without starting a second activity', async () => { iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); iosSink.publish(snapshotFor([], 1)); + await iosSink.waitForNativeTerminal?.(); - expect(mockState.started.length).toBe(1); - const updated = mockState.updated.at(-1) as GlanceableLiveActivityContentState | undefined; - expect(updated?.status).toBe('empty'); - expect(updated?.running).toBe(0); - expect(updated?.needsInput).toBe(0); - expect(updated?.reconnecting).toBe(0); + expect(mockState.started).toMatchObject([ + { + ended: true, + props: { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + }, + ]); }); it('mirrors the stale content-state with counts onto the Live Activity', () => { @@ -662,7 +821,9 @@ describe('iosSink Live Activity content-state', () => { expect(delivery.registerTokens).not.toHaveBeenCalled(); }); - it('ends an adopted leftover activity when publish receives ineligible work', async () => { + it('gives adopted empty work the native terminal window without a publisher timer', async () => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); mockState.instances = [ { getPushToken: vi.fn().mockResolvedValue(null), @@ -673,11 +834,15 @@ describe('iosSink Live Activity content-state', () => { ]; iosSink.publish(snapshotFor([], 1, 'empty')); + await iosSink.waitForNativeTerminal?.(); - await vi.waitFor(() => { - expect(mockState.ended.length).toBe(1); - }); - expect(mockState.ended[0]?.policy).toBe('immediate'); + expect(mockState.ended).toMatchObject([ + { + policy: { after: new Date(NOW + 8000) }, + props: { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + contentDate: new Date(NOW), + }, + ]); expect(mockState.updated.length).toBe(0); expect(subscriptions.has('activity')).toBe(false); }); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index fc90c07360..b1e7ea1dcb 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -1,9 +1,10 @@ import { + GLANCEABLE_TERMINAL_MS, type GlanceableAgentsSnapshot, isEligibleGlanceableWork, } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; -import { type LiveActivity } from 'expo-widgets'; +import { after, type LiveActivity } from 'expo-widgets'; import { i18n } from '@/i18n'; import { getGlanceableDelivery, type GlanceableSink } from '@/lib/glanceable/sink-registry'; @@ -27,9 +28,6 @@ let revision = 0; /** In-flight native `update`; `end` awaits it so its contentDate is never older. */ let inFlightUpdate: Promise | null = null; let lastProps: Partial | null = null; -// Native instances remain discoverable until end settles. Their JS wrappers -// have no stable identity, so do not adopt while any local end is pending. -let pendingEnds = 0; function translate(key: string): string { return i18n.t(key); @@ -47,24 +45,32 @@ function isActivityKitUnavailable(error: unknown): boolean { ); } -/** - * Adopt the newest ActivityKit instance into the in-memory handle. After a - * process restart the JS handle is null while ActivityKit still holds the - * activity, so end/publish must adopt before acting. Returns null when none - * exists. Only ActivityKit unavailability is permanent; a transient error - * leaves denial unset so a later call retries. - */ -function adoptExistingActivity(): Activity | null { - if (pendingEnds > 0) { - return null; - } +/** Recheck native state even when JavaScript missed the remote terminal snapshot. */ +function refreshActivity(): boolean { try { - return ActiveAgentsLiveActivity.getInstances().at(-1) ?? null; + if (activity !== null) { + const { state } = activity.getInfo(); + if (state === 'active' || state === 'stale') { + return true; + } + getGlanceableDelivery().cleanupTokens('activity', readEndingToken(activity)); + activity = null; + inFlightUpdate = null; + lastProps = null; + revision = 0; + } + // Wrappers change on discovery; only native IDs identify pending ends. + activity = + ActiveAgentsLiveActivity.getInstances().findLast( + instance => !endingActivities.has(instance.getInfo().id) + ) ?? null; + return true; } catch (error) { if (isActivityKitUnavailable(error)) { activityKitDeniedState = true; } - return null; + // Do not update an unverified cached handle or start a duplicate on a read failure. + return false; } } @@ -92,42 +98,127 @@ async function readEndingToken(instance: Activity): Promise { } } -async function endNow(): Promise { - // A process restart leaves the JS handle null while ActivityKit still - // holds the activity; adopt it so the end actually clears the Lock Screen. - activity ??= adoptExistingActivity(); - const endingToken = activity === null ? undefined : readEndingToken(activity); - // Capture before end removes native discovery, without waiting for the network. - getGlanceableDelivery().cleanupTokens('activity', endingToken); - if (activity === null) { +type EndIntent = { + dismissAt: number | null; + props: Partial | null; +}; +type EndingActivity = { + id: string; + instance: Activity; + update: Promise | null; + token: Promise; + intent: EndIntent; + pending: Promise | null; +}; +// Only pending native submissions live in JS. Native discovery owns terminal visibility. +const endingActivities = new Map(); + +async function finishEnd(ending: EndingActivity): Promise { + let completed = false; + try { + try { + await ending.update; + } catch { + // A rejected update must not block the end; its contentDate still advances. + } + await ending.token; + if (ending.intent.dismissAt !== null) { + ending.intent.dismissAt = Date.now() + GLANCEABLE_TERMINAL_MS; + } + // Read the latest intent at the native boundary. Privacy can supersede an + // empty snapshot during either await, including an already-submitted end. + for (;;) { + const intent = ending.intent; + // eslint-disable-next-line no-await-in-loop -- serialize a privacy dismissal after an in-flight native end + await ending.instance.end( + intent.dismissAt === null ? 'immediate' : after(new Date(intent.dismissAt)), + intent.props ?? undefined, + new Date() + ); + if (intent === ending.intent) { + break; + } + } + completed = true; + } catch (error) { + // Native reports missing IDs as dismissed; only confirmed absence settles a failed end. + if (ending.instance.getInfo().state !== 'dismissed') { + throw error; + } + completed = true; + } finally { + ending.pending = null; + if (completed) { + endingActivities.delete(ending.id); + } + } +} + +async function scheduleEnd(ending: EndingActivity): Promise { + if (ending.pending !== null) { return; } - // Detach before yielding so a concurrent start owns independent state. - const endingActivity = activity; - const endingUpdate = inFlightUpdate; - const endingProps = lastProps; + ending.pending = finishEnd(ending); + try { + await ending.pending; + } catch { + // Foreground publication is best-effort; background callers await the original task. + } +} + +function endNow( + dismissAt: number | null = null, + props: Partial | null = lastProps +): void { + const targets = new Map(); + if (dismissAt === null) { + try { + // Privacy must include terminal content, even after JS state was discarded. + for (const instance of ActiveAgentsLiveActivity.getInstances(true)) { + targets.set(instance.getInfo().id, instance); + } + } catch (error) { + if (isActivityKitUnavailable(error)) { + activityKitDeniedState = true; + } + } + } else if (!refreshActivity()) { + return; + } + const currentId = activity?.getInfo().id; + if (activity !== null && currentId !== undefined) { + targets.set(currentId, activity); + } + for (const [id, instance] of targets) { + if (!endingActivities.has(id)) { + const token = readEndingToken(instance); + // Capture before end, and retire tokens before fresh work can register. + getGlanceableDelivery().cleanupTokens('activity', token); + endingActivities.set(id, { + id, + instance, + update: id === currentId ? inFlightUpdate : null, + token, + intent: { dismissAt, props }, + pending: null, + }); + } + } activity = null; inFlightUpdate = null; lastProps = null; revision = 0; - pendingEnds += 1; - - // ActivityKit (iOS 17.2+) discards an end whose contentDate is older than the - // last content write. Native `update` stamps its own later wall-clock, so wait - // for the in-flight update and pass a fresh `Date()` — never the earlier JS - // stamp or the snapshot's logical `updatedAt`, which is recorded beforehand. - if (endingUpdate !== null) { - try { - await endingUpdate; - } catch { - // A rejected update must not block the end; the contentDate still advances. + for (const ending of endingActivities.values()) { + if ( + dismissAt === null && + (ending.intent.dismissAt !== null || (props !== null && props !== ending.intent.props)) + ) { + ending.intent = { dismissAt: null, props: props ?? ending.intent.props }; } + void scheduleEnd(ending); } - try { - await endingToken; - await endingActivity.end('immediate', endingProps ?? undefined, new Date()); - } finally { - pendingEnds -= 1; + if (dismissAt === null && endingActivities.size === 0) { + getGlanceableDelivery().cleanupTokens('activity'); } } @@ -164,10 +255,16 @@ export function _resetIosSinkForTests(): void { revision = 0; inFlightUpdate = null; lastProps = null; - pendingEnds = 0; + endingActivities.clear(); } export const iosSink: GlanceableSink = { + async waitForNativeTerminal() { + await Promise.all( + [...endingActivities.values()].map((ending): Promise | null => ending.pending) + ); + }, + publish(snapshot) { const props = buildGlanceableViewProps(snapshot, {}, translate); ActiveAgentsWidget.updateSnapshot(props); @@ -178,68 +275,42 @@ export const iosSink: GlanceableSink = { { date: new Date(snapshot.expiresAt), props: buildExpiredProps(snapshot) }, ]); } - // Mirror the published snapshot onto a present Live Activity so the empty - // "No work in progress" and stale "Can't update now" copy shows during the - // terminal window before `endImmediate` ends it. Never start an activity - // here: start is reserved for the first eligible emit. Track the update's - // promise so a later `end` awaits it and carries a contentDate not older - // than the native write (ActivityKit ignores an older end). Adopt a - // leftover instance first: after a process restart the JS handle is null - // while ActivityKit still holds the activity. - const adopted = activity === null; - activity ??= adoptExistingActivity(); - if (activity !== null) { - if (adopted && !isEligibleGlanceableWork(snapshot)) { - // The publisher's process-local `activityStarted` is false on a fresh - // process, so an ineligible snapshot only reaches `publish` and the - // terminal `endImmediate` never fires for it. End the adopted leftover - // instead of mirroring it onto the Lock Screen. - void endNow(); - return; - } - lastProps = buildGlanceableLiveActivityContentState(snapshot); + const contentState = buildGlanceableLiveActivityContentState(snapshot); + if (!isEligibleGlanceableWork(snapshot)) { + // ActivityKit owns removal after this call, even if JavaScript stops. + // The after-date retains Lock Screen content, not the Dynamic Island. + const immediate = snapshot.status === 'signed_out' || snapshot.status === 'privacy'; + endNow(immediate ? null : Date.now() + GLANCEABLE_TERMINAL_MS, contentState); + return; + } + // Never start here. Recheck cached native work and preserve update/end ordering. + if (refreshActivity() && activity !== null) { + lastProps = contentState; inFlightUpdate = activity.update(lastProps); } }, startOrUpdate(snapshot, ctx) { - if (activityKitDeniedState || !isEligibleGlanceableWork(snapshot)) { + if (activityKitDeniedState || !isEligibleGlanceableWork(snapshot) || !refreshActivity()) { return; } const contentState = buildGlanceableLiveActivityContentState(snapshot); if (activity === null) { - // Adopt the newest existing instance before starting a second one, so a - // process restart updates the activity it started earlier. - activity = adoptExistingActivity(); - if (getActivityKitDenied()) { - return; - } - const adopted = activity !== null; - inFlightUpdate = null; - - if (activity === null) { - try { - activity = ActiveAgentsLiveActivity.start(contentState, OPEN_AGENTS_URL); - inFlightUpdate = null; - } catch (error) { - // Only ActivityKit unavailability is permanent; a transient - // StartLiveActivityException leaves denial unset so a later emit retries. - if (isActivityKitUnavailable(error)) { - activityKitDeniedState = true; - } - activity = null; - return; + try { + activity = ActiveAgentsLiveActivity.start(contentState, OPEN_AGENTS_URL); + inFlightUpdate = null; + } catch (error) { + // Only ActivityKit unavailability is permanent; transient starts retry later. + if (isActivityKitUnavailable(error)) { + activityKitDeniedState = true; } + return; } - lastProps = contentState; revision = snapshot.revision; getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId, activity); - if (adopted) { - inFlightUpdate = activity.update(contentState); - } return; } @@ -257,6 +328,6 @@ export const iosSink: GlanceableSink = { }, endImmediate() { - void endNow(); + endNow(); }, }; diff --git a/apps/mobile/src/lib/glanceable/publisher.ts b/apps/mobile/src/lib/glanceable/publisher.ts index 8ac8476465..f23088b0c9 100644 --- a/apps/mobile/src/lib/glanceable/publisher.ts +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -251,7 +251,9 @@ export class GlanceablePublisher { this.terminalTimer = null; this.activityStarted = false; for (const sink of this.sinks) { - sink.endImmediate(); + if (!sink.waitForNativeTerminal) { + sink.endImmediate(); + } } }, this.terminalMs); } diff --git a/apps/mobile/src/lib/glanceable/sink-registry.ts b/apps/mobile/src/lib/glanceable/sink-registry.ts index dd350abf29..9b46bd4616 100644 --- a/apps/mobile/src/lib/glanceable/sink-registry.ts +++ b/apps/mobile/src/lib/glanceable/sink-registry.ts @@ -17,6 +17,8 @@ export type GlanceableSinkContext = { export type GlanceableSink = { publish(snapshot: GlanceableAgentsSnapshot): void; + /** Owns native terminal dismissal; await submission, never schedule a later JS end. */ + waitForNativeTerminal?(): Promise; endImmediate(): void; startOrUpdate(snapshot: GlanceableAgentsSnapshot, ctx: GlanceableSinkContext): void; }; diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index bea5943484..d6182db384 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -91,14 +91,18 @@ vi.mock('expo-secure-store', () => ({ deleteItemAsync: mocks.deleteItemAsync, })); -vi.mock('expo-widgets', () => ({ - addPushToStartTokenListener: ( - listener: (event: { activityPushToStartToken: string }) => void - ) => { - mocks.startTokenListeners.add(listener); - return { remove: () => mocks.startTokenListeners.delete(listener) }; - }, -})); +vi.mock('expo-widgets', async () => { + const { after } = await import('expo-widgets/src/Widgets'); + return { + after, + addPushToStartTokenListener: ( + listener: (event: { activityPushToStartToken: string }) => void + ) => { + mocks.startTokenListeners.add(listener); + return { remove: () => mocks.startTokenListeners.delete(listener) }; + }, + }; +}); vi.mock('expo-widgets/src/ExpoWidgets', () => ({ default: { @@ -115,7 +119,9 @@ vi.mock('@/glanceable-ios/active-agents-live-activity', async () => { // Keep the real expo-widgets wrapper between the sink and the native handles. const { LiveActivityFactory } = await import('expo-widgets/src/Widgets'); return { - ActiveAgentsLiveActivity: new LiveActivityFactory('ActiveAgentsLiveActivity', () => null), + ActiveAgentsLiveActivity: new LiveActivityFactory('ActiveAgentsLiveActivity', () => ({ + banner: null, + })), }; }); vi.mock('@/glanceable-ios/active-agents-widget', () => ({ @@ -938,9 +944,18 @@ describe('setupNotificationBackgroundHandler', () => { describe('cold iOS background delivery', () => { const rows = new Map(); const native = { + id: 'remote-activity', exists: true, token: null as string | null, tokenRead: null as Promise | null, + updateRead: null as Promise | null, + endRead: null as Promise | null, + endError: null as Error | null, + infoError: null as Error | null, + dismissAt: null as number | null, + props: null as string | null, + contentDate: null as number | null, + policies: [] as string[], observers: new Set<(token: string) => void>(), }; @@ -954,12 +969,13 @@ describe('cold iOS background delivery', () => { async function loadColdBackground() { vi.resetModules(); await import('@/lib/glanceable/delivery-registration'); - const [notifications, registry, persist, sink, cleanup] = await Promise.all([ + const [notifications, registry, persist, sink, cleanup, blank] = await Promise.all([ import('./notifications'), import('@/lib/glanceable/sink-registry'), import('@/lib/glanceable/persist'), import('@/glanceable-ios/ios-sink'), import('@/lib/auth/logout-cleanup'), + import('@/lib/glanceable/cleanup'), ]); persist._setSecureStoreForTests(secureStoreMock); for (const listener of mocks.startTokenListeners) { @@ -977,6 +993,8 @@ describe('cold iOS background delivery', () => { }) => Promise; return { cleanup, + blank, + sink, deliver: async (overrides: Partial) => { const result = await executor({ data: { @@ -1035,26 +1053,51 @@ describe('cold iOS background delivery', () => { rows.delete(token); return { success: true }; }); + native.id = 'remote-activity'; native.exists = true; native.token = null; native.tokenRead = null; + native.updateRead = null; + native.endRead = null; + native.endError = null; + native.infoError = null; + native.dismissAt = null; + native.props = null; + native.contentDate = null; + native.policies = []; native.observers.clear(); - mocks.nativeInstances.mockImplementation(() => { - if (!native.exists) { + mocks.nativeInstances.mockImplementation((includeEnded = false) => { + if ( + !native.exists && + (!includeEnded || native.dismissAt === null || native.dismissAt <= Date.now()) + ) { return []; } + const id = native.id; + const isDismissed = () => + id !== native.id || + (!native.exists && (native.dismissAt === null || native.dismissAt <= Date.now())); const listeners = new Set<(event: { activityId: string; pushToken: string }) => void>(); - // Model the patched native factory: adoption starts observation on this handle. + // Model native adoption and retained end handles; the JS adapter remains real. native.observers.add(token => { for (const listener of listeners) { - listener({ activityId: 'remote-activity', pushToken: token }); + listener({ activityId: id, pushToken: token }); } }); return [ { + getInfo: () => { + if (native.infoError !== null) { + throw native.infoError; + } + if (isDismissed()) { + return { id, state: 'dismissed' }; + } + return { id, state: native.exists ? 'active' : 'ended' }; + }, getPushToken: async () => { await native.tokenRead; - if (!native.exists) { + if (!native.exists || id !== native.id) { throw new Error('Activity no longer exists'); } return native.token; @@ -1069,13 +1112,29 @@ describe('cold iOS background delivery', () => { listeners.add(listener); return { remove: () => listeners.delete(listener) }; }, - update: async () => { - await Promise.resolve(); + update: async (props: string) => { + await native.updateRead; + if (!isDismissed()) { + native.props = props; + } }, - end: async () => { + // eslint-disable-next-line max-params -- match the installed expo-widgets native end contract + end: async (policy: string, afterDate?: number, props?: string, contentDate?: number) => { + await native.endRead; + if (isDismissed()) { + throw Object.assign(new Error('Live Activity not found'), { + code: 'ERR_LIVE_ACTIVITY_NOT_FOUND', + }); + } + if (native.endError !== null) { + throw native.endError; + } native.exists = false; + native.dismissAt = policy === 'after' ? (afterDate ?? null) : Date.now(); + native.props = props ?? null; + native.contentDate = contentDate ?? null; + native.policies.push(policy); native.observers.clear(); - await Promise.resolve(); }, }, ]; @@ -1088,6 +1147,100 @@ describe('cold iOS background delivery', () => { vi.useRealTimers(); }); + it.each(['dismissed', 'missing'] as const)( + 'completes fresh background terminal work after a %s target during a pending update', + async absence => { + const update = deferred(); + native.updateRead = update.promise; + const background = await loadColdBackground(); + expect(await background.deliver({ running: 2 })).toBe(0); + const applying = background.deliver({ + updatedAt: '2026-01-02T00:00:01.000Z', + status: 'empty', + running: 0, + eligibleStartedAt: null, + }); + await vi.advanceTimersByTimeAsync(0); + expect(native.policies).toEqual([]); + + native.exists = false; + native.dismissAt = absence === 'dismissed' ? Date.now() : null; + update.resolve(); + const earlierResults = await Promise.allSettled([applying]); + + native.id = 'fresh-activity'; + native.exists = true; + native.dismissAt = null; + native.updateRead = null; + expect(await background.deliver({ updatedAt: '2026-01-02T00:00:02.000Z', running: 3 })).toBe( + 0 + ); + expect(JSON.parse(native.props ?? '{}')).toMatchObject({ status: 'happy', running: 3 }); + await expect( + background.deliver({ + updatedAt: '2026-01-02T00:00:03.000Z', + status: 'empty', + running: 0, + eligibleStartedAt: null, + }) + ).resolves.toBe(0); + + expect(native.exists).toBe(false); + expect(native.policies).toEqual(['after']); + expect(native.dismissAt).toBe(Date.now() + 8000); + expect(JSON.parse(native.props ?? '{}')).toMatchObject({ status: 'empty', running: 0 }); + expect(earlierResults).toEqual([{ status: 'fulfilled', value: 0 }]); + } + ); + + it.each(['active', 'ended', 'unavailable'] as const)( + 'rejects and retries a native end failure when the target state is %s', + async state => { + const background = await loadColdBackground(); + expect(await background.deliver({ running: 2 })).toBe(0); + const end = deferred(); + native.endRead = end.promise; + native.endError = new Error('Native end temporarily unavailable'); + const applying = background.deliver({ + updatedAt: '2026-01-02T00:00:01.000Z', + status: 'empty', + running: 0, + eligibleStartedAt: null, + }); + const rejected = expect(applying).rejects.toThrow(); + await vi.advanceTimersByTimeAsync(0); + background.sink.iosSink.endImmediate(); + if (state === 'ended') { + native.exists = false; + native.dismissAt = Date.now() + 8000; + } + if (state === 'unavailable') { + native.infoError = new Error('Native state temporarily unavailable'); + } + end.resolve(); + await rejected; + expect(native.policies).toEqual([]); + expect(JSON.parse(native.props ?? '{}')).toMatchObject({ running: 2 }); + + native.endError = null; + native.infoError = null; + native.endRead = null; + // Remotely ended content is absent from eligible discovery, but still needs cleanup. + native.exists = false; + native.dismissAt = Date.now() + 8000; + await expect( + background.deliver({ + updatedAt: '2026-01-02T00:00:02.000Z', + status: 'empty', + running: 0, + eligibleStartedAt: null, + }) + ).resolves.toBe(0); + expect(native.policies).toEqual(['immediate']); + expect(native.dismissAt).toBe(Date.now()); + } + ); + it('registers late and rotated tokens from an adopted native handle through the real widget wrapper', async () => { const background = await loadColdBackground(); expect(await background.deliver({})).toBe(0); @@ -1125,13 +1278,14 @@ describe('cold iOS background delivery', () => { return { success: true }; }); const background = await loadColdBackground(); - expect(await background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null })).toBe( - 0 - ); + const applying = background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null }); + await vi.advanceTimersByTimeAsync(0); expect(native.exists).toBe(true); read.resolve(); - await vi.advanceTimersByTimeAsync(0); + expect(await applying).toBe(0); expect(native.exists).toBe(false); + expect(native.policies).toEqual(['after']); + expect(native.dismissAt).toBe(Date.now() + 8000); expect(rows.has('ended-activity-token')).toBe(true); deletion.resolve(); @@ -1142,6 +1296,77 @@ describe('cold iOS background delivery', () => { expect(await background.cleanup.readLogoutCleanupTombstone()).toBeNull(); }); + it('waits for the real adapter to submit the native deadline before background completion', async () => { + vi.setSystemTime(Date.parse('2026-01-02T00:00:00.000Z')); + const end = deferred(); + native.endRead = end.promise; + const background = await loadColdBackground(); + let completed = false; + const apply = async () => { + const result = await background.deliver({ + status: 'empty', + running: 0, + eligibleStartedAt: null, + }); + completed = true; + return result; + }; + const applying = apply(); + await vi.advanceTimersByTimeAsync(0); + expect(completed).toBe(false); + expect(native.policies).toEqual([]); + + end.resolve(); + expect(await applying).toBe(0); + expect(native.policies).toEqual(['after']); + expect(native.dismissAt).toBe(Date.parse('2026-01-02T00:00:08.000Z')); + expect(native.contentDate).toBe(Date.parse('2026-01-02T00:00:00.000Z')); + expect(JSON.parse(native.props ?? '{}')).toEqual({ + status: 'empty', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }); + expect(rows.has('scope-token')).toBe(true); + }); + + it('immediately dismisses an ended adopted handle and rejects old-scope work after privacy', async () => { + const background = await loadColdBackground(); + expect(await background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null })).toBe( + 0 + ); + expect(native.dismissAt).toBe(Date.now() + 8000); + background.blank.writePrivacySnapshotAndEnd(); + await background.sink.iosSink.waitForNativeTerminal?.(); + await background.cleanup.awaitActivityCleanupSettled(); + + expect(native.policies).toEqual(['after', 'immediate']); + expect(native.dismissAt).toBe(Date.now()); + expect(JSON.parse(native.props ?? '{}')).toMatchObject({ status: 'privacy', running: 0 }); + emitNativeToken('late-old-token'); + expect(await background.deliver({ running: 7 })).toBe(1); + await vi.advanceTimersByTimeAsync(0); + expect(rows.size).toBe(0); + expect(JSON.parse(native.props ?? '{}')).toMatchObject({ status: 'privacy', running: 0 }); + }); + + it('orders privacy after an already-submitted terminal end without restoring terminal content', async () => { + const end = deferred(); + native.endRead = end.promise; + const background = await loadColdBackground(); + const applying = background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null }); + await vi.advanceTimersByTimeAsync(0); + background.blank.writeSignedOutSnapshotAndEnd(); + end.resolve(); + expect(await applying).toBe(0); + await background.sink.iosSink.waitForNativeTerminal?.(); + + expect(native.policies).toEqual(['after', 'immediate']); + expect(native.dismissAt).toBe(Date.now()); + expect(JSON.parse(native.props ?? '{}')).toMatchObject({ status: 'signed_out', running: 0 }); + }); + it('tombstones only the failed cold idle token after native discovery disappears', async () => { native.token = 'failed-activity-token'; rows.set(native.token, { kind: 'ios_activity', organizationId: 'org-9' }); diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index 3f19d71dd1..8abc51d8b6 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -70,11 +70,9 @@ export function parseNotificationData(data: unknown): PushData | null { return parsed.success ? parsed.data : null; } -// Pending 8 s terminal end for a non-eligible remote snapshot. Mirrors the -// in-app publisher's terminal window: publish the empty counts, then end the -// Live Activity / Android ongoing after GLANCEABLE_TERMINAL_MS. A newer -// eligible snapshot cancels it, and the terminal-blank epoch gate skips a -// stale end after a logout/org switch already ended the surface. +// Fallback terminal end for sinks without a native terminal contract. +// Native sinks submit dismissal during publish and never receive this later end. +// A newer eligible snapshot or terminal-blank epoch cancels the fallback. let glanceableTerminalTimer: ReturnType | null = null; function cancelGlanceableTerminalEnd(): void { @@ -102,7 +100,9 @@ function scheduleGlanceableTerminalEnd(): void { return; } for (const sink of getGlanceableSinks()) { - sink.endImmediate(); + if (!sink.waitForNativeTerminal) { + sink.endImmediate(); + } } }, GLANCEABLE_TERMINAL_MS); } @@ -164,7 +164,8 @@ export async function applyGlanceablePushData( }; const ctx = { userId, organizationId }; - if (isEligibleGlanceableWork(snapshot)) { + const eligible = isEligibleGlanceableWork(snapshot); + if (eligible) { cancelGlanceableTerminalEnd(); for (const sink of getGlanceableSinks()) { sink.publish(snapshot); @@ -174,11 +175,17 @@ export async function applyGlanceablePushData( for (const sink of getGlanceableSinks()) { sink.publish(snapshot); } - // A remote snapshot with no eligible work must end the Live Activity and - // the Android ongoing after the terminal window; widgets keep the last - // published counts (their endImmediate is a no-op). + // Native sinks already submitted their terminal work during publish. + // Keep the existing fallback for other sinks; widgets retain their timeline. scheduleGlanceableTerminalEnd(); } + // Do not finish a background task before ActivityKit accepts the native end. + // All publication happens before this await, so it cannot restore an old scope. + if (!eligible) { + await Promise.all( + getGlanceableSinks().map((sink): Promise | undefined => sink.waitForNativeTerminal?.()) + ); + } return true; } diff --git a/patches/expo-widgets@57.0.11.patch b/patches/expo-widgets@57.0.11.patch index 752679ef2e..29e859b15d 100644 --- a/patches/expo-widgets@57.0.11.patch +++ b/patches/expo-widgets@57.0.11.patch @@ -1,14 +1,213 @@ +diff --git a/build/Widgets.d.ts b/build/Widgets.d.ts +index 6886a8f..fdcfa44 100644 +--- a/build/Widgets.d.ts ++++ b/build/Widgets.d.ts +@@ -34,4 +34,6 @@ export declare class LiveActivity { + private nativeLiveActivity; + constructor(nativeLiveActivity: NativeLiveActivity); ++ /** Native identity and current state, including an external end. */ ++ getInfo(): ReturnType; + /** + * Updates the Live Activity's content. The UI reflects the new properties immediately. +@@ -76,6 +78,7 @@ export declare class LiveActivityFactory { + /** + * Returns all currently active instances of this Live Activity type. ++ * Set includeEnded for privacy dismissal of native-retained terminal instances. + */ +- getInstances(): LiveActivity[]; ++ getInstances(includeEnded?: boolean): LiveActivity[]; + } + /** +diff --git a/build/Widgets.js b/build/Widgets.js +index 6cd5127..3ca28e5 100644 +--- a/build/Widgets.js ++++ b/build/Widgets.js +@@ -48,4 +48,8 @@ export class LiveActivity { + this.nativeLiveActivity = nativeLiveActivity; + } ++ /** Native identity and current state, including an external end. */ ++ getInfo() { ++ return this.nativeLiveActivity.getInfo(); ++ } + /** + * Updates the Live Activity's content. The UI reflects the new properties immediately. +@@ -108,8 +112,9 @@ export class LiveActivityFactory { + /** + * Returns all currently active instances of this Live Activity type. ++ * Set includeEnded for privacy dismissal of native-retained terminal instances. + */ +- getInstances() { ++ getInstances(includeEnded = false) { + return this.nativeLiveActivityFactory +- .getInstances() ++ .getInstances(includeEnded) + .map((instance) => new LiveActivity(instance)); + } +diff --git a/build/Widgets.types.d.ts b/build/Widgets.types.d.ts +index f623ece..e6a5a03 100644 +--- a/build/Widgets.types.d.ts ++++ b/build/Widgets.types.d.ts +@@ -254,7 +254,8 @@ export declare class NativeLiveActivityFactory extends SharedObject { + constructor(name: string, layout: string); + start(props: string, url?: string): NativeLiveActivity; +- getInstances(): NativeLiveActivity[]; ++ getInstances(includeEnded?: boolean): NativeLiveActivity[]; + } + export declare class NativeLiveActivity extends SharedObject { ++ getInfo(): { id: string; state: 'active' | 'stale' | 'ended' | 'dismissed' }; + update(props: string): Promise; + end(dismissalPolicy?: string, afterDate?: number, state?: string, contentDate?: number): Promise; +diff --git a/ios/LiveActivity.swift b/ios/LiveActivity.swift +index c4b5bcc..b444df0 100644 +--- a/ios/LiveActivity.swift ++++ b/ios/LiveActivity.swift +@@ -6,4 +6,35 @@ final class LiveActivity: SharedObject { + let name: String + private var pushTokenObserverTask: Task? ++ // Native identity survives JS wrapper release/recreation, but not process exit. ++ private static let activitiesLock = NSLock() ++ private static var retainedActivities: [String: AnyObject] = [:] ++ ++ @available(iOS 16.1, *) ++ static func currentActivities() -> [Activity] { ++ activitiesLock.withLock { ++ for activity in Activity.activities { ++ retainedActivities[activity.id] = activity ++ } ++ let activities = retainedActivities.values.compactMap { $0 as? Activity } ++ for activity in activities where activity.activityState == .dismissed { ++ retainedActivities.removeValue(forKey: activity.id) ++ } ++ return activities.filter { $0.activityState != .dismissed } ++ } ++ } ++ ++ func getInfo() throws -> [String: String] { ++ guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() } ++ let state = Self.currentActivities().first(where: { $0.id == id })?.activityState ?? .dismissed ++ let value: String ++ switch state { ++ case .active: value = "active" ++ case .stale: value = "stale" ++ case .ended: value = "ended" ++ case .dismissed: value = "dismissed" ++ @unknown default: value = "dismissed" ++ } ++ return ["id": id, "state": value] ++ } +- ++ + init(id: String, name: String) { +@@ -27,5 +58,5 @@ final class LiveActivity: SharedObject { + guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() } +- ++ +- guard let activity = Activity.activities.first(where: { $0.id == id }) else { ++ guard let activity = Self.currentActivities().first(where: { $0.id == id }) else { + throw LiveActivityNotFoundException(id) + } +@@ -48,5 +79,5 @@ final class LiveActivity: SharedObject { + guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() } +- ++ +- guard let activity = Activity.activities.first(where: { $0.id == id }) else { ++ guard let activity = Self.currentActivities().first(where: { $0.id == id }) else { + throw LiveActivityNotFoundException(id) + } +@@ -59,4 +90,7 @@ final class LiveActivity: SharedObject { + @available(iOS 16.1, *) + func observePushTokenUpdates(for activity: Activity, pushNotificationsEnabled: Bool) { ++ Self.activitiesLock.withLock { ++ Self.retainedActivities[activity.id] = activity ++ } + guard pushNotificationsEnabled else { + return diff --git a/ios/LiveActivityFactory.swift b/ios/LiveActivityFactory.swift +index caf1e05..821fc64 100644 --- a/ios/LiveActivityFactory.swift +++ b/ios/LiveActivityFactory.swift -@@ -43,7 +43,9 @@ final class LiveActivityFactory: SharedObject { +@@ -40,9 +40,13 @@ final class LiveActivityFactory: SharedObject { + } +- ++ +- func getInstances() throws -> [LiveActivity] { ++ func getInstances(includeEnded: Bool = false) throws -> [LiveActivity] { guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() } - - return Activity.activities.map { activity in +- ++ +- return Activity.activities.map { activity in - LiveActivity(id: activity.id, name: name) ++ return LiveActivity.currentActivities().filter { ++ $0.activityState == .active || $0.activityState == .stale || (includeEnded && $0.activityState == .ended) ++ }.map { activity in + let instance = LiveActivity(id: activity.id, name: name) + instance.observePushTokenUpdates(for: activity, pushNotificationsEnabled: LiveActivityFactory.pushNotificationsEnabled) + return instance } } +diff --git a/ios/WidgetsModule.swift b/ios/WidgetsModule.swift +index 678a7d1..c182a7a 100644 +--- a/ios/WidgetsModule.swift ++++ b/ios/WidgetsModule.swift +@@ -91,10 +91,14 @@ public final class WidgetsModule: Module { + } +- ++ +- Function("getInstances") { (liveActivity: LiveActivityFactory) in +- try liveActivity.getInstances() ++ Function("getInstances") { (liveActivity: LiveActivityFactory, includeEnded: Bool?) in ++ try liveActivity.getInstances(includeEnded: includeEnded ?? false) + } + } +- ++ + Class("LiveActivity", LiveActivity.self) { ++ Function("getInfo") { (instance: LiveActivity) in ++ try instance.getInfo() ++ } ++ + AsyncFunction("update") { (instance: LiveActivity, props: String) in + try await instance.update(props: props) +diff --git a/src/Widgets.ts b/src/Widgets.ts +index f5bbbba..741d73d 100644 +--- a/src/Widgets.ts ++++ b/src/Widgets.ts +@@ -79,4 +79,9 @@ export class LiveActivity { + } +- ++ ++ /** Native identity and current state, including an external end. */ ++ getInfo(): ReturnType { ++ return this.nativeLiveActivity.getInfo(); ++ } ++ + /** + * Updates the Live Activity's content. The UI reflects the new properties immediately. +@@ -160,8 +165,9 @@ export class LiveActivityFactory { + /** + * Returns all currently active instances of this Live Activity type. ++ * Set includeEnded for privacy dismissal of native-retained terminal instances. + */ +- getInstances() { ++ getInstances(includeEnded = false) { + return this.nativeLiveActivityFactory +- .getInstances() ++ .getInstances(includeEnded) + .map((instance) => new LiveActivity(instance)); + } +diff --git a/src/Widgets.types.ts b/src/Widgets.types.ts +index 37a1b14..a1134c3 100644 +--- a/src/Widgets.types.ts ++++ b/src/Widgets.types.ts +@@ -283,8 +283,9 @@ export declare class NativeLiveActivityFactory extends SharedObject { + constructor(name: string, layout: string); + start(props: string, url?: string): NativeLiveActivity; +- getInstances(): NativeLiveActivity[]; ++ getInstances(includeEnded?: boolean): NativeLiveActivity[]; } +- ++ + export declare class NativeLiveActivity extends SharedObject { ++ getInfo(): { id: string; state: 'active' | 'stale' | 'ended' | 'dismissed' }; + update(props: string): Promise; + end( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7cdc5b2cf..280f460abc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,7 +154,7 @@ packageExtensionsChecksum: sha256-1pgKZxx87NNMe1poF5N5u5kZB/qlEyILBQPxofM1shE= patchedDependencies: expo-router@57.0.10: 616f8a79932a3ebc7d31343b53b3fc425a14bfb89bb86cdafa614cefa4e603ed expo-server-sdk: 7850520582b5b394397b35d1ea195192fe78589d8a6a748fe15177b818c4ed0b - expo-widgets@57.0.11: 3e90bdda241862937ae562137f60f4bc916a8820b827ebf0af8e61a365b96f2e + expo-widgets@57.0.11: 0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7 react-native-appsflyer@6.18.0: 82df99378c830e774b0f01796d8be595da114d1d13393d85ddd47d565c5c2aab importers: @@ -542,7 +542,7 @@ importers: version: 57.0.2(expo@57.0.10)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-widgets: specifier: 57.0.11 - version: 57.0.11(patch_hash=3e90bdda241862937ae562137f60f4bc916a8820b827ebf0af8e61a365b96f2e)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 57.0.11(patch_hash=0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) i18next: specifier: ^26.3.6 version: 26.3.6(typescript@6.0.3) @@ -28194,7 +28194,7 @@ snapshots: optionalDependencies: '@babel/runtime': 7.29.7 expo: 57.0.10(@babel/core@7.29.7)(@expo/metro-runtime@57.0.8)(bufferutil@4.1.0)(expo-router@57.0.10)(expo-widgets@57.0.11)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-widgets: 57.0.11(patch_hash=3e90bdda241862937ae562137f60f4bc916a8820b827ebf0af8e61a365b96f2e)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-widgets: 57.0.11(patch_hash=0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -30530,7 +30530,7 @@ snapshots: expo: 57.0.10(@babel/core@7.29.7)(@expo/metro-runtime@57.0.8)(bufferutil@4.1.0)(expo-router@57.0.10)(expo-widgets@57.0.11)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react-native: 0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-widgets@57.0.11(patch_hash=3e90bdda241862937ae562137f60f4bc916a8820b827ebf0af8e61a365b96f2e)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-widgets@57.0.11(patch_hash=0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/plist': 0.8.1 '@expo/ui': 57.0.12(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.10)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) From fbe77b1361d19434520edd63003ca24c4d782311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 2 Sep 2026 16:03:46 +0200 Subject: [PATCH 26/43] feat(mobile): add an Active Agents on/off setting Add a master switch for the glanceable Active Agents surfaces. Off blanks every surface, unregisters its push tokens, refuses the in-app publisher, drops remote snapshots in the headless push handler, and never prompts for ActivityKit permission. Declare the APNs Live Activity vars in the notifications worker config and document what still has to land before the deploy: the .p8 auth key in the Secrets Store, then its binding. Fix the session-ingest typecheck. Its DO tests import the notifications sources, which read bindings off a global `Env` this package does not have. --- ENVIRONMENT.md | 10 +++-- .../(tabs)/(2_agents)/index.mounted.test.tsx | 19 ++++++-- .../src/app/(app)/(tabs)/(2_agents)/index.tsx | 2 +- .../app-unlock-screen.test-helpers.tsx | 10 ++++- .../preferences-screen.mounted.test.tsx | 8 ++++ .../src/components/preferences-screen.tsx | 15 +++++++ apps/mobile/src/i18n/locales/af.json | 1 + apps/mobile/src/i18n/locales/am.json | 1 + apps/mobile/src/i18n/locales/ar.json | 1 + apps/mobile/src/i18n/locales/az.json | 1 + apps/mobile/src/i18n/locales/be.json | 1 + apps/mobile/src/i18n/locales/bg.json | 1 + apps/mobile/src/i18n/locales/bn.json | 1 + apps/mobile/src/i18n/locales/bs.json | 1 + apps/mobile/src/i18n/locales/ca.json | 1 + apps/mobile/src/i18n/locales/ckb.json | 1 + apps/mobile/src/i18n/locales/cs.json | 1 + apps/mobile/src/i18n/locales/cy.json | 1 + apps/mobile/src/i18n/locales/da.json | 1 + apps/mobile/src/i18n/locales/de.json | 1 + apps/mobile/src/i18n/locales/el.json | 1 + apps/mobile/src/i18n/locales/en.json | 1 + apps/mobile/src/i18n/locales/es.json | 1 + apps/mobile/src/i18n/locales/et.json | 1 + apps/mobile/src/i18n/locales/eu.json | 1 + apps/mobile/src/i18n/locales/fa.json | 1 + apps/mobile/src/i18n/locales/fi.json | 1 + apps/mobile/src/i18n/locales/fil.json | 1 + apps/mobile/src/i18n/locales/fr.json | 1 + apps/mobile/src/i18n/locales/ga.json | 1 + apps/mobile/src/i18n/locales/gl.json | 1 + apps/mobile/src/i18n/locales/gu.json | 1 + apps/mobile/src/i18n/locales/ha.json | 1 + apps/mobile/src/i18n/locales/he.json | 1 + apps/mobile/src/i18n/locales/hi.json | 1 + apps/mobile/src/i18n/locales/hr.json | 1 + apps/mobile/src/i18n/locales/ht.json | 1 + apps/mobile/src/i18n/locales/hu.json | 1 + apps/mobile/src/i18n/locales/hy.json | 1 + apps/mobile/src/i18n/locales/id.json | 1 + apps/mobile/src/i18n/locales/ig.json | 1 + apps/mobile/src/i18n/locales/is.json | 1 + apps/mobile/src/i18n/locales/it.json | 1 + apps/mobile/src/i18n/locales/ja.json | 1 + apps/mobile/src/i18n/locales/ka.json | 1 + apps/mobile/src/i18n/locales/kk.json | 1 + apps/mobile/src/i18n/locales/km.json | 1 + apps/mobile/src/i18n/locales/kn.json | 1 + apps/mobile/src/i18n/locales/ko.json | 1 + apps/mobile/src/i18n/locales/lo.json | 1 + apps/mobile/src/i18n/locales/lt.json | 1 + apps/mobile/src/i18n/locales/lv.json | 1 + apps/mobile/src/i18n/locales/mg.json | 1 + apps/mobile/src/i18n/locales/mi.json | 1 + apps/mobile/src/i18n/locales/mk.json | 1 + apps/mobile/src/i18n/locales/ml.json | 1 + apps/mobile/src/i18n/locales/mn.json | 1 + apps/mobile/src/i18n/locales/mr.json | 1 + apps/mobile/src/i18n/locales/ms.json | 1 + apps/mobile/src/i18n/locales/mt.json | 1 + apps/mobile/src/i18n/locales/my.json | 1 + apps/mobile/src/i18n/locales/nb.json | 1 + apps/mobile/src/i18n/locales/ne.json | 1 + apps/mobile/src/i18n/locales/nl.json | 1 + apps/mobile/src/i18n/locales/om.json | 1 + apps/mobile/src/i18n/locales/or.json | 1 + apps/mobile/src/i18n/locales/pa.json | 1 + apps/mobile/src/i18n/locales/pl.json | 1 + apps/mobile/src/i18n/locales/ps.json | 1 + apps/mobile/src/i18n/locales/pt-BR.json | 1 + apps/mobile/src/i18n/locales/pt.json | 1 + apps/mobile/src/i18n/locales/ro.json | 1 + apps/mobile/src/i18n/locales/ru.json | 1 + apps/mobile/src/i18n/locales/si.json | 1 + apps/mobile/src/i18n/locales/sk.json | 1 + apps/mobile/src/i18n/locales/sl.json | 1 + apps/mobile/src/i18n/locales/so.json | 1 + apps/mobile/src/i18n/locales/sq.json | 1 + apps/mobile/src/i18n/locales/sr.json | 1 + apps/mobile/src/i18n/locales/sv.json | 1 + apps/mobile/src/i18n/locales/sw.json | 1 + apps/mobile/src/i18n/locales/ta.json | 1 + apps/mobile/src/i18n/locales/te.json | 1 + apps/mobile/src/i18n/locales/th.json | 1 + apps/mobile/src/i18n/locales/tr.json | 1 + apps/mobile/src/i18n/locales/uk.json | 1 + apps/mobile/src/i18n/locales/ur.json | 1 + apps/mobile/src/i18n/locales/uz.json | 1 + apps/mobile/src/i18n/locales/vi.json | 1 + apps/mobile/src/i18n/locales/yo.json | 1 + apps/mobile/src/i18n/locales/zh-Hans.json | 1 + apps/mobile/src/i18n/locales/zh-Hant.json | 1 + apps/mobile/src/i18n/locales/zu.json | 1 + .../mobile/src/lib/auth/auth-context.test.tsx | 19 +++++--- apps/mobile/src/lib/auth/auth-context.tsx | 2 + apps/mobile/src/lib/auth/credentials.test.ts | 3 ++ .../src/lib/glanceable/activity-kit-prompt.ts | 10 ++++- .../mobile/src/lib/glanceable/enabled.test.ts | 44 +++++++++++++++++++ apps/mobile/src/lib/glanceable/enabled.ts | 33 ++++++++++++++ apps/mobile/src/lib/glanceable/mount.tsx | 37 ++++++++++++++-- .../lib/hooks/use-glanceable-preference.ts | 30 +++++++++++++ apps/mobile/src/lib/notifications.test.ts | 28 +++++++++++- apps/mobile/src/lib/notifications.ts | 7 +++ apps/mobile/src/lib/storage-keys.ts | 3 ++ .../src/lib/glanceable-delivery.test.ts | 5 ++- services/notifications/wrangler.jsonc | 10 +++++ .../src/dos/UserConnectionDO.test.ts | 6 +-- .../src/notifications-bindings.d.ts | 28 ++++++++++++ 108 files changed, 392 insertions(+), 24 deletions(-) create mode 100644 apps/mobile/src/lib/glanceable/enabled.test.ts create mode 100644 apps/mobile/src/lib/glanceable/enabled.ts create mode 100644 apps/mobile/src/lib/hooks/use-glanceable-preference.ts create mode 100644 services/session-ingest/src/notifications-bindings.d.ts diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index 06c34eeb69..de24c2f1e9 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -325,10 +325,12 @@ When `VERCEL_TARGET_ENV` is absent in local development or a script process, tra ### Notifications Worker -- `APNS_TEAM_ID` - Apple Developer team ID for the token-based APNs key used to send Live Activity pushes. [SERVER] -- `APNS_KEY_ID` - APNs key identifier (`kid`) for the Live Activity push key. [SERVER] -- `APNS_PRIVATE_KEY` - PKCS#8 ES256 `.p8` private key contents for APNs provider-token signing. `[SECRET]` -- `APNS_TOPIC` - iOS app bundle id (`com.kilocode.kiloapp`); Live Activity pushes use `.push-type.liveactivity`. [SERVER] +- `APNS_TEAM_ID` - Apple Developer team ID for the token-based APNs key used to send Live Activity pushes. Declare it in `services/notifications/wrangler.jsonc` under `vars`. [SERVER] +- `APNS_KEY_ID` - APNs key identifier (`kid`) for the Live Activity push key. Declare it beside `APNS_TEAM_ID`. [SERVER] +- `APNS_PRIVATE_KEY` - PKCS#8 ES256 `.p8` private key contents for APNs provider-token signing. Store the key in the Secrets Store first, then add its `secrets_store_secrets` binding; a binding for a missing secret fails the deploy. `[SECRET]` +- `APNS_TOPIC` - iOS app bundle id (`com.kilocode.kiloapp`); Live Activity pushes use `.push-type.liveactivity`. Already set in `vars`. [SERVER] + +Until all four values reach the worker it logs `APNs Live Activity credentials missing` and skips Live Activity pushes. Every other glanceable delivery, including the Expo aggregate push, keeps working. - `KILO_WEB_API_BASE_URL` - Base origin of the web app, used to reach the internal `glanceable-agents-snapshot` route; `https://app.kilo.ai` in production. [SERVER] ### KiloClaw Controller diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx index 25595355e3..7442940d54 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx @@ -1,6 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated -- react-test-renderer is the DOM-free renderer used to mount React/RN trees under vitest (node env, no jsdom); its React 19 deprecation notice points to the DOM-based Testing Library, which cannot render this app's non-DOM tree, and @testing-library/react-native cannot be transformed by the current vitest pipeline (react-native ships Flow). See src/test/render-with-providers.tsx. */ /* eslint-disable max-lines -- mounted route outcomes and Settings recovery share the native boundary harness. */ -import * as SecureStore from 'expo-secure-store'; import { createElement } from 'react'; import TestRenderer, { act } from 'react-test-renderer'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -64,8 +63,21 @@ vi.mock('expo-router', async () => { }; }); +// `failIdentityReadOnce` fails the identity read alone. The master-switch read +// runs first, and its own failure keeps the surfaces on rather than skipping +// recovery, so a first-call rejection would not exercise the identity path. +const storage = vi.hoisted(() => ({ failIdentityReadOnce: false })); vi.mock('expo-secure-store', () => ({ - getItemAsync: vi.fn((key: string) => (key === ACTIVE_USER_ID_KEY ? 'u1' : null)), + getItemAsync: vi.fn((key: string) => { + if (key !== ACTIVE_USER_ID_KEY) { + return null; + } + if (storage.failIdentityReadOnce) { + storage.failIdentityReadOnce = false; + throw new Error('storage unavailable'); + } + return 'u1'; + }), })); vi.mock('@/glanceable-ios/ios-sink', () => ({ @@ -392,6 +404,7 @@ describe('Agents ActivityKit Settings recovery', () => { afterEach(() => { unregisterGlanceableSink(sink); + storage.failIdentityReadOnce = false; }); function changeAppState(state: string) { @@ -447,7 +460,7 @@ describe('Agents ActivityKit Settings recovery', () => { changeAppState('background'); activityKit.available = true; - vi.mocked(SecureStore.getItemAsync).mockRejectedValueOnce(new Error('storage unavailable')); + storage.failIdentityReadOnce = true; changeAppState('active'); await flushMicrotasks(); expect(surface.activity).toBeNull(); diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx index 9dcf628b12..9114b166be 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx @@ -144,7 +144,7 @@ export default function AgentSessionList() { // changing route focus, so also retry recovery when the app becomes active. useFocusEffect( useCallback(() => { - showActivityKitDisabledAlertOnce(); + void showActivityKitDisabledAlertOnce(); void recoverGlanceableActivityKit(); const subscription = AppState.addEventListener('change', state => { if (state === 'active') { diff --git a/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx b/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx index ba916fd6b7..9323511179 100644 --- a/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx +++ b/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx @@ -1,4 +1,4 @@ -/* eslint-disable typescript-eslint/no-deprecated -- Use the repository's DOM-free mounted renderer. */ +/* eslint-disable typescript-eslint/no-deprecated, max-lines -- Use the repository's DOM-free mounted renderer; one shared harness mocks every native module the five layouts reach. */ import { createElement, type ElementType, type ReactElement, useState } from 'react'; import { type AppStateStatus } from 'react-native'; import { act, type ReactTestInstance } from 'react-test-renderer'; @@ -97,6 +97,7 @@ vi.mock('@/components/ui/icons', () => ({ Brain: 'Icon', CheckCircle2: 'Icon', CornerDownLeft: 'Icon', + Gauge: 'Icon', Globe: 'Icon', Info: 'Icon', Loader: 'Icon', @@ -207,6 +208,13 @@ vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ setKeepScreenOn: vi.fn(), }), })); +vi.mock('@/lib/hooks/use-glanceable-preference', () => ({ + useGlanceablePreference: () => ({ + glanceableEnabled: true, + hasLoaded: true, + setGlanceableEnabled: vi.fn(), + }), +})); vi.mock('@/lib/hooks/use-pr-review-footer-preference', () => ({ usePrReviewFooterPreference: () => ({ prReviewFooter: true, diff --git a/apps/mobile/src/components/preferences-screen.mounted.test.tsx b/apps/mobile/src/components/preferences-screen.mounted.test.tsx index 976dfad412..ad84edd478 100644 --- a/apps/mobile/src/components/preferences-screen.mounted.test.tsx +++ b/apps/mobile/src/components/preferences-screen.mounted.test.tsx @@ -43,6 +43,7 @@ vi.mock('@/components/ui/icons', () => ({ Bell: 'Bell', Brain: 'Brain', CornerDownLeft: 'CornerDownLeft', + Gauge: 'Gauge', Globe: 'Globe', MessageSquare: 'MessageSquare', Shield: 'Shield', @@ -76,6 +77,13 @@ vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ setKeepScreenOn: vi.fn(), }), })); +vi.mock('@/lib/hooks/use-glanceable-preference', () => ({ + useGlanceablePreference: () => ({ + glanceableEnabled: true, + hasLoaded: true, + setGlanceableEnabled: vi.fn(), + }), +})); vi.mock('@/lib/hooks/use-pr-review-footer-preference', () => ({ usePrReviewFooterPreference: () => ({ prReviewFooter: true, diff --git a/apps/mobile/src/components/preferences-screen.tsx b/apps/mobile/src/components/preferences-screen.tsx index f1f5bf75fd..aa5e0afe57 100644 --- a/apps/mobile/src/components/preferences-screen.tsx +++ b/apps/mobile/src/components/preferences-screen.tsx @@ -3,6 +3,7 @@ import { Bell, Brain, CornerDownLeft, + Gauge, Globe, type LucideIcon, MessageSquare, @@ -21,6 +22,7 @@ import { Text } from '@/components/ui/text'; import { useAppUnlock } from '@/lib/app-unlock-context'; import { attemptPushRegistrationReconciliation } from '@/lib/auth/push-registration-reconciliation'; import { useCurrentUserId } from '@/lib/hooks/use-current-user-id'; +import { useGlanceablePreference } from '@/lib/hooks/use-glanceable-preference'; import { getResolvedLanguage, useLanguagePreference } from '@/lib/hooks/use-language-preference'; import { useKeepScreenOnPreference } from '@/lib/hooks/use-keep-screen-on-preference'; import { usePrReviewFooterPreference } from '@/lib/hooks/use-pr-review-footer-preference'; @@ -101,6 +103,11 @@ export function PreferencesScreen() { hasLoaded: keepScreenOnLoaded, setKeepScreenOn, } = useKeepScreenOnPreference(); + const { + glanceableEnabled, + hasLoaded: glanceableLoaded, + setGlanceableEnabled, + } = useGlanceablePreference(); const { prReviewFooter, hasLoaded: prReviewFooterLoaded, @@ -154,6 +161,14 @@ export function PreferencesScreen() { disabled={!keepScreenOnLoaded} onValueChange={setKeepScreenOn} /> + ({ clearAgentModelPreference: vi.fn(), })); -const { clearKeepScreenOnPreference, clearReasoningPreference, clearPrReviewFooterPreference } = - vi.hoisted(() => ({ - clearKeepScreenOnPreference: vi.fn(), - clearReasoningPreference: vi.fn(), - clearPrReviewFooterPreference: vi.fn(), - })); +const { + clearKeepScreenOnPreference, + clearReasoningPreference, + clearPrReviewFooterPreference, + clearGlanceablePreference, +} = vi.hoisted(() => ({ + clearKeepScreenOnPreference: vi.fn(), + clearReasoningPreference: vi.fn(), + clearPrReviewFooterPreference: vi.fn(), + clearGlanceablePreference: vi.fn(), +})); vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ clearKeepScreenOnPreference })); +vi.mock('@/lib/hooks/use-glanceable-preference', () => ({ clearGlanceablePreference })); vi.mock('@/lib/hooks/use-reasoning-preference', () => ({ clearReasoningPreference })); @@ -563,6 +569,7 @@ describe('sign-out teardown ordering', () => { }); expect(clearKeepScreenOnPreference).toHaveBeenCalled(); + expect(clearGlanceablePreference).toHaveBeenCalled(); expect(clearReasoningPreference).toHaveBeenCalled(); expect(clearPrReviewFooterPreference).toHaveBeenCalled(); }); diff --git a/apps/mobile/src/lib/auth/auth-context.tsx b/apps/mobile/src/lib/auth/auth-context.tsx index b0840a71aa..7313313a51 100644 --- a/apps/mobile/src/lib/auth/auth-context.tsx +++ b/apps/mobile/src/lib/auth/auth-context.tsx @@ -44,6 +44,7 @@ import { } from '@/lib/auth/token-owner'; import { chainSave } from '@/lib/hooks/save-chain'; import { clearAgentModelPreference } from '@/lib/hooks/use-persisted-agent-model'; +import { clearGlanceablePreference } from '@/lib/hooks/use-glanceable-preference'; import { clearKeepScreenOnPreference } from '@/lib/hooks/use-keep-screen-on-preference'; import { clearPrReviewFooterPreference } from '@/lib/hooks/use-pr-review-footer-preference'; import { clearReasoningPreference } from '@/lib/hooks/use-reasoning-preference'; @@ -392,6 +393,7 @@ export function AuthProvider({ children }: { readonly children: ReactNode }) { clearKeepScreenOnPreference(); clearSessionScopedState(); clearPrReviewFooterPreference(); + clearGlanceablePreference(); } finally { queryClient.clear(); setSessionEnded(ended); diff --git a/apps/mobile/src/lib/auth/credentials.test.ts b/apps/mobile/src/lib/auth/credentials.test.ts index b6e2c291ed..354551e2b8 100644 --- a/apps/mobile/src/lib/auth/credentials.test.ts +++ b/apps/mobile/src/lib/auth/credentials.test.ts @@ -61,6 +61,9 @@ vi.mock('@/lib/hooks/use-persisted-agent-model', () => ({ clearAgentModelPrefere vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ clearKeepScreenOnPreference: vi.fn(), })); +vi.mock('@/lib/hooks/use-glanceable-preference', () => ({ + clearGlanceablePreference: vi.fn(), +})); vi.mock('@/lib/hooks/use-pr-review-footer-preference', () => ({ clearPrReviewFooterPreference: vi.fn(), })); diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts index 75260977dc..4f405e0325 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -10,6 +10,7 @@ import { clearActivityKitDeniedIfAvailable, getActivityKitDenied } from '@/glanc import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; import { getLastGlanceableSnapshot, getLocalScopeKey } from '@/lib/glanceable/persist'; +import { readGlanceableEnabled } from '@/lib/glanceable/enabled'; import { getGlanceableSinks } from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { i18n } from '@/i18n'; @@ -22,13 +23,17 @@ import { i18n } from '@/i18n'; let alertShown = false; -export function showActivityKitDisabledAlertOnce(): void { +export async function showActivityKitDisabledAlertOnce(): Promise { if (Platform.OS !== 'ios' || alertShown) { return; } if (!getActivityKitDenied()) { return; } + // Never ask for an OS permission the user turned the feature off for. + if (!(await readGlanceableEnabled())) { + return; + } alertShown = true; Alert.alert( i18n.t('glanceable.activityKitDisabledTitle'), @@ -49,6 +54,9 @@ export async function recoverGlanceableActivityKit(): Promise { if (Platform.OS !== 'ios' || !getActivityKitDenied()) { return; } + if (!(await readGlanceableEnabled())) { + return; + } const authEpoch = currentAuthEpoch(); const blankEpoch = getTerminalBlankEpoch(); const scopeKey = getLocalScopeKey(); diff --git a/apps/mobile/src/lib/glanceable/enabled.test.ts b/apps/mobile/src/lib/glanceable/enabled.test.ts new file mode 100644 index 0000000000..78810f8815 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/enabled.test.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + parseGlanceableEnabled, + readGlanceableEnabled, + serializeGlanceableEnabled, +} from './enabled'; + +const { getItemAsync } = vi.hoisted(() => ({ getItemAsync: vi.fn() })); +vi.mock('expo-secure-store', () => ({ getItemAsync })); + +describe('parseGlanceableEnabled', () => { + it.each([ + [null, true], + ['true', true], + ['', true], + ['nonsense', true], + ['false', false], + ])('reads %j as %s', (raw, expected) => { + expect(parseGlanceableEnabled(raw)).toBe(expected); + }); + + it('round-trips both states', () => { + expect(parseGlanceableEnabled(serializeGlanceableEnabled(false))).toBe(false); + expect(parseGlanceableEnabled(serializeGlanceableEnabled(true))).toBe(true); + }); +}); + +describe('readGlanceableEnabled', () => { + beforeEach(() => { + getItemAsync.mockReset(); + }); + + it('reads the stored switch', async () => { + getItemAsync.mockResolvedValue('false'); + expect(await readGlanceableEnabled()).toBe(false); + expect(getItemAsync).toHaveBeenCalledWith('glanceable-surfaces-enabled'); + }); + + it('keeps the surfaces on when the read fails', async () => { + getItemAsync.mockRejectedValue(new Error('storage unavailable')); + expect(await readGlanceableEnabled()).toBe(true); + }); +}); diff --git a/apps/mobile/src/lib/glanceable/enabled.ts b/apps/mobile/src/lib/glanceable/enabled.ts new file mode 100644 index 0000000000..c6b1955778 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/enabled.ts @@ -0,0 +1,33 @@ +import * as SecureStore from 'expo-secure-store'; + +import { GLANCEABLE_ENABLED_KEY } from '@/lib/storage-keys'; + +/** + * Master switch for the Active Agents glanceable surfaces, kept free of the + * preference store's toast and Sentry imports so the publisher, the background + * push, and the ActivityKit prompt can read it without loading that graph. + * + * Default-on: only the exact stored string 'false' turns the surfaces off, so a + * missing or unreadable value keeps the behavior the app ships with. The OS + * gates (iOS Live Activities, Android notification permission) stay in force + * above this switch. + */ +export function parseGlanceableEnabled(raw: string | null): boolean { + return raw !== 'false'; +} + +export function serializeGlanceableEnabled(value: boolean): string { + return value ? 'true' : 'false'; +} + +/** + * Disk read for callers with no React state, including the headless background + * push whose process starts with the in-memory default. + */ +export async function readGlanceableEnabled(): Promise { + try { + return parseGlanceableEnabled(await SecureStore.getItemAsync(GLANCEABLE_ENABLED_KEY)); + } catch { + return true; + } +} diff --git a/apps/mobile/src/lib/glanceable/mount.tsx b/apps/mobile/src/lib/glanceable/mount.tsx index f50680f6a9..a288117bf9 100644 --- a/apps/mobile/src/lib/glanceable/mount.tsx +++ b/apps/mobile/src/lib/glanceable/mount.tsx @@ -7,6 +7,7 @@ import { } from '@/lib/active-sessions-live'; import { useAuth } from '@/lib/auth/auth-context'; import { useCurrentUserId } from '@/lib/hooks/use-current-user-id'; +import { useGlanceablePreference } from '@/lib/hooks/use-glanceable-preference'; import { useOrganization } from '@/lib/organization-context'; import { useTRPC } from '@/lib/trpc'; @@ -15,7 +16,7 @@ import { persistGlanceableSink, restorePersistedGlanceable, } from './persist'; -import { getTerminalBlankEpoch } from './cleanup'; +import { getTerminalBlankEpoch, writePrivacySnapshotAndEnd } from './cleanup'; import { GlanceablePublisher } from './publisher'; import { getGlanceableSinks, registerGlanceableSink } from './sink-registry'; @@ -35,6 +36,7 @@ export function GlanceablePublisherMount(): null { const { organizationId, isLoaded } = useOrganization(); const { token } = useAuth(); const { userId } = useCurrentUserId(); + const { glanceableEnabled, hasLoaded: glanceableLoaded } = useGlanceablePreference(); const input = useMemo(() => buildActiveSessionsTrayInput(organizationId), [organizationId]); const queryKey = useMemo(() => trpc.activeSessions.list.queryKey(input), [trpc, input]); @@ -42,6 +44,17 @@ export function GlanceablePublisherMount(): null { const signedIn = token != null; + // The one place the master switch is applied. Turning it off blanks every + // surface and unregisters its push tokens, so the server stops targeting this + // device; the publisher effect below then refuses to subscribe. Blanking also + // runs on a launch that is already off, which clears a surface left behind by + // a build that had no switch. + useEffect(() => { + if (glanceableLoaded && !glanceableEnabled) { + writePrivacySnapshotAndEnd(); + } + }, [glanceableEnabled, glanceableLoaded]); + // Populate the persisted last snapshot once so cleanup/org-fence can see it, // and so the publisher below seeds its revision from the persisted value. const [restored, setRestored] = useState(false); @@ -60,7 +73,14 @@ export function GlanceablePublisherMount(): null { }, []); useEffect(() => { - if (!isLoaded || !signedIn || userId === undefined || !restored) { + if ( + !isLoaded || + !signedIn || + userId === undefined || + !restored || + !glanceableLoaded || + !glanceableEnabled + ) { return undefined; } @@ -101,7 +121,18 @@ export function GlanceablePublisherMount(): null { unsubscribe(); publisher.dispose(); }; - }, [queryClient, queryKey, targetHash, isLoaded, signedIn, userId, organizationId, restored]); + }, [ + queryClient, + queryKey, + targetHash, + isLoaded, + signedIn, + userId, + organizationId, + restored, + glanceableEnabled, + glanceableLoaded, + ]); return null; } diff --git a/apps/mobile/src/lib/hooks/use-glanceable-preference.ts b/apps/mobile/src/lib/hooks/use-glanceable-preference.ts new file mode 100644 index 0000000000..732d751b1a --- /dev/null +++ b/apps/mobile/src/lib/hooks/use-glanceable-preference.ts @@ -0,0 +1,30 @@ +import { useSyncExternalStore } from 'react'; + +import { + parseGlanceableEnabled, + serializeGlanceableEnabled, +} from '@/lib/glanceable/enabled'; +import { createSecureStorePreference } from '@/lib/hooks/secure-store-preference'; +import { GLANCEABLE_ENABLED_KEY } from '@/lib/storage-keys'; + +/** Reactive view of the Active Agents master switch; see `glanceable/enabled`. */ +const store = createSecureStorePreference({ + key: GLANCEABLE_ENABLED_KEY, + defaultValue: true, + parse: parseGlanceableEnabled, + serialize: serializeGlanceableEnabled, +}); + +export function clearGlanceablePreference() { + store.clear(); +} + +function setGlanceableEnabled(value: boolean) { + store.set(value); +} + +export function useGlanceablePreference() { + const glanceableEnabled = useSyncExternalStore(store.subscribe, store.get); + const hasLoaded = useSyncExternalStore(store.subscribe, store.getHasLoaded); + return { glanceableEnabled, hasLoaded, setGlanceableEnabled }; +} diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index 242c563af6..afc580dd04 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -20,7 +20,11 @@ import { registerGlanceableSink, unregisterGlanceableSink, } from '@/lib/glanceable/sink-registry'; -import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; +import { + ACTIVE_USER_ID_KEY, + GLANCEABLE_ENABLED_KEY, + ORGANIZATION_STORAGE_KEY, +} from '@/lib/storage-keys'; import { _setGlanceableSinksLoaderForTests, applyGlanceablePushData, @@ -545,6 +549,28 @@ describe('applyGlanceablePushData', () => { unregisterGlanceableSink(sink); }); + it('drops a remote snapshot while the Active Agents switch is off', async () => { + mocks.getItemAsync.mockImplementation((key: string) => { + if (key === GLANCEABLE_ENABLED_KEY) { + return 'false'; + } + return key === ACTIVE_USER_ID_KEY ? 'u1' : 'org-9'; + }); + _setLastGlanceableSnapshotForTests(glanceableSnapshot({ scopeKey: SCOPE_KEY, revision: 1 })); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + const result = await applyGlanceablePushData( + activeGlanceablePush({ scopeKey: SCOPE_KEY, updatedAt: '2026-01-03T00:00:00.000Z' }) + ); + + expect(result).toBe(false); + expect(sink.publish).not.toHaveBeenCalled(); + expect(sink.startOrUpdate).not.toHaveBeenCalled(); + + unregisterGlanceableSink(sink); + }); + it('applies a newer remote snapshot and re-registers under the selected organization', async () => { _setLastGlanceableSnapshotForTests( glanceableSnapshot({ diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index 8abc51d8b6..8e363c7089 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -27,6 +27,7 @@ import { import { captureEvent } from '@/lib/analytics/posthog'; import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; +import { readGlanceableEnabled } from '@/lib/glanceable/enabled'; import { getLastGlanceableSnapshot, getLocalScopeKey, @@ -133,6 +134,12 @@ export async function applyGlanceablePushData( return false; } + // The headless process starts with the in-memory default, so read the switch + // from disk. Dropping the push leaves the surfaces the in-app blank produced. + if (!(await readGlanceableEnabled())) { + return false; + } + const organizationId = await getSelectedOrganizationId(); const userId = await getActiveUserId(); if ( diff --git a/apps/mobile/src/lib/storage-keys.ts b/apps/mobile/src/lib/storage-keys.ts index ddba8089a1..a447e72424 100644 --- a/apps/mobile/src/lib/storage-keys.ts +++ b/apps/mobile/src/lib/storage-keys.ts @@ -36,6 +36,9 @@ export const RETURN_SENDS_MESSAGE_KEY = 'return-sends-message'; /** Revocable per-host list of markdown link hosts that open without an Alert. */ export const TRUSTED_HOSTS_KEY = 'trusted-hosts'; export const PR_REVIEW_FOOTER_KEY = 'pr-review-footer-enabled'; +/** Master switch for the glanceable Active Agents surfaces (widgets, Live Activity, + * Android ongoing). Off blanks every surface and unregisters its push tokens. */ +export const GLANCEABLE_ENABLED_KEY = 'glanceable-surfaces-enabled'; /** SQLCipher database key for the encrypted persistence store (DEC-01). */ export const PERSIST_DB_KEY = 'persist-db-key'; /** diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts index 5e91ce270f..d2034b5269 100644 --- a/services/notifications/src/lib/glanceable-delivery.test.ts +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -553,7 +553,10 @@ describe('NotificationsService.refreshGlanceableSessions', () => { 'sign', 'verify', ])) as CryptoKeyPair; - const der = new Uint8Array(await crypto.subtle.exportKey('pkcs8', pair.privateKey)); + // `exportKey` types the return as ArrayBuffer | JsonWebKey; 'pkcs8' always yields the buffer. + const der = new Uint8Array( + (await crypto.subtle.exportKey('pkcs8', pair.privateKey)) as ArrayBuffer + ); return `-----BEGIN PRIVATE KEY-----\n${btoa(String.fromCharCode(...der))}\n-----END PRIVATE KEY-----`; } diff --git a/services/notifications/wrangler.jsonc b/services/notifications/wrangler.jsonc index 3fa241e4fe..19f0aaef9c 100644 --- a/services/notifications/wrangler.jsonc +++ b/services/notifications/wrangler.jsonc @@ -11,6 +11,11 @@ "vars": { "WORKER_ENV": "production", "KILO_WEB_API_BASE_URL": "https://app.kilo.ai", + // Live Activity push topic: the iOS bundle id. The client appends + // `.push-type.liveactivity`. APNS_TEAM_ID and APNS_KEY_ID belong beside + // this once the APNs auth key exists; until all four values are present the + // worker logs "credentials missing" and skips Live Activity delivery. + "APNS_TOPIC": "com.kilocode.kiloapp", }, "routes": [ @@ -62,6 +67,11 @@ }, ], + // Add once the .p8 auth key is stored (secret_name APNS_PRIVATE_KEY_PROD): + // { "binding": "APNS_PRIVATE_KEY", "store_id": "342a86d9e3a94da698e82d0c6e2a36f0", + // "secret_name": "APNS_PRIVATE_KEY_PROD" } + // A binding for a secret that does not exist fails the deploy, so the secret + // must land first. "secrets_store_secrets": [ { "binding": "EXPO_ACCESS_TOKEN", diff --git a/services/session-ingest/src/dos/UserConnectionDO.test.ts b/services/session-ingest/src/dos/UserConnectionDO.test.ts index 581f0cef20..1b712c5d63 100644 --- a/services/session-ingest/src/dos/UserConnectionDO.test.ts +++ b/services/session-ingest/src/dos/UserConnectionDO.test.ts @@ -700,7 +700,7 @@ describe('UserConnectionDO', () => { sendHeartbeat(doInstance, cliWs, [makeSession('s1', 'question')]); await flushAsync(); messages.length = 0; - const reset = Promise.withResolvers(); + const reset = Promise.withResolvers(); sessionIngestMocks.resetAttentionStatusOnCliDisconnect.mockImplementation( () => reset.promise ); @@ -708,7 +708,7 @@ describe('UserConnectionDO', () => { const disconnect = disconnectCli(doInstance, cliWs); await flushAsync(); expect(messages).toEqual([]); - reset.resolve(); + reset.resolve(undefined); await disconnect; await flushAsync(); expect(messages.map(message => message.data)).toMatchObject([ @@ -753,7 +753,7 @@ describe('UserConnectionDO', () => { refreshGlanceableSessions: async () => { throw new Error('transport unavailable'); }, - } as Env['NOTIFICATIONS'], + } as unknown as Env['NOTIFICATIONS'], }); const cliWs = addCliSocket(mockCtx, 'cli-1', [], undefined, 'usr_1'); sendHeartbeat(doInstance, cliWs, [makeSession('s1', 'retry')]); diff --git a/services/session-ingest/src/notifications-bindings.d.ts b/services/session-ingest/src/notifications-bindings.d.ts new file mode 100644 index 0000000000..537fcbca39 --- /dev/null +++ b/services/session-ingest/src/notifications-bindings.d.ts @@ -0,0 +1,28 @@ +// The DO and metadata tests import `../../notifications/src`, whose modules read +// their bindings off a global `Env`. This package has no global `Env` (its +// `wrangler types` output names the interface `CloudflareBindings`), and +// notifications' own output cannot be included here because it embeds a whole +// workerd runtime that collides with `@cloudflare/workers-types`. So declare the +// global `Env` those modules expect: this package's bindings plus the +// notifications-only ones they touch. +import type { NotificationChannelDO } from '../../notifications/src/index'; + +declare global { + interface Env extends CloudflareBindings { + WORKER_ENV: string; + KILO_WEB_API_BASE_URL: string; + NEXTAUTH_SECRET: SecretsStoreSecret; + INTERNAL_API_SECRET: SecretsStoreSecret; + EXPO_ACCESS_TOKEN: SecretsStoreSecret; + RECEIPTS_QUEUE: Queue; + NOTIFICATION_CHANNEL_DO: DurableObjectNamespace; + EVENT_SERVICE: Fetcher & { + isUserInContext(userId: string, context: string): Promise; + }; + PUSH_SINK_MODE?: string; + APNS_TEAM_ID?: string; + APNS_KEY_ID?: string; + APNS_TOPIC?: string; + APNS_PRIVATE_KEY?: SecretsStoreSecret; + } +} \ No newline at end of file From e92f63c7a4fa462029a7135b3d51148a2f72013b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 2 Sep 2026 16:15:12 +0200 Subject: [PATCH 27/43] feat(notifications): land the APNs Live Activity credentials Register a team-scoped APNs auth key (KRYMZL626P, sandbox and production) and store the .p8 in the Secrets Store. Declare the team and key ids as vars, bind the private key, and regenerate the Worker bindings. Live Activity push now has every value it needs. A backup of the .p8 is in the 1Password "Eng / Product" vault; Apple never serves it a second time. --- ENVIRONMENT.md | 10 ++++---- .../lib/hooks/use-glanceable-preference.ts | 5 +--- .../notifications/worker-configuration.d.ts | 9 ++++++-- services/notifications/wrangler.jsonc | 23 +++++++++++-------- .../src/notifications-bindings.d.ts | 2 +- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index de24c2f1e9..8fc2e58f44 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -325,13 +325,15 @@ When `VERCEL_TARGET_ENV` is absent in local development or a script process, tra ### Notifications Worker -- `APNS_TEAM_ID` - Apple Developer team ID for the token-based APNs key used to send Live Activity pushes. Declare it in `services/notifications/wrangler.jsonc` under `vars`. [SERVER] -- `APNS_KEY_ID` - APNs key identifier (`kid`) for the Live Activity push key. Declare it beside `APNS_TEAM_ID`. [SERVER] -- `APNS_PRIVATE_KEY` - PKCS#8 ES256 `.p8` private key contents for APNs provider-token signing. Store the key in the Secrets Store first, then add its `secrets_store_secrets` binding; a binding for a missing secret fails the deploy. `[SECRET]` +- `APNS_TEAM_ID` - Apple Developer team ID for the token-based APNs key used to send Live Activity pushes. Set in `services/notifications/wrangler.jsonc` under `vars`. [SERVER] +- `APNS_KEY_ID` - APNs key identifier (`kid`) for the Live Activity push key. Set beside `APNS_TEAM_ID`. [SERVER] +- `APNS_PRIVATE_KEY` - PKCS#8 ES256 `.p8` private key contents for APNs provider-token signing. Stored as one line: the PEM decoder strips every whitespace character, so the newlines are not needed. Store the key in the Secrets Store first, then add its `secrets_store_secrets` binding; a binding for a missing secret fails the deploy. `[SECRET]` - `APNS_TOPIC` - iOS app bundle id (`com.kilocode.kiloapp`); Live Activity pushes use `.push-type.liveactivity`. Already set in `vars`. [SERVER] +- `KILO_WEB_API_BASE_URL` - Base origin of the web app, used to reach the internal `glanceable-agents-snapshot` route; `https://app.kilo.ai` in production. [SERVER] Until all four values reach the worker it logs `APNs Live Activity credentials missing` and skips Live Activity pushes. Every other glanceable delivery, including the Expo aggregate push, keeps working. -- `KILO_WEB_API_BASE_URL` - Base origin of the web app, used to reach the internal `glanceable-agents-snapshot` route; `https://app.kilo.ai` in production. [SERVER] + +The key is team-scoped for all topics and valid in both the sandbox and production APNs environments. A backup of the `.p8` lives in the 1Password "Eng / Product" vault as "Apple AuthKey KRYMZL626P (.p8)"; Apple never serves it a second time. ### KiloClaw Controller diff --git a/apps/mobile/src/lib/hooks/use-glanceable-preference.ts b/apps/mobile/src/lib/hooks/use-glanceable-preference.ts index 732d751b1a..916dc13db6 100644 --- a/apps/mobile/src/lib/hooks/use-glanceable-preference.ts +++ b/apps/mobile/src/lib/hooks/use-glanceable-preference.ts @@ -1,9 +1,6 @@ import { useSyncExternalStore } from 'react'; -import { - parseGlanceableEnabled, - serializeGlanceableEnabled, -} from '@/lib/glanceable/enabled'; +import { parseGlanceableEnabled, serializeGlanceableEnabled } from '@/lib/glanceable/enabled'; import { createSecureStorePreference } from '@/lib/hooks/secure-store-preference'; import { GLANCEABLE_ENABLED_KEY } from '@/lib/storage-keys'; diff --git a/services/notifications/worker-configuration.d.ts b/services/notifications/worker-configuration.d.ts index 49bcf264da..9476bbde2a 100644 --- a/services/notifications/worker-configuration.d.ts +++ b/services/notifications/worker-configuration.d.ts @@ -1,12 +1,17 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 459375004e00f5f56035f61e8a2a25cb) +// Generated by Wrangler by running `wrangler types` (hash: 8d60884f70698bfeb921900c6f2c4813) // Runtime types generated with workerd@1.20260603.1 2026-02-01 nodejs_compat interface __BaseEnv_Env { HYPERDRIVE: Hyperdrive; RECEIPTS_QUEUE: Queue; + APNS_PRIVATE_KEY: SecretsStoreSecret; EXPO_ACCESS_TOKEN: SecretsStoreSecret; NEXTAUTH_SECRET: SecretsStoreSecret; INTERNAL_API_SECRET: SecretsStoreSecret; + KILO_WEB_API_BASE_URL: "https://app.kilo.ai"; + APNS_TOPIC: "com.kilocode.kiloapp"; + APNS_TEAM_ID: "X96D76J65Z"; + APNS_KEY_ID: "KRYMZL626P"; WORKER_ENV: string; PUSH_SINK_MODE: string; NOTIFICATION_CHANNEL_DO: DurableObjectNamespace; @@ -24,7 +29,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types diff --git a/services/notifications/wrangler.jsonc b/services/notifications/wrangler.jsonc index 19f0aaef9c..ba51bd0628 100644 --- a/services/notifications/wrangler.jsonc +++ b/services/notifications/wrangler.jsonc @@ -11,11 +11,14 @@ "vars": { "WORKER_ENV": "production", "KILO_WEB_API_BASE_URL": "https://app.kilo.ai", - // Live Activity push topic: the iOS bundle id. The client appends - // `.push-type.liveactivity`. APNS_TEAM_ID and APNS_KEY_ID belong beside - // this once the APNs auth key exists; until all four values are present the - // worker logs "credentials missing" and skips Live Activity delivery. + // APNs auth for Live Activity push. The topic is the iOS bundle id; the + // client appends `.push-type.liveactivity`. The team and key ids are public + // identifiers, so they live here; the .p8 itself is a Secrets Store secret. + // Until all four values are present the worker logs "credentials missing" + // and skips Live Activity delivery. "APNS_TOPIC": "com.kilocode.kiloapp", + "APNS_TEAM_ID": "X96D76J65Z", + "APNS_KEY_ID": "KRYMZL626P", }, "routes": [ @@ -67,12 +70,14 @@ }, ], - // Add once the .p8 auth key is stored (secret_name APNS_PRIVATE_KEY_PROD): - // { "binding": "APNS_PRIVATE_KEY", "store_id": "342a86d9e3a94da698e82d0c6e2a36f0", - // "secret_name": "APNS_PRIVATE_KEY_PROD" } - // A binding for a secret that does not exist fails the deploy, so the secret - // must land first. "secrets_store_secrets": [ + { + // The APNs .p8 auth key, stored as one line: `pemToDer()` strips every + // whitespace character, so the newlines the file had are not needed. + "binding": "APNS_PRIVATE_KEY", + "store_id": "342a86d9e3a94da698e82d0c6e2a36f0", + "secret_name": "APNS_PRIVATE_KEY_PROD", + }, { "binding": "EXPO_ACCESS_TOKEN", "store_id": "342a86d9e3a94da698e82d0c6e2a36f0", diff --git a/services/session-ingest/src/notifications-bindings.d.ts b/services/session-ingest/src/notifications-bindings.d.ts index 537fcbca39..9bc040d467 100644 --- a/services/session-ingest/src/notifications-bindings.d.ts +++ b/services/session-ingest/src/notifications-bindings.d.ts @@ -25,4 +25,4 @@ declare global { APNS_TOPIC?: string; APNS_PRIVATE_KEY?: SecretsStoreSecret; } -} \ No newline at end of file +} From a71c13e290283003cd7812a1e15af814e375a91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 2 Sep 2026 17:28:35 +0200 Subject: [PATCH 28/43] chore(dev): remove the dev:mobile:open script The app-side consumer of `dev_session_token` was deleted in #5641, so the script printed a successful sign-in and the app stayed on the login screen. Nothing else imports it. --- dev/local/mobile-open-routes.ts | 187 -------------------------------- dev/local/mobile-open.test.ts | 55 ---------- dev/local/mobile-open.ts | 186 ------------------------------- package.json | 1 - 4 files changed, 429 deletions(-) delete mode 100644 dev/local/mobile-open-routes.ts delete mode 100644 dev/local/mobile-open.test.ts delete mode 100644 dev/local/mobile-open.ts diff --git a/dev/local/mobile-open-routes.ts b/dev/local/mobile-open-routes.ts deleted file mode 100644 index 57bca93fe6..0000000000 --- a/dev/local/mobile-open-routes.ts +++ /dev/null @@ -1,187 +0,0 @@ -export const MOBILE_OPEN_ROUTES = [ - { name: 'home', path: '/home', description: 'Home tab' }, - { name: 'sessions', path: '/cloud/sessions', description: 'Session list (Agents tab)' }, - { name: 'session-list', path: '/cloud/sessions', description: 'Alias of sessions' }, - { - name: 'session', - path: '/cloud/sessions/', - description: 'One session. Pass --session-id=.', - }, - { name: 'settings', path: '/profile/preferences', description: 'Settings / preferences' }, - { name: 'profile', path: '/profile', description: 'Profile tab' }, -] as const; - -const NAMED_PATHS: Record = { - home: '/home', - sessions: '/cloud/sessions', - 'session-list': '/cloud/sessions', - settings: '/profile/preferences', - profile: '/profile', -}; - -export type MobileOpenPlatform = 'ios' | 'android'; - -export type MobileOpenOptions = { - email: string; - route: string; - sessionId: string | null; - platform: MobileOpenPlatform | null; - udid: string | null; - serial: string | null; -}; - -export function printMobileOpenUsage(): void { - console.log('Usage: pnpm dev:mobile:open --email [options]'); - console.log(''); - console.log('Issues a device session for a seeded user and opens the mobile dev build'); - console.log('on that route. Dev-build only: the app reads session tokens from the URL'); - console.log('when __DEV__ is true.'); - console.log(''); - console.log('Routes:'); - for (const route of MOBILE_OPEN_ROUTES) { - console.log(` ${route.name.padEnd(14)} ${route.path.padEnd(32)} ${route.description}`); - } - console.log(' / raw web path already in the universal-link table'); - console.log(''); - console.log('Options:'); - console.log(' --email= Seeded user email (required)'); - console.log(' --session-id= Required when is session'); - console.log(' --ios Open on the booted iOS simulator'); - console.log(' --android Open on a connected Android device/emulator'); - console.log(' --udid= iOS simulator UDID (default: booted)'); - console.log(' --serial= Android serial (default: first adb device)'); - console.log(''); - console.log('Examples:'); - console.log(' pnpm dev:mobile:open'); - console.log(' pnpm dev:mobile:open --email ada@example.com home'); - console.log(' pnpm dev:mobile:open --email ada@example.com session --session-id ses_1 --ios'); -} - -function isValidEmail(email: string): boolean { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); -} - -function takeFlagValue( - args: string[], - index: number, - flag: string -): { value: string; consumed: number } { - const arg = args[index]; - if (arg.length > flag.length && arg[flag.length] === '=') { - const inline = arg.slice(flag.length + 1).trim(); - if (!inline) { - throw new Error(`${flag} requires a value`); - } - return { value: inline, consumed: 1 }; - } - const next = args[index + 1]; - if (next === undefined || next.startsWith('--')) { - throw new Error(`${flag} requires a value`); - } - return { value: next.trim(), consumed: 2 }; -} - -export function parseMobileOpenArgs(args: string[]): MobileOpenOptions | null { - if (args.length === 0 || args.includes('--help') || args.includes('-h')) { - return null; - } - - let email: string | null = null; - let route: string | null = null; - let sessionId: string | null = null; - let platform: MobileOpenPlatform | null = null; - let udid: string | null = null; - let serial: string | null = null; - - for (let index = 0; index < args.length; index++) { - const arg = args[index]; - if (arg === '--ios') { - platform = 'ios'; - continue; - } - if (arg === '--android') { - platform = 'android'; - continue; - } - if (arg === '--email' || arg.startsWith('--email=')) { - const taken = takeFlagValue(args, index, '--email'); - email = taken.value; - index += taken.consumed - 1; - continue; - } - if (arg === '--session-id' || arg.startsWith('--session-id=')) { - const taken = takeFlagValue(args, index, '--session-id'); - sessionId = taken.value; - index += taken.consumed - 1; - continue; - } - if (arg === '--udid' || arg.startsWith('--udid=')) { - const taken = takeFlagValue(args, index, '--udid'); - udid = taken.value; - index += taken.consumed - 1; - continue; - } - if (arg === '--serial' || arg.startsWith('--serial=')) { - const taken = takeFlagValue(args, index, '--serial'); - serial = taken.value; - index += taken.consumed - 1; - continue; - } - if (arg.startsWith('--')) { - throw new Error(`Unknown argument: ${arg}`); - } - if (route !== null) { - throw new Error(`Unexpected positional argument: ${arg}`); - } - route = arg.trim(); - } - - if (!email) { - throw new Error('--email is required'); - } - if (!isValidEmail(email)) { - throw new Error(`email is not a valid address: ${email}`); - } - if (!route) { - throw new Error('route is required'); - } - - return { email, route, sessionId, platform, udid, serial }; -} - -export function resolveMobileOpenRoute(route: string, sessionId: string | null): string { - if (route === 'session') { - if (!sessionId) { - throw new Error('session requires --session-id='); - } - if (sessionId.includes('/') || sessionId.includes('?')) { - throw new Error('--session-id must be a single path segment'); - } - return `/cloud/sessions/${sessionId}`; - } - if (route.startsWith('/')) { - return route; - } - const named = NAMED_PATHS[route]; - if (!named) { - const names = MOBILE_OPEN_ROUTES.map(entry => entry.name).join(', '); - throw new Error(`Unknown route: ${route}. Known routes: ${names}`); - } - return named; -} - -export function buildDevSessionUrl( - pathName: string, - credentials: { - token: string; - refreshToken: string; - expiresIn: number; - } -): string { - const params = new URLSearchParams({ - dev_session_token: credentials.token, - dev_session_refresh: credentials.refreshToken, - dev_session_expires_in: String(credentials.expiresIn), - }); - return `kiloapp://${pathName}?${params.toString()}`; -} diff --git a/dev/local/mobile-open.test.ts b/dev/local/mobile-open.test.ts deleted file mode 100644 index 95fb314f1d..0000000000 --- a/dev/local/mobile-open.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { - buildDevSessionUrl, - MOBILE_OPEN_ROUTES, - parseMobileOpenArgs, - resolveMobileOpenRoute, -} from './mobile-open-routes'; - -test('parseMobileOpenArgs lists usage when called without arguments', () => { - assert.equal(parseMobileOpenArgs([]), null); - assert.equal(parseMobileOpenArgs(['--help']), null); -}); - -test('parseMobileOpenArgs reads email and a named route', () => { - assert.deepEqual(parseMobileOpenArgs(['--email', 'ada@example.com', 'home']), { - email: 'ada@example.com', - route: 'home', - sessionId: null, - platform: null, - udid: null, - serial: null, - }); -}); - -test('resolveMobileOpenRoute maps names and raw paths', () => { - assert.equal(resolveMobileOpenRoute('home', null), '/home'); - assert.equal(resolveMobileOpenRoute('sessions', null), '/cloud/sessions'); - assert.equal(resolveMobileOpenRoute('settings', null), '/profile/preferences'); - assert.equal(resolveMobileOpenRoute('/profile', null), '/profile'); - assert.equal(resolveMobileOpenRoute('session', 'ses_1'), '/cloud/sessions/ses_1'); -}); - -test('resolveMobileOpenRoute rejects an unknown name and a missing session id', () => { - assert.throws(() => resolveMobileOpenRoute('unknown', null), /Unknown route/); - assert.throws(() => resolveMobileOpenRoute('session', null), /session requires --session-id/); -}); - -test('buildDevSessionUrl puts credentials on the kiloapp URL', () => { - const url = buildDevSessionUrl('/home', { - token: 'tok', - refreshToken: 'ref', - expiresIn: 3600, - }); - assert.equal( - url, - 'kiloapp:///home?dev_session_token=tok&dev_session_refresh=ref&dev_session_expires_in=3600' - ); -}); - -test('route list includes the E2E screens', () => { - const names = MOBILE_OPEN_ROUTES.map(route => route.name); - assert.deepEqual(names, ['home', 'sessions', 'session-list', 'session', 'settings', 'profile']); -}); diff --git a/dev/local/mobile-open.ts b/dev/local/mobile-open.ts deleted file mode 100644 index b89e99d312..0000000000 --- a/dev/local/mobile-open.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { createHash, randomBytes } from 'node:crypto'; -import path from 'node:path'; - -import { device_refresh_tokens, device_sessions, kilocode_users } from '@kilocode/db/schema'; -import { signKiloToken } from '@kilocode/worker-utils'; -import { eq } from 'drizzle-orm'; - -import { resolveAndroidEnvironment } from './mobile-android'; -import { - buildDevSessionUrl, - parseMobileOpenArgs, - printMobileOpenUsage, - resolveMobileOpenRoute, -} from './mobile-open-routes'; - -// dev/local is ESM (dev/local/package.json sets type: module); dev/seed is CommonJS. -// A static named import across that boundary fails, because tsx's CJS output -// hides the named exports from Node's module lexer. Import at call time instead: -// a dynamic import resolves the names at runtime and keeps this the only file -// that has to know the two directories disagree. -async function seedLib() { - const [db, users] = await Promise.all([import('../seed/lib/db'), import('../seed/lib/users')]); - return { getSeedDb: db.getSeedDb, resolveSeedUserId: users.resolveSeedUserId }; -} - -const ACCESS_TOKEN_SECONDS = 60 * 60; -const REFRESH_TOKEN_SECONDS = 30 * 24 * 60 * 60; -const DEV_USER_AGENT = 'kilo-dev-mobile-open'; - -function hashToken(token: string): string { - return createHash('sha256').update(token).digest('hex'); -} - -async function issueDevMobileSession(userId: string): Promise<{ - token: string; - refreshToken: string; - expiresIn: number; -}> { - const secret = process.env.NEXTAUTH_SECRET; - if (!secret) { - throw new Error( - 'NEXTAUTH_SECRET is not set for this worktree. Run pnpm dev:worktree:prepare first.' - ); - } - - const { getSeedDb } = await seedLib(); - const db = getSeedDb(); - const [user] = await db - .select({ - id: kilocode_users.id, - apiTokenPepper: kilocode_users.api_token_pepper, - }) - .from(kilocode_users) - .where(eq(kilocode_users.id, userId)) - .limit(1); - if (!user) { - throw new Error(`User ${userId} was not found`); - } - - const [session] = await db - .insert(device_sessions) - .values({ - kilo_user_id: user.id, - user_agent: DEV_USER_AGENT, - }) - .returning({ id: device_sessions.id }); - if (!session) { - throw new Error('Failed to create device session'); - } - - const { token } = await signKiloToken({ - userId: user.id, - pepper: user.apiTokenPepper, - secret, - expiresInSeconds: ACCESS_TOKEN_SECONDS, - env: process.env.NODE_ENV ?? 'development', - extra: { deviceSessionId: session.id }, - }); - const refreshToken = randomBytes(32).toString('base64url'); - const expiresAt = new Date(Date.now() + REFRESH_TOKEN_SECONDS * 1000).toISOString(); - await db.insert(device_refresh_tokens).values({ - token_hash: hashToken(refreshToken), - device_session_id: session.id, - expires_at: expiresAt, - }); - - return { - token, - refreshToken, - expiresIn: ACCESS_TOKEN_SECONDS, - }; -} - -function detectIosBooted(): boolean { - try { - const output = execFileSync('xcrun', ['simctl', 'list', 'devices', 'booted'], { - encoding: 'utf8', - }); - return output.includes('(Booted)'); - } catch { - return false; - } -} - -function firstAndroidSerial(): string | null { - try { - const env = resolveAndroidEnvironment({ - home: process.env.HOME ?? '', - path: process.env.PATH ?? '', - }); - const output = execFileSync(env.adb, ['devices'], { encoding: 'utf8' }); - const lines = output.split('\n').slice(1); - for (const line of lines) { - const [serial, state] = line.trim().split(/\s+/); - if (serial && state === 'device') { - return serial; - } - } - return null; - } catch { - return null; - } -} - -function openOnIos(url: string, udid: string | null): void { - const target = udid ?? 'booted'; - execFileSync('xcrun', ['simctl', 'openurl', target, url], { stdio: 'inherit' }); -} - -function openOnAndroid(url: string, serial: string | null): void { - const env = resolveAndroidEnvironment({ - home: process.env.HOME ?? '', - path: process.env.PATH ?? '', - }); - const args = ['shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', url]; - if (serial) { - execFileSync(env.adb, ['-s', serial, ...args], { stdio: 'inherit' }); - return; - } - execFileSync(env.adb, args, { stdio: 'inherit' }); -} - -export async function runMobileOpen(args: string[]): Promise { - const options = parseMobileOpenArgs(args); - if (!options) { - printMobileOpenUsage(); - return; - } - - const webPath = resolveMobileOpenRoute(options.route, options.sessionId); - const { resolveSeedUserId } = await seedLib(); - const userId = await resolveSeedUserId(options.email); - const credentials = await issueDevMobileSession(userId); - const url = buildDevSessionUrl(webPath, credentials); - - let platform = options.platform; - if (!platform) { - if (detectIosBooted()) { - platform = 'ios'; - } else if (firstAndroidSerial()) { - platform = 'android'; - } else { - throw new Error( - 'No booted iOS simulator or connected Android device. Boot one, or pass --ios / --android.' - ); - } - } - - if (platform === 'ios') { - openOnIos(url, options.udid); - } else { - openOnAndroid(url, options.serial); - } - - console.log(`Opened ${webPath} as ${options.email} (${userId}) on ${platform}`); -} - -const isMain = - process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename); -if (isMain) { - runMobileOpen(process.argv.slice(2)).catch(error => { - console.error(error instanceof Error ? error.message : error); - process.exit(1); - }); -} diff --git a/package.json b/package.json index ab9d244b9a..dd8370b23c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "dev:env:mobile": "tsx dev/local/mobile-env.ts", "dev:mobile:android": "tsx dev/local/mobile-android.ts", "dev:mobile:ios": "tsx dev/local/mobile-ios-build.ts", - "dev:mobile:open": "tsx dev/local/mobile-open.ts", "test:dev-local": "tsx --test dev/local/*.test.ts dev/local/env-sync/*.test.ts dev/local/scripts/*.test.ts dev/seed/lib/*.test.ts", "test:mobile-workflow": "tsx --test dev/local/mobile-native-build.test.ts dev/local/mobile-ios-build.test.ts dev/local/mobile-android-build.test.ts dev/local/mobile-android.test.ts dev/local/mobile-workflow.test.ts", "dev:setup-env": "tsx dev/local/setup-env.ts", From 52b9558db2a3a842f428dc63368f963afa5ec3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 2 Sep 2026 19:06:37 +0200 Subject: [PATCH 29/43] feat(mobile): redesign the glanceable iOS surfaces around three states Replace the four-state count model with three states the user named: needs input (orange), working (green), and idle (white). The `retry` status folds into needs input, because it means one thing to the user: the agent waits and cannot go on alone. The "Reconnecting" label is gone. The Dynamic Island now shows the Kilo mark and one number, colored by the state it counts and ranked needs-input, working, idle. The Lock Screen banner and every widget family show one row per non-zero state, each with a glyph whose shape differs as well as its color, so the state reads in the accented Lock Screen rendering too. The "Open agents" line is gone from every surface; the whole surface already deep-links there, so the label stays in the spoken text. Copy the Kilo mark into the shared app group and bake its path into the stringified `'widget'` layouts, because the widget extension cannot resolve a bundle asset and the notifications Worker cannot know a device path. Fix two defects the redesign uncovered: - The layouts referenced the imported `WIDGET_LOGO_PLACEHOLDER`, which is an undefined global once the layout source is re-evaluated in the widget process. Each layout now repeats the literal, and a test keeps the copies equal. - Widget props carried null fields. `updateTimeline` writes them to the shared `UserDefaults`, which rejects a null and threw out through the host function, so no widget family ever rendered. `toWidgetProps` omits them. --- apps/mobile/app.config.ts | 2 +- apps/mobile/assets/images/logo-widget.png | Bin 0 -> 2766 bytes .../active-agents-widget.test.ts | 20 +- .../glanceable-android/android-sink.test.ts | 40 +-- .../src/glanceable-android/register.test.ts | 14 +- .../glanceable-android/widget-props.test.ts | 26 +- .../src/glanceable-android/widget-props.ts | 4 +- .../active-agents-live-activity.tsx | 228 +++++++++++----- .../glanceable-ios/active-agents-widget.tsx | 258 +++++++++++++----- .../glanceable-ios/ios-sink.native.test.ts | 12 +- .../src/glanceable-ios/ios-sink.test.ts | 78 ++++-- apps/mobile/src/glanceable-ios/ios-sink.ts | 27 +- apps/mobile/src/glanceable-ios/register.ts | 6 + apps/mobile/src/glanceable-ios/view-props.ts | 34 ++- .../src/glanceable-ios/widget-logo.test.ts | 32 +++ apps/mobile/src/glanceable-ios/widget-logo.ts | 93 +++++++ apps/mobile/src/i18n/locales/af.json | 4 +- apps/mobile/src/i18n/locales/am.json | 4 +- apps/mobile/src/i18n/locales/ar.json | 4 +- apps/mobile/src/i18n/locales/az.json | 4 +- apps/mobile/src/i18n/locales/be.json | 4 +- apps/mobile/src/i18n/locales/bg.json | 4 +- apps/mobile/src/i18n/locales/bn.json | 4 +- apps/mobile/src/i18n/locales/bs.json | 4 +- apps/mobile/src/i18n/locales/ca.json | 4 +- apps/mobile/src/i18n/locales/ckb.json | 4 +- apps/mobile/src/i18n/locales/cs.json | 4 +- apps/mobile/src/i18n/locales/cy.json | 4 +- apps/mobile/src/i18n/locales/da.json | 4 +- apps/mobile/src/i18n/locales/de.json | 4 +- apps/mobile/src/i18n/locales/el.json | 4 +- apps/mobile/src/i18n/locales/en.json | 4 +- apps/mobile/src/i18n/locales/es.json | 4 +- apps/mobile/src/i18n/locales/et.json | 4 +- apps/mobile/src/i18n/locales/eu.json | 4 +- apps/mobile/src/i18n/locales/fa.json | 4 +- apps/mobile/src/i18n/locales/fi.json | 4 +- apps/mobile/src/i18n/locales/fil.json | 4 +- apps/mobile/src/i18n/locales/fr.json | 4 +- apps/mobile/src/i18n/locales/ga.json | 4 +- apps/mobile/src/i18n/locales/gl.json | 4 +- apps/mobile/src/i18n/locales/gu.json | 4 +- apps/mobile/src/i18n/locales/ha.json | 4 +- apps/mobile/src/i18n/locales/he.json | 4 +- apps/mobile/src/i18n/locales/hi.json | 4 +- apps/mobile/src/i18n/locales/hr.json | 4 +- apps/mobile/src/i18n/locales/ht.json | 4 +- apps/mobile/src/i18n/locales/hu.json | 4 +- apps/mobile/src/i18n/locales/hy.json | 4 +- apps/mobile/src/i18n/locales/id.json | 4 +- apps/mobile/src/i18n/locales/ig.json | 4 +- apps/mobile/src/i18n/locales/is.json | 4 +- apps/mobile/src/i18n/locales/it.json | 4 +- apps/mobile/src/i18n/locales/ja.json | 4 +- apps/mobile/src/i18n/locales/ka.json | 4 +- apps/mobile/src/i18n/locales/kk.json | 2 +- apps/mobile/src/i18n/locales/km.json | 2 +- apps/mobile/src/i18n/locales/kn.json | 4 +- apps/mobile/src/i18n/locales/ko.json | 4 +- apps/mobile/src/i18n/locales/lo.json | 2 +- apps/mobile/src/i18n/locales/lt.json | 2 +- apps/mobile/src/i18n/locales/lv.json | 4 +- apps/mobile/src/i18n/locales/mg.json | 4 +- apps/mobile/src/i18n/locales/mi.json | 4 +- apps/mobile/src/i18n/locales/mk.json | 4 +- apps/mobile/src/i18n/locales/ml.json | 2 +- apps/mobile/src/i18n/locales/mn.json | 4 +- apps/mobile/src/i18n/locales/mr.json | 4 +- apps/mobile/src/i18n/locales/ms.json | 4 +- apps/mobile/src/i18n/locales/mt.json | 4 +- apps/mobile/src/i18n/locales/my.json | 4 +- apps/mobile/src/i18n/locales/nb.json | 4 +- apps/mobile/src/i18n/locales/ne.json | 4 +- apps/mobile/src/i18n/locales/nl.json | 2 +- apps/mobile/src/i18n/locales/om.json | 4 +- apps/mobile/src/i18n/locales/or.json | 4 +- apps/mobile/src/i18n/locales/pa.json | 4 +- apps/mobile/src/i18n/locales/pl.json | 4 +- apps/mobile/src/i18n/locales/ps.json | 2 +- apps/mobile/src/i18n/locales/pt-BR.json | 4 +- apps/mobile/src/i18n/locales/pt.json | 4 +- apps/mobile/src/i18n/locales/ro.json | 4 +- apps/mobile/src/i18n/locales/ru.json | 4 +- apps/mobile/src/i18n/locales/si.json | 4 +- apps/mobile/src/i18n/locales/sk.json | 4 +- apps/mobile/src/i18n/locales/sl.json | 4 +- apps/mobile/src/i18n/locales/so.json | 4 +- apps/mobile/src/i18n/locales/sq.json | 4 +- apps/mobile/src/i18n/locales/sr.json | 4 +- apps/mobile/src/i18n/locales/sv.json | 4 +- apps/mobile/src/i18n/locales/sw.json | 4 +- apps/mobile/src/i18n/locales/ta.json | 4 +- apps/mobile/src/i18n/locales/te.json | 4 +- apps/mobile/src/i18n/locales/th.json | 2 +- apps/mobile/src/i18n/locales/tr.json | 2 +- apps/mobile/src/i18n/locales/uk.json | 4 +- apps/mobile/src/i18n/locales/ur.json | 4 +- apps/mobile/src/i18n/locales/uz.json | 2 +- apps/mobile/src/i18n/locales/vi.json | 4 +- apps/mobile/src/i18n/locales/yo.json | 4 +- apps/mobile/src/i18n/locales/zh-Hans.json | 4 +- apps/mobile/src/i18n/locales/zh-Hant.json | 4 +- apps/mobile/src/i18n/locales/zu.json | 4 +- .../glanceable/activity-kit-prompt.test.ts | 2 +- .../src/lib/glanceable/activity-kit-prompt.ts | 6 +- .../mobile/src/lib/glanceable/cleanup.test.ts | 42 ++- apps/mobile/src/lib/glanceable/cleanup.ts | 17 +- .../src/lib/glanceable/presentation.test.ts | 40 ++- .../mobile/src/lib/glanceable/presentation.ts | 35 ++- .../src/lib/glanceable/publisher.test.ts | 18 +- apps/mobile/src/lib/glanceable/publisher.ts | 25 +- .../src/lib/glanceable/sink-registry.ts | 41 +++ apps/mobile/src/lib/notification-path.test.ts | 2 +- apps/mobile/src/lib/notifications.test.ts | 10 +- .../src/glanceable-agents-snapshot.test.ts | 22 +- .../src/glanceable-agents-snapshot.ts | 39 ++- packages/notifications/src/push-data.ts | 4 +- .../src/push-presentation.test.ts | 2 +- .../report-consumer.glanceable.test.ts | 12 +- .../src/lib/glanceable-delivery.test.ts | 44 +-- .../src/lib/glanceable-delivery.ts | 4 +- .../src/lib/glanceable-refresh.ts | 4 +- 122 files changed, 1039 insertions(+), 532 deletions(-) create mode 100644 apps/mobile/assets/images/logo-widget.png create mode 100644 apps/mobile/src/glanceable-ios/widget-logo.test.ts create mode 100644 apps/mobile/src/glanceable-ios/widget-logo.ts diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 59711cdcab..e1fcf224f4 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -272,7 +272,7 @@ const config: ExpoConfig = { { name: 'ActiveAgentsWidget', displayName: 'Active Agents', - description: 'Counts of running, needs-input, and reconnecting agents', + description: 'Agents that need input, are working, or are idle', contentMarginsDisabled: false, supportedFamilies: [ 'systemSmall', diff --git a/apps/mobile/assets/images/logo-widget.png b/apps/mobile/assets/images/logo-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..8e72fff48d269ce0b4458617695e5a82eba21460 GIT binary patch literal 2766 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 U-_p|D7ytkO07*qoM6N<$f{j``EC2ui literal 0 HcmV?d00001 diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts index 1ce6c7a4f3..b7bb8ada34 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts +++ b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts @@ -31,8 +31,8 @@ type MockElement = { const COPY: Record = { 'glanceable.needsInput': 'Needs input', - 'glanceable.reconnecting': 'Reconnecting', - 'glanceable.running': 'Running', + 'glanceable.idle': 'Idle', + 'glanceable.running': 'Working', 'glanceable.empty': 'No work in progress', 'glanceable.expired': 'Status expired', 'glanceable.stale': 'Updates delayed', @@ -122,20 +122,14 @@ describe('renderActiveAgentsWidget', () => { const rep = render(props, 250); const text = collectText(rep.light); - expect(text).toEqual(['1 Needs input', '1 Running', 'Open agents']); + expect(text).toEqual(['1 Needs input', '1 Working', 'Open agents']); }); it.each([ { width: 120, visibleText: ['2 Needs input'] }, { width: 250, - visibleText: [ - '2 Needs input', - '3 Reconnecting', - '4 Running', - 'Updates delayed', - 'Open agents', - ], + visibleText: ['2 Needs input', '4 Working', '3 Idle', 'Updates delayed', 'Open agents'], }, ])( 'speaks stale numeric counts and keeps the deep link at width $width', @@ -144,7 +138,7 @@ describe('renderActiveAgentsWidget', () => { { ...snapshotFor([], 0, 'stale'), needsInput: 2, - reconnecting: 3, + idle: 3, running: 4, }, {}, @@ -154,7 +148,7 @@ describe('renderActiveAgentsWidget', () => { for (const surface of [rep.light, rep.dark]) { expect(surface.props.accessibilityLabel).toBe( - 'Updates delayed, 2 Needs input, 3 Reconnecting, 4 Running, Open agents' + 'Updates delayed, 2 Needs input, 4 Working, 3 Idle, Open agents' ); expect(collectText(surface)).toEqual(visibleText); expect(surface.props.clickAction).toBe('OPEN_URI'); @@ -170,7 +164,7 @@ describe('renderActiveAgentsWidget', () => { status: 'expired', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, }, {}, translate diff --git a/apps/mobile/src/glanceable-android/android-sink.test.ts b/apps/mobile/src/glanceable-android/android-sink.test.ts index c6e0924870..9a15cb1933 100644 --- a/apps/mobile/src/glanceable-android/android-sink.test.ts +++ b/apps/mobile/src/glanceable-android/android-sink.test.ts @@ -123,7 +123,7 @@ function snapshotFor( const MIXED = { ...snapshotFor([], 0, 'happy'), needsInput: 2, - reconnecting: 3, + idle: 3, running: 4, }; @@ -179,9 +179,9 @@ afterEach(() => { }); describe('androidSink start and update', () => { - it('keeps idle delivery available before and after work arrives in the background', async () => { + it('keeps scope delivery available before and after work arrives in the background', async () => { const publisher = new GlanceablePublisher({ sinks: [androidSink], now: () => NOW }); - publisher.handleSessions([{ status: 'idle' }], CTX); + publisher.handleSessions([], CTX); await flushAsync(); expect(mocks.getNotification()).toBeNull(); expect(getCurrentWidgetProps()?.statusLine).toBe('No work in progress'); @@ -189,9 +189,9 @@ describe('androidSink start and update', () => { publisher.applySnapshot(snapshotFor([{ status: 'busy' }], 1), CTX); await flushAsync(); - expect(mocks.getNotification()?.text).toBe('1 Running'); + expect(mocks.getNotification()?.text).toBe('1 Working'); - publisher.handleSessions([{ status: 'idle' }], CTX); + publisher.handleSessions([], CTX); await vi.advanceTimersByTimeAsync(8000); expect(mocks.getNotification()).toBeNull(); expect(subscriptions).toEqual(new Set(['scope'])); @@ -203,7 +203,7 @@ describe('androidSink start and update', () => { await flushAsync(); expect(mocks.getNotification()).toEqual({ title: 'Active agents', - text: '2 Needs input, 3 Reconnecting, 4 Running', + text: '2 Needs input, 4 Working, 3 Idle', compactText: '2', promotion: true, }); @@ -212,16 +212,16 @@ describe('androidSink start and update', () => { await flushAsync(); expect(mocks.getNotification()).toEqual({ title: 'Active agents', - text: '3 Reconnecting, 4 Running', - compactText: '3', + text: '4 Working, 3 Idle', + compactText: '4', promotion: true, }); - androidSink.startOrUpdate({ ...MIXED, revision: 3, needsInput: 0, reconnecting: 0 }, CTX); + androidSink.startOrUpdate({ ...MIXED, revision: 3, needsInput: 0, idle: 0 }, CTX); await flushAsync(); expect(mocks.getNotification()).toEqual({ title: 'Active agents', - text: '4 Running', + text: '4 Working', compactText: '4', promotion: true, }); @@ -229,14 +229,14 @@ describe('androidSink start and update', () => { expect(mocks.native.update).toHaveBeenCalledTimes(2); expect(mocks.native.start).toHaveBeenCalledWith( 'Active agents', - '2 Needs input, 3 Reconnecting, 4 Running', + '2 Needs input, 4 Working, 3 Idle', 'Open agents', '2', true ); expect(mocks.native.update).toHaveBeenLastCalledWith( 'Active agents', - '4 Running', + '4 Working', 'Open agents', '4', true, @@ -251,7 +251,7 @@ describe('androidSink start and update', () => { expect(mocks.getNotification()).toEqual({ title: 'Active agents', - text: '2 Needs input, 3 Reconnecting, 4 Running', + text: '2 Needs input, 4 Working, 3 Idle', compactText: '2', promotion: false, }); @@ -268,8 +268,8 @@ describe('androidSink start and update', () => { expect(mocks.getNotification()).toEqual({ title: 'Active agents', - text: '3 Reconnecting, 4 Running', - compactText: '3', + text: '4 Working, 3 Idle', + compactText: '4', promotion: true, }); expect(mocks.native.start).toHaveBeenCalledTimes(1); @@ -409,10 +409,10 @@ describe('androidSink widget publish and end', () => { const notification = mocks.getNotification(); expect(notification?.text).toContain(i18n.t('glanceable.stale')); - expect(notification?.text).toContain('2 Needs input, 3 Reconnecting, 4 Running'); + expect(notification?.text).toContain('2 Needs input, 4 Working, 3 Idle'); expect(notification?.compactText).toBe('2'); expect(getCurrentWidgetProps()?.accessibilityLabel).toContain( - '2 Needs input, 3 Reconnecting, 4 Running, Open agents' + '2 Needs input, 4 Working, 3 Idle, Open agents' ); }); @@ -554,7 +554,7 @@ describe('androidSink widget publish and end', () => { expect(() => { androidSink.publish(empty); }).toThrow('Cannot persist the active agents notification timeout'); - expect(mocks.getNotification()?.text).toBe('2 Needs input, 3 Reconnecting, 4 Running'); + expect(mocks.getNotification()?.text).toBe('2 Needs input, 4 Working, 3 Idle'); expect(mocks.getRequestedNotificationDeadline()).toBeNull(); vi.setSystemTime(NOW + 3000); @@ -588,7 +588,7 @@ describe('androidSink widget publish and end', () => { expect(mocks.getRequestedNotificationDeadline()).toBeNull(); expect(mocks.getNotification()).toMatchObject({ - text: '2 Needs input, 3 Reconnecting, 4 Running', + text: '2 Needs input, 4 Working, 3 Idle', compactText: '2', }); expect(mocks.getWidgetDeadline()).toBe(method === 'publish' ? NOW + 28_800_000 : 0); @@ -648,7 +648,7 @@ describe('handleAppStateActive permission alert', () => { await handleAppStateActive(); expect(mocks.getNotification()).toEqual({ title: 'Active agents', - text: '2 Needs input, 3 Reconnecting, 4 Running', + text: '2 Needs input, 4 Working, 3 Idle', compactText: '2', promotion: true, }); diff --git a/apps/mobile/src/glanceable-android/register.test.ts b/apps/mobile/src/glanceable-android/register.test.ts index 2978c7896e..ff5666a783 100644 --- a/apps/mobile/src/glanceable-android/register.test.ts +++ b/apps/mobile/src/glanceable-android/register.test.ts @@ -143,9 +143,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { const handler = await registerAfterRestart(snapshotFor()); const rendered = await runWidgetTask(handler, width); const expected = - width === 120 - ? ['1 Needs input'] - : ['1 Needs input', '1 Reconnecting', '2 Running', 'Open agents']; + width === 120 ? ['2 Needs input'] : ['2 Needs input', '2 Working', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -211,7 +209,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { androidSink.publish({ ...snapshotFor([{ status: 'busy' }]), revision: stored.revision + 1 }); const rendered = await runWidgetTask(handler, width); - const expected = width === 120 ? ['1 Running'] : ['1 Running', 'Open agents']; + const expected = width === 120 ? ['1 Working'] : ['1 Working', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -233,7 +231,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { vi.setSystemTime(Date.parse(old.expiresAt)); const rendered = await runWidgetTask(handler, width); - const expected = width === 120 ? ['1 Running'] : ['1 Running', 'Open agents']; + const expected = width === 120 ? ['1 Working'] : ['1 Working', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); }); @@ -263,8 +261,8 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { const handler = await registerAfterRestart(null); const current = await runWidgetTask(handler, width); - expect(collectText(current.light)).toContain('1 Needs input'); - expect(collectText(current.dark)).toContain('1 Needs input'); + expect(collectText(current.light)).toContain('2 Needs input'); + expect(collectText(current.dark)).toContain('2 Needs input'); expect(mocks.getDeadline()).toBe(expiresAt); vi.setSystemTime(expiresAt); @@ -311,7 +309,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { androidSink.publish({ ...snapshotFor([{ status: 'busy' }]), revision: stored.revision + 1 }); read.resolve(JSON.stringify(stored)); const rendered = await rendering; - const expected = width === 120 ? ['1 Running'] : ['1 Running', 'Open agents']; + const expected = width === 120 ? ['1 Working'] : ['1 Working', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); diff --git a/apps/mobile/src/glanceable-android/widget-props.test.ts b/apps/mobile/src/glanceable-android/widget-props.test.ts index 2fe145a0f6..dd2a867e6a 100644 --- a/apps/mobile/src/glanceable-android/widget-props.test.ts +++ b/apps/mobile/src/glanceable-android/widget-props.test.ts @@ -15,8 +15,8 @@ const NOW = 1_750_000_000_000; const COPY: Record = { 'glanceable.needsInput': 'Needs input', - 'glanceable.reconnecting': 'Reconnecting', - 'glanceable.running': 'Running', + 'glanceable.idle': 'Idle', + 'glanceable.running': 'Working', 'glanceable.waiting': 'Waiting for agents', 'glanceable.empty': 'No work in progress', 'glanceable.stale': 'Updates delayed', @@ -45,7 +45,7 @@ function snapshotFor( const MIXED = { ...snapshotFor([], 0, 'happy'), needsInput: 2, - reconnecting: 3, + idle: 3, running: 4, }; @@ -56,14 +56,14 @@ describe('buildAndroidWidgetProps', () => { expect(props.primaryCount).toBe(2); expect(props.countLines).toEqual([ { label: 'Needs input', count: 2 }, - { label: 'Reconnecting', count: 3 }, - { label: 'Running', count: 4 }, + { label: 'Working', count: 4 }, + { label: 'Idle', count: 3 }, ]); }); it.each([ - ['happy', '2 Needs input, 3 Reconnecting, 4 Running, Open agents'], - ['stale', 'Updates delayed, 2 Needs input, 3 Reconnecting, 4 Running, Open agents'], + ['happy', '2 Needs input, 4 Working, 3 Idle, Open agents'], + ['stale', 'Updates delayed, 2 Needs input, 4 Working, 3 Idle, Open agents'], ] as const)( 'includes numeric counts and the action in the %s spoken label', (status, expected) => { @@ -165,13 +165,13 @@ describe('current widget deadline rendering', () => { describe('buildOngoingNotificationText', () => { it('lists every ranked numeric count for happy work', () => { expect(buildOngoingNotificationText(MIXED, {}, translate)).toBe( - '2 Needs input, 3 Reconnecting, 4 Running' + '2 Needs input, 4 Working, 3 Idle' ); }); it('adds the translated stale warning without losing eligible counts', () => { expect(buildOngoingNotificationText({ ...MIXED, status: 'stale' }, {}, translate)).toBe( - 'Updates delayed, 2 Needs input, 3 Reconnecting, 4 Running' + 'Updates delayed, 2 Needs input, 4 Working, 3 Idle' ); }); @@ -190,10 +190,10 @@ describe('buildOngoingNotificationText', () => { describe('buildCompactNotificationText', () => { it.each([ - { needsInput: 2, reconnecting: 3, running: 4, expected: '2' }, - { needsInput: 0, reconnecting: 3, running: 4, expected: '3' }, - { needsInput: 0, reconnecting: 0, running: 4, expected: '4' }, - { needsInput: 0, reconnecting: 0, running: 0, expected: null }, + { needsInput: 2, idle: 3, running: 4, expected: '2' }, + { needsInput: 0, idle: 3, running: 4, expected: '4' }, + { needsInput: 0, idle: 3, running: 0, expected: '3' }, + { needsInput: 0, idle: 0, running: 0, expected: null }, ])('uses the ranked primary number $expected, not the total or full summary', counts => { const snapshot = { ...MIXED, ...counts }; expect(buildCompactNotificationText(snapshot, {})).toBe(counts.expected); diff --git a/apps/mobile/src/glanceable-android/widget-props.ts b/apps/mobile/src/glanceable-android/widget-props.ts index 71434f679c..6d2c111bd3 100644 --- a/apps/mobile/src/glanceable-android/widget-props.ts +++ b/apps/mobile/src/glanceable-android/widget-props.ts @@ -20,7 +20,7 @@ type AndroidWidgetCount = { label: string; count: number }; export type AndroidWidgetProps = { /** Translated locked copy; null while counts show (happy). Stale carries both. */ statusLine: string | null; - /** Non-zero count lines in rank order (needs-input, reconnecting, running). */ + /** Non-zero count lines in rank order (needs-input, running, idle). */ countLines: AndroidWidgetCount[]; /** Top-ranked count label for compact widths; null when no eligible work. */ primaryLabel: string | null; @@ -85,7 +85,7 @@ function buildExpiredWidgetProps( status: 'expired', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }, {}, diff --git a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx index 87f2238305..599644fcab 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx @@ -1,16 +1,23 @@ -import { Text, VStack } from '@expo/ui/swift-ui'; +import { HStack, Image, Spacer, Text, VStack } from '@expo/ui/swift-ui'; import { accessibilityElement, accessibilityLabel, + cornerRadius, font, foregroundStyle, frame, + monospacedDigit, + multilineTextAlignment, + padding, + resizable, } from '@expo/ui/swift-ui/modifiers'; -import { createLiveActivity } from 'expo-widgets'; +import { createLiveActivity, type LiveActivityComponent } from 'expo-widgets'; import { PlatformColor } from 'react-native'; import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; +import { withWidgetLogo } from './widget-logo'; + /* eslint-disable new-cap -- PlatformColor is a React Native factory function, not a constructor */ // The layout function below is marked with the `'widget'` directive, so Babel @@ -20,10 +27,16 @@ import { type GlanceableLiveActivityContentState } from '@kilocode/notifications // The server pushes raw counts + status (it cannot translate), and the // foreground app passes the same raw shape, so the inlined English copy below // is the single producer of the displayed Live Activity copy. +// +// The one value resolved after stringification is the `__KILO_WIDGET_LOGO_URI__` +// literal below: `withWidgetLogo` swaps it for the app-group path of the mark. + +type ContentState = Partial; -export const ActiveAgentsLiveActivity = createLiveActivity< - Partial ->('ActiveAgentsLiveActivity', (props, environment) => { +// Babel replaces the annotated arrow with its source string, so `layout` is a +// string at runtime while TypeScript still checks it as a component — the same +// shape `expo-widgets` casts internally. +const layout: LiveActivityComponent = (props, environment) => { 'widget'; const dark = environment.colorScheme === 'dark'; @@ -39,26 +52,46 @@ export const ActiveAgentsLiveActivity = createLiveActivity< } as const; const statusLine = status === 'happy' ? null : STATUS_LINE[status]; - const countLines = [ - { label: 'Needs input', count: props.needsInput ?? 0 }, - { label: 'Reconnecting', count: props.reconnecting ?? 0 }, - { label: 'Running', count: props.running ?? 0 }, - ].filter(line => line.count > 0); + // Rank order: what the user must act on, then what is making progress, then + // what is only connected. The Dynamic Island shows one number, so this + // ranking decides what a glance says. The glyphs differ in shape as well as + // color (exclamation / filled / hollow) so the state reads without color. + const countLines = ( + [ + { + label: 'Needs input', + count: props.needsInput ?? 0, + icon: 'exclamationmark.circle.fill', + color: PlatformColor('systemOrange'), + }, + { + label: 'Working', + count: props.running ?? 0, + icon: 'circle.fill', + color: PlatformColor('systemGreen'), + }, + { + label: 'Idle', + count: props.idle ?? 0, + icon: 'circle', + color: PlatformColor('label'), + }, + ] as const + ).filter(line => line.count > 0); const hasCounts = countLines.length > 0; const primary = countLines[0] ?? null; - const primaryLabel = primary === null ? null : primary.label; const primaryCount = String(primary === null ? 0 : primary.count); - // Elapsed time shows while eligible counts exist, including the stale status, - // so the running work keeps its elapsed timer when updates stop. + // Elapsed time shows while any count exists, including the stale status, so + // the work keeps its elapsed timer when updates stop. const elapsedAnchor = hasCounts ? (props.eligibleStartedAt ?? null) : null; - // Spoken label: status word, numeric counts, then Open agents. Stale keeps - // its status word; happy (no status line) speaks counts then Open agents. - const openAgentsCopy = 'Open agents'; + // Spoken label: status word, numeric counts, then Open agents. The whole + // surface deep-links to the agents list, so "Open agents" stays in the + // spoken label even though no line draws it. const spokenParts = [ ...(statusLine !== null ? [statusLine] : []), ...countLines.map(line => `${line.count} ${line.label}`), - openAgentsCopy, + 'Open agents', ]; const accessibility = spokenParts.join(', '); @@ -67,71 +100,116 @@ export const ActiveAgentsLiveActivity = createLiveActivity< dark ? PlatformColor('secondaryLabel') : PlatformColor('tertiaryLabel') ); - const countRows = countLines.map(line => ( - - {`${line.count} ${line.label}`} - - )); - - const showOpenAgents = status === 'happy' || status === 'stale'; - const openAgentsControl = ( - - {openAgentsCopy} - + // The literal, not the imported constant: the widget transform stringifies + // this function's source, so an imported binding would be an undefined global + // in the widget process. It must stay equal to `WIDGET_LOGO_PLACEHOLDER`, which + // `withWidgetLogo` replaces with the app-group path. + // The annotation widens the literal: the token is replaced after this file is + // stringified, so the empty-path branch below is reachable at runtime. + // eslint-disable-next-line typescript-eslint/no-inferrable-types -- see above + const logoUri: string = '__KILO_WIDGET_LOGO_URI__'; + const logo = (size: number) => + logoUri.length === 0 ? null : ( + + ); + + // One row per non-zero state: a colored glyph carries the state (readable + // without color), a fixed-width count, then the label. The first row is + // emphasised so a glance lands on it. + const countRow = (line: (typeof countLines)[number], isPrimary: boolean) => ( + + + + {String(line.count)} + + + {line.label} + + ); + const countRows = countLines.map((line, index) => countRow(line, index === 0)); + + const elapsed = + elapsedAnchor === null ? null : ( + + ); + return { banner: ( - - {hasCounts ? ( - - {countRows} - - ) : null} - {statusLine !== null ? {statusLine} : null} - {elapsedAnchor !== null ? ( - - ) : null} - {showOpenAgents ? openAgentsControl : null} - - ), - compactLeading: ( - - {hasCounts ? primaryCount : statusLine} - + {logo(22)} + {hasCounts ? ( + + {countRows} + + ) : ( + + {statusLine} + + )} + + {elapsed} + ), + // The Dynamic Island's leading slot is the app-identity slot, so it holds + // the Kilo mark; the trailing slot carries the ranked count. + compactLeading: {logo(18)}, + // One number, colored by the state it counts: orange needs input, green + // working, white idle. compactTrailing: ( - {hasCounts ? (primaryLabel ?? primaryCount) : ''} + {hasCounts ? primaryCount : ''} ), minimal: ( @@ -139,25 +217,33 @@ export const ActiveAgentsLiveActivity = createLiveActivity< ), expandedLeading: ( - + {countRows} ), expandedTrailing: ( - - {statusLine !== null ? {statusLine} : null} - {elapsedAnchor !== null ? ( - + + {statusLine !== null && hasCounts ? ( + {statusLine} ) : null} + {elapsed} ), expandedBottom: ( - + + {logo(16)} {statusLine !== null && !hasCounts ? ( - {statusLine} + + {statusLine} + ) : null} - {showOpenAgents ? openAgentsControl : null} - + + ), }; -}); +}; + +export const ActiveAgentsLiveActivity = createLiveActivity( + 'ActiveAgentsLiveActivity', + withWidgetLogo(layout) +); diff --git a/apps/mobile/src/glanceable-ios/active-agents-widget.tsx b/apps/mobile/src/glanceable-ios/active-agents-widget.tsx index 094f25a204..5654c6decb 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-widget.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-widget.tsx @@ -1,17 +1,21 @@ -import { Text, VStack } from '@expo/ui/swift-ui'; +import { HStack, Image, Spacer, Text, VStack } from '@expo/ui/swift-ui'; import { accessibilityElement, accessibilityLabel, containerBackground, + cornerRadius, font, foregroundStyle, frame, + monospacedDigit, + resizable, widgetURL, } from '@expo/ui/swift-ui/modifiers'; -import { createWidget } from 'expo-widgets'; +import { createWidget, type WidgetEnvironment } from 'expo-widgets'; import { PlatformColor } from 'react-native'; import { type GlanceableViewProps } from './view-props'; +import { withWidgetLogo } from './widget-logo'; /* eslint-disable new-cap -- PlatformColor is a React Native factory function, not a constructor */ @@ -21,85 +25,207 @@ import { type GlanceableViewProps } from './view-props'; // `PlatformColor`) or a built-in. Do not call `@/` helpers or i18n from here — // translated copy arrives through `props`. The inlined English fallbacks below // only render while the gallery placeholder has no snapshot props. +// +// The one value resolved after stringification is the `__KILO_WIDGET_LOGO_URI__` +// literal below: `withWidgetLogo` swaps it for the app-group path of the mark. -export const ActiveAgentsWidget = createWidget>( - 'ActiveAgentsWidget', - (props, environment) => { - 'widget'; - - const family = environment.widgetFamily; - const dark = environment.colorScheme === 'dark'; - const counts = props.countLines ?? []; - const hasCounts = counts.length > 0; - const primaryLabel = props.primaryLabel ?? null; - const primaryCount = props.primaryCount ?? 0; - const statusLine = props.statusLine ?? (hasCounts ? null : 'No work in progress'); - const openAgentsLabel = props.openAgentsLabel ?? ''; - const showOpenAgents = props.showOpenAgents === true; - const compact = ['systemSmall', 'accessoryCircular', 'accessoryInline'].includes(family); - - const primaryForeground = foregroundStyle(PlatformColor('label')); - const mutedForeground = foregroundStyle( - dark ? PlatformColor('secondaryLabel') : PlatformColor('tertiaryLabel') +type WidgetProps = Partial; + +// Babel replaces the annotated arrow with its source string, so `layout` is a +// string at runtime while TypeScript still checks it as a component. +const layout: (props: WidgetProps, environment: WidgetEnvironment) => React.JSX.Element = ( + props, + environment +) => { + 'widget'; + + const family = environment.widgetFamily; + const dark = environment.colorScheme === 'dark'; + const counts = props.countLines ?? []; + const hasCounts = counts.length > 0; + const primaryLabel = props.primaryLabel ?? null; + const primaryKind = props.primaryKind ?? null; + const primaryCount = props.primaryCount ?? 0; + const statusLine = props.statusLine ?? (hasCounts ? null : 'No work in progress'); + const elapsedAnchor = props.elapsedAnchor ?? null; + + // Circle-based glyphs whose shapes differ as well as their colors, because + // the Lock Screen families render in an accented mode that flattens tint. + const GLYPH = { + needsInput: { icon: 'exclamationmark.circle.fill', color: PlatformColor('systemOrange') }, + running: { icon: 'circle.fill', color: PlatformColor('systemGreen') }, + idle: { icon: 'circle', color: PlatformColor('label') }, + } as const; + + const primaryForeground = foregroundStyle(PlatformColor('label')); + const mutedForeground = foregroundStyle( + dark ? PlatformColor('secondaryLabel') : PlatformColor('tertiaryLabel') + ); + const a11y = [ + accessibilityElement('combine'), + accessibilityLabel(props.accessibilityLabel ?? ''), + ]; + + // The literal, not the imported constant: the widget transform stringifies + // this function's source, so an imported binding would be an undefined global + // in the widget process. It must stay equal to `WIDGET_LOGO_PLACEHOLDER`, which + // `withWidgetLogo` replaces with the app-group path. + // The annotation widens the literal: the token is replaced after this file is + // stringified, so the empty-path branch below is reachable at runtime. + // eslint-disable-next-line typescript-eslint/no-inferrable-types -- see above + const logoUri: string = '__KILO_WIDGET_LOGO_URI__'; + const logo = (size: number) => + logoUri.length === 0 ? null : ( + + ); + + // `compact` is the Lock Screen rectangle, which is four lines tall and narrow + // enough that a subheadline label truncates once the mark takes its width. + const countRow = ( + line: { label: string; kind: string; count: number }, + isPrimary: boolean, + compact: boolean + ) => { + const glyph = GLYPH[line.kind as keyof typeof GLYPH]; + const emphasis = isPrimary ? 'headline' : 'subheadline'; + const countStyle = compact ? 'caption' : emphasis; + const labelStyle = compact ? 'caption' : 'subheadline'; + const glyphSize = isPrimary ? 14 : 12; + return ( + + + + {String(line.count)} + + + {line.label} + + ); - const a11y = [ - accessibilityElement('combine'), - accessibilityLabel(props.accessibilityLabel ?? ''), - ]; - - const countRows = counts.map(line => ( - - {`${line.count} ${line.label}`} - - )); - - if (compact) { - const label = hasCounts - ? `${primaryCount}${primaryLabel !== null ? ` ${primaryLabel}` : ''}` - : (statusLine ?? ''); - - return ( + }; + + // accessoryCircular has room for one number, and accessoryInline for one + // glyph plus one line of text, so neither carries the mark. + if (family === 'accessoryCircular') { + return ( + + {primaryKind === null ? null : ( + + )} - {label} + {hasCounts ? String(primaryCount) : '—'} - ); - } + + ); + } + if (family === 'accessoryInline') { + const label = hasCounts + ? `${primaryCount}${primaryLabel !== null ? ` ${primaryLabel}` : ''}` + : (statusLine ?? ''); return ( - + {primaryKind === null ? null : ( + + )} + {label} + + ); + } + + const elapsed = + elapsedAnchor === null ? null : ( + + ); + + // accessoryRectangular is the Lock Screen row: the mark plus the two + // top-ranked lines is all that fits. + if (family === 'accessoryRectangular') { + return ( + + {logo(14)} {hasCounts ? ( - - {countRows} + + {counts.slice(0, 2).map((line, index) => countRow(line, index === 0, true))} - ) : null} - {statusLine !== null ? {statusLine} : null} - {showOpenAgents ? ( - - {openAgentsLabel} - - ) : null} - + ) : ( + {statusLine} + )} + + ); } + + // systemSmall has room for the mark, then every non-zero line; the wider + // families add the elapsed timer on the header row. + const wide = family !== 'systemSmall'; + return ( + + + {logo(20)} + + {wide ? elapsed : null} + + {hasCounts ? ( + + {counts.map((line, index) => countRow(line, index === 0, false))} + + ) : null} + {statusLine !== null ? ( + {statusLine} + ) : null} + {wide ? null : elapsed} + + + ); +}; + +export const ActiveAgentsWidget = createWidget( + 'ActiveAgentsWidget', + withWidgetLogo(layout) ); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts index 8df12bb359..2c3abffdf7 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts @@ -78,7 +78,7 @@ const native = vi.hoisted(() => { vi.mock('expo-widgets', async () => { const { after } = await import('expo-widgets/src/Widgets'); - return { after }; + return { after, widgetsDirectory: 'file:///app-group/ExpoWidgets/' }; }); vi.mock('expo-widgets/src/ExpoWidgets', () => ({ default: { @@ -143,7 +143,7 @@ function firstActivity(): NativeRecord { function remoteEnd(record: NativeRecord): void { record.state = 'ended'; - record.props = { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }; + record.props = { status: 'empty', running: 0, needsInput: 0, idle: 0 }; record.dismissAt = Date.now() + 8000; } @@ -165,7 +165,7 @@ describe('native adapter recovery', () => { const sink = await loadSink(); sink.startOrUpdate(snapshot([{ status: 'busy' }]), CTX); remoteEnd(firstActivity()); - const fresh = snapshot([{ status: 'busy' }, { status: 'retry' }], 2); + const fresh = snapshot([{ status: 'busy' }, { status: 'idle' }], 2); if (path === 'publish then start') { sink.publish(fresh); } @@ -176,7 +176,7 @@ describe('native adapter recovery', () => { { props: { running: 1, - reconnecting: 1, + idle: 1, eligibleStartedAt: new Date(NOW - 60_000).toISOString(), }, }, @@ -264,7 +264,9 @@ describe('native adapter terminal privacy', () => { await restarted.waitForNativeTerminal?.(); expect(firstActivity()).toMatchObject({ state: 'dismissed', dismissAt: NOW }); - expect(native.snapshots.at(-1)).toMatchObject({ primaryCount: 0, showOpenAgents: false }); + expect(native.snapshots.at(-1)).toMatchObject({ primaryCount: 0 }); + // Omitted, not null: UserDefaults rejects a null value. See toWidgetProps. + expect(Object.values(native.snapshots.at(-1) ?? {})).not.toContain(null); expect(native.ignoredUpdates).toEqual([]); }); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index 99525f7c49..6efbc14035 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -21,7 +21,7 @@ import { getActivityKitDenied, iosSink, } from './ios-sink'; -import { buildGlanceableViewProps, type GlanceableViewProps } from './view-props'; +import { buildGlanceableViewProps, type GlanceableViewProps, toWidgetProps } from './view-props'; // Native surfaces are unreachable under vitest: expo-widgets factories, the // swift-ui component tree, and react-native are stubbed so the sink is the real @@ -57,6 +57,7 @@ const mockState = vi.hoisted(() => ({ vi.mock('expo-widgets', () => ({ after: (date: Date) => ({ after: date }), + widgetsDirectory: 'file:///app-group/ExpoWidgets/', createLiveActivity: () => ({ start: (props: unknown, url?: string) => { if (mockState.startError !== null) { @@ -182,9 +183,9 @@ afterEach(() => { }); describe('iosSink start and update', () => { - it('registers an idle scope without a Live Activity and accepts later background work', () => { + it('registers a session-less scope without a Live Activity and accepts later background work', () => { const publisher = new GlanceablePublisher({ sinks: [iosSink], now: () => NOW }); - publisher.handleSessions([{ status: 'idle' }], CTX); + publisher.handleSessions([], CTX); expect(mockState.started).toEqual([]); expect(mockState.snapshots.at(-1)).toMatchObject({ statusLine: 'No work in progress' }); @@ -528,14 +529,14 @@ describe('iosSink end', () => { vi.setSystemTime(NOW); const publisher = new GlanceablePublisher({ sinks: [iosSink], now: () => NOW }); publisher.handleSessions([{ status: 'busy' }], CTX); - publisher.handleSessions([{ status: 'idle' }], CTX); + publisher.handleSessions([], CTX); await iosSink.waitForNativeTerminal?.(); expect(mockState.started).toMatchObject([ { ended: true, dismissAt: NOW + 8000, - props: { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + props: { status: 'empty', running: 0, needsInput: 0, idle: 0 }, }, ]); expect(subscriptions).toEqual(new Set(['scope'])); @@ -596,7 +597,7 @@ describe('iosSink end', () => { await iosSink.waitForNativeTerminal?.(); expect(mockState.started[0]).toMatchObject({ dismissAt: NOW, - props: { status, running: 0, needsInput: 0, reconnecting: 0 }, + props: { status, running: 0, needsInput: 0, idle: 0 }, }); } ); @@ -626,7 +627,7 @@ describe('iosSink end', () => { await iosSink.waitForNativeTerminal?.(); expect(visible).toBe(false); - expect(content).toMatchObject({ status, running: 0, needsInput: 0, reconnecting: 0 }); + expect(content).toMatchObject({ status, running: 0, needsInput: 0, idle: 0 }); expect(mockState.started[0]?.dismissAt).toBe(NOW); } ); @@ -667,19 +668,19 @@ describe('iosSink end', () => { expect(subscriptions).toEqual(new Set(['scope', 'activity'])); }); - it('retains the elapsed anchor when running work becomes reconnecting', async () => { + it('retains the elapsed anchor when running work becomes idle', async () => { vi.useFakeTimers(); vi.setSystemTime(NOW); const publisher = new GlanceablePublisher({ sinks: [iosSink], now: () => Date.now() }); publisher.handleSessions([{ status: 'busy' }], CTX); vi.setSystemTime(NOW + 60_000); - publisher.handleSessions([{ status: 'retry' }], CTX); + publisher.handleSessions([{ status: 'idle' }], CTX); await vi.advanceTimersByTimeAsync(1000); expect(mockState.started).toMatchObject([ { ended: false, dismissAt: null, - props: { running: 0, reconnecting: 1, eligibleStartedAt: new Date(NOW).toISOString() }, + props: { running: 0, idle: 1, eligibleStartedAt: new Date(NOW).toISOString() }, }, ]); publisher.dispose(); @@ -700,7 +701,7 @@ describe('iosSink widget publish', () => { expect(mockState.timeline).toHaveLength(2); expect(mockState.timeline[0]?.props).toMatchObject({ primaryCount: 1, - showOpenAgents: true, + primaryKind: 'running', }); const expired = mockState.timeline[1]; expect(expired?.date.getTime()).toBe(Date.parse(snapshot.expiresAt)); @@ -709,7 +710,8 @@ describe('iosSink widget publish', () => { expect(expiredProps.countLines).toEqual([]); expect(expiredProps.primaryCount).toBe(0); expect(expiredProps.statusLine).toBe('Status expired'); - expect(expiredProps.showOpenAgents).toBe(false); + // Omitted, not null: UserDefaults rejects a null value. See toWidgetProps. + expect(expiredProps.primaryKind).toBeUndefined(); } ); @@ -746,10 +748,8 @@ describe('iosSink widget publish', () => { statusLine, countLines: [], primaryCount: 0, - primaryLabel: null, - elapsedAnchor: null, - showOpenAgents: false, }); + expect(Object.values(visible?.props ?? {})).not.toContain(null); } expect(mockState.timeline).toHaveLength(1); }); @@ -768,12 +768,12 @@ describe('iosSink widget publish', () => { ['signed_out', [], 'Sign in to see agents', 0, false], ['privacy', [], 'Agents hidden', 0, false], ]; - for (const [status, sessions, statusLine, counts, showOpenAgents] of cases) { + for (const [status, sessions, statusLine, counts, hasPrimary] of cases) { iosSink.publish(snapshotFor(sessions, 0, status)); - const props = mockState.snapshots.at(-1) as GlanceableViewProps; + const props = mockState.snapshots.at(-1) as Partial; expect(props.statusLine).toBe(statusLine); expect(props.countLines).toHaveLength(counts); - expect(props.showOpenAgents).toBe(showOpenAgents); + expect(props.primaryKind === undefined).toBe(!hasPrimary); } }); }); @@ -787,7 +787,7 @@ describe('iosSink Live Activity content-state', () => { expect(mockState.started).toMatchObject([ { ended: true, - props: { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + props: { status: 'empty', running: 0, needsInput: 0, idle: 0 }, }, ]); }); @@ -839,7 +839,7 @@ describe('iosSink Live Activity content-state', () => { expect(mockState.ended).toMatchObject([ { policy: { after: new Date(NOW + 8000) }, - props: { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + props: { status: 'empty', running: 0, needsInput: 0, idle: 0 }, contentDate: new Date(NOW), }, ]); @@ -885,10 +885,10 @@ describe('clearActivityKitDeniedIfAvailable', () => { }); describe('buildGlanceableViewProps', () => { - it('ranks the compact primary count as needs-input, then reconnecting, then running', () => { + it('ranks the compact primary count as needs-input, then running, then idle', () => { const props = buildGlanceableViewProps( snapshotFor( - [{ status: 'busy' }, { status: 'busy' }, { status: 'retry' }, { status: 'question' }], + [{ status: 'busy' }, { status: 'busy' }, { status: 'idle' }, { status: 'question' }], 0 ), {}, @@ -898,8 +898,8 @@ describe('buildGlanceableViewProps', () => { expect(props.primaryCount).toBe(1); expect(props.countLines.map(line => line.label)).toEqual([ 'glanceable.needsInput', - 'glanceable.reconnecting', 'glanceable.running', + 'glanceable.idle', ]); }); @@ -922,10 +922,9 @@ describe('buildGlanceableViewProps', () => { 'accessibilityLabel', 'countLines', 'elapsedAnchor', - 'openAgentsLabel', 'primaryCount', + 'primaryKind', 'primaryLabel', - 'showOpenAgents', 'statusLine', ]); expect(json).not.toContain('user-9f3a-leak'); @@ -966,3 +965,32 @@ describe('buildGlanceableViewProps', () => { expect(empty.accessibilityLabel).toBe('glanceable.empty, glanceable.openAgents'); }); }); + +describe('toWidgetProps', () => { + it('omits every null field so the UserDefaults write cannot throw', () => { + const props = toWidgetProps( + buildGlanceableViewProps(snapshotFor([], 1, 'empty'), {}, key => key) + ); + + expect(Object.values(props)).not.toContain(null); + expect('primaryLabel' in props).toBe(false); + expect('primaryKind' in props).toBe(false); + expect('elapsedAnchor' in props).toBe(false); + expect(props.statusLine).toBe('glanceable.empty'); + }); + + it('keeps every non-null field', () => { + const source = buildGlanceableViewProps( + snapshotFor([{ status: 'question' }], 0), + {}, + key => key + ); + + expect(toWidgetProps(source)).toMatchObject({ + primaryLabel: 'glanceable.needsInput', + primaryKind: 'needsInput', + primaryCount: 1, + countLines: [{ kind: 'needsInput', count: 1 }], + }); + }); +}); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index b1e7ea1dcb..a546a61b51 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -15,6 +15,7 @@ import { buildGlanceableLiveActivityContentState, buildGlanceableViewProps, type GlanceableViewProps, + toWidgetProps, } from './view-props'; /** Open-agents destination, kept in step with the inlined widget URL. */ @@ -75,17 +76,19 @@ function refreshActivity(): boolean { } function buildExpiredProps(snapshot: GlanceableAgentsSnapshot): Partial { - return buildGlanceableViewProps( - { - ...snapshot, - status: 'expired', - running: 0, - needsInput: 0, - reconnecting: 0, - eligibleStartedAt: null, - }, - {}, - translate + return toWidgetProps( + buildGlanceableViewProps( + { + ...snapshot, + status: 'expired', + running: 0, + needsInput: 0, + idle: 0, + eligibleStartedAt: null, + }, + {}, + translate + ) ); } @@ -266,7 +269,7 @@ export const iosSink: GlanceableSink = { }, publish(snapshot) { - const props = buildGlanceableViewProps(snapshot, {}, translate); + const props = toWidgetProps(buildGlanceableViewProps(snapshot, {}, translate)); ActiveAgentsWidget.updateSnapshot(props); // updateSnapshot replaces the timeline, so terminal copy needs no expiry frame. if (snapshot.status !== 'signed_out' && snapshot.status !== 'privacy') { diff --git a/apps/mobile/src/glanceable-ios/register.ts b/apps/mobile/src/glanceable-ios/register.ts index 029cac2370..73b7095c49 100644 --- a/apps/mobile/src/glanceable-ios/register.ts +++ b/apps/mobile/src/glanceable-ios/register.ts @@ -1,6 +1,7 @@ import { registerGlanceableSink } from '@/lib/glanceable/sink-registry'; import { iosSink } from './ios-sink'; +import { ensureWidgetLogo } from './widget-logo'; // Registers the iOS Live Activity and widget sink at import time. The root // layout imports this file, so the surface lifecycle subscribes to the @@ -8,3 +9,8 @@ import { iosSink } from './ios-sink'; // dependency here: the publisher is plain state, and widgets get translated // copy through the sink, not through a mounted component tree. registerGlanceableSink(iosSink); + +// Copy the Kilo mark into the shared app group so the widget extension can read +// it. Fire and forget: it lands long before the first snapshot arrives, and a +// failure only costs the logo. +void ensureWidgetLogo(); diff --git a/apps/mobile/src/glanceable-ios/view-props.ts b/apps/mobile/src/glanceable-ios/view-props.ts index f03cc3128d..d057ab9c7d 100644 --- a/apps/mobile/src/glanceable-ios/view-props.ts +++ b/apps/mobile/src/glanceable-ios/view-props.ts @@ -5,16 +5,16 @@ import { import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; import { + type GlanceableCountKind, glanceableCountLines, glanceableSpokenLabel, glanceableStatusCopyKey, type GlanceableSurfaceFlags, primaryGlanceableCount, - resolveGlanceableStatus, } from '@/lib/glanceable/presentation'; -/** One translated count line. */ -type GlanceableCount = { label: string; count: number }; +/** One translated count line. `kind` picks the glyph and the color. */ +type GlanceableCount = { label: string; kind: GlanceableCountKind; count: number }; /** * The props every iOS surface renders. The builder below is the only producer, @@ -24,18 +24,16 @@ type GlanceableCount = { label: string; count: number }; export type GlanceableViewProps = { /** Translated locked copy; null while counts show (happy). Stale carries both. */ statusLine: string | null; - /** Non-zero count lines in rank order (needs-input, reconnecting, running). */ + /** Non-zero count lines in rank order (needs-input, running, idle). */ countLines: GlanceableCount[]; /** Top-ranked count label for compact surfaces; null when no eligible work. */ primaryLabel: string | null; + /** Top-ranked count state for compact surfaces; null when no eligible work. */ + primaryKind: GlanceableCountKind | null; /** Top-ranked count value for compact surfaces; 0 when no eligible work. */ primaryCount: number; /** ISO anchor for the elapsed timer; shows while eligible work runs, incl. stale. */ elapsedAnchor: string | null; - /** Translated "Open agents" affordance. */ - openAgentsLabel: string; - /** True for happy and stale — the only statuses that show counts. */ - showOpenAgents: boolean; /** Spoken label: status word, numeric counts, then Open agents. Never a title or id. */ accessibilityLabel: string; }; @@ -46,7 +44,6 @@ export function buildGlanceableViewProps( flags: GlanceableSurfaceFlags, translate: (key: string) => string ): GlanceableViewProps { - const status = resolveGlanceableStatus(snapshot, flags); const statusKey = glanceableStatusCopyKey(snapshot, flags); const primary = primaryGlanceableCount(snapshot); @@ -54,17 +51,30 @@ export function buildGlanceableViewProps( statusLine: statusKey === null ? null : translate(statusKey), countLines: glanceableCountLines(snapshot).map(line => ({ label: translate(line.key), + kind: line.kind, count: line.count, })), primaryLabel: primary === null ? null : translate(primary.key), + primaryKind: primary === null ? null : primary.kind, primaryCount: primary === null ? 0 : primary.count, elapsedAnchor: isEligibleGlanceableWork(snapshot) ? snapshot.eligibleStartedAt : null, - openAgentsLabel: translate('glanceable.openAgents'), - showOpenAgents: status === 'happy' || status === 'stale', accessibilityLabel: glanceableSpokenLabel(snapshot, flags, translate), }; } +/** + * Drop the null fields before a widget write. + * + * `updateTimeline` stores the props in the shared `UserDefaults`, which rejects + * a null value and throws an Objective-C exception out through the host + * function. An absent key reads back as `undefined`, which every layout already + * defaults, so omitting the field is the lossless form. + */ +export function toWidgetProps(props: GlanceableViewProps): Partial { + const entries = Object.entries(props).filter(([, value]) => value !== null); + return Object.fromEntries(entries) as Partial; +} + /** * Build the Live Activity content-state from a snapshot. The server pushes the * same raw shape, so the widget extension's `active-agents-live-activity.tsx` @@ -77,7 +87,7 @@ export function buildGlanceableLiveActivityContentState( status: snapshot.status, running: snapshot.running, needsInput: snapshot.needsInput, - reconnecting: snapshot.reconnecting, + idle: snapshot.idle, eligibleStartedAt: snapshot.eligibleStartedAt, }; } diff --git a/apps/mobile/src/glanceable-ios/widget-logo.test.ts b/apps/mobile/src/glanceable-ios/widget-logo.test.ts new file mode 100644 index 0000000000..ea3ad05fc4 --- /dev/null +++ b/apps/mobile/src/glanceable-ios/widget-logo.test.ts @@ -0,0 +1,32 @@ +/* eslint-disable eslint-plugin-import/no-nodejs-modules, eslint-plugin-unicorn/prefer-module -- this test reads the layout sources from disk, which is the only place the placeholder is observable */ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const PLACEHOLDER = '__KILO_WIDGET_LOGO_URI__'; +const LAYOUT_FILES = ['active-agents-live-activity.tsx', 'active-agents-widget.tsx']; + +const read = (file: string) => readFileSync(join(__dirname, file), 'utf8'); + +/** + * The `'widget'` layouts are stringified by Babel and re-evaluated inside the + * widget process, where an imported binding is an undefined global that throws + * and blanks the whole surface. So the placeholder must appear as a literal in + * each layout source, never as the imported `WIDGET_LOGO_PLACEHOLDER` + * identifier. These assertions read the sources because no widget transform + * runs under vitest. + */ +describe('widget logo placeholder', () => { + it('matches the token widget-logo.ts replaces', () => { + expect(read('widget-logo.ts')).toContain(`= '${PLACEHOLDER}'`); + }); + + for (const file of LAYOUT_FILES) { + it(`is a literal in ${file}`, () => { + const source = read(file); + expect(source).toContain(`= '${PLACEHOLDER}'`); + expect(source).not.toContain('logoUri = WIDGET_LOGO_PLACEHOLDER'); + }); + } +}); diff --git a/apps/mobile/src/glanceable-ios/widget-logo.ts b/apps/mobile/src/glanceable-ios/widget-logo.ts new file mode 100644 index 0000000000..0c89debcad --- /dev/null +++ b/apps/mobile/src/glanceable-ios/widget-logo.ts @@ -0,0 +1,93 @@ +import type * as ExpoFileSystem from 'expo-file-system'; +// eslint-disable-next-line no-restricted-imports -- the asset resolver, not the Image component +import { Image } from 'react-native'; +import { widgetsDirectory } from 'expo-widgets'; + +/** + * The Kilo mark the Live Activity and the widgets draw. + * + * The widget extension is a separate process: it cannot resolve a bundle asset, + * and the Live Activity content-state cannot carry the path either, because the + * notifications Worker produces the same shape and knows no device path. So the + * mark is copied into the shared app group once and its absolute path is baked + * into the stringified layouts at registration time — see `withWidgetLogo`. + */ + +const LOGO_FILE_NAME = 'kilo-logo.png'; + +/** + * The token the `'widget'` layouts carry until `withWidgetLogo` resolves it. + * + * Each layout repeats this literal inline rather than importing it: the widget + * transform stringifies the layout source, so an imported binding would be an + * undefined global in the widget process. `widget-logo.test.ts` keeps the two + * copies equal. + */ +const WIDGET_LOGO_PLACEHOLDER = '__KILO_WIDGET_LOGO_URI__'; + +// `widgetsDirectory` is typed `string`, but the iOS constant returns `String?` +// (nil without an app group) and the native module is absent on Android, so the +// value really is nullable. +const appGroupDirectory = widgetsDirectory as string | null; + +/** App-group path of the copied mark; empty when the app group is unavailable. */ +const WIDGET_LOGO_URI = appGroupDirectory === null ? '' : `${appGroupDirectory}${LOGO_FILE_NAME}`; + +/** + * Resolve the logo placeholder inside a stringified `'widget'` layout. + * + * This is the boundary between two representations of one value: Babel's + * widget plugin replaces a `'widget'` function with a template literal of its + * source, so the layout is a string in the app, while a unit test (which runs + * no widget transform) still holds the real function. Only the string form + * carries a placeholder to patch. + */ +export function withWidgetLogo(layout: T): T { + // eslint-disable-next-line anti-slop/no-runtime-typeof -- the two representations are the contract; see above + if (typeof layout !== 'string') { + return layout; + } + const patched = layout.split(WIDGET_LOGO_PLACEHOLDER).join(WIDGET_LOGO_URI); + // eslint-disable-next-line anti-slop/no-chained-type-assertions -- the layout source IS the component to expo-widgets + return patched as unknown as T; +} + +let copy: Promise | null = null; + +/** + * Copy the bundled mark into the app group once per process. Idempotent and + * best effort: on failure the surfaces render without a logo, and the promise + * never rejects into a caller. + */ +export async function ensureWidgetLogo(): Promise { + copy ??= copyLogo(); + await copy; +} + +async function copyLogo(): Promise { + try { + if (WIDGET_LOGO_URI === '') { + return; + } + // Lazy require keeps expo-file-system's native module out of the pure test + // graph, the same reason the sink registry defers its Sentry import. + // eslint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires, unicorn/prefer-module -- lazy native load + const { File } = require('expo-file-system') as typeof ExpoFileSystem; + const target = new File(WIDGET_LOGO_URI); + if (target.exists) { + return; + } + // eslint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires, unicorn/prefer-module -- the Metro asset registry needs a static require + const assetModule = require('../../assets/images/logo-widget.png') as number; + const asset = Image.resolveAssetSource(assetModule); + if (asset.uri.startsWith('file://')) { + // Release build: the asset is a file inside the app bundle. + new File(asset.uri).copySync(target, { overwrite: true }); + return; + } + // Dev build: the asset is served by Metro over HTTP. + await File.downloadFileAsync(asset.uri, target, { idempotent: true }); + } catch { + // A missing logo is cosmetic; every surface renders without it. + } +} diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 0bd55850af..19e48365aa 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -3222,9 +3222,9 @@ "signedOut": "Meld aan om agente te sien", "privacy": "Agente versteek", "openAgents": "Maak agente oop", - "running": "LOOP", + "running": "Werk", "needsInput": "benodig invoer", - "reconnecting": "Verbind tans weer", + "idle": "Onaktief", "channelName": "Aktiewe agente", "activityKitDisabledTitle": "Regstreekse Aktiwiteite is af", "activityKitDisabledBody": "Skakel Regstreekse Aktiwiteite in Instellings aan om Aktiewe Agente op die Sluitskerm te sien." diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 34ab895c60..49cf1dd240 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -3222,9 +3222,9 @@ "signedOut": "ወኪሎችን ለማየት ይግቡ", "privacy": "ወኪሎች ተደብቀዋል", "openAgents": "ወኪሎችን ይክፈቱ", - "running": "በስራ ላይ", + "running": "በመስራት ላይ", "needsInput": "ግብዓት ይፈልጋል", - "reconnecting": "እንደገና በመገናኘት ላይ", + "idle": "በእረፍት", "channelName": "ንቁ ወኪሎች", "activityKitDisabledTitle": "የቀጥታ እንቅስቃሴዎች ጠፍተዋል", "activityKitDisabledBody": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ ለማየት በቅንብሮች ውስጥ የቀጥታ እንቅስቃሴዎችን ያብሩ።" diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index dfc8c9d088..8c73fda5f1 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -3310,9 +3310,9 @@ "signedOut": "سجّل الدخول لرؤية الوكلاء", "privacy": "الوكلاء مخفيون", "openAgents": "فتح الوكلاء", - "running": "قيد التشغيل", + "running": "جارٍ العمل", "needsInput": "يتطلب إدخالًا", - "reconnecting": "جارٍ إعادة الاتصال", + "idle": "خامل", "channelName": "الوكلاء النشطون", "activityKitDisabledTitle": "الأنشطة المباشرة متوقفة", "activityKitDisabledBody": "فعّل الأنشطة المباشرة في الإعدادات لرؤية الوكلاء النشطين على شاشة القفل." diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 7cecbf584e..cf05076274 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -3222,9 +3222,9 @@ "signedOut": "Agentləri görmək üçün daxil olun", "privacy": "Agentlər gizlədilib", "openAgents": "Agentləri açın", - "running": "İŞLƏYİR", + "running": "İşlənir", "needsInput": "GİRİŞ TƏLƏB OLUNUR", - "reconnecting": "Yenidən qoşulur", + "idle": "Boşda", "channelName": "Aktiv agentlər", "activityKitDisabledTitle": "Canlı fəaliyyətlər söndürülüb", "activityKitDisabledBody": "Kilid ekranında aktiv agentləri görmək üçün Parametrlərdə Canlı fəaliyyətləri aktivləşdirin." diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 1169294639..66255ad5a9 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -3266,9 +3266,9 @@ "signedOut": "Увайдзіце, каб бачыць агентаў", "privacy": "Агенты схаваны", "openAgents": "Адкрыць агентаў", - "running": "ПРАЦУЕ", + "running": "Працуе", "needsInput": "патрабуецца ўвод", - "reconnecting": "Паўторнае падключэнне", + "idle": "Чакае", "channelName": "Актыўныя агенты", "activityKitDisabledTitle": "Жывыя дзеянні выключаны", "activityKitDisabledBody": "Уключыце «Жывыя дзеянні» ў «Наладах», каб бачыць актыўных агентаў на экране блакіроўкі." diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index ee98d1cf75..2d10f0c0ed 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -3222,9 +3222,9 @@ "signedOut": "Влезте, за да видите агентите", "privacy": "Агентите са скрити", "openAgents": "Отворете агентите", - "running": "Изпълнява се", + "running": "Работи", "needsInput": "изисква въвеждане", - "reconnecting": "Повторно свързване", + "idle": "Неактивен", "channelName": "Активни агенти", "activityKitDisabledTitle": "Дейностите на живо са изключени", "activityKitDisabledBody": "Включете Дейности на живо в Настройки, за да виждате активните агенти на заключения екран." diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index f1597d76d7..0b9871c03e 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -3222,9 +3222,9 @@ "signedOut": "এজেন্টগুলি দেখতে সাইন ইন করুন", "privacy": "এজেন্টগুলি লুকানো আছে", "openAgents": "এজেন্টগুলি খুলুন", - "running": "চলছে", + "running": "কাজ চলছে", "needsInput": "ইনপুট প্রয়োজন", - "reconnecting": "পুনরায় সংযোগ করা হচ্ছে", + "idle": "নিষ্ক্রিয়", "channelName": "সক্রিয় এজেন্ট", "activityKitDisabledTitle": "সরাসরি কার্যকলাপ বন্ধ আছে", "activityKitDisabledBody": "লক স্ক্রিনে সক্রিয় এজেন্টগুলি দেখতে সেটিংসে সরাসরি কার্যকলাপ চালু করুন।" diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index bb59ecf2f8..4fd9dad52d 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -3244,9 +3244,9 @@ "signedOut": "Prijavite se da biste vidjeli agente", "privacy": "Agenti su skriveni", "openAgents": "Otvorite agente", - "running": "RADI", + "running": "U toku", "needsInput": "treba unos", - "reconnecting": "Ponovno povezivanje", + "idle": "Neaktivan", "channelName": "Aktivni agenti", "activityKitDisabledTitle": "Aktivnosti uživo su isključene", "activityKitDisabledBody": "Uključite aktivnosti uživo u Postavkama da biste vidjeli aktivne agente na zaključanom ekranu." diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 94e59f8ea0..cb63a180d6 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -3244,9 +3244,9 @@ "signedOut": "Inicia la sessió per veure els agents", "privacy": "Agents ocults", "openAgents": "Obre els agents", - "running": "EN EXECUCIÓ", + "running": "Treballant", "needsInput": "requereix entrada", - "reconnecting": "Reconnectant", + "idle": "Inactiu", "channelName": "Agents actius", "activityKitDisabledTitle": "Les activitats en directe estan desactivades", "activityKitDisabledBody": "Activa les activitats en directe a Configuració per veure els agents actius a la pantalla de bloqueig." diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index ed42980f7c..277c793427 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -3222,9 +3222,9 @@ "signedOut": "بچۆ ژوورەوە بۆ بینینی ئەجێنتەکان", "privacy": "ئەجێنتەکان شاراونەتەوە", "openAgents": "کردنەوەی ئەجێنتەکان", - "running": "لە کاردایە", + "running": "کارکردن", "needsInput": "پێویستی بە داخڵکردن", - "reconnecting": "لە پەیوەستبوونەوەدایە", + "idle": "بێکار", "channelName": "ئەجێنتە چالاکەکان", "activityKitDisabledTitle": "چالاکییە ڕاستەوخۆکان ناچالاکن", "activityKitDisabledBody": "چالاکییە ڕاستەوخۆکان لە ڕێکخستنەکان چالاک بکە بۆ بینینی ئەجێنتە چالاکەکان لە شاشەی قوفڵ." diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 2b6b41e222..085af93ad9 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -3266,9 +3266,9 @@ "signedOut": "Přihlaste se pro zobrazení agentů", "privacy": "Agenti jsou skrytí", "openAgents": "Otevřít agenty", - "running": "BĚŽÍ", + "running": "Pracuji", "needsInput": "vyžaduje vstup", - "reconnecting": "Obnovování připojení", + "idle": "Nečinný", "channelName": "Aktivní agenti", "activityKitDisabledTitle": "Živé aktivity jsou vypnuté", "activityKitDisabledBody": "Zapněte Živé aktivity v Nastavení, abyste viděli aktivní agenty na zamknuté obrazovce." diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index 6d744fc87c..ceb75b620b 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -3310,9 +3310,9 @@ "signedOut": "Mewngofnodwch i weld asiantau", "privacy": "Asiantau wedi'u cuddio", "openAgents": "Agorwch asiantau", - "running": "YN RHEDEG", + "running": "Yn gweithio", "needsInput": "angen mewnbwn", - "reconnecting": "Yn ailgysylltu", + "idle": "Segur", "channelName": "Asiantau gweithredol", "activityKitDisabledTitle": "Mae Gweithgareddau Byw wedi'u diffodd", "activityKitDisabledBody": "Trowch Weithgareddau Byw ymlaen yn Gosodiadau i weld Asiantau gweithredol ar y Sgrin Glo." diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index 4b75074d9f..d067349bbe 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -3222,9 +3222,9 @@ "signedOut": "Log ind for at se agenter", "privacy": "Agenter er skjult", "openAgents": "Åbn agenter", - "running": "KØRER", + "running": "Arbejder", "needsInput": "kræver input", - "reconnecting": "Genopretter forbindelsen", + "idle": "Inaktiv", "channelName": "Aktive agenter", "activityKitDisabledTitle": "Liveaktiviteter er slået fra", "activityKitDisabledBody": "Slå Liveaktiviteter til i Indstillinger for at se aktive agenter på låseskærmen." diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 6a9e9aedb7..4e85a3ab68 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -3222,9 +3222,9 @@ "signedOut": "Melde dich an, um Agenten zu sehen", "privacy": "Agenten ausgeblendet", "openAgents": "Agenten öffnen", - "running": "LÄUFT", + "running": "Wird bearbeitet", "needsInput": "Eingabe erforderlich", - "reconnecting": "Verbindung wird wiederhergestellt", + "idle": "Inaktiv", "channelName": "Aktive Agenten", "activityKitDisabledTitle": "Live-Aktivitäten sind deaktiviert", "activityKitDisabledBody": "Aktiviere Live-Aktivitäten in den Einstellungen, um aktive Agenten auf dem Sperrbildschirm zu sehen." diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index 15a7c2da49..95f1ac9458 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -3222,9 +3222,9 @@ "signedOut": "Συνδεθείτε για να δείτε τους πράκτορες", "privacy": "Οι πράκτορες είναι κρυφοί", "openAgents": "Ανοίξτε τους πράκτορες", - "running": "Σε εξέλιξη", + "running": "Εργασία", "needsInput": "χρειάζεται είσοδο", - "reconnecting": "Επανασύνδεση", + "idle": "Αδρανής", "channelName": "Ενεργοί πράκτορες", "activityKitDisabledTitle": "Οι Ζωντανές δραστηριότητες είναι απενεργοποιημένες", "activityKitDisabledBody": "Ενεργοποιήστε τις Ζωντανές δραστηριότητες στις Ρυθμίσεις για να δείτε τους ενεργούς πράκτορες στην Οθόνη κλειδώματος." diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index b6ba9838ab..3900299fb3 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -3222,9 +3222,9 @@ "signedOut": "Sign in to see agents", "privacy": "Agents hidden", "openAgents": "Open agents", - "running": "Running", + "running": "Working", "needsInput": "Needs input", - "reconnecting": "Reconnecting", + "idle": "Idle", "channelName": "Active agents", "activityKitDisabledTitle": "Live Activities are off", "activityKitDisabledBody": "Turn on Live Activities in Settings to see Active Agents on the Lock Screen." diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index c76acb150e..431e070e60 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -3244,9 +3244,9 @@ "signedOut": "Inicia sesión para ver los agentes", "privacy": "Agentes ocultos", "openAgents": "Abrir agentes", - "running": "EN EJECUCIÓN", + "running": "Trabajando", "needsInput": "requiere entrada", - "reconnecting": "Reconectando", + "idle": "Inactivo", "channelName": "Agentes activos", "activityKitDisabledTitle": "Las actividades en directo están desactivadas", "activityKitDisabledBody": "Activa las actividades en directo en Ajustes para ver los agentes activos en la pantalla de bloqueo." diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index 112c66705f..2fe6e4c891 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -3222,9 +3222,9 @@ "signedOut": "Agentide nägemiseks logige sisse", "privacy": "Agendid on peidetud", "openAgents": "Avage agendid", - "running": "TÖÖTAB", + "running": "Töötamine", "needsInput": "vajab sisendit", - "reconnecting": "Ühenduse taastamine", + "idle": "Ooterežiimis", "channelName": "Aktiivsed agendid", "activityKitDisabledTitle": "Reaalajas tegevused on välja lülitatud", "activityKitDisabledBody": "Lülitage seadetes reaalajas tegevused sisse, et näha aktiivseid agente lukustuskuval." diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 3ba86e28a4..7649dd7a62 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -3222,9 +3222,9 @@ "signedOut": "Hasi saioa agenteak ikusteko", "privacy": "Agenteak ezkutatuta", "openAgents": "Ireki agenteak", - "running": "Exekutatzen", + "running": "Lanean", "needsInput": "sarreraren zain", - "reconnecting": "Berriro konektatzen", + "idle": "Geldirik", "channelName": "Agente aktiboak", "activityKitDisabledTitle": "Zuzeneko jarduerak desaktibatuta daude", "activityKitDisabledBody": "Aktibatu Zuzeneko jarduerak Ezarpenetan, Agente aktiboak Blokeo-pantailan ikusteko." diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 372f731dd8..a88ed04fae 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -3222,9 +3222,9 @@ "signedOut": "برای دیدن عامل‌ها وارد شوید", "privacy": "عامل‌ها پنهان هستند", "openAgents": "عامل‌ها را باز کنید", - "running": "در حال اجرا", + "running": "در حال کار", "needsInput": "نیاز به ورودی", - "reconnecting": "در حال اتصال مجدد", + "idle": "غیرفعال", "channelName": "عامل‌های فعال", "activityKitDisabledTitle": "فعالیت‌های زنده خاموش هستند", "activityKitDisabledBody": "برای دیدن عامل‌های فعال در صفحه قفل، فعالیت‌های زنده را در تنظیمات روشن کنید." diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index c402e6e10f..94bc174fe0 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -3222,9 +3222,9 @@ "signedOut": "Kirjaudu sisään nähdäksesi agentit", "privacy": "Agentit piilotettu", "openAgents": "Avaa agentit", - "running": "KÄYNNISSÄ", + "running": "Työstetään", "needsInput": "vaatii syötettä", - "reconnecting": "Yhdistetään uudelleen", + "idle": "Vapaalla", "channelName": "Aktiiviset agentit", "activityKitDisabledTitle": "Live-aktiviteetit ovat pois päältä", "activityKitDisabledBody": "Ota live-aktiviteetit käyttöön Asetuksissa, niin näet aktiiviset agentit lukitulla näytöllä." diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index c6a1139155..20c6296ebd 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -3222,9 +3222,9 @@ "signedOut": "Mag-sign in para makita ang mga agent", "privacy": "Nakatago ang mga agent", "openAgents": "Buksan ang mga agent", - "running": "TUMATAKBO", + "running": "Gumagawa", "needsInput": "kailangan ng input", - "reconnecting": "Muling kumokonekta", + "idle": "Idle", "channelName": "Mga aktibong agent", "activityKitDisabledTitle": "Naka-off ang Mga Live na Aktibidad", "activityKitDisabledBody": "I-on ang Mga Live na Aktibidad sa Mga setting para makita ang Mga aktibong agent sa Naka-lock na Screen." diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index dd5e40ea79..068fd6e7c0 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -3244,9 +3244,9 @@ "signedOut": "Connectez-vous pour voir les agents", "privacy": "Agents masqués", "openAgents": "Ouvrir les agents", - "running": "EN COURS", + "running": "En cours", "needsInput": "saisie requise", - "reconnecting": "Reconnexion en cours", + "idle": "Inactif", "channelName": "Agents actifs", "activityKitDisabledTitle": "Les activités en direct sont désactivées", "activityKitDisabledBody": "Activez les activités en direct dans Réglages pour voir les agents actifs sur l'écran verrouillé." diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index cd3a4796ca..bbaa640eda 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -3288,9 +3288,9 @@ "signedOut": "Sínigh isteach chun gníomhairí a fheiceáil", "privacy": "Gníomhairí i bhfolach", "openAgents": "Oscail gníomhairí", - "running": "AG RITH", + "running": "Ag obair", "needsInput": "teastaíonn ionchur", - "reconnecting": "Ag athcheangal", + "idle": "Díomhaoin", "channelName": "Gníomhairí gníomhacha", "activityKitDisabledTitle": "Tá Gníomhaíochtaí Beo as", "activityKitDisabledBody": "Cumasaigh Gníomhaíochtaí Beo sna Socruithe chun Gníomhairí gníomhacha a fheiceáil ar an Scáileán Glasála." diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index ab04c0c60a..35a873cabd 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -3222,9 +3222,9 @@ "signedOut": "Inicia sesión para ver os axentes", "privacy": "Axentes ocultos", "openAgents": "Abrir axentes", - "running": "Executando", + "running": "Traballando", "needsInput": "precisa entrada", - "reconnecting": "Reconectando", + "idle": "Inactivo", "channelName": "Axentes activos", "activityKitDisabledTitle": "As actividades en directo están desactivadas", "activityKitDisabledBody": "Activa as actividades en directo en Configuración para ver os axentes activos na pantalla de bloqueo." diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index 218d791b1e..c692499b7d 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -3222,9 +3222,9 @@ "signedOut": "એજન્ટો જોવા માટે સાઇન ઇન કરો", "privacy": "એજન્ટો છુપાવેલા છે", "openAgents": "એજન્ટો ખોલો", - "running": "ચાલી રહ્યું છે", + "running": "કામ થઈ રહ્યું છે", "needsInput": "ઇનપુટ જરૂરી", - "reconnecting": "ફરી કનેક્ટ થઈ રહ્યું છે", + "idle": "નિષ્ક્રિય", "channelName": "સક્રિય એજન્ટો", "activityKitDisabledTitle": "લાઇવ પ્રવૃત્તિઓ બંધ છે", "activityKitDisabledBody": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો જોવા માટે સેટિંગ્સમાં લાઇવ પ્રવૃત્તિઓ ચાલુ કરો." diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 814fa14c51..4436987d39 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -3222,9 +3222,9 @@ "signedOut": "Shiga don ganin wakilai", "privacy": "An ɓoye wakilai", "openAgents": "Buɗe wakilai", - "running": "Ana gudana", + "running": "Yana aiki", "needsInput": "yana buƙatar bayani", - "reconnecting": "Ana sake haɗawa", + "idle": "Rashin aiki", "channelName": "Wakilai da ke aiki", "activityKitDisabledTitle": "Ayyukan Kai Tsaye suna a kashe", "activityKitDisabledBody": "Kunna Ayyukan Kai Tsaye a cikin Saituna don ganin Wakilai da ke Aiki a kan Allon Kulle." diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index a17f8b693e..cc3e79065c 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -3244,9 +3244,9 @@ "signedOut": "היכנס כדי לראות סוכנים", "privacy": "הסוכנים מוסתרים", "openAgents": "פתח סוכנים", - "running": "רץ", + "running": "עובד", "needsInput": "נדרש קלט", - "reconnecting": "מתחבר מחדש", + "idle": "בטל", "channelName": "סוכנים פעילים", "activityKitDisabledTitle": "פעילויות בזמן אמת כבויות", "activityKitDisabledBody": "הפעל פעילויות בזמן אמת בהגדרות כדי לראות סוכנים פעילים במסך הנעילה." diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 92d07709a7..5b3a0f5f02 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -3222,9 +3222,9 @@ "signedOut": "एजेंट देखने के लिए साइन इन करें", "privacy": "एजेंट छिपे हुए हैं", "openAgents": "एजेंट खोलें", - "running": "चालू", + "running": "काम कर रहा है", "needsInput": "इनपुट आवश्यक", - "reconnecting": "फिर से कनेक्ट हो रहा है", + "idle": "निष्क्रिय", "channelName": "सक्रिय एजेंट", "activityKitDisabledTitle": "लाइव ऐक्टिविटी बंद हैं", "activityKitDisabledBody": "लॉक स्क्रीन पर सक्रिय एजेंट देखने के लिए सेटिंग में लाइव ऐक्टिविटी चालू करें।" diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 43fec8ca2f..f84b330d4d 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -3244,9 +3244,9 @@ "signedOut": "Prijavite se da biste vidjeli agente", "privacy": "Agenti su skriveni", "openAgents": "Otvorite agente", - "running": "RADI", + "running": "Radim", "needsInput": "treba unos", - "reconnecting": "Ponovno povezivanje", + "idle": "Neaktivan", "channelName": "Aktivni agenti", "activityKitDisabledTitle": "Aktivnosti uživo su isključene", "activityKitDisabledBody": "Uključite Aktivnosti uživo u Postavkama kako biste vidjeli aktivne agente na zaključanom zaslonu." diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 4aa30d8ef7..2d891fcb67 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -3222,9 +3222,9 @@ "signedOut": "Konekte pou wè ajans yo", "privacy": "Ajans yo kache", "openAgents": "Louvri ajans yo", - "running": "AP KOURI", + "running": "Ap travay", "needsInput": "bezwen input", - "reconnecting": "Ap rekonekte", + "idle": "Anchaj", "channelName": "Ajans aktif yo", "activityKitDisabledTitle": "Aktivite an dirèk yo fèmen", "activityKitDisabledBody": "Aktive Aktivite an dirèk nan Paramèt pou wè Ajans aktif yo sou Ekran bloke a." diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index f10a9de85a..2718acd105 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -3222,9 +3222,9 @@ "signedOut": "Jelentkezzen be az ügynökök megtekintéséhez", "privacy": "Ügynökök elrejtve", "openAgents": "Ügynökök megnyitása", - "running": "Folyamatban", + "running": "Feldolgozás", "needsInput": "bemenetet igényel", - "reconnecting": "Újracsatlakozás", + "idle": "Tétlen", "channelName": "Aktív ügynökök", "activityKitDisabledTitle": "Az Élő tevékenységek ki vannak kapcsolva", "activityKitDisabledBody": "Kapcsolja be az Élő tevékenységeket a Beállításokban, hogy az aktív ügynökök megjelenjenek a zárolási képernyőn." diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index a06bdcfff1..9a4c05c1db 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -3222,9 +3222,9 @@ "signedOut": "Մուտք գործեք՝ գործակալներին տեսնելու համար", "privacy": "Գործակալները թաքցված են", "openAgents": "Բացեք գործակալները", - "running": "Ընթացքի մեջ է", + "running": "Մշակվում է", "needsInput": "մուտքագրման կարիք ունի", - "reconnecting": "Կրկին միացում", + "idle": "Պարապ", "channelName": "Ակտիվ գործակալներ", "activityKitDisabledTitle": "Ուղիղ ակտիվություններն անջատված են", "activityKitDisabledBody": "Միացրեք «Ուղիղ ակտիվություններ»-ը Կարգավորումներում՝ ակտիվ գործակալներին կողպման էկրանին տեսնելու համար։" diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index 67793b2042..f9f825ec01 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -3222,9 +3222,9 @@ "signedOut": "Masuk untuk melihat agen", "privacy": "Agen disembunyikan", "openAgents": "Buka agen", - "running": "BERJALAN", + "running": "Mengerjakan", "needsInput": "memerlukan input", - "reconnecting": "Menghubungkan kembali", + "idle": "Idle", "channelName": "Agen aktif", "activityKitDisabledTitle": "Aktivitas Langsung nonaktif", "activityKitDisabledBody": "Aktifkan Aktivitas Langsung di Pengaturan untuk melihat Agen Aktif di Layar Terkunci." diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 7b45367b0f..6555b27610 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -3222,9 +3222,9 @@ "signedOut": "Banye iji hụ ndị ọrụ", "privacy": "Ezochiri ndị ọrụ", "openAgents": "Mepee ndị ọrụ", - "running": "NA-AGBA", + "running": "Na-arụ ọrụ", "needsInput": "chọrọ ntinye", - "reconnecting": "Na-ejikọ ọzọ", + "idle": "Ọrụ na-agaghị", "channelName": "Ndị ọrụ na-arụ ọrụ", "activityKitDisabledTitle": "Agbanyụrụ Ihe Omume Dị Ndụ", "activityKitDisabledBody": "Gbanye Ihe Omume Dị Ndụ na Ntọala iji hụ Ndị ọrụ na-arụ ọrụ na Ihuenyo Mkpọchi." diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 5e206ab969..b1e7d93a0a 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -3222,9 +3222,9 @@ "signedOut": "Skráðu þig inn til að sjá umboð", "privacy": "Umboð falin", "openAgents": "Opna umboð", - "running": "Í gangi", + "running": "Vinnur", "needsInput": "þarfnast inntaks", - "reconnecting": "Tengist aftur", + "idle": "Í bið", "channelName": "Virk umboð", "activityKitDisabledTitle": "Slökkt er á Beinni virkni", "activityKitDisabledBody": "Kveiktu á Beinni virkni í Stillingum til að sjá Virk umboð á Lásskjánum." diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 38584728e4..0c4956f997 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -3244,9 +3244,9 @@ "signedOut": "Accedi per vedere gli agenti", "privacy": "Agenti nascosti", "openAgents": "Apri agenti", - "running": "IN ESECUZIONE", + "running": "In corso", "needsInput": "richiede input", - "reconnecting": "Riconnessione in corso", + "idle": "Inattivo", "channelName": "Agenti attivi", "activityKitDisabledTitle": "Le attività in tempo reale sono disattivate", "activityKitDisabledBody": "Attiva le attività in tempo reale in Impostazioni per vedere gli agenti attivi sulla schermata di blocco." diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 00db1ffba8..4b545b646e 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -3222,9 +3222,9 @@ "signedOut": "エージェントを表示するにはサインインしてください", "privacy": "エージェントは非表示です", "openAgents": "エージェントを開く", - "running": "実行中", + "running": "作業中", "needsInput": "入力が必要", - "reconnecting": "再接続中", + "idle": "アイドル", "channelName": "アクティブなエージェント", "activityKitDisabledTitle": "ライブアクティビティはオフです", "activityKitDisabledBody": "ロック画面にアクティブなエージェントを表示するには、設定でライブアクティビティをオンにしてください。" diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index b28ee3f31f..382abb8d2d 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -3222,9 +3222,9 @@ "signedOut": "შედით აგენტების სანახავად", "privacy": "აგენტები დამალულია", "openAgents": "აგენტების გახსნა", - "running": "მუშაობს", + "running": "მუშავდება", "needsInput": "მოითხოვს შეყვანას", - "reconnecting": "კავშირის აღდგენა", + "idle": "უქმე", "channelName": "აქტიური აგენტები", "activityKitDisabledTitle": "ცოცხალი აქტივობები გამორთულია", "activityKitDisabledBody": "ჩართეთ ცოცხალი აქტივობები პარამეტრებში, რათა დაბლოკვის ეკრანზე აქტიური აგენტები ნახოთ." diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 6ce99d35ec..743ca71e1e 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -3224,7 +3224,7 @@ "openAgents": "Агенттерді ашу", "running": "Орындалуда", "needsInput": "енгізу қажет", - "reconnecting": "Қайта қосылуда", + "idle": "Бос тұр", "channelName": "Белсенді агенттер", "activityKitDisabledTitle": "Тікелей әрекеттер өшірулі", "activityKitDisabledBody": "Құлыптау экранында белсенді агенттерді көру үшін Параметрлерде тікелей әрекеттерді қосыңыз." diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 3ec6b559da..83af731ecf 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -3224,7 +3224,7 @@ "openAgents": "បើកភ្នាក់ងារ", "running": "កំពុងដំណើរការ", "needsInput": "ត្រូវការបញ្ចូល", - "reconnecting": "កំពុងភ្ជាប់ឡើងវិញ", + "idle": "ទំនេរ", "channelName": "ភ្នាក់ងារសកម្ម", "activityKitDisabledTitle": "សកម្មភាពបន្តផ្ទាល់ត្រូវបានបិទ", "activityKitDisabledBody": "បើកសកម្មភាពបន្តផ្ទាល់នៅក្នុងការកំណត់ ដើម្បីមើលភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ។" diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 3d4fc5ee95..c9c9a41795 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -3222,9 +3222,9 @@ "signedOut": "ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೈನ್ ಇನ್ ಮಾಡಿ", "privacy": "ಏಜೆಂಟ್‌ಗಳನ್ನು ಮರೆಮಾಡಲಾಗಿದೆ", "openAgents": "ಏಜೆಂಟ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ", - "running": "ಚಾಲನೆಯಲ್ಲಿದೆ", + "running": "ಕೆಲಸ ಮಾಡುತ್ತಿದೆ", "needsInput": "ಇನ್‌ಪುಟ್ ಅಗತ್ಯವಿದೆ", - "reconnecting": "ಮರುಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ", + "idle": "ನಿಷ್ಕ್ರಿಯ", "channelName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು", "activityKitDisabledTitle": "ನೇರ ಚಟುವಟಿಕೆಗಳು ಆಫ್ ಆಗಿವೆ", "activityKitDisabledBody": "ಲಾಕ್ ಪರದೆಯಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ನೇರ ಚಟುವಟಿಕೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ." diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 866c435d18..369814b23a 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -3222,9 +3222,9 @@ "signedOut": "에이전트를 보려면 로그인하세요", "privacy": "에이전트 숨겨짐", "openAgents": "에이전트 열기", - "running": "실행 중", + "running": "작업 중", "needsInput": "입력 필요", - "reconnecting": "다시 연결 중", + "idle": "유휴", "channelName": "활성 에이전트", "activityKitDisabledTitle": "실시간 현황이 꺼져 있습니다", "activityKitDisabledBody": "잠금 화면에서 활성 에이전트를 보려면 설정에서 실시간 현황을 켜세요." diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index 1865497041..e6df3cd89a 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -3224,7 +3224,7 @@ "openAgents": "ເປີດຕົວແທນ", "running": "ກຳລັງດຳເນີນການ", "needsInput": "ຕ້ອງການຂໍ້ມູນເຂົ້າ", - "reconnecting": "ກຳລັງເຊື່ອມຕໍ່ຄືນ", + "idle": "ບໍ່ຫຍຸ້ງ", "channelName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ", "activityKitDisabledTitle": "ກິດຈະກຳສົດປິດຢູ່", "activityKitDisabledBody": "ເປີດກິດຈະກຳສົດໃນການຕັ້ງຄ່າ ເພື່ອເບິ່ງຕົວແທນທີ່ກຳລັງເຮັດວຽກໃນໜ້າຈໍລັອກ." diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index e6d615aa21..1de77fad32 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -3268,7 +3268,7 @@ "openAgents": "Atidaryti agentus", "running": "Vykdoma", "needsInput": "reikia įvesties", - "reconnecting": "Jungiamasi iš naujo", + "idle": "Neaktyvus", "channelName": "Aktyvūs agentai", "activityKitDisabledTitle": "Tiesioginės veiklos išjungtos", "activityKitDisabledBody": "Nustatymuose įjunkite tiesiogines veiklas, kad užrakinimo ekrane matytumėte aktyvius agentus." diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index a3fbdfe85c..b331707645 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -3244,9 +3244,9 @@ "signedOut": "Pieraksties, lai redzētu aģentus", "privacy": "Aģenti ir paslēpti", "openAgents": "Atvērt aģentus", - "running": "DARBOJAS", + "running": "Apstrādā", "needsInput": "nepieciešama ievade", - "reconnecting": "Atkārtoti izveido savienojumu", + "idle": "Dīkstāvē", "channelName": "Aktīvie aģenti", "activityKitDisabledTitle": "Tiešraides aktivitātes ir izslēgtas", "activityKitDisabledBody": "Ieslēdz tiešraides aktivitātes iestatījumos, lai bloķēšanas ekrānā redzētu aktīvos aģentus." diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index bf3a275fec..f2d169a211 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -3222,9 +3222,9 @@ "signedOut": "Midira mba hahitana ny agent", "privacy": "Nafenina ny agent", "openAgents": "Sokafy ny agent", - "running": "MANDEHA", + "running": "Miasa", "needsInput": "mila fampidirana", - "reconnecting": "Mampifandray indray", + "idle": "Tsy mihetsika", "channelName": "Agent mavitrika", "activityKitDisabledTitle": "Tsy mandeha ny Hetsika Mivantana", "activityKitDisabledBody": "Alefaso ao amin'ny Fikirana ny Hetsika Mivantana mba hahitana ny Agent Mavitrika eo amin'ny Efijery Fihidy." diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index ea6de3bdd5..c3b2943b2a 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -3222,9 +3222,9 @@ "signedOut": "Takiuru kia kite i ngā māngai", "privacy": "Kua huna ngā māngai", "openAgents": "Whakatuwheratia ngā māngai", - "running": "Kei te oma", + "running": "Kei te mahi", "needsInput": "e hiahia ana ki te whakaurunga", - "reconnecting": "Kei te hono anō", + "idle": "Kore mahi", "channelName": "Ngā māngai hohe", "activityKitDisabledTitle": "Kua whakawetohia ngā Mahi Mataora", "activityKitDisabledBody": "Whakakāngia ngā Mahi Mataora i Ngā tautuhinga kia kite i ngā Māngai Hohe i te Mata Maukati." diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 28bb97d69f..3a98f3e83a 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -3222,9 +3222,9 @@ "signedOut": "Најавете се за да ги видите агентите", "privacy": "Агентите се скриени", "openAgents": "Отворете ги агентите", - "running": "Во тек", + "running": "Работи", "needsInput": "бара внес", - "reconnecting": "Повторно поврзување", + "idle": "Неактивен", "channelName": "Активни агенти", "activityKitDisabledTitle": "Активностите во живо се исклучени", "activityKitDisabledBody": "Вклучете Активности во живо во Поставки за да ги видите активните агенти на заклучениот екран." diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 024bb01f36..00e9682071 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -3224,7 +3224,7 @@ "openAgents": "ഏജന്റുകളെ തുറക്കുക", "running": "പ്രവർത്തിക്കുന്നു", "needsInput": "ഇൻപുട്ട് ആവശ്യമാണ്", - "reconnecting": "വീണ്ടും ബന്ധിപ്പിക്കുന്നു", + "idle": "നിഷ്ക്രിയം", "channelName": "സജീവ ഏജന്റുകൾ", "activityKitDisabledTitle": "തത്സമയ പ്രവർത്തനങ്ങൾ ഓഫാണ്", "activityKitDisabledBody": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണാൻ ക്രമീകരണങ്ങളിൽ തത്സമയ പ്രവർത്തനങ്ങൾ പ്രവർത്തനക്ഷമമാക്കുക." diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index ae394e5673..ccafaf1c42 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -3222,9 +3222,9 @@ "signedOut": "Агентуудыг харахын тулд нэвтэрнэ үү", "privacy": "Агентуудыг нуусан", "openAgents": "Агентуудыг нээх", - "running": "АЖИЛЛАЖ БАЙНА", + "running": "Ажиллаж байна", "needsInput": "оролт шаардлагатай", - "reconnecting": "Дахин холбогдож байна", + "idle": "Сул зогсож", "channelName": "Идэвхтэй агентууд", "activityKitDisabledTitle": "Шууд үйл ажиллагаа унтраалттай байна", "activityKitDisabledBody": "Түгжээтэй дэлгэц дээр Идэвхтэй агентуудыг харахын тулд Тохиргоо хэсэгт Шууд үйл ажиллагааг асаана уу." diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index 6b2daf95c8..bd753c2ffa 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -3222,9 +3222,9 @@ "signedOut": "एजंट्स पाहण्यासाठी साइन इन करा", "privacy": "एजंट्स लपवले आहेत", "openAgents": "एजंट्स उघडा", - "running": "चालू आहे", + "running": "कार्यरत", "needsInput": "इनपुट आवश्यक", - "reconnecting": "पुन्हा जोडत आहे", + "idle": "निष्क्रिय", "channelName": "सक्रिय एजंट्स", "activityKitDisabledTitle": "थेट क्रियाकलाप बंद आहेत", "activityKitDisabledBody": "लॉक स्क्रीनवर सक्रिय एजंट्स पाहण्यासाठी सेटिंग्जमध्ये थेट क्रियाकलाप सुरू करा." diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 9391156b30..19b240c460 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -3222,9 +3222,9 @@ "signedOut": "Log masuk untuk melihat ejen", "privacy": "Ejen disembunyikan", "openAgents": "Buka ejen", - "running": "Sedang berjalan", + "running": "Memproses…", "needsInput": "perlu input", - "reconnecting": "Menyambung semula", + "idle": "Melahu", "channelName": "Ejen aktif", "activityKitDisabledTitle": "Aktiviti Langsung dimatikan", "activityKitDisabledBody": "Hidupkan Aktiviti Langsung dalam Tetapan untuk melihat Ejen Aktif pada Skrin Kunci." diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index 7bc54c4090..2bc264845c 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -3288,9 +3288,9 @@ "signedOut": "Idħol biex tara l-aġenti", "privacy": "Aġenti moħbija", "openAgents": "Iftaħ l-aġenti", - "running": "Għaddej", + "running": "Qed jaħdem", "needsInput": "jeħtieġ input", - "reconnecting": "Qed jerġa' jaqbad", + "idle": "Idle", "channelName": "Aġenti attivi", "activityKitDisabledTitle": "L-Attivitajiet Diretti huma mitfija", "activityKitDisabledBody": "Ixgħel l-Attivitajiet Diretti fis-Settings biex tara l-Aġenti Attivi fuq l-Iskrin Imsakkar." diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index 03d5fdc6c8..7d498ece2c 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -3222,9 +3222,9 @@ "signedOut": "agent များကို ကြည့်ရန် ဝင်ပါ", "privacy": "agent များကို ဝှက်ထားသည်", "openAgents": "agent များကို ဖွင့်ပါ", - "running": "လည်ပတ်နေသည်", + "running": "လုပ်ဆောင်နေသည်", "needsInput": "ထည့်သွင်းမှု လိုအပ်သည်", - "reconnecting": "ပြန်ချိတ်ဆက်နေသည်", + "idle": "နားနေသည်", "channelName": "လုပ်ဆောင်နေသော agent များ", "activityKitDisabledTitle": "တိုက်ရိုက်လှုပ်ရှားမှုများ ပိတ်ထားသည်", "activityKitDisabledBody": "သော့ခတ်မျက်နှာပြင်တွင် လုပ်ဆောင်နေသော agent များကို ကြည့်ရန် ဆက်တင်များတွင် တိုက်ရိုက်လှုပ်ရှားမှုများကို ဖွင့်ပါ။" diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index 75718f9ee7..dee8ad6546 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -3222,9 +3222,9 @@ "signedOut": "Logg inn for å se agenter", "privacy": "Agenter er skjult", "openAgents": "Åpne agenter", - "running": "KJØRER", + "running": "Jobber", "needsInput": "trenger innspill", - "reconnecting": "Kobler til på nytt", + "idle": "Ledig", "channelName": "Aktive agenter", "activityKitDisabledTitle": "Oppdateringer i sanntid er av", "activityKitDisabledBody": "Slå på Oppdateringer i sanntid i Innstillinger for å se Aktive agenter på låst skjerm." diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index 3241044e80..a61e70e2f5 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -3222,9 +3222,9 @@ "signedOut": "एजेन्टहरू हेर्न साइन इन गर्नुहोस्", "privacy": "एजेन्टहरू लुकाइएका छन्", "openAgents": "एजेन्टहरू खोल्नुहोस्", - "running": "चलिरहेको", + "running": "काम गर्दै", "needsInput": "इनपुट चाहिन्छ", - "reconnecting": "पुनः जडान गर्दै", + "idle": "निष्क्रिय", "channelName": "सक्रिय एजेन्टहरू", "activityKitDisabledTitle": "प्रत्यक्ष गतिविधिहरू बन्द छन्", "activityKitDisabledBody": "लक स्क्रिनमा सक्रिय एजेन्टहरू हेर्न सेटिङ्समा प्रत्यक्ष गतिविधिहरू चालू गर्नुहोस्।" diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 4ca9eb7aff..1a6f4f592c 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -3224,7 +3224,7 @@ "openAgents": "Agents openen", "running": "Bezig", "needsInput": "heeft invoer nodig", - "reconnecting": "Opnieuw verbinden", + "idle": "Inactief", "channelName": "Actieve agents", "activityKitDisabledTitle": "Liveactiviteiten staan uit", "activityKitDisabledBody": "Schakel liveactiviteiten in via Instellingen om actieve agents op het toegangsscherm te zien." diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index 8896cc8a17..5ec0e2b0f9 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -3222,9 +3222,9 @@ "signedOut": "Eejentoota arguuf seenaa", "privacy": "Eejentoonni dhokamaniiru", "openAgents": "Eejentoota banaa", - "running": "Hojii irra jira", + "running": "Hojachaa jira", "needsInput": "seensa barbaada", - "reconnecting": "Irra deebi'ee walqabachaa jira", + "idle": "Hojii irraa boqachaa", "channelName": "Eejentoota hojii irra jiran", "activityKitDisabledTitle": "Sochiiwwan Kallattii cufamaniiru", "activityKitDisabledBody": "Eejentoota hojii irra jiran Iskiriinii Qulfii irratti arguuf, Qindaa'ina keessatti Sochiiwwan Kallattii banaa." diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 3cdf2349d9..fff9f1b9b7 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -3222,9 +3222,9 @@ "signedOut": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସାଇନ୍ ଇନ୍ କରନ୍ତୁ", "privacy": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଲୁଚାଯାଇଛି", "openAgents": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଖୋଲନ୍ତୁ", - "running": "ଚାଲୁଛି", + "running": "କାମ କରୁଛି", "needsInput": "ଇନପୁଟ୍ ଆବଶ୍ୟକ", - "reconnecting": "ପୁଣି ସଂଯୋଗ ହେଉଛି", + "idle": "ନିଷ୍କ୍ରିୟ", "channelName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ", "activityKitDisabledTitle": "ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ବନ୍ଦ ଅଛି", "activityKitDisabledBody": "ଲକ୍ ସ୍କ୍ରିନ୍‌ରେ ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସେଟିଂସ୍‌ରେ ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ଚାଲୁ କରନ୍ତୁ।" diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 80b10058b4..2da0e5f9ec 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -3222,9 +3222,9 @@ "signedOut": "ਏਜੰਟ ਦੇਖਣ ਲਈ ਸਾਈਨ ਇਨ ਕਰੋ", "privacy": "ਏਜੰਟ ਲੁਕਾਏ ਗਏ ਹਨ", "openAgents": "ਏਜੰਟ ਖੋਲ੍ਹੋ", - "running": "ਚੱਲ ਰਿਹਾ ਹੈ", + "running": "ਕੰਮ ਹੋ ਰਿਹਾ ਹੈ", "needsInput": "ਇਨਪੁੱਟ ਦੀ ਲੋੜ ਹੈ", - "reconnecting": "ਮੁੜ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", + "idle": "ਸੁਸਤ", "channelName": "ਸਰਗਰਮ ਏਜੰਟ", "activityKitDisabledTitle": "ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਬੰਦ ਹਨ", "activityKitDisabledBody": "ਲਾਕ ਸਕ੍ਰੀਨ 'ਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦੇਖਣ ਲਈ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਚਾਲੂ ਕਰੋ।" diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index f7df298003..cae22208c3 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -3266,9 +3266,9 @@ "signedOut": "Zaloguj się, aby zobaczyć agentów", "privacy": "Agenci ukryci", "openAgents": "Otwórz agentów", - "running": "W toku", + "running": "Pracuję", "needsInput": "wymaga danych", - "reconnecting": "Ponowne łączenie", + "idle": "Bezczynny", "channelName": "Aktywni agenci", "activityKitDisabledTitle": "Wydarzenia na żywo są wyłączone", "activityKitDisabledBody": "Włącz wydarzenia na żywo w Ustawieniach, aby widzieć aktywnych agentów na ekranie blokady." diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index d6056290e4..5ff861860e 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -3224,7 +3224,7 @@ "openAgents": "اجنټان پرانیزئ", "running": "روان", "needsInput": "ورودی ته اړتیا لري", - "reconnecting": "بیا نښلېږي", + "idle": "بې کاره", "channelName": "فعال اجنټان", "activityKitDisabledTitle": "ژوندي فعالیتونه بند دي", "activityKitDisabledBody": "په قلف شوې پرده کې د فعالو اجنټانو د لیدلو لپاره په ترتیباتو کې ژوندي فعالیتونه فعال کړئ." diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index cc0729130f..8d663f6448 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -3244,9 +3244,9 @@ "signedOut": "Entre para ver os agentes", "privacy": "Agentes ocultos", "openAgents": "Abrir agentes", - "running": "Em execução", + "running": "Trabalhando", "needsInput": "requer entrada", - "reconnecting": "Reconectando", + "idle": "Ocioso", "channelName": "Agentes ativos", "activityKitDisabledTitle": "As Atividades ao Vivo estão desativadas", "activityKitDisabledBody": "Ative as Atividades ao Vivo em Ajustes para ver os agentes ativos na Tela Bloqueada." diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 4515a17b9e..795ef77929 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -3244,9 +3244,9 @@ "signedOut": "Inicie sessão para ver os agentes", "privacy": "Agentes ocultos", "openAgents": "Abrir agentes", - "running": "EM EXECUÇÃO", + "running": "A trabalhar", "needsInput": "requer entrada", - "reconnecting": "A restabelecer ligação", + "idle": "Inativo", "channelName": "Agentes ativos", "activityKitDisabledTitle": "As Atividades em tempo real estão desativadas", "activityKitDisabledBody": "Ative as Atividades em tempo real nas Definições para ver os agentes ativos no Ecrã bloqueado." diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 0f2fe86094..eea2d62b24 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -3244,9 +3244,9 @@ "signedOut": "Autentifică-te pentru a vedea agenții", "privacy": "Agenți ascunși", "openAgents": "Deschide agenții", - "running": "Rulează", + "running": "Se procesează", "needsInput": "necesită introducere", - "reconnecting": "Se reconectează", + "idle": "Inactiv", "channelName": "Agenți activi", "activityKitDisabledTitle": "Activitățile live sunt dezactivate", "activityKitDisabledBody": "Activează Activități live în Setări pentru a vedea Agenții activi pe ecranul de blocare." diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 3ec04d68cc..3d0e6e5514 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -3266,9 +3266,9 @@ "signedOut": "Войдите, чтобы видеть агентов", "privacy": "Агенты скрыты", "openAgents": "Открыть агентов", - "running": "Выполняется", + "running": "Работаю...", "needsInput": "требует ввода", - "reconnecting": "Повторное подключение", + "idle": "Неактивен", "channelName": "Активные агенты", "activityKitDisabledTitle": "Эфир активности выключен", "activityKitDisabledBody": "Включите Эфир активности в Настройках, чтобы видеть активных агентов на экране блокировки." diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index 2e5119447e..dc6766d58f 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -3222,9 +3222,9 @@ "signedOut": "නියෝජිතයන් බැලීමට පුරනය වන්න", "privacy": "නියෝජිතයන් සඟවා ඇත", "openAgents": "නියෝජිතයන් විවෘත කරන්න", - "running": "ධාවනය වෙමින්", + "running": "වැඩ කරමින්", "needsInput": "ආදානය අවශ්යයි", - "reconnecting": "නැවත සම්බන්ධ වෙමින්", + "idle": "නිශ්චල", "channelName": "සක්‍රිය නියෝජිතයන්", "activityKitDisabledTitle": "සජීවී ක්‍රියාකාරකම් අක්‍රියයි", "activityKitDisabledBody": "අගුළු තිරයේ සක්‍රිය නියෝජිතයන් බැලීමට සැකසුම් තුළ සජීවී ක්‍රියාකාරකම් සක්‍රිය කරන්න." diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index a89a7f1e75..3939581616 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -3266,9 +3266,9 @@ "signedOut": "Prihláste sa na zobrazenie agentov", "privacy": "Agenti sú skrytí", "openAgents": "Otvoriť agentov", - "running": "Prebieha", + "running": "Pracuje sa", "needsInput": "vyžaduje vstup", - "reconnecting": "Opätovné pripájanie", + "idle": "Nečinný", "channelName": "Aktívni agenti", "activityKitDisabledTitle": "Živé aktivity sú vypnuté", "activityKitDisabledBody": "Zapnite živé aktivity v Nastaveniach, aby sa aktívni agenti zobrazovali na zamknutej obrazovke." diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index ee0e0948aa..e5cc63fdac 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -3266,9 +3266,9 @@ "signedOut": "Prijavite se za ogled agentov", "privacy": "Agenti so skriti", "openAgents": "Odprite agente", - "running": "DELUJE", + "running": "Delam", "needsInput": "potrebuje vnos", - "reconnecting": "Ponovno povezovanje", + "idle": "Nedejavno", "channelName": "Aktivni agenti", "activityKitDisabledTitle": "Dejavnosti v živo so izklopljene", "activityKitDisabledBody": "V Nastavitvah vklopite Dejavnosti v živo, da bodo Aktivni agenti prikazani na zaklenjenem zaslonu." diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 2884b1c8e1..e53747aa03 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -3222,9 +3222,9 @@ "signedOut": "Soo gal si aad u aragto wakiillada", "privacy": "Wakiillada waa la qariyay", "openAgents": "Fur wakiillada", - "running": "Socodaya", + "running": "Waa shaqaynayaa", "needsInput": "u baahan wax-soo-gal", - "reconnecting": "Dib u xiriirinaya", + "idle": "Firfircooni la'aan", "channelName": "Wakiillada firfircoon", "activityKitDisabledTitle": "Hawlaha Tooska ah waa daman", "activityKitDisabledBody": "Ku daar Hawlaha Tooska ah Dejinta si aad Wakiillada firfircoon ugu aragto Shaashadda Qufulka." diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index c5f01dfa0c..5a9b1f2556 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -3222,9 +3222,9 @@ "signedOut": "Identifikohuni për të parë agjentët", "privacy": "Agjentët janë fshehur", "openAgents": "Hapni agjentët", - "running": "Në ekzekutim", + "running": "Duke punuar", "needsInput": "ka nevojë për të dhëna", - "reconnecting": "Duke u rilidhur", + "idle": "I papunë", "channelName": "Agjentët aktivë", "activityKitDisabledTitle": "Aktivitetet e drejtpërdrejta janë çaktivizuar", "activityKitDisabledBody": "Aktivizoni Aktivitetet e drejtpërdrejta te Cilësimet për të parë Agjentët aktivë në Ekranin e kyçjes." diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index 2126246925..c3bf3ff1aa 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -3244,9 +3244,9 @@ "signedOut": "Prijavite se da biste videli agente", "privacy": "Agenti su skriveni", "openAgents": "Otvorite agente", - "running": "U toku", + "running": "Radim…", "needsInput": "zahteva unos", - "reconnecting": "Ponovno povezivanje", + "idle": "Neaktivan", "channelName": "Aktivni agenti", "activityKitDisabledTitle": "Aktivnosti uživo su isključene", "activityKitDisabledBody": "Uključite Aktivnosti uživo u Podešavanjima da biste videli aktivne agente na zaključanom ekranu." diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index b6f50d473b..ba7e3ae8ab 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -3222,9 +3222,9 @@ "signedOut": "Logga in för att se agenter", "privacy": "Agenter dolda", "openAgents": "Öppna agenter", - "running": "KÖRS", + "running": "Arbetar", "needsInput": "kräver indata", - "reconnecting": "Återansluter", + "idle": "Inaktiv", "channelName": "Aktiva agenter", "activityKitDisabledTitle": "Liveaktiviteter är avstängda", "activityKitDisabledBody": "Aktivera liveaktiviteter i Inställningar för att se Aktiva agenter på låsskärmen." diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index f1c297f50e..340a476d8a 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -3222,9 +3222,9 @@ "signedOut": "Ingia ili uone mawakala", "privacy": "Mawakala wamefichwa", "openAgents": "Fungua mawakala", - "running": "Inaendelea", + "running": "Inafanya kazi", "needsInput": "inahitaji mchango", - "reconnecting": "Inaunganisha tena", + "idle": "Hakikazi", "channelName": "Mawakala wanaofanya kazi", "activityKitDisabledTitle": "Shughuli za Moja kwa Moja zimezimwa", "activityKitDisabledBody": "Washa Shughuli za Moja kwa Moja katika Mipangilio ili uone Mawakala wanaofanya kazi kwenye Skrini Iliyofungwa." diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index d4c2e21df8..1029869b84 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -3222,9 +3222,9 @@ "signedOut": "முகவர்களைக் காண உள்நுழையவும்", "privacy": "முகவர்கள் மறைக்கப்பட்டுள்ளனர்", "openAgents": "முகவர்களைத் திறக்கவும்", - "running": "இயங்குகிறது", + "running": "வேலை செய்கிறது", "needsInput": "உள்ளீடு தேவை", - "reconnecting": "மீண்டும் இணைக்கிறது", + "idle": "செயலற்று", "channelName": "செயலில் உள்ள முகவர்கள்", "activityKitDisabledTitle": "நேரலைச் செயல்பாடுகள் முடக்கப்பட்டுள்ளன", "activityKitDisabledBody": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காண அமைப்புகளில் நேரலைச் செயல்பாடுகளை இயக்கவும்." diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index df34e541c6..dac27e3e50 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -3222,9 +3222,9 @@ "signedOut": "ఏజెంట్లను చూడటానికి సైన్ ఇన్ చేయండి", "privacy": "ఏజెంట్లు దాచబడ్డారు", "openAgents": "ఏజెంట్లను తెరవండి", - "running": "నడుస్తోంది", + "running": "పని జరుగుతోంది", "needsInput": "ఇన్పుట్ అవసరం", - "reconnecting": "మళ్లీ కనెక్ట్ అవుతోంది", + "idle": "నిష్క్రియం", "channelName": "చురుకైన ఏజెంట్లు", "activityKitDisabledTitle": "ప్రత్యక్ష కార్యకలాపాలు ఆఫ్‌లో ఉన్నాయి", "activityKitDisabledBody": "లాక్ స్క్రీన్‌పై చురుకైన ఏజెంట్లను చూడటానికి సెట్టింగ్‌లలో ప్రత్యక్ష కార్యకలాపాలను ఆన్ చేయండి." diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index aff97f684d..602a61603a 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -3224,7 +3224,7 @@ "openAgents": "เปิดเอเจนต์", "running": "กำลังทำงาน", "needsInput": "ต้องป้อนข้อมูล", - "reconnecting": "กำลังเชื่อมต่อใหม่", + "idle": "ว่าง", "channelName": "เอเจนต์ที่กำลังทำงาน", "activityKitDisabledTitle": "กิจกรรมสดปิดอยู่", "activityKitDisabledBody": "เปิดกิจกรรมสดในการตั้งค่าเพื่อดูเอเจนต์ที่กำลังทำงานบนหน้าจอล็อค" diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index 5be616ce08..f83e38f307 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -3224,7 +3224,7 @@ "openAgents": "Ajanları açın", "running": "Çalışıyor", "needsInput": "Girdi gerekli", - "reconnecting": "Yeniden bağlanılıyor", + "idle": "Boşta", "channelName": "Etkin ajanlar", "activityKitDisabledTitle": "Canlı Etkinlikler kapalı", "activityKitDisabledBody": "Etkin ajanları Kilit Ekranı'nda görmek için Ayarlar'dan Canlı Etkinlikler'i açın." diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 833c34f1a0..f2c778aff7 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -3266,9 +3266,9 @@ "signedOut": "Увійдіть, щоб бачити агентів", "privacy": "Агентів приховано", "openAgents": "Відкрити агентів", - "running": "Виконується", + "running": "Працює", "needsInput": "потребує вводу", - "reconnecting": "Повторне підключення", + "idle": "Неактивний", "channelName": "Активні агенти", "activityKitDisabledTitle": "Дії наживо вимкнено", "activityKitDisabledBody": "Увімкніть «Дії наживо» в «Параметрах», щоб бачити активних агентів на замкненому екрані." diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index ee6624f91c..9b82d9597f 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -3222,9 +3222,9 @@ "signedOut": "ایجنٹس دیکھنے کے لیے سائن ان کریں", "privacy": "ایجنٹس چھپے ہوئے ہیں", "openAgents": "ایجنٹس کھولیں", - "running": "چل رہا ہے", + "running": "کام جاری ہے", "needsInput": "ان پٹ درکار", - "reconnecting": "دوبارہ منسلک ہو رہا ہے", + "idle": "غیر فعال", "channelName": "فعال ایجنٹس", "activityKitDisabledTitle": "لائیو سرگرمیاں بند ہیں", "activityKitDisabledBody": "لاک اسکرین پر فعال ایجنٹس دیکھنے کے لیے ترتیبات میں لائیو سرگرمیاں فعال کریں۔" diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 1b14dcf338..265a3e5be8 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -3224,7 +3224,7 @@ "openAgents": "Agentlarni oching", "running": "Ishlamoqda", "needsInput": "kiritish kerak", - "reconnecting": "Qayta ulanmoqda", + "idle": "Kutmoqda", "channelName": "Faol agentlar", "activityKitDisabledTitle": "Jonli faoliyatlar o'chirilgan", "activityKitDisabledBody": "Qulflangan ekranda Faol agentlarni ko'rish uchun Sozlamalarda Jonli faoliyatlarni yoqing." diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 496f5813bd..351d35e1aa 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -3222,9 +3222,9 @@ "signedOut": "Đăng nhập để xem tác nhân", "privacy": "Đã ẩn tác nhân", "openAgents": "Mở tác nhân", - "running": "ĐANG CHẠY", + "running": "Đang xử lý", "needsInput": "cần nhập", - "reconnecting": "Đang kết nối lại", + "idle": "Không hoạt động", "channelName": "Tác nhân đang hoạt động", "activityKitDisabledTitle": "Hoạt động trực tiếp đang tắt", "activityKitDisabledBody": "Bật Hoạt động trực tiếp trong Cài đặt để xem các tác nhân đang hoạt động trên Màn hình khóa." diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index be65657955..d980f3127a 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -3222,9 +3222,9 @@ "signedOut": "Wọle lati ri awọn aṣoju", "privacy": "Awọn aṣoju wa ni ipamọ", "openAgents": "Ṣii awọn aṣoju", - "running": "ǸJẸ́ ṢÍṢIṢẸ́", + "running": "Nṣiṣẹ́", "needsInput": "nilo igbewọle", - "reconnecting": "Ti n tun sopọ", + "idle": "Ìsinmi", "channelName": "Awọn aṣoju to n ṣiṣẹ", "activityKitDisabledTitle": "Awọn Iṣẹ Lọwọlọwọ wa ni pipa", "activityKitDisabledBody": "Tan Awọn Iṣẹ Lọwọlọwọ ninu Eto lati ri Awọn aṣoju to n ṣiṣẹ lori Iboju Titiipa." diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index b893c10d2a..3fd941a508 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -3222,9 +3222,9 @@ "signedOut": "请登录以查看代理", "privacy": "代理已隐藏", "openAgents": "打开代理", - "running": "运行中", + "running": "工作中", "needsInput": "需要输入", - "reconnecting": "正在重新连接", + "idle": "空闲", "channelName": "活动代理", "activityKitDisabledTitle": "实时活动已关闭", "activityKitDisabledBody": "请在“设置”中开启“实时活动”,以在锁定屏幕上查看活动代理。" diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index ef5a87fbeb..fd880073aa 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -3222,9 +3222,9 @@ "signedOut": "請登入以查看代理", "privacy": "代理已隱藏", "openAgents": "開啟代理", - "running": "執行中", + "running": "處理中", "needsInput": "需要輸入", - "reconnecting": "正在重新連線", + "idle": "閒置", "channelName": "使用中的代理", "activityKitDisabledTitle": "即時動態已關閉", "activityKitDisabledBody": "請在「設定」中開啟「即時動態」,以在鎖定畫面上查看使用中的代理。" diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index ea821bfc57..a103a0bf90 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -3222,9 +3222,9 @@ "signedOut": "Ngena ngemvume ukuze ubone ama-agent", "privacy": "Ama-agent afihliwe", "openAgents": "Vula ama-agent", - "running": "IYASEBENZA", + "running": "Iyasebenza", "needsInput": "idinga okokufaka", - "reconnecting": "Ixhuma kabusha", + "idle": "Banga", "channelName": "Ama-agent asebenzayo", "activityKitDisabledTitle": "Imisebenzi Ebukhoma ivaliwe", "activityKitDisabledBody": "Vula Imisebenzi Ebukhoma ku-Izilungiselelo ukuze ubone Ama-agent asebenzayo Esikrinini Esikhiyiwe." diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts index 79f1500606..e9f57035ae 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts @@ -217,7 +217,7 @@ describe('recoverGlanceableActivityKit', () => { status: 'happy', running: 0, needsInput: 1, - reconnecting: 0, + idle: 0, }); publisher.dispose(); } diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts index 4f405e0325..2bd49df932 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -11,7 +11,7 @@ import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; import { getLastGlanceableSnapshot, getLocalScopeKey } from '@/lib/glanceable/persist'; import { readGlanceableEnabled } from '@/lib/glanceable/enabled'; -import { getGlanceableSinks } from '@/lib/glanceable/sink-registry'; +import { forEachSink } from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { i18n } from '@/i18n'; @@ -89,7 +89,7 @@ export async function recoverGlanceableActivityKit(): Promise { if (!isEligibleGlanceableWork(snapshot)) { return; } - for (const sink of getGlanceableSinks()) { + forEachSink('recover_start_or_update', sink => { sink.startOrUpdate(snapshot, { userId, organizationId }); - } + }); } diff --git a/apps/mobile/src/lib/glanceable/cleanup.test.ts b/apps/mobile/src/lib/glanceable/cleanup.test.ts index 6f13311da0..b79e34efd5 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.test.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.test.ts @@ -63,12 +63,42 @@ describe('cleanup', () => { expect(calls.map(call => call.type)).toEqual(['publish', 'endImmediate']); const snapshot = lastSnapshot(calls); expect(snapshot.status).toBe('signed_out'); - expect(snapshot.running + snapshot.needsInput + snapshot.reconnecting).toBe(0); + expect(snapshot.running + snapshot.needsInput + snapshot.idle).toBe(0); } finally { unregisterGlanceableSink(sink); } }); + it('blanks every other sink when one sink throws', () => { + // A throwing WidgetKit or ActivityKit host function must not reach the + // caller: `writeSignedOutSnapshotAndEnd` runs inside the auth transition, + // so a propagated failure would abort the sign-in outright. + const throwing: GlanceableSink = { + publish() { + throw new Error('Exception in HostFunction: '); + }, + startOrUpdate() { + throw new Error('Exception in HostFunction: '); + }, + endImmediate() { + throw new Error('Exception in HostFunction: '); + }, + }; + const { sink, calls } = makeSink(); + registerGlanceableSink(throwing); + registerGlanceableSink(sink); + try { + expect(() => { + writeSignedOutSnapshotAndEnd(); + }).not.toThrow(); + expect(calls.map(call => call.type)).toEqual(['publish', 'endImmediate']); + expect(lastSnapshot(calls).status).toBe('signed_out'); + } finally { + unregisterGlanceableSink(sink); + unregisterGlanceableSink(throwing); + } + }); + it('blanks to privacy on org switch', () => { const { sink, calls } = makeSink(); registerGlanceableSink(sink); @@ -76,7 +106,7 @@ describe('cleanup', () => { writePrivacySnapshotAndEnd(); const snapshot = lastSnapshot(calls); expect(snapshot.status).toBe('privacy'); - expect(snapshot.running + snapshot.needsInput + snapshot.reconnecting).toBe(0); + expect(snapshot.running + snapshot.needsInput + snapshot.idle).toBe(0); } finally { unregisterGlanceableSink(sink); } @@ -143,7 +173,7 @@ describe('cleanup', () => { status: 'happy', running: 2, needsInput: 1, - reconnecting: 1, + idle: 1, eligibleStartedAt: '2026-08-26T23:00:00.000Z', }; _setLastGlanceableSnapshotForTests(seeded); @@ -171,7 +201,7 @@ describe('cleanup', () => { status: 'expired', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }); } @@ -194,7 +224,7 @@ describe('cleanup', () => { status, running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }; _setLastGlanceableSnapshotForTests(terminal); @@ -212,7 +242,7 @@ describe('cleanup', () => { expiresAt: terminal.expiresAt, running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }); } diff --git a/apps/mobile/src/lib/glanceable/cleanup.ts b/apps/mobile/src/lib/glanceable/cleanup.ts index d57c0ddb42..25aac19cb3 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.ts @@ -6,7 +6,7 @@ import { import { getLastGlanceableSnapshot } from './persist'; import { withStatus } from './publisher'; -import { getGlanceableDelivery, getGlanceableSinks } from './sink-registry'; +import { forEachSink, getGlanceableDelivery } from './sink-registry'; // Monotonic epoch bumped on every terminal blank (signed-out or privacy). The // publisher captures it at construction and refuses to emit once it advances, @@ -68,7 +68,7 @@ function buildTerminalSnapshot(status: 'signed_out' | 'privacy'): GlanceableAgen status, running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }; } @@ -79,15 +79,14 @@ function writeTerminalAndEnd(status: 'signed_out' | 'privacy'): void { terminalBlankEpoch += 1; getGlanceableDelivery().cleanupTokens('scope'); const snapshot = buildTerminalSnapshot(status); - const sinks = getGlanceableSinks(); // Write the snapshot first, then end: the surface shows the terminal copy // before the native activity ends. - for (const sink of sinks) { + forEachSink('terminal_publish', sink => { sink.publish(snapshot); - } - for (const sink of sinks) { + }); + forEachSink('terminal_end', sink => { sink.endImmediate(); - } + }); } /** Blank on logout or direct account switch. */ @@ -115,7 +114,7 @@ export function republishLastSnapshotStale(): void { return; } const snapshot = withStatus(previous, 'stale', Date.now()); - for (const sink of getGlanceableSinks()) { + forEachSink('stale_publish', sink => { sink.publish(snapshot); - } + }); } diff --git a/apps/mobile/src/lib/glanceable/presentation.test.ts b/apps/mobile/src/lib/glanceable/presentation.test.ts index 1ce873734d..28436a5ce8 100644 --- a/apps/mobile/src/lib/glanceable/presentation.test.ts +++ b/apps/mobile/src/lib/glanceable/presentation.test.ts @@ -51,7 +51,7 @@ describe('presentation precedence', () => { }); describe('primary rank and locked copy keys', () => { - it('ranks needs-input, then reconnecting, then running', () => { + it('ranks needs-input, then running, then idle', () => { const mixed = snapshot({ sessions: [ { status: 'busy' }, @@ -61,13 +61,33 @@ describe('primary rank and locked copy keys', () => { { status: 'question' }, ], }); - expect(primaryGlanceableCount(mixed)).toEqual({ key: 'glanceable.needsInput', count: 1 }); + // `retry` folds into needs-input, so the question plus the retry make 2. + expect(primaryGlanceableCount(mixed)).toEqual({ + key: 'glanceable.needsInput', + kind: 'needsInput', + count: 2, + }); - const noInput = snapshot({ sessions: [{ status: 'busy' }, { status: 'retry' }] }); - expect(primaryGlanceableCount(noInput)).toEqual({ key: 'glanceable.reconnecting', count: 1 }); + const noInput = snapshot({ sessions: [{ status: 'busy' }, { status: 'idle' }] }); + expect(primaryGlanceableCount(noInput)).toEqual({ + key: 'glanceable.running', + kind: 'running', + count: 1, + }); + + const onlyIdle = snapshot({ sessions: [{ status: 'idle' }] }); + expect(primaryGlanceableCount(onlyIdle)).toEqual({ + key: 'glanceable.idle', + kind: 'idle', + count: 1, + }); const onlyRunning = snapshot({ sessions: [{ status: 'busy' }, { status: 'busy' }] }); - expect(primaryGlanceableCount(onlyRunning)).toEqual({ key: 'glanceable.running', count: 2 }); + expect(primaryGlanceableCount(onlyRunning)).toEqual({ + key: 'glanceable.running', + kind: 'running', + count: 2, + }); expect(primaryGlanceableCount(snapshot({}))).toBeNull(); }); @@ -143,8 +163,8 @@ describe('numeric spoken label', () => { describe('numeric spoken label', () => { const copy: Record = { 'glanceable.needsInput': 'Needs input', - 'glanceable.reconnecting': 'Reconnecting', - 'glanceable.running': 'Running', + 'glanceable.idle': 'Idle', + 'glanceable.running': 'Working', 'glanceable.waiting': 'Waiting for agents', 'glanceable.empty': 'No work in progress', 'glanceable.stale': 'Updates delayed', @@ -157,19 +177,19 @@ describe('numeric spoken label', () => { const mixed = { ...snapshot({ status: 'happy' }), needsInput: 2, - reconnecting: 3, + idle: 3, running: 4, }; it('speaks each numeric count in rank order before Open agents', () => { expect(glanceableSpokenLabel(mixed, {}, translate)).toBe( - '2 Needs input, 3 Reconnecting, 4 Running, Open agents' + '2 Needs input, 4 Working, 3 Idle, Open agents' ); }); it('speaks the translated stale warning before retained numeric counts', () => { expect(glanceableSpokenLabel({ ...mixed, status: 'stale' }, {}, translate)).toBe( - 'Updates delayed, 2 Needs input, 3 Reconnecting, 4 Running, Open agents' + 'Updates delayed, 2 Needs input, 4 Working, 3 Idle, Open agents' ); }); diff --git a/apps/mobile/src/lib/glanceable/presentation.ts b/apps/mobile/src/lib/glanceable/presentation.ts index 4d533ad2b6..91cf3198e6 100644 --- a/apps/mobile/src/lib/glanceable/presentation.ts +++ b/apps/mobile/src/lib/glanceable/presentation.ts @@ -18,30 +18,35 @@ export const GLANCEABLE_STATUS_COPY_KEY = { privacy: 'glanceable.privacy', } as const satisfies Record, string>; -export type GlanceableCountKey = - | 'glanceable.running' - | 'glanceable.needsInput' - | 'glanceable.reconnecting'; +export type GlanceableCountKey = 'glanceable.running' | 'glanceable.needsInput' | 'glanceable.idle'; -export type GlanceableCountLine = { key: GlanceableCountKey; count: number }; +/** The state a count line stands for. Surfaces map it to a glyph and a color. */ +export type GlanceableCountKind = 'needsInput' | 'running' | 'idle'; -/** Rank order: needs-input, then reconnecting, then running. */ -const COUNT_ORDER: readonly { +export type GlanceableCountLine = { key: GlanceableCountKey; - field: 'running' | 'needsInput' | 'reconnecting'; -}[] = [ - { key: 'glanceable.needsInput', field: 'needsInput' }, - { key: 'glanceable.reconnecting', field: 'reconnecting' }, - { key: 'glanceable.running', field: 'running' }, + kind: GlanceableCountKind; + count: number; +}; + +/** + * Rank order: what the user must act on, then what is making progress, then + * what is only connected. Compact surfaces show the first line only, so this + * ranking decides what a glance says. + */ +const COUNT_ORDER: readonly { key: GlanceableCountKey; kind: GlanceableCountKind }[] = [ + { key: 'glanceable.needsInput', kind: 'needsInput' }, + { key: 'glanceable.running', kind: 'running' }, + { key: 'glanceable.idle', kind: 'idle' }, ]; /** Every non-zero count in rank order (expanded, medium, large, spoken). */ export function glanceableCountLines(snapshot: GlanceableAgentsSnapshot): GlanceableCountLine[] { const lines: GlanceableCountLine[] = []; - for (const { key, field } of COUNT_ORDER) { - const count = snapshot[field]; + for (const { key, kind } of COUNT_ORDER) { + const count = snapshot[kind]; if (count > 0) { - lines.push({ key, count }); + lines.push({ key, kind, count }); } } return lines; diff --git a/apps/mobile/src/lib/glanceable/publisher.test.ts b/apps/mobile/src/lib/glanceable/publisher.test.ts index 6615ea8479..5dcc0f34fa 100644 --- a/apps/mobile/src/lib/glanceable/publisher.test.ts +++ b/apps/mobile/src/lib/glanceable/publisher.test.ts @@ -84,8 +84,9 @@ describe('GlanceablePublisher', () => { ); const snapshot = lastSnapshot(calls, 'startOrUpdate'); expect(snapshot.running).toBe(2); - expect(snapshot.needsInput).toBe(1); - expect(snapshot.reconnecting).toBe(1); + // `retry` folds into needs-input: both mean the agent cannot go on alone. + expect(snapshot.needsInput).toBe(2); + expect(snapshot.idle).toBe(1); expect(snapshot.status).toBe('happy'); }); @@ -121,15 +122,18 @@ describe('GlanceablePublisher', () => { expect(count(calls, 'startOrUpdate')).toBe(started); }); - it('publishes empty for idle-only sessions without starting or ending', () => { + it('starts for idle-only sessions but not when no session is connected', () => { vi.useFakeTimers(); const { sink, calls } = makeSink(); const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); - publisher.handleSessions([{ status: 'idle' }, { status: 'idle' }], PUB_CTX); + publisher.handleSessions([], PUB_CTX); expect(count(calls, 'startOrUpdate')).toBe(0); expect(lastSnapshot(calls, 'publish').status).toBe('empty'); vi.advanceTimersByTime(8000); expect(count(calls, 'endImmediate')).toBe(0); + // An idle agent is still connected, so the notch shows it ranked last. + publisher.handleSessions([{ status: 'idle' }, { status: 'idle' }], PUB_CTX); + expect(lastSnapshot(calls, 'startOrUpdate')).toMatchObject({ status: 'happy', idle: 2 }); publisher.dispose(); }); @@ -139,7 +143,7 @@ describe('GlanceablePublisher', () => { publisher.handleFetchStarted(PUB_CTX); expect(lastSnapshot(calls, 'publish').status).toBe('waiting'); expect(count(calls, 'startOrUpdate')).toBe(0); - publisher.handleSessions([{ status: 'idle' }], PUB_CTX); + publisher.handleSessions([], PUB_CTX); expect(lastSnapshot(calls, 'publish').status).toBe('empty'); }); @@ -148,7 +152,7 @@ describe('GlanceablePublisher', () => { const { sink, calls } = makeSink(); const publisher = new GlanceablePublisher({ sinks: [sink], now: () => now }); publisher.handleSessions( - [{ status: 'busy' }, { status: 'question' }, { status: 'retry' }], + [{ status: 'busy' }, { status: 'question' }, { status: 'idle' }], PUB_CTX ); const successful = lastSnapshot(calls, 'publish'); @@ -169,7 +173,7 @@ describe('GlanceablePublisher', () => { status, running: expectedCount, needsInput: expectedCount, - reconnecting: expectedCount, + idle: expectedCount, }); } expect(lastSnapshot(calls, 'publish').eligibleStartedAt).toBeNull(); diff --git a/apps/mobile/src/lib/glanceable/publisher.ts b/apps/mobile/src/lib/glanceable/publisher.ts index f23088b0c9..6b2e136cb1 100644 --- a/apps/mobile/src/lib/glanceable/publisher.ts +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -13,6 +13,7 @@ import { getGlanceableDelivery, type GlanceableSink, type GlanceableSinkContext, + guardSink, } from './sink-registry'; /** @@ -59,7 +60,7 @@ export function withStatus( ...snapshot, revision: snapshot.revision + 1, status: expired ? 'expired' : 'stale', - ...(expired ? { running: 0, needsInput: 0, reconnecting: 0, eligibleStartedAt: null } : {}), + ...(expired ? { running: 0, needsInput: 0, idle: 0, eligibleStartedAt: null } : {}), }; } const updatedAt = new Date(now).toISOString(); @@ -217,14 +218,22 @@ export class GlanceablePublisher { private emit(snapshot: GlanceableAgentsSnapshot, ctx: GlanceableSinkContext): void { for (const sink of this.sinks) { - sink.publish(snapshot); - sink.startOrUpdate(snapshot, ctx); + // Guarded separately: a failing widget timeline write must not skip the + // Live Activity start that follows it. + guardSink('emit_publish', () => { + sink.publish(snapshot); + }); + guardSink('emit_start_or_update', () => { + sink.startOrUpdate(snapshot, ctx); + }); } } private publish(snapshot: GlanceableAgentsSnapshot): void { for (const sink of this.sinks) { - sink.publish(snapshot); + guardSink('publish', () => { + sink.publish(snapshot); + }); } } @@ -251,9 +260,11 @@ export class GlanceablePublisher { this.terminalTimer = null; this.activityStarted = false; for (const sink of this.sinks) { - if (!sink.waitForNativeTerminal) { - sink.endImmediate(); - } + guardSink('terminal_end', () => { + if (!sink.waitForNativeTerminal) { + sink.endImmediate(); + } + }); } }, this.terminalMs); } diff --git a/apps/mobile/src/lib/glanceable/sink-registry.ts b/apps/mobile/src/lib/glanceable/sink-registry.ts index 9b46bd4616..5b9c15dda6 100644 --- a/apps/mobile/src/lib/glanceable/sink-registry.ts +++ b/apps/mobile/src/lib/glanceable/sink-registry.ts @@ -1,3 +1,4 @@ +import type * as SentryReactNative from '@sentry/react-native'; import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { type LiveActivity } from 'expo-widgets'; @@ -37,6 +38,46 @@ export function getGlanceableSinks(): readonly GlanceableSink[] { return [...sinks]; } +function reportSinkFailure(operation: string, error: unknown): void { + try { + // Lazy require keeps @sentry/react-native out of the pure test graph, the + // same reason the Android permission reader defers its import. + // eslint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires, unicorn/prefer-module -- lazy native load + const Sentry = require('@sentry/react-native') as typeof SentryReactNative; + Sentry.captureException(error, { + tags: { 'error.subsystem': 'glanceable', 'error.operation': operation }, + }); + } catch { + // Reporting is best effort; a missing reporter must not mask the guard. + } +} + +/** + * Run one sink operation and swallow its failure. A native surface must never + * throw into the auth transition, the org switch, or the in-app publisher: a + * throwing WidgetKit or ActivityKit host function there would abort a sign-in + * or kill the publisher effect. The background push path deliberately does NOT + * use this — a native failure must reject so the OS retries the push. + */ +export function guardSink(operation: string, run: () => void): void { + try { + run(); + } catch (error) { + reportSinkFailure(operation, error); + } +} + +/** `guardSink` for every registered sink. One sink's failure never skips the rest. */ +export function forEachSink(operation: string, run: (sink: GlanceableSink) => void): void { + // Snapshot the list: a sink may register or unregister from inside `run`. + const registered = [...sinks]; + for (const sink of registered) { + guardSink(operation, () => { + run(sink); + }); + } +} + /** * Activity-token registrar, set by a later token slice. No-op by default. * `unregisterTokens` reports only the tokens whose unregister failed, so diff --git a/apps/mobile/src/lib/notification-path.test.ts b/apps/mobile/src/lib/notification-path.test.ts index 234ca30c42..421b0f0377 100644 --- a/apps/mobile/src/lib/notification-path.test.ts +++ b/apps/mobile/src/lib/notification-path.test.ts @@ -66,7 +66,7 @@ describe('notificationPathForData', () => { status: 'happy', running: 1, needsInput: 0, - reconnecting: 0, + idle: 0, updatedAt: '2026-01-01T00:00:00.000Z', expiresAt: '2026-01-01T08:00:00.000Z', eligibleStartedAt: '2026-01-01T00:00:00.000Z', diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index afc580dd04..661cc8c117 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -431,7 +431,7 @@ function glanceableSnapshot( status: 'happy', running: 1, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: '2026-01-01T00:00:00.000Z', ...overrides, }; @@ -620,7 +620,7 @@ describe('applyGlanceablePushData', () => { status: 'empty', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }) ); @@ -642,7 +642,7 @@ describe('applyGlanceablePushData', () => { status: 'empty', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }) ); @@ -672,7 +672,7 @@ describe('applyGlanceablePushData', () => { status: 'empty', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }) ); @@ -1402,7 +1402,7 @@ describe('cold iOS background delivery', () => { status: 'empty', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }); expect(rows.has('scope-token')).toBe(true); diff --git a/packages/app-shared/src/glanceable-agents-snapshot.test.ts b/packages/app-shared/src/glanceable-agents-snapshot.test.ts index c59de79ca7..5571e549ce 100644 --- a/packages/app-shared/src/glanceable-agents-snapshot.test.ts +++ b/packages/app-shared/src/glanceable-agents-snapshot.test.ts @@ -12,7 +12,7 @@ import { const NOW = 1_750_000_000_000; describe('countGlanceableSessions', () => { - it('maps busy/question/permission/retry and ignores idle and unknown', () => { + it('maps busy to running, question/permission/retry to needs-input, idle to idle', () => { const counts = countGlanceableSessions([ { status: 'busy' }, { status: 'busy' }, @@ -26,7 +26,7 @@ describe('countGlanceableSessions', () => { { status: 'failed' }, { status: 'mystery' }, ]); - expect(counts).toEqual({ running: 2, needsInput: 3, reconnecting: 1 }); + expect(counts).toEqual({ running: 2, needsInput: 4, idle: 2 }); }); it('counts Cloud Agent-shaped and CLI-shaped rows together on status alone', () => { @@ -34,18 +34,24 @@ describe('countGlanceableSessions', () => { const cliRow = { status: 'retry', connectionId: 'cli-1' }; expect(countGlanceableSessions([cloudRow, cliRow])).toEqual({ running: 1, - needsInput: 0, - reconnecting: 1, + needsInput: 1, + idle: 0, }); }); - it('produces zero eligible counts for idle-only sessions', () => { + it('counts idle-only sessions as idle', () => { expect(countGlanceableSessions([{ status: 'idle' }, { status: 'idle' }])).toEqual({ running: 0, needsInput: 0, - reconnecting: 0, + idle: 2, }); }); + + it('ignores a completed or unknown status entirely', () => { + expect( + countGlanceableSessions([{ status: 'completed' }, { status: 'failed' }, { status: 'nope' }]) + ).toEqual({ running: 0, needsInput: 0, idle: 0 }); + }); }); describe('buildOpaqueScopeKey', () => { @@ -102,9 +108,9 @@ describe('buildGlanceableSnapshot', () => { expect(second.needsInput).toBe(1); }); - it('clears eligibleStartedAt when no eligible work remains', () => { + it('clears eligibleStartedAt when no session is connected', () => { const snapshot = buildGlanceableSnapshot({ - sessions: [{ status: 'idle' }], + sessions: [{ status: 'completed' }], userId: 'u1', organizationId: null, now: NOW, diff --git a/packages/app-shared/src/glanceable-agents-snapshot.ts b/packages/app-shared/src/glanceable-agents-snapshot.ts index e7498e80f1..7f31e36dce 100644 --- a/packages/app-shared/src/glanceable-agents-snapshot.ts +++ b/packages/app-shared/src/glanceable-agents-snapshot.ts @@ -46,9 +46,12 @@ export const glanceableAgentsSnapshotSchema = z.object({ accountEpoch: z.number().int().optional(), organizationBound: z.boolean(), status: z.enum(['waiting', 'empty', 'happy', 'stale', 'expired', 'signed_out', 'privacy']), + /** Sessions actively doing something. */ running: z.number().int().min(0), + /** Sessions waiting on the user, including one whose CLI dropped mid-question. */ needsInput: z.number().int().min(0), - reconnecting: z.number().int().min(0), + /** Sessions connected but doing nothing. */ + idle: z.number().int().min(0), /** ISO 8601 timestamp or null; binds the elapsed-time display. */ eligibleStartedAt: z.string().nullable(), }); @@ -58,18 +61,23 @@ export type GlanceableAgentsSnapshot = z.infer 0; + // Idle counts: a connected agent doing nothing is still something the user + // wants on the Lock Screen, and the Dynamic Island ranks it last. + const eligible = counts.running + counts.needsInput + counts.idle > 0; const now = input.now; const updatedAt = new Date(now).toISOString(); const eligibleStartedAt = eligible ? (input.previousEligibleStartedAt ?? updatedAt) : null; @@ -158,14 +169,14 @@ export function buildGlanceableSnapshot( status: input.status ?? (eligible ? 'happy' : 'empty'), running: counts.running, needsInput: counts.needsInput, - reconnecting: counts.reconnecting, + idle: counts.idle, eligibleStartedAt, }; } -/** True when any eligible count is non-zero. */ +/** True when any agent is connected, whether working, waiting, or idle. */ export function isEligibleGlanceableWork(snapshot: GlanceableAgentsSnapshot): boolean { - return snapshot.running + snapshot.needsInput + snapshot.reconnecting > 0; + return snapshot.running + snapshot.needsInput + snapshot.idle > 0; } /** diff --git a/packages/notifications/src/push-data.ts b/packages/notifications/src/push-data.ts index 26957bec2e..a170dd2e37 100644 --- a/packages/notifications/src/push-data.ts +++ b/packages/notifications/src/push-data.ts @@ -91,7 +91,7 @@ export const pushDataSchema = z.discriminatedUnion('type', [ status: z.enum(['waiting', 'empty', 'happy', 'stale', 'expired', 'signed_out', 'privacy']), running: z.number().int().min(0), needsInput: z.number().int().min(0), - reconnecting: z.number().int().min(0), + idle: z.number().int().min(0), updatedAt: z.string(), expiresAt: z.string(), eligibleStartedAt: z.string().nullable(), @@ -109,5 +109,5 @@ export type PushData = z.infer; */ export type GlanceableLiveActivityContentState = Pick< Extract, - 'status' | 'running' | 'needsInput' | 'reconnecting' | 'eligibleStartedAt' + 'status' | 'running' | 'needsInput' | 'idle' | 'eligibleStartedAt' >; diff --git a/packages/notifications/src/push-presentation.test.ts b/packages/notifications/src/push-presentation.test.ts index 9de3852551..6b126e611b 100644 --- a/packages/notifications/src/push-presentation.test.ts +++ b/packages/notifications/src/push-presentation.test.ts @@ -28,7 +28,7 @@ const variants = [ status: 'happy', running: 1, needsInput: 0, - reconnecting: 0, + idle: 0, updatedAt: '2026-01-01T00:00:00.000Z', expiresAt: '2026-01-01T08:00:00.000Z', eligibleStartedAt: '2026-01-01T00:00:00.000Z', diff --git a/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts b/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts index 48bc6cce3e..2d6f9f30c3 100644 --- a/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts +++ b/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts @@ -236,7 +236,7 @@ describe('committed cloud eligibility refresh', () => { { status: 'happy', running: 1, - reconnecting: 0, + idle: 0, organizationBound: organizationId !== null, }, ]); @@ -271,9 +271,9 @@ describe('committed cloud eligibility refresh', () => { { status: 'happy', running: status === 'busy' ? 1 : 0, - reconnecting: status === 'retry' ? 1 : 0, + needsInput: status === 'retry' ? 1 : 0, }, - { status: 'empty', running: 0, needsInput: 0, reconnecting: 0, eligibleStartedAt: null }, + { status: 'empty', running: 0, needsInput: 0, idle: 0, eligibleStartedAt: null }, ]); } ); @@ -290,7 +290,7 @@ describe('committed cloud eligibility refresh', () => { await fixture.consume(); expect(fixture.messages.map(message => message.data)).toMatchObject([ { running: 1, eligibleStartedAt: occurredAt }, - { running: 0, reconnecting: 1, eligibleStartedAt: occurredAt }, + { running: 0, needsInput: 1, eligibleStartedAt: occurredAt }, ]); }); @@ -463,6 +463,8 @@ describe('committed cloud eligibility refresh', () => { message.data?.type === 'active_agents_glanceable' && !message.data.organizationBound ) .map(message => message.data) - ).toMatchObject([{ running: 1, needsInput: 0, reconnecting: 0 }]); + // The busy CLI row counts as running and the warm-idle cloud row as idle: + // the counts read `status` alone, so cloud and CLI sessions merge. + ).toMatchObject([{ running: 1, needsInput: 0, idle: 1 }]); }); }); diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts index d2034b5269..ae6a997ab2 100644 --- a/services/notifications/src/lib/glanceable-delivery.test.ts +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -37,7 +37,7 @@ const snapshot: ActiveAgentsGlanceable = { status: 'happy', running: 2, needsInput: 1, - reconnecting: 0, + idle: 0, updatedAt: '2026-08-27T10:00:00.000Z', expiresAt: '2026-08-27T18:00:00.000Z', eligibleStartedAt: '2026-08-27T09:00:00.000Z', @@ -401,7 +401,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); await service.refreshGlanceableSessions(personalRefresh); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); - current = freshSnapshot({ running: 0, reconnecting: 1 }); + current = freshSnapshot({ running: 0, idle: 1 }); await createService().refreshGlanceableSessions(personalRefresh); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:20:00.000Z')); current = freshSnapshot({ running: 0, needsInput: 1 }); @@ -412,7 +412,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { .map(message => message.data) ).toMatchObject([ { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 1 }, - { reconnecting: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 2 }, + { idle: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 2 }, { needsInput: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 3 }, ]); }); @@ -445,7 +445,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { release.resolve(); await oldIdle; vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:20:00.000Z')); - current = freshSnapshot({ running: 0, reconnecting: 1 }); + current = freshSnapshot({ running: 0, idle: 1 }); await createService().refreshGlanceableSessions(personalRefresh); expect( messages @@ -455,7 +455,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { { status: 'happy', eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, { status: 'empty', eligibleStartedAt: null }, { status: 'happy', eligibleStartedAt: '2026-08-27T10:10:00.000Z' }, - { reconnecting: 1, eligibleStartedAt: '2026-08-27T10:10:00.000Z' }, + { idle: 1, eligibleStartedAt: '2026-08-27T10:10:00.000Z' }, ]); }); @@ -515,7 +515,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { await createService().refreshGlanceableSessions(personalRefresh); unavailable = false; vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); - current = freshSnapshot({ running: 0, reconnecting: 1 }); + current = freshSnapshot({ running: 0, idle: 1 }); vi.mocked(sendPushNotifications).mockRejectedValueOnce(new Error('Expo unavailable')); await createService().refreshGlanceableSessions(personalRefresh); await createService().refreshGlanceableSessions(personalRefresh); @@ -525,7 +525,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { .map(message => message.data) ).toMatchObject([ { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, - { reconnecting: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { idle: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, ]); }); @@ -536,7 +536,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { current = freshSnapshot({ status: 'stale', running: 0, eligibleStartedAt: null }); await createService().refreshGlanceableSessions(personalRefresh); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); - current = freshSnapshot({ running: 0, reconnecting: 1 }); + current = freshSnapshot({ running: 0, idle: 1 }); await createService().refreshGlanceableSessions(personalRefresh); expect( messages @@ -544,7 +544,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { .map(message => message.data) ).toMatchObject([ { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, - { reconnecting: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { idle: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, ]); }); @@ -581,19 +581,19 @@ describe('NotificationsService.refreshGlanceableSessions', () => { status: 'empty', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); - current = freshSnapshot({ running: 0, reconnecting: 1 }); + current = freshSnapshot({ running: 0, idle: 1 }); await createService().refreshGlanceableSessions(personalRefresh); expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ ['old-activity', 'end'], ['scope-token', 'start'], ]); expect(JSON.parse(apns[1].aps['content-state'].props)).toMatchObject({ - reconnecting: 1, + idle: 1, eligibleStartedAt: '2026-08-27T10:00:01.000Z', }); expect([...activityRows.keys()]).toEqual(['scope-token']); @@ -739,13 +739,13 @@ describe('NotificationsService.refreshGlanceableSessions', () => { } expect(activityRows.get('old-activity')).toEqual(renewedRow); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:02.000Z')); - current = freshSnapshot({ running: 0, reconnecting: 1 }); + current = freshSnapshot({ running: 0, idle: 1 }); await createService().refreshGlanceableSessions(personalRefresh); expect(liveActivityProps()).toMatchObject([ { running: 0, needsInput: 0, - reconnecting: 1, + idle: 1, eligibleStartedAt: '2026-08-27T10:00:01.000Z', }, ]); @@ -800,7 +800,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { status: 'happy', running: 0, needsInput: 1, - reconnecting: 0, + idle: 0, eligibleStartedAt: '2026-08-27T10:00:01.000Z', }, ]); @@ -942,10 +942,10 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); } vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:02.000Z')); - current = freshSnapshot({ running: 0, reconnecting: 1 }); + current = freshSnapshot({ running: 0, idle: 1 }); await createService().refreshGlanceableSessions(personalRefresh); expect(liveActivityProps()).toMatchObject([ - { running: 0, needsInput: 0, reconnecting: 1, eligibleStartedAt: '2026-08-27T10:00:01.000Z' }, + { running: 0, needsInput: 0, idle: 1, eligibleStartedAt: '2026-08-27T10:00:01.000Z' }, ]); expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ ['old-activity', 'end'], @@ -1088,9 +1088,9 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); } } - current = freshSnapshot({ running: 0, reconnecting: 1 }); + current = freshSnapshot({ running: 0, idle: 1 }); await createService().refreshGlanceableSessions(personalRefresh); - expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 0, reconnecting: 1 }]); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 0, idle: 1 }]); expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ ['old-activity', 'end'], ['old-activity', 'end'], @@ -1344,7 +1344,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { status: 'empty', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, eligibleStartedAt: null, }, }, @@ -1632,7 +1632,7 @@ describe('toGlanceableContentState', () => { status: 'happy', running: 2, needsInput: 1, - reconnecting: 0, + idle: 0, eligibleStartedAt: '2026-08-27T09:00:00.000Z', }); }); @@ -1718,7 +1718,7 @@ describe('deliverGlanceableSnapshot', () => { expect(props.status).toBe('happy'); expect(props.running).toBe(2); expect(props.needsInput).toBe(1); - expect(props.reconnecting).toBe(0); + expect(props.idle).toBe(0); expect(props).not.toHaveProperty('type'); expect(props).not.toHaveProperty('accountEpoch'); expect(props).not.toHaveProperty('scopeKey'); diff --git a/services/notifications/src/lib/glanceable-delivery.ts b/services/notifications/src/lib/glanceable-delivery.ts index 280a256880..fc5bf1825b 100644 --- a/services/notifications/src/lib/glanceable-delivery.ts +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -57,7 +57,7 @@ export function toGlanceableContentState( status: snapshot.status, running: snapshot.running, needsInput: snapshot.needsInput, - reconnecting: snapshot.reconnecting, + idle: snapshot.idle, eligibleStartedAt: snapshot.eligibleStartedAt, }; return { @@ -144,7 +144,7 @@ export async function deliverGlanceableSnapshot( const iosTokens = await deps.listIosActivityTokens(params.userId, params.organizationId); if (deps.isCurrent && !(await deps.isCurrent())) return; - const eligible = snapshot.running + snapshot.needsInput + snapshot.reconnecting > 0; + const eligible = snapshot.running + snapshot.needsInput + snapshot.idle > 0; const iosSends = apnsSendsForTokens(iosTokens, eligible); if (iosSends.length > 0) { await deps.sendIosLiveActivity( diff --git a/services/notifications/src/lib/glanceable-refresh.ts b/services/notifications/src/lib/glanceable-refresh.ts index 20bf0a1ebc..e688b9f372 100644 --- a/services/notifications/src/lib/glanceable-refresh.ts +++ b/services/notifications/src/lib/glanceable-refresh.ts @@ -56,7 +56,7 @@ export async function refreshGlanceableSnapshot( const current = refreshStateSchema.parse(await tx.get(key)); if (current.revision !== request.revision) return null; const eligibleStartedAt = - snapshot.running + snapshot.needsInput + snapshot.reconnecting > 0 + snapshot.running + snapshot.needsInput + snapshot.idle > 0 ? (current.eligibleStartedAt ?? snapshot.eligibleStartedAt ?? request.updatedAt) : null; await tx.put(key, { ...current, eligibleStartedAt }); @@ -74,7 +74,7 @@ export async function refreshGlanceableSnapshot( }); if (committed === null) return; - const eligible = committed.running + committed.needsInput + committed.reconnecting > 0; + const eligible = committed.running + committed.needsInput + committed.idle > 0; await deliverGlanceableSnapshot(scope, { ...deps, buildSnapshot: async () => committed, From e9a014f0194ae6e396f0029893279e9c4855cb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 2 Sep 2026 22:17:26 +0200 Subject: [PATCH 30/43] feat(mobile): finish the glanceable surfaces Three pieces of the Active Agents work, all in the same files. Drop the in-app master switch. The surfaces are the feature, so a preference that turned them off only added a state to test and a row to explain. That removes `glanceable/enabled.ts`, the preference hook, its storage key, the preferences row, and the `settings.activeAgentsSubtitle` key in all 87 catalogs. Carry the needs-input wait to the surfaces. `statusUpdatedAt` reached the client as raw Postgres text (`2026-09-02 17:28:02.242039+00`), which Hermes' `Date.parse` rejects, so the oldest wait was always dropped as unparseable. `active-sessions-list.ts` now normalizes it to ISO 8601 in both mappers. Draw a zero count instead of hiding its row. Dropping a zero row moved every row below it as work changed state, and a surface the user only glances at must not reflow. The spoken label and the Android notification line still skip the zeros, where "0 Working" is noise rather than layout. Localize the surfaces. The widget families already drew translated copy from their timeline props, but the Live Activity could not: the notifications Worker pushes the same raw content state and knows no locale. Its copy is now baked into the stringified layout at registration, the boundary that already bakes the app-group path of the mark, and the app re-bakes it on every language change. The widget extension also declared no localizations, so iOS treated it as English-only and laid every surface out left-to-right on an Arabic or Hebrew device; `withWidgetLocalizations` declares the app's language list on the extension. Three layout bugs the long translations exposed: - A long label wrapped to two lines and broke the row grid. The label now shrinks and tightens instead, and truncates only past 60% scale. - `layoutPriority` on the label starved the count, so the small widget drew a glyph and a word with no number. The count takes its space first. - `truncationMode` suppresses `minimumScaleFactor`, which is why the label truncated rather than shrinking. Removed; tail is the default. The widget process also formats dates in the device language, so a user who overrode the app language read translated labels beside a wait formatted in another language. The layouts now pin SwiftUI's `locale` environment value. --- apps/mobile/app.config.ts | 12 +- apps/mobile/knip.json | 1 + apps/mobile/package.json | 1 + .../mobile/plugins/withWidgetLocalizations.js | 43 +++ .../src/app/(app)/(tabs)/(2_agents)/index.tsx | 2 +- .../app-unlock-screen.test-helpers.tsx | 7 - .../src/components/preferences-screen.tsx | 15 - .../active-agents-widget.test.ts | 5 +- .../src/glanceable-android/register.test.ts | 11 +- .../glanceable-android/widget-props.test.ts | 7 +- .../src/glanceable-android/widget-props.ts | 6 +- .../active-agents-live-activity.tsx | 261 ++++++++++-------- .../glanceable-ios/active-agents-widget.tsx | 180 ++++++++---- .../glanceable-ios/ios-sink.native.test.ts | 8 +- .../src/glanceable-ios/ios-sink.test.ts | 73 +++-- apps/mobile/src/glanceable-ios/ios-sink.ts | 2 +- .../src/glanceable-ios/layout-copy.test.ts | 68 +++++ apps/mobile/src/glanceable-ios/layout-copy.ts | 73 +++++ apps/mobile/src/glanceable-ios/register.ts | 12 + apps/mobile/src/glanceable-ios/view-props.ts | 25 +- apps/mobile/src/i18n/locales/af.json | 1 - apps/mobile/src/i18n/locales/am.json | 1 - apps/mobile/src/i18n/locales/ar.json | 1 - apps/mobile/src/i18n/locales/az.json | 1 - apps/mobile/src/i18n/locales/be.json | 1 - apps/mobile/src/i18n/locales/bg.json | 1 - apps/mobile/src/i18n/locales/bn.json | 1 - apps/mobile/src/i18n/locales/bs.json | 1 - apps/mobile/src/i18n/locales/ca.json | 1 - apps/mobile/src/i18n/locales/ckb.json | 1 - apps/mobile/src/i18n/locales/cs.json | 1 - apps/mobile/src/i18n/locales/cy.json | 1 - apps/mobile/src/i18n/locales/da.json | 1 - apps/mobile/src/i18n/locales/de.json | 1 - apps/mobile/src/i18n/locales/el.json | 1 - apps/mobile/src/i18n/locales/en.json | 1 - apps/mobile/src/i18n/locales/es.json | 1 - apps/mobile/src/i18n/locales/et.json | 1 - apps/mobile/src/i18n/locales/eu.json | 1 - apps/mobile/src/i18n/locales/fa.json | 1 - apps/mobile/src/i18n/locales/fi.json | 1 - apps/mobile/src/i18n/locales/fil.json | 1 - apps/mobile/src/i18n/locales/fr.json | 1 - apps/mobile/src/i18n/locales/ga.json | 1 - apps/mobile/src/i18n/locales/gl.json | 1 - apps/mobile/src/i18n/locales/gu.json | 1 - apps/mobile/src/i18n/locales/ha.json | 1 - apps/mobile/src/i18n/locales/he.json | 1 - apps/mobile/src/i18n/locales/hi.json | 1 - apps/mobile/src/i18n/locales/hr.json | 1 - apps/mobile/src/i18n/locales/ht.json | 1 - apps/mobile/src/i18n/locales/hu.json | 1 - apps/mobile/src/i18n/locales/hy.json | 1 - apps/mobile/src/i18n/locales/id.json | 1 - apps/mobile/src/i18n/locales/ig.json | 1 - apps/mobile/src/i18n/locales/is.json | 1 - apps/mobile/src/i18n/locales/it.json | 1 - apps/mobile/src/i18n/locales/ja.json | 1 - apps/mobile/src/i18n/locales/ka.json | 1 - apps/mobile/src/i18n/locales/kk.json | 1 - apps/mobile/src/i18n/locales/km.json | 1 - apps/mobile/src/i18n/locales/kn.json | 1 - apps/mobile/src/i18n/locales/ko.json | 1 - apps/mobile/src/i18n/locales/lo.json | 1 - apps/mobile/src/i18n/locales/lt.json | 1 - apps/mobile/src/i18n/locales/lv.json | 1 - apps/mobile/src/i18n/locales/mg.json | 1 - apps/mobile/src/i18n/locales/mi.json | 1 - apps/mobile/src/i18n/locales/mk.json | 1 - apps/mobile/src/i18n/locales/ml.json | 1 - apps/mobile/src/i18n/locales/mn.json | 1 - apps/mobile/src/i18n/locales/mr.json | 1 - apps/mobile/src/i18n/locales/ms.json | 1 - apps/mobile/src/i18n/locales/mt.json | 1 - apps/mobile/src/i18n/locales/my.json | 1 - apps/mobile/src/i18n/locales/nb.json | 1 - apps/mobile/src/i18n/locales/ne.json | 1 - apps/mobile/src/i18n/locales/nl.json | 1 - apps/mobile/src/i18n/locales/om.json | 1 - apps/mobile/src/i18n/locales/or.json | 1 - apps/mobile/src/i18n/locales/pa.json | 1 - apps/mobile/src/i18n/locales/pl.json | 1 - apps/mobile/src/i18n/locales/ps.json | 1 - apps/mobile/src/i18n/locales/pt-BR.json | 1 - apps/mobile/src/i18n/locales/pt.json | 1 - apps/mobile/src/i18n/locales/ro.json | 1 - apps/mobile/src/i18n/locales/ru.json | 1 - apps/mobile/src/i18n/locales/si.json | 1 - apps/mobile/src/i18n/locales/sk.json | 1 - apps/mobile/src/i18n/locales/sl.json | 1 - apps/mobile/src/i18n/locales/so.json | 1 - apps/mobile/src/i18n/locales/sq.json | 1 - apps/mobile/src/i18n/locales/sr.json | 1 - apps/mobile/src/i18n/locales/sv.json | 1 - apps/mobile/src/i18n/locales/sw.json | 1 - apps/mobile/src/i18n/locales/ta.json | 1 - apps/mobile/src/i18n/locales/te.json | 1 - apps/mobile/src/i18n/locales/th.json | 1 - apps/mobile/src/i18n/locales/tr.json | 1 - apps/mobile/src/i18n/locales/uk.json | 1 - apps/mobile/src/i18n/locales/ur.json | 1 - apps/mobile/src/i18n/locales/uz.json | 1 - apps/mobile/src/i18n/locales/vi.json | 1 - apps/mobile/src/i18n/locales/yo.json | 1 - apps/mobile/src/i18n/locales/zh-Hans.json | 1 - apps/mobile/src/i18n/locales/zh-Hant.json | 1 - apps/mobile/src/i18n/locales/zu.json | 1 - .../mobile/src/lib/auth/auth-context.test.tsx | 19 +- apps/mobile/src/lib/auth/auth-context.tsx | 2 - .../src/lib/glanceable/activity-kit-prompt.ts | 10 +- .../mobile/src/lib/glanceable/cleanup.test.ts | 8 +- apps/mobile/src/lib/glanceable/cleanup.ts | 2 +- .../mobile/src/lib/glanceable/enabled.test.ts | 44 --- apps/mobile/src/lib/glanceable/enabled.ts | 33 --- apps/mobile/src/lib/glanceable/mount.tsx | 37 +-- .../mobile/src/lib/glanceable/presentation.ts | 32 ++- .../src/lib/glanceable/publisher.test.ts | 2 +- apps/mobile/src/lib/glanceable/publisher.ts | 6 +- .../lib/hooks/use-glanceable-preference.ts | 27 -- apps/mobile/src/lib/notification-path.test.ts | 2 +- apps/mobile/src/lib/notifications.test.ts | 58 ++-- apps/mobile/src/lib/notifications.ts | 7 - apps/mobile/src/lib/storage-keys.ts | 1 - apps/web/src/lib/active-sessions-list.ts | 33 +++ .../glanceable-agents-snapshot-server.test.ts | 25 ++ .../src/glanceable-agents-snapshot.test.ts | 80 +++++- .../src/glanceable-agents-snapshot.ts | 60 +++- packages/notifications/src/push-data.ts | 6 +- .../src/push-presentation.test.ts | 2 +- pnpm-lock.yaml | 3 + .../report-consumer.glanceable.test.ts | 11 +- .../src/lib/glanceable-delivery.test.ts | 131 ++++----- .../src/lib/glanceable-delivery.ts | 2 +- .../src/lib/glanceable-refresh.ts | 16 +- 134 files changed, 885 insertions(+), 673 deletions(-) create mode 100644 apps/mobile/plugins/withWidgetLocalizations.js create mode 100644 apps/mobile/src/glanceable-ios/layout-copy.test.ts create mode 100644 apps/mobile/src/glanceable-ios/layout-copy.ts delete mode 100644 apps/mobile/src/lib/glanceable/enabled.test.ts delete mode 100644 apps/mobile/src/lib/glanceable/enabled.ts delete mode 100644 apps/mobile/src/lib/hooks/use-glanceable-preference.ts diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index e1fcf224f4..fed24300ba 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -259,6 +259,11 @@ const config: ExpoConfig = { }, ], './plugins/withAndroidManifestFix', + // Declares the app's languages on the widget extension, which expo-widgets + // leaves English-only. This must be registered BEFORE 'expo-widgets': + // dangerous mods run in reverse registration order, so the earlier entry + // runs last and sees the Info.plist expo-widgets has already written. + ['./plugins/withWidgetLocalizations', { languages: [...SUPPORTED_LANGUAGES] }], // Aggregate "Active Agents" glanceable surfaces: one Live Activity plus Home // Screen and Lock Screen widgets, rendered by src/glanceable-ios. The widget // target reuses the existing app group; no second group is created. @@ -272,12 +277,15 @@ const config: ExpoConfig = { { name: 'ActiveAgentsWidget', displayName: 'Active Agents', - description: 'Agents that need input, are working, or are idle', + description: 'Your agents at a glance: needs input, working, idle', contentMarginsDisabled: false, + // Home Screen: the small square and the medium row. `systemLarge` + // is deliberately absent — three counts cannot fill a card that + // tall, and the whitespace read as an unfinished widget. Add it + // back only with a layout that earns the extra area. supportedFamilies: [ 'systemSmall', 'systemMedium', - 'systemLarge', 'accessoryCircular', 'accessoryRectangular', 'accessoryInline', diff --git a/apps/mobile/knip.json b/apps/mobile/knip.json index 42b4508470..0b4516eb49 100644 --- a/apps/mobile/knip.json +++ b/apps/mobile/knip.json @@ -3,6 +3,7 @@ "entry": ["src/app/**/*.{ts,tsx}", "src/glanceable-android/register.ts"], "project": ["src/**/*.{ts,tsx}"], "ignoreDependencies": [ + "@expo/plist", "expo-updates", "expo-system-ui", "react-native-android-widget", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 3225182b46..02be036c6e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -124,6 +124,7 @@ "zod": "catalog:" }, "devDependencies": { + "@expo/plist": "0.8.1", "@sentry/cli": "catalog:", "@types/react": "19.2.14", "@types/react-test-renderer": "^19.1.0", diff --git a/apps/mobile/plugins/withWidgetLocalizations.js b/apps/mobile/plugins/withWidgetLocalizations.js new file mode 100644 index 0000000000..17d204914a --- /dev/null +++ b/apps/mobile/plugins/withWidgetLocalizations.js @@ -0,0 +1,43 @@ +const fs = require('fs'); +const path = require('path'); +const plist = require('@expo/plist').default; +const { withDangerousMod } = require('expo/config-plugins'); + +// Declares the app's languages on the widget extension. +// +// expo-widgets writes the extension's Info.plist with four keys and no +// localization list, so iOS treats the extension as English-only. Two things +// break: the Live Activity and every widget family lay out left-to-right on an +// Arabic or Hebrew device, and the widget gallery copy cannot localize. The +// main app declares the same list for the same reason — see `CFBundleLocalizations` +// in app.config.ts. +// +// This must run after the `expo-widgets` plugin: dangerous mods run in the +// order they are registered, and expo-widgets rewrites the file. +const TARGET_NAME = 'ExpoWidgetsTarget'; + +module.exports = function withWidgetLocalizations(config, { languages } = {}) { + if (!Array.isArray(languages) || languages.length === 0) { + throw new Error('withWidgetLocalizations needs a non-empty `languages` array'); + } + return withDangerousMod(config, [ + 'ios', + async modConfig => { + const infoPlistPath = path.join( + modConfig.modRequest.platformProjectRoot, + TARGET_NAME, + 'Info.plist' + ); + if (!fs.existsSync(infoPlistPath)) { + throw new Error(`withWidgetLocalizations: ${infoPlistPath} is missing`); + } + const parsed = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')); + parsed.CFBundleLocalizations = [...languages]; + // The extension has no .lproj resources, so name the development language + // explicitly; otherwise iOS picks the first entry of the list above. + parsed.CFBundleDevelopmentRegion = 'en'; + fs.writeFileSync(infoPlistPath, plist.build(parsed)); + return modConfig; + }, + ]); +}; diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx index 9114b166be..9dcf628b12 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx @@ -144,7 +144,7 @@ export default function AgentSessionList() { // changing route focus, so also retry recovery when the app becomes active. useFocusEffect( useCallback(() => { - void showActivityKitDisabledAlertOnce(); + showActivityKitDisabledAlertOnce(); void recoverGlanceableActivityKit(); const subscription = AppState.addEventListener('change', state => { if (state === 'active') { diff --git a/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx b/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx index 9323511179..49daa1ad38 100644 --- a/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx +++ b/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx @@ -208,13 +208,6 @@ vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ setKeepScreenOn: vi.fn(), }), })); -vi.mock('@/lib/hooks/use-glanceable-preference', () => ({ - useGlanceablePreference: () => ({ - glanceableEnabled: true, - hasLoaded: true, - setGlanceableEnabled: vi.fn(), - }), -})); vi.mock('@/lib/hooks/use-pr-review-footer-preference', () => ({ usePrReviewFooterPreference: () => ({ prReviewFooter: true, diff --git a/apps/mobile/src/components/preferences-screen.tsx b/apps/mobile/src/components/preferences-screen.tsx index aa5e0afe57..f1f5bf75fd 100644 --- a/apps/mobile/src/components/preferences-screen.tsx +++ b/apps/mobile/src/components/preferences-screen.tsx @@ -3,7 +3,6 @@ import { Bell, Brain, CornerDownLeft, - Gauge, Globe, type LucideIcon, MessageSquare, @@ -22,7 +21,6 @@ import { Text } from '@/components/ui/text'; import { useAppUnlock } from '@/lib/app-unlock-context'; import { attemptPushRegistrationReconciliation } from '@/lib/auth/push-registration-reconciliation'; import { useCurrentUserId } from '@/lib/hooks/use-current-user-id'; -import { useGlanceablePreference } from '@/lib/hooks/use-glanceable-preference'; import { getResolvedLanguage, useLanguagePreference } from '@/lib/hooks/use-language-preference'; import { useKeepScreenOnPreference } from '@/lib/hooks/use-keep-screen-on-preference'; import { usePrReviewFooterPreference } from '@/lib/hooks/use-pr-review-footer-preference'; @@ -103,11 +101,6 @@ export function PreferencesScreen() { hasLoaded: keepScreenOnLoaded, setKeepScreenOn, } = useKeepScreenOnPreference(); - const { - glanceableEnabled, - hasLoaded: glanceableLoaded, - setGlanceableEnabled, - } = useGlanceablePreference(); const { prReviewFooter, hasLoaded: prReviewFooterLoaded, @@ -161,14 +154,6 @@ export function PreferencesScreen() { disabled={!keepScreenOnLoaded} onValueChange={setKeepScreenOn} /> - { expect(text).toEqual(['1 Needs input']); }); - it('shows every non-zero count and the Open agents affordance at a wide width', () => { + it('shows every count, zeros included, and the Open agents affordance at a wide width', () => { const props = buildAndroidWidgetProps( snapshotFor([{ status: 'question' }, { status: 'busy' }], 0), {}, @@ -122,7 +122,8 @@ describe('renderActiveAgentsWidget', () => { const rep = render(props, 250); const text = collectText(rep.light); - expect(text).toEqual(['1 Needs input', '1 Working', 'Open agents']); + // The zero row draws so the rows hold still as work moves between states. + expect(text).toEqual(['1 Needs input', '1 Working', '0 Idle', 'Open agents']); }); it.each([ diff --git a/apps/mobile/src/glanceable-android/register.test.ts b/apps/mobile/src/glanceable-android/register.test.ts index ff5666a783..e6f1b1e0c1 100644 --- a/apps/mobile/src/glanceable-android/register.test.ts +++ b/apps/mobile/src/glanceable-android/register.test.ts @@ -143,7 +143,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { const handler = await registerAfterRestart(snapshotFor()); const rendered = await runWidgetTask(handler, width); const expected = - width === 120 ? ['2 Needs input'] : ['2 Needs input', '2 Working', 'Open agents']; + width === 120 ? ['2 Needs input'] : ['2 Needs input', '2 Working', '0 Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -209,7 +209,8 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { androidSink.publish({ ...snapshotFor([{ status: 'busy' }]), revision: stored.revision + 1 }); const rendered = await runWidgetTask(handler, width); - const expected = width === 120 ? ['1 Working'] : ['1 Working', 'Open agents']; + const expected = + width === 120 ? ['1 Working'] : ['0 Needs input', '1 Working', '0 Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -231,7 +232,8 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { vi.setSystemTime(Date.parse(old.expiresAt)); const rendered = await runWidgetTask(handler, width); - const expected = width === 120 ? ['1 Working'] : ['1 Working', 'Open agents']; + const expected = + width === 120 ? ['1 Working'] : ['0 Needs input', '1 Working', '0 Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); }); @@ -309,7 +311,8 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { androidSink.publish({ ...snapshotFor([{ status: 'busy' }]), revision: stored.revision + 1 }); read.resolve(JSON.stringify(stored)); const rendered = await rendering; - const expected = width === 120 ? ['1 Working'] : ['1 Working', 'Open agents']; + const expected = + width === 120 ? ['1 Working'] : ['0 Needs input', '1 Working', '0 Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); diff --git a/apps/mobile/src/glanceable-android/widget-props.test.ts b/apps/mobile/src/glanceable-android/widget-props.test.ts index dd2a867e6a..6d84fa48bf 100644 --- a/apps/mobile/src/glanceable-android/widget-props.test.ts +++ b/apps/mobile/src/glanceable-android/widget-props.test.ts @@ -82,7 +82,9 @@ describe('buildAndroidWidgetProps', () => { ][] = [ ['waiting', [], 'Waiting for agents', 0, false], ['empty', [], 'No work in progress', 0, false], - ['stale', [{ status: 'busy' }], 'Updates delayed', 1, true], + // Counts show for stale, and all three rows draw whenever they show, so + // the widget's rows never reflow as work moves between states. + ['stale', [{ status: 'busy' }], 'Updates delayed', 3, true], ['expired', [], 'Status expired', 0, false], ['signed_out', [], 'Sign in to see agents', 0, false], ['privacy', [], 'Agents hidden', 0, false], @@ -97,11 +99,10 @@ describe('buildAndroidWidgetProps', () => { it('carries no title, organization name, or raw id into the widget payload', () => { const snapshot = buildGlanceableSnapshot({ - sessions: [{ status: 'busy' }], + sessions: [{ status: 'question', statusUpdatedAt: new Date(NOW - 60_000).toISOString() }], userId: 'user-9f3a-leak', organizationId: 'org-acme-7-leak', now: NOW, - previousEligibleStartedAt: new Date(NOW - 60_000).toISOString(), }); const props = buildAndroidWidgetProps(snapshot, {}, translate); diff --git a/apps/mobile/src/glanceable-android/widget-props.ts b/apps/mobile/src/glanceable-android/widget-props.ts index 6d2c111bd3..2bfe423954 100644 --- a/apps/mobile/src/glanceable-android/widget-props.ts +++ b/apps/mobile/src/glanceable-android/widget-props.ts @@ -86,7 +86,7 @@ function buildExpiredWidgetProps( running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }, {}, translate @@ -118,7 +118,9 @@ export function buildOngoingNotificationText( ): string { const status = resolveGlanceableStatus(snapshot, flags); if (status === 'happy' || status === 'stale') { - const lines = glanceableCountLines(snapshot); + // A sentence, not a layout: a zero row holds a widget's rows still, but + // "0 Working" in a notification line is only noise. + const lines = glanceableCountLines(snapshot).filter(line => line.count > 0); if (lines.length > 0) { const counts = lines.map(line => `${line.count} ${translate(line.key)}`).join(', '); return status === 'stale' ? `${translate('glanceable.stale')}, ${counts}` : counts; diff --git a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx index 599644fcab..9d5d71928c 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx @@ -2,12 +2,16 @@ import { HStack, Image, Spacer, Text, VStack } from '@expo/ui/swift-ui'; import { accessibilityElement, accessibilityLabel, + allowsTightening, cornerRadius, + environment, font, foregroundStyle, frame, + layoutPriority, + lineLimit, + minimumScaleFactor, monospacedDigit, - multilineTextAlignment, padding, resizable, } from '@expo/ui/swift-ui/modifiers'; @@ -16,6 +20,7 @@ import { PlatformColor } from 'react-native'; import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; +import { withGlanceableCopy } from './layout-copy'; import { withWidgetLogo } from './widget-logo'; /* eslint-disable new-cap -- PlatformColor is a React Native factory function, not a constructor */ @@ -24,66 +29,75 @@ import { withWidgetLogo } from './widget-logo'; // stringifies it and the watcher extension re-evaluates the source. Everything // it references must be a watcher global (`Text`, `VStack`, the modifiers, // `PlatformColor`) or a built-in. Do not call `@/` helpers or i18n from here. -// The server pushes raw counts + status (it cannot translate), and the -// foreground app passes the same raw shape, so the inlined English copy below -// is the single producer of the displayed Live Activity copy. // -// The one value resolved after stringification is the `__KILO_WIDGET_LOGO_URI__` -// literal below: `withWidgetLogo` swaps it for the app-group path of the mark. +// Two values are resolved after stringification, both from literals below: +// `withWidgetLogo` swaps `__KILO_WIDGET_LOGO_URI__` for the app-group path of +// the mark, and `withGlanceableCopy` swaps `__KILO_GLANCEABLE_COPY__` for the +// translated copy. The copy is baked in rather than passed through the content +// state because the notifications Worker pushes the same raw shape and knows +// no locale. type ContentState = Partial; // Babel replaces the annotated arrow with its source string, so `layout` is a // string at runtime while TypeScript still checks it as a component — the same // shape `expo-widgets` casts internally. -const layout: LiveActivityComponent = (props, environment) => { +const layout: LiveActivityComponent = props => { 'widget'; - const dark = environment.colorScheme === 'dark'; + // The literal, not the imported constant: the widget transform stringifies + // this function's source, so an imported binding would be an undefined + // global in the widget process. `withGlanceableCopy` replaces the token, + // quotes included, with the translated copy as a JSON source literal. + // eslint-disable-next-line typescript-eslint/no-inferrable-types -- see above + const copySource: string = '__KILO_GLANCEABLE_COPY__'; + const COPY = JSON.parse(copySource) as Record; + // The tag SwiftUI formats the relative wait with; English when the bake is + // somehow missing it, which is what the widget process would have used anyway. + const locale = COPY.locale ?? 'en'; const status = props.status ?? 'empty'; - const STATUS_LINE = { - waiting: 'Updating agents', - empty: 'No work in progress', - stale: "Can't update now", - expired: 'Status expired', - signed_out: 'Sign in to see agents', - privacy: 'Agents hidden', - } as const; - const statusLine = status === 'happy' ? null : STATUS_LINE[status]; + const statusLine = status === 'happy' ? null : COPY[status]; // Rank order: what the user must act on, then what is making progress, then // what is only connected. The Dynamic Island shows one number, so this // ranking decides what a glance says. The glyphs differ in shape as well as // color (exclamation / filled / hollow) so the state reads without color. - const countLines = ( - [ - { - label: 'Needs input', - count: props.needsInput ?? 0, - icon: 'exclamationmark.circle.fill', - color: PlatformColor('systemOrange'), - }, - { - label: 'Working', - count: props.running ?? 0, - icon: 'circle.fill', - color: PlatformColor('systemGreen'), - }, - { - label: 'Idle', - count: props.idle ?? 0, - icon: 'circle', - color: PlatformColor('label'), - }, - ] as const - ).filter(line => line.count > 0); - const hasCounts = countLines.length > 0; - const primary = countLines[0] ?? null; + const countLines = [ + { + kind: 'needsInput', + label: COPY.needsInput, + count: props.needsInput ?? 0, + icon: 'exclamationmark.circle.fill', + color: PlatformColor('systemOrange'), + }, + { + kind: 'running', + label: COPY.running, + count: props.running ?? 0, + icon: 'circle.fill', + color: PlatformColor('systemGreen'), + }, + { + kind: 'idle', + label: COPY.idle, + count: props.idle ?? 0, + icon: 'circle', + color: PlatformColor('label'), + }, + // `as const` keeps each `icon` an SF Symbol literal, which the Image prop + // type requires. + ] as const; + // A zero row still draws, so the rows never reflow as work changes state. + // `primary` skips the zeros: one number on the Dynamic Island must be a + // number worth showing. + const primary = countLines.find(line => line.count > 0) ?? null; + const hasCounts = primary !== null; const primaryCount = String(primary === null ? 0 : primary.count); - // Elapsed time shows while any count exists, including the stale status, so - // the work keeps its elapsed timer when updates stop. - const elapsedAnchor = hasCounts ? (props.eligibleStartedAt ?? null) : null; + // Only the needs-input row carries a duration, and only the oldest wait: a + // blocked agent is the one interval the user can act on. Working and idle + // durations tell the user nothing they can use. + const needsInputSince = (props.needsInput ?? 0) > 0 ? (props.needsInputSince ?? null) : null; // Spoken label: status word, numeric counts, then Open agents. The whole // surface deep-links to the agents list, so "Open agents" stays in the @@ -91,14 +105,14 @@ const layout: LiveActivityComponent = (props, environment) => { const spokenParts = [ ...(statusLine !== null ? [statusLine] : []), ...countLines.map(line => `${line.count} ${line.label}`), - 'Open agents', + COPY.openAgents, ]; const accessibility = spokenParts.join(', '); const primaryForeground = foregroundStyle(PlatformColor('label')); - const mutedForeground = foregroundStyle( - dark ? PlatformColor('secondaryLabel') : PlatformColor('tertiaryLabel') - ); + // `secondaryLabel` in both appearances: `tertiaryLabel` on the light widget + // background left the ranked-down rows too faint to read. + const mutedForeground = foregroundStyle(PlatformColor('secondaryLabel')); // The literal, not the imported constant: the widget transform stringifies // this function's source, so an imported binding would be an undefined global @@ -117,15 +131,20 @@ const layout: LiveActivityComponent = (props, environment) => { ); // One row per non-zero state: a colored glyph carries the state (readable - // without color), a fixed-width count, then the label. The first row is - // emphasised so a glance lands on it. + // without color), a fixed-width count, then the label. Every row shares one + // type size so the counts line up on a grid; only the label dims to rank + // them, because a second font size in a two-line banner reads as a mistake. const countRow = (line: (typeof countLines)[number], isPrimary: boolean) => ( - - + + @@ -134,57 +153,75 @@ const layout: LiveActivityComponent = (props, environment) => { {line.label} + {line.kind === 'needsInput' && needsInputSince !== null ? ( + + ) : null} ); - const countRows = countLines.map((line, index) => countRow(line, index === 0)); + // The emphasised row is the ranked primary, not the first row: with zeros + // drawn the first row is often a 0, and emphasising that would point the + // user at the state with nothing in it. + const countRows = countLines.map(line => countRow(line, line === primary)); - const elapsed = - elapsedAnchor === null ? null : ( - - ); + // The mark, then the rows. The Lock Screen banner and the expanded Dynamic + // Island draw the same block, so one glance teaches both surfaces. + const markAndRows = (markSize: number) => ( + + {logo(markSize)} + {hasCounts ? ( + + {countRows} + + ) : ( + {statusLine} + )} + + + ); return { banner: ( - {logo(22)} - {hasCounts ? ( - - {countRows} - - ) : ( - - {statusLine} - - )} - - {elapsed} + {markAndRows(26)} ), // The Dynamic Island's leading slot is the app-identity slot, so it holds @@ -216,34 +253,42 @@ const layout: LiveActivityComponent = (props, environment) => { {hasCounts ? primaryCount : ''} ), - expandedLeading: ( - - {countRows} - - ), - expandedTrailing: ( - - {statusLine !== null && hasCounts ? ( - {statusLine} - ) : null} - {elapsed} - - ), + // The whole expanded island is the bottom region: it is the only one wide + // enough for a labelled row, and it clears the rounded corners that clip + // the flanking regions. The leading and trailing regions stay empty and + // take no height. expandedBottom: ( - - {logo(16)} - {statusLine !== null && !hasCounts ? ( - - {statusLine} - - ) : null} - + + {markAndRows(24)} ), }; }; -export const ActiveAgentsLiveActivity = createLiveActivity( - 'ActiveAgentsLiveActivity', - withWidgetLogo(layout) -); +const LIVE_ACTIVITY_NAME = 'ActiveAgentsLiveActivity'; + +const registerLayout = () => + createLiveActivity(LIVE_ACTIVITY_NAME, withGlanceableCopy(withWidgetLogo(layout))); + +export const ActiveAgentsLiveActivity = registerLayout(); + +/** + * Re-bake the stored layout in the active language. + * + * Constructing the factory only writes the layout into the shared app group, + * and the name identifies the native Live Activity type, so the fresh factory + * is discarded and `ActiveAgentsLiveActivity` stays the handle. The app boots + * in English and applies the stored language afterwards, so this runs once the + * language settles as well as on every later change. + */ +export function refreshActiveAgentsLiveActivityCopy(): void { + registerLayout(); +} diff --git a/apps/mobile/src/glanceable-ios/active-agents-widget.tsx b/apps/mobile/src/glanceable-ios/active-agents-widget.tsx index 5654c6decb..f6bd049cf3 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-widget.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-widget.tsx @@ -2,11 +2,16 @@ import { HStack, Image, Spacer, Text, VStack } from '@expo/ui/swift-ui'; import { accessibilityElement, accessibilityLabel, + allowsTightening, containerBackground, cornerRadius, + environment, font, foregroundStyle, frame, + layoutPriority, + lineLimit, + minimumScaleFactor, monospacedDigit, resizable, widgetURL, @@ -14,6 +19,7 @@ import { import { createWidget, type WidgetEnvironment } from 'expo-widgets'; import { PlatformColor } from 'react-native'; +import { withGlanceableCopy } from './layout-copy'; import { type GlanceableViewProps } from './view-props'; import { withWidgetLogo } from './widget-logo'; @@ -23,31 +29,48 @@ import { withWidgetLogo } from './widget-logo'; // stringifies it and the widget extension re-evaluates the source. Everything // it references must be a widget global (`Text`, `VStack`, the modifiers, // `PlatformColor`) or a built-in. Do not call `@/` helpers or i18n from here — -// translated copy arrives through `props`. The inlined English fallbacks below -// only render while the gallery placeholder has no snapshot props. +// translated copy arrives through `props`, and the gallery placeholder (which +// has no props) falls back to the baked copy below. // -// The one value resolved after stringification is the `__KILO_WIDGET_LOGO_URI__` -// literal below: `withWidgetLogo` swaps it for the app-group path of the mark. +// Two values are resolved after stringification, both from literals below: +// `withWidgetLogo` swaps `__KILO_WIDGET_LOGO_URI__` for the app-group path of +// the mark, and `withGlanceableCopy` swaps `__KILO_GLANCEABLE_COPY__` for the +// translated copy. type WidgetProps = Partial; // Babel replaces the annotated arrow with its source string, so `layout` is a // string at runtime while TypeScript still checks it as a component. -const layout: (props: WidgetProps, environment: WidgetEnvironment) => React.JSX.Element = ( +const layout: (props: WidgetProps, widgetEnvironment: WidgetEnvironment) => React.JSX.Element = ( props, - environment + widgetEnvironment ) => { 'widget'; - const family = environment.widgetFamily; - const dark = environment.colorScheme === 'dark'; + // The literal, not the imported constant: the widget transform stringifies + // this function's source, so an imported binding would be an undefined + // global in the widget process. `withGlanceableCopy` replaces the token, + // quotes included, with the translated copy as a JSON source literal. + // eslint-disable-next-line typescript-eslint/no-inferrable-types -- see above + const copySource: string = '__KILO_GLANCEABLE_COPY__'; + const COPY = JSON.parse(copySource) as Record; + // The tag SwiftUI formats the relative wait with; English when the bake is + // somehow missing it, which is what the widget process would have used anyway. + const locale = COPY.locale ?? 'en'; + + const family = widgetEnvironment.widgetFamily; const counts = props.countLines ?? []; - const hasCounts = counts.length > 0; const primaryLabel = props.primaryLabel ?? null; const primaryKind = props.primaryKind ?? null; + // Only the medium row is wide enough for a wait beside the label; in the + // small square the pair wraps and truncates both halves. + const wide = family === 'systemMedium'; + const needsInputSince = props.needsInputSince ?? null; + // The rows carry zeros too, so their number never says whether work exists — + // the ranked primary does, because it is null only when every count is zero. + const hasCounts = primaryKind !== null; const primaryCount = props.primaryCount ?? 0; - const statusLine = props.statusLine ?? (hasCounts ? null : 'No work in progress'); - const elapsedAnchor = props.elapsedAnchor ?? null; + const statusLine = props.statusLine ?? (hasCounts ? null : COPY.empty); // Circle-based glyphs whose shapes differ as well as their colors, because // the Lock Screen families render in an accented mode that flattens tint. @@ -58,10 +81,14 @@ const layout: (props: WidgetProps, environment: WidgetEnvironment) => React.JSX. } as const; const primaryForeground = foregroundStyle(PlatformColor('label')); - const mutedForeground = foregroundStyle( - dark ? PlatformColor('secondaryLabel') : PlatformColor('tertiaryLabel') - ); + // `secondaryLabel` in both appearances: `tertiaryLabel` on the light widget + // background left the ranked-down rows too faint to read. + const mutedForeground = foregroundStyle(PlatformColor('secondaryLabel')); const a11y = [ + // The widget process takes its locale from the device language, so without + // this the relative wait would be formatted in a different language than + // the labels the app translated into the props. + environment({ key: 'locale', value: locale }), accessibilityElement('combine'), accessibilityLabel(props.accessibilityLabel ?? ''), ]; @@ -90,17 +117,21 @@ const layout: (props: WidgetProps, environment: WidgetEnvironment) => React.JSX. compact: boolean ) => { const glyph = GLYPH[line.kind as keyof typeof GLYPH]; - const emphasis = isPrimary ? 'headline' : 'subheadline'; - const countStyle = compact ? 'caption' : emphasis; - const labelStyle = compact ? 'caption' : 'subheadline'; - const glyphSize = isPrimary ? 14 : 12; + // Every row shares one type size and one glyph size so the counts and the + // labels line up on a grid; only the label colour ranks them, because a + // second font size in a three-row list reads as a mistake. + const textStyle = compact ? 'caption' : 'subheadline'; return ( - - + + @@ -108,12 +139,28 @@ const layout: (props: WidgetProps, environment: WidgetEnvironment) => React.JSX. {line.label} + {wide ? : null} + {wide && line.kind === 'needsInput' && needsInputSince !== null ? ( + + ) : null} ); }; @@ -127,7 +174,7 @@ const layout: (props: WidgetProps, environment: WidgetEnvironment) => React.JSX. )} React.JSX. ); } - const elapsed = - elapsedAnchor === null ? null : ( - - ); - // accessoryRectangular is the Lock Screen row: the mark plus the two // top-ranked lines is all that fits. if (family === 'accessoryRectangular') { @@ -180,10 +218,10 @@ const layout: (props: WidgetProps, environment: WidgetEnvironment) => React.JSX. spacing={6} modifiers={[widgetURL('kiloapp:///cloud/sessions'), ...a11y]} > - {logo(14)} + {logo(18)} {hasCounts ? ( - {counts.slice(0, 2).map((line, index) => countRow(line, index === 0, true))} + {counts.map(line => countRow(line, line.kind === primaryKind, true))} ) : ( {statusLine} @@ -193,39 +231,59 @@ const layout: (props: WidgetProps, environment: WidgetEnvironment) => React.JSX. ); } - // systemSmall has room for the mark, then every non-zero line; the wider - // families add the elapsed timer on the header row. - const wide = family !== 'systemSmall'; + const systemRows = hasCounts ? ( + + {counts.map(line => countRow(line, line.kind === primaryKind, false))} + + ) : ( + {statusLine} + ); + + const systemModifiers = [ + widgetURL('kiloapp:///cloud/sessions'), + containerBackground(PlatformColor('systemBackground'), 'widget'), + ...a11y, + ]; + + // The medium family is wide, not tall: the mark sits beside the rows and the + // whole block centres, the same composition as the Live Activity banner. A + // vertical layout there left the right half of the card empty. + if (wide) { + return ( + + {logo(34)} + {systemRows} + + ); + } + return ( - + - {logo(20)} + {logo(26)} - {wide ? elapsed : null} - {hasCounts ? ( - - {counts.map((line, index) => countRow(line, index === 0, false))} - - ) : null} - {statusLine !== null ? ( - {statusLine} - ) : null} - {wide ? null : elapsed} + {/* The mark sits at the top and the counts at the bottom, so the card + reads as one composed block. */} + {systemRows} ); }; -export const ActiveAgentsWidget = createWidget( - 'ActiveAgentsWidget', - withWidgetLogo(layout) -); +const WIDGET_NAME = 'ActiveAgentsWidget'; + +const registerLayout = () => + createWidget(WIDGET_NAME, withGlanceableCopy(withWidgetLogo(layout))); + +export const ActiveAgentsWidget = registerLayout(); + +/** + * Re-bake the stored layout in the active language. Only the gallery + * placeholder reads this copy — a placed widget gets translated copy through + * its timeline props — but the placeholder is the first thing the user sees in + * the widget picker, so it must not stay English after a language change. + */ +export function refreshActiveAgentsWidgetCopy(): void { + registerLayout(); +} diff --git a/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts index 2c3abffdf7..6139cd1a88 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts @@ -122,7 +122,6 @@ function snapshot(sessions: { status: string }[], revision = 0): GlanceableAgent sessions, now: NOW + revision, previousRevision: revision, - previousEligibleStartedAt: new Date(NOW - 60_000).toISOString(), }); } @@ -174,11 +173,8 @@ describe('native adapter recovery', () => { expect(native.records.filter(record => record.state === 'active')).toMatchObject([ { - props: { - running: 1, - idle: 1, - eligibleStartedAt: new Date(NOW - 60_000).toISOString(), - }, + // No row needs input, so the content state carries no wait. + props: { running: 1, idle: 1, needsInputSince: null }, }, ]); expect(native.records).toHaveLength(2); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index 6efbc14035..e5ac92d4b5 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -21,7 +21,12 @@ import { getActivityKitDenied, iosSink, } from './ios-sink'; -import { buildGlanceableViewProps, type GlanceableViewProps, toWidgetProps } from './view-props'; +import { + buildGlanceableLiveActivityContentState, + buildGlanceableViewProps, + type GlanceableViewProps, + toWidgetProps, +} from './view-props'; // Native surfaces are unreachable under vitest: expo-widgets factories, the // swift-ui component tree, and react-native are stubbed so the sink is the real @@ -146,7 +151,7 @@ const delivery = { }; function snapshotFor( - sessions: { status: string }[], + sessions: { status: string; statusUpdatedAt?: string }[], revision = 0, status?: GlanceableAgentsSnapshot['status'] ): GlanceableAgentsSnapshot { @@ -668,20 +673,24 @@ describe('iosSink end', () => { expect(subscriptions).toEqual(new Set(['scope', 'activity'])); }); - it('retains the elapsed anchor when running work becomes idle', async () => { + it('carries the wait only while a row needs input', async () => { vi.useFakeTimers(); vi.setSystemTime(NOW); + const waited = new Date(NOW - 600_000).toISOString(); const publisher = new GlanceablePublisher({ sinks: [iosSink], now: () => Date.now() }); - publisher.handleSessions([{ status: 'busy' }], CTX); + publisher.handleSessions([{ status: 'question', statusUpdatedAt: waited }], CTX); + await vi.advanceTimersByTimeAsync(1000); + expect(mockState.started).toMatchObject([ + { ended: false, props: { needsInput: 1, needsInputSince: waited } }, + ]); + + // The wait clears with the state it described; it is read from the rows, so + // no stale anchor survives the transition to work that needs nothing. vi.setSystemTime(NOW + 60_000); - publisher.handleSessions([{ status: 'idle' }], CTX); + publisher.handleSessions([{ status: 'busy' }], CTX); await vi.advanceTimersByTimeAsync(1000); expect(mockState.started).toMatchObject([ - { - ended: false, - dismissAt: null, - props: { running: 0, idle: 1, eligibleStartedAt: new Date(NOW).toISOString() }, - }, + { ended: false, props: { running: 1, needsInput: 0, needsInputSince: null } }, ]); publisher.dispose(); }); @@ -763,7 +772,9 @@ describe('iosSink widget publish', () => { boolean, ][] = [ ['empty', [], 'No work in progress', 0, false], - ['stale', [{ status: 'busy' }], "Can't update now", 1, true], + // Stale draws rows, and all three draw whenever rows draw, so the + // surface never reflows as work moves between states. + ['stale', [{ status: 'busy' }], "Can't update now", 3, true], ['expired', [], 'Status expired', 0, false], ['signed_out', [], 'Sign in to see agents', 0, false], ['privacy', [], 'Agents hidden', 0, false], @@ -904,15 +915,13 @@ describe('buildGlanceableViewProps', () => { }); it('carries no title, organization name, or raw id into the widget JSON', () => { - // Decouple the eligible-start anchor from `updatedAt` so the assertion below - // proves the builder copies `eligibleStartedAt` (not `updatedAt`) into - // `elapsedAnchor`; on a fresh snapshot the two timestamps are equal. + // A waiting row with its own status timestamp, so the assertion below + // covers the one field that carries a time into the widget payload. const snapshot = buildGlanceableSnapshot({ - sessions: [{ status: 'busy' }], + sessions: [{ status: 'question', statusUpdatedAt: new Date(NOW - 60_000).toISOString() }], userId: 'user-9f3a-leak', organizationId: 'org-acme-7-leak', now: NOW, - previousEligibleStartedAt: new Date(NOW - 60_000).toISOString(), }); const props = buildGlanceableViewProps(snapshot, {}, key => key); @@ -921,7 +930,7 @@ describe('buildGlanceableViewProps', () => { expect(Object.keys(props).toSorted()).toEqual([ 'accessibilityLabel', 'countLines', - 'elapsedAnchor', + 'needsInputSince', 'primaryCount', 'primaryKind', 'primaryLabel', @@ -935,17 +944,25 @@ describe('buildGlanceableViewProps', () => { expect(json).not.toContain('title'); }); - it('shows the elapsed anchor for stale with eligible counts', () => { - const stale = snapshotFor([{ status: 'busy' }], 1, 'stale'); - const props = buildGlanceableViewProps(stale, {}, key => key); + it('carries the oldest wait through the stale status', () => { + const waited = new Date(NOW - 600_000).toISOString(); + const stale = snapshotFor([{ status: 'question', statusUpdatedAt: waited }], 1, 'stale'); - expect(props.elapsedAnchor).toBe(stale.eligibleStartedAt); + // Stale means updates stopped, not that the wait ended, so the Live + // Activity keeps reporting how long the agent has been blocked. Only that + // surface carries the wait — no widget family is wide enough for it. + expect(buildGlanceableLiveActivityContentState(stale).needsInputSince).toBe(waited); }); - it('hides the elapsed anchor when no eligible counts exist', () => { - const props = buildGlanceableViewProps(snapshotFor([], 1, 'empty'), {}, key => key); + it('reports no wait unless a row needs input', () => { + const working = snapshotFor( + [{ status: 'busy', statusUpdatedAt: new Date(NOW - 600_000).toISOString() }], + 1 + ); + expect(buildGlanceableLiveActivityContentState(working).needsInputSince).toBeNull(); - expect(props.elapsedAnchor).toBeNull(); + const empty = snapshotFor([], 1, 'empty'); + expect(buildGlanceableLiveActivityContentState(empty).needsInputSince).toBeNull(); }); it('speaks the status word, numeric counts, then Open agents', () => { @@ -975,7 +992,7 @@ describe('toWidgetProps', () => { expect(Object.values(props)).not.toContain(null); expect('primaryLabel' in props).toBe(false); expect('primaryKind' in props).toBe(false); - expect('elapsedAnchor' in props).toBe(false); + expect('needsInputSince' in props).toBe(false); expect(props.statusLine).toBe('glanceable.empty'); }); @@ -990,7 +1007,11 @@ describe('toWidgetProps', () => { primaryLabel: 'glanceable.needsInput', primaryKind: 'needsInput', primaryCount: 1, - countLines: [{ kind: 'needsInput', count: 1 }], + countLines: [ + { kind: 'needsInput', count: 1 }, + { kind: 'running', count: 0 }, + { kind: 'idle', count: 0 }, + ], }); }); }); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index a546a61b51..557295112d 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -84,7 +84,7 @@ function buildExpiredProps(snapshot: GlanceableAgentsSnapshot): Partial readFileSync(join(__dirname, file), 'utf8'); + +/** Stands in for an untransformed layout, which is a function, not a string. */ +const untransformedLayout = () => null; + +/** + * The `'widget'` layouts are stringified by Babel and re-evaluated inside the + * widget process, where an imported binding is an undefined global that throws + * and blanks the whole surface. So the placeholder must appear as a literal in + * each layout source. These assertions read the sources because no widget + * transform runs under vitest. + */ +describe('glanceable layout copy placeholder', () => { + it('matches the token layout-copy.ts replaces', () => { + expect(read('layout-copy.ts')).toContain(`= '${PLACEHOLDER}'`); + }); + + for (const file of LAYOUT_FILES) { + it(`is a literal in ${file}`, () => { + expect(read(file)).toContain(`= '${PLACEHOLDER}'`); + }); + } +}); + +describe('withGlanceableCopy', () => { + it('leaves the untransformed function alone', () => { + expect(withGlanceableCopy(untransformedLayout)).toBe(untransformedLayout); + }); + + it('replaces the quoted token with a JSON source literal the layout can parse', () => { + const prefix = 'const copySource = '; + const source = withGlanceableCopy(`${prefix}'${PLACEHOLDER}';`); + expect(source).not.toContain(PLACEHOLDER); + // The patched text must be a valid source literal, so copy that contains an + // apostrophe ("Can't update now") cannot break the layout the widget + // process evaluates. A JSON string literal is also valid JSON, so parsing + // twice reads the copy back the way the layout's `JSON.parse` does. + const literal = source.slice(prefix.length, -1); + expect(JSON.parse(JSON.parse(literal) as string)).toEqual(glanceableLayoutCopy()); + }); + + it('covers every status the layouts render, plus the language tag', () => { + expect(Object.keys(glanceableLayoutCopy()).toSorted()).toEqual([ + 'empty', + 'expired', + 'idle', + 'locale', + 'needsInput', + 'openAgents', + 'privacy', + 'running', + 'signed_out', + 'stale', + 'waiting', + ]); + }); +}); diff --git a/apps/mobile/src/glanceable-ios/layout-copy.ts b/apps/mobile/src/glanceable-ios/layout-copy.ts new file mode 100644 index 0000000000..b76a4caa8a --- /dev/null +++ b/apps/mobile/src/glanceable-ios/layout-copy.ts @@ -0,0 +1,73 @@ +import { i18n } from '@/i18n'; +import { GLANCEABLE_STATUS_COPY_KEY } from '@/lib/glanceable/presentation'; + +/** + * Translated copy for the stringified `'widget'` layouts. + * + * The widget extension is a separate process that re-evaluates the layout + * source, so a layout cannot call i18n. The widget families read their copy + * from the timeline props, but the Live Activity cannot: the notifications + * Worker pushes the same raw content state and knows no locale, so a + * background push would draw English on a localized device. The copy is + * therefore baked into the layout source at registration, the same boundary + * `withWidgetLogo` uses for the app-group path of the mark. + */ + +/** + * The token the `'widget'` layouts carry until `withGlanceableCopy` resolves + * it. Each layout repeats this literal inline rather than importing it: the + * widget transform stringifies the layout source, so an imported binding would + * be an undefined global in the widget process. `layout-copy.test.ts` keeps + * the copies equal. + */ +const COPY_PLACEHOLDER = '__KILO_GLANCEABLE_COPY__'; + +/** + * Every layout string in the active language, plus the language tag itself. + * + * The tag is not copy: the widget process takes its locale from the device + * language, so a user who overrides the app language would otherwise read + * translated labels beside a relative wait ("28 min") formatted in the device + * language. The layouts feed the tag to SwiftUI's `locale` environment value + * so the whole surface speaks one language. + * + * The slot names are the layouts' own field names, and the status slots match + * `GlanceableAgentsSnapshot['status']` so a layout can index this by status. + */ +export function glanceableLayoutCopy() { + return { + waiting: i18n.t(GLANCEABLE_STATUS_COPY_KEY.waiting), + empty: i18n.t(GLANCEABLE_STATUS_COPY_KEY.empty), + stale: i18n.t(GLANCEABLE_STATUS_COPY_KEY.stale), + expired: i18n.t(GLANCEABLE_STATUS_COPY_KEY.expired), + signed_out: i18n.t(GLANCEABLE_STATUS_COPY_KEY.signed_out), + privacy: i18n.t(GLANCEABLE_STATUS_COPY_KEY.privacy), + needsInput: i18n.t('glanceable.needsInput'), + running: i18n.t('glanceable.running'), + idle: i18n.t('glanceable.idle'), + openAgents: i18n.t('glanceable.openAgents'), + locale: i18n.language, + }; +} + +/** + * Resolve the copy placeholder inside a stringified `'widget'` layout. + * + * This is the same two-representation boundary as `withWidgetLogo`: Babel's + * widget plugin replaces a `'widget'` function with a template literal of its + * source, so the layout is a string in the app while a unit test (which runs + * no widget transform) still holds the real function. Only the string form + * carries a placeholder to patch. The replacement includes the surrounding + * quotes, so `JSON.stringify` produces a correctly escaped source literal for + * copy that contains an apostrophe. + */ +export function withGlanceableCopy(layout: T): T { + // eslint-disable-next-line anti-slop/no-runtime-typeof -- the two representations are the contract; see above + if (typeof layout !== 'string') { + return layout; + } + const source = JSON.stringify(JSON.stringify(glanceableLayoutCopy())); + const patched = layout.split(`'${COPY_PLACEHOLDER}'`).join(source); + // eslint-disable-next-line anti-slop/no-chained-type-assertions -- the layout source IS the component to expo-widgets + return patched as unknown as T; +} diff --git a/apps/mobile/src/glanceable-ios/register.ts b/apps/mobile/src/glanceable-ios/register.ts index 73b7095c49..57d337d830 100644 --- a/apps/mobile/src/glanceable-ios/register.ts +++ b/apps/mobile/src/glanceable-ios/register.ts @@ -1,5 +1,8 @@ +import { i18n } from '@/i18n'; import { registerGlanceableSink } from '@/lib/glanceable/sink-registry'; +import { refreshActiveAgentsLiveActivityCopy } from './active-agents-live-activity'; +import { refreshActiveAgentsWidgetCopy } from './active-agents-widget'; import { iosSink } from './ios-sink'; import { ensureWidgetLogo } from './widget-logo'; @@ -14,3 +17,12 @@ registerGlanceableSink(iosSink); // it. Fire and forget: it lands long before the first snapshot arrives, and a // failure only costs the logo. void ensureWidgetLogo(); + +// The layouts bake their copy in at import, when i18n still holds English: the +// stored language is applied a few ticks later. Re-bake on every language +// change so both the Live Activity and the widget gallery placeholder follow +// the user's language. +i18n.on('languageChanged', () => { + refreshActiveAgentsLiveActivityCopy(); + refreshActiveAgentsWidgetCopy(); +}); diff --git a/apps/mobile/src/glanceable-ios/view-props.ts b/apps/mobile/src/glanceable-ios/view-props.ts index d057ab9c7d..aee90d52c3 100644 --- a/apps/mobile/src/glanceable-ios/view-props.ts +++ b/apps/mobile/src/glanceable-ios/view-props.ts @@ -1,7 +1,4 @@ -import { - type GlanceableAgentsSnapshot, - isEligibleGlanceableWork, -} from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; import { @@ -11,6 +8,7 @@ import { glanceableStatusCopyKey, type GlanceableSurfaceFlags, primaryGlanceableCount, + resolveGlanceableStatus, } from '@/lib/glanceable/presentation'; /** One translated count line. `kind` picks the glyph and the color. */ @@ -32,8 +30,13 @@ export type GlanceableViewProps = { primaryKind: GlanceableCountKind | null; /** Top-ranked count value for compact surfaces; 0 when no eligible work. */ primaryCount: number; - /** ISO anchor for the elapsed timer; shows while eligible work runs, incl. stale. */ - elapsedAnchor: string | null; + /** + * ISO timestamp of the longest-running needs-input wait, or null when + * nothing waits. Only the needs-input row carries a duration: a wait is the + * one interval the user can act on. Only `systemMedium` is wide enough to + * draw it. + */ + needsInputSince: string | null; /** Spoken label: status word, numeric counts, then Open agents. Never a title or id. */ accessibilityLabel: string; }; @@ -46,10 +49,14 @@ export function buildGlanceableViewProps( ): GlanceableViewProps { const statusKey = glanceableStatusCopyKey(snapshot, flags); const primary = primaryGlanceableCount(snapshot); + // Only these two statuses draw rows; the rest draw their status line, so the + // locked frames carry no count payload at all. + const status = resolveGlanceableStatus(snapshot, flags); + const showCounts = status === 'happy' || status === 'stale'; return { statusLine: statusKey === null ? null : translate(statusKey), - countLines: glanceableCountLines(snapshot).map(line => ({ + countLines: (showCounts ? glanceableCountLines(snapshot) : []).map(line => ({ label: translate(line.key), kind: line.kind, count: line.count, @@ -57,7 +64,7 @@ export function buildGlanceableViewProps( primaryLabel: primary === null ? null : translate(primary.key), primaryKind: primary === null ? null : primary.kind, primaryCount: primary === null ? 0 : primary.count, - elapsedAnchor: isEligibleGlanceableWork(snapshot) ? snapshot.eligibleStartedAt : null, + needsInputSince: showCounts && snapshot.needsInput > 0 ? snapshot.needsInputSince : null, accessibilityLabel: glanceableSpokenLabel(snapshot, flags, translate), }; } @@ -88,6 +95,6 @@ export function buildGlanceableLiveActivityContentState( running: snapshot.running, needsInput: snapshot.needsInput, idle: snapshot.idle, - eligibleStartedAt: snapshot.eligibleStartedAt, + needsInputSince: snapshot.needsInputSince, }; } diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 19e48365aa..01a96cb127 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Wys die agent se denke uitgevou wanneer dit klaarmaak.", "keepScreenOn": "Hou skerm aan op sessiebladsy", "keepScreenOnSubtitle": "Hou die skerm wakker terwyl die sessie werk.", - "activeAgentsSubtitle": "Wys agent-tellings op jou legstukke en sluitskerm.", "appearance": "Voorkoms", "notifications": "Kennisgewings", "notificationsSubtitle": "Drukvoorkeure", diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 49cf1dd240..f4fd9b7d41 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "ወኪሉ ሲጨርስ የወኪሉን አስተሳሰብ በሰፊው አሳይ።", "keepScreenOn": "በክፍለ ጊዜ ገጽ ላይ ሲሆኑ ስክሪኑን አብራ", "keepScreenOnSubtitle": "ክፍለ ጊዜው በሚሰራበት ጊዜ ስክሪኑን ነቅቶ ያዝ።", - "activeAgentsSubtitle": "የወኪል ቁጥሮችን በዊጅቶችዎ እና በመቆለፊያ ማያ ገጽ ላይ ያሳዩ።", "appearance": "መልክ", "notifications": "ማሳወቂያዎች", "notificationsSubtitle": "የግፋ ምርጫዎች", diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index 8c73fda5f1..c9f2a98b36 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "إظهار تفكير الوكيل موسّعًا عند انتهائه.", "keepScreenOn": "إبقاء الشاشة مضاءة أثناء صفحة الجلسة", "keepScreenOnSubtitle": "إبقاء الشاشة مستيقظة أثناء عمل الجلسة.", - "activeAgentsSubtitle": "إظهار أعداد الوكلاء على عناصر واجهتك وشاشة القفل.", "appearance": "المظهر", "notifications": "الإشعارات", "notificationsSubtitle": "تفضيلات الإشعارات الفورية", diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index cf05076274..aa00483fe7 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Bitəndə agentin düşüncəsini genişləndirilmiş göstər.", "keepScreenOn": "Sessiya səhifəsində ikən ekranı açıq saxla", "keepScreenOnSubtitle": "Sessiya işləyərkən ekranı oyaq saxla.", - "activeAgentsSubtitle": "Agent saylarını vidcetlərinizdə və kilid ekranında göstərin.", "appearance": "Görünüş", "notifications": "Bildirişlər", "notificationsSubtitle": "Push seçimləri", diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 66255ad5a9..bcd998f942 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Паказваць развагі агента разгорнутымі, калі ён скончыць.", "keepScreenOn": "Трымаць экран уключаным на старонцы сесіі", "keepScreenOnSubtitle": "Трымаць экран уключаным, пакуль сесія працуе.", - "activeAgentsSubtitle": "Паказваць колькасць агентаў на віджэтах і экране блакіроўкі.", "appearance": "Знешні выгляд", "notifications": "Апавяшчэнні", "notificationsSubtitle": "Налады push-паведамленняў", diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 2d10f0c0ed..015b525632 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Показвай мисленето на агента разгънато, когато приключи.", "keepScreenOn": "Дръж екрана включен на страницата на сесията", "keepScreenOnSubtitle": "Дръж екрана буден, докато сесията работи.", - "activeAgentsSubtitle": "Показвайте броя на агентите в приспособленията и заключения екран.", "appearance": "Изглед", "notifications": "Известия", "notificationsSubtitle": "Предпочитания за push известия", diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index 0b9871c03e..f23faeef6f 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "শেষ হলে এজেন্টের চিন্তাভাবনা প্রসারিত দেখান।", "keepScreenOn": "সেশন পেজে থাকাকালীন স্ক্রিন চালু রাখুন", "keepScreenOnSubtitle": "সেশন কাজ করার সময় স্ক্রিন জাগ্রত রাখুন।", - "activeAgentsSubtitle": "আপনার উইজেট এবং লক স্ক্রিনে এজেন্টের সংখ্যা দেখান।", "appearance": "চেহারা", "notifications": "বিজ্ঞপ্তি", "notificationsSubtitle": "পুশ পছন্দ", diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index 4fd9dad52d..ba8899dca5 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Prikaži agentovo razmišljanje prošireno kada završi.", "keepScreenOn": "Zadrži ekran uključen na stranici sesije", "keepScreenOnSubtitle": "Održavaj ekran budnim dok sesija radi.", - "activeAgentsSubtitle": "Prikaži broj agenata na vidžetima i zaključanom ekranu.", "appearance": "Izgled", "notifications": "Obavijesti", "notificationsSubtitle": "Postavke push obavijesti", diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index cb63a180d6..63fbcbb197 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Mostra el raonament de l'agent ampliat quan acaba.", "keepScreenOn": "Mantén la pantalla encesa a la pàgina de sessió", "keepScreenOnSubtitle": "Mantén la pantalla activa mentre la sessió treballa.", - "activeAgentsSubtitle": "Mostra el nombre d'agents als ginys i a la pantalla de bloqueig.", "appearance": "Aparença", "notifications": "Notificacions", "notificationsSubtitle": "Preferències de notificacions push", diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 277c793427..68fefa48f1 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "بیرکردنەوەی ئەجێنتەکە پیشان بدە کە کاتێک تەواو دەبێت فراوان کراوە.", "keepScreenOn": "شاشەکە بەردەوام ڕاگرە لە پەڕەی دانیشتن", "keepScreenOnSubtitle": "شاشەکە بەخەبەر ڕابگرە کاتێک دانیشتنەکە کار دەکات.", - "activeAgentsSubtitle": "ژمارەی بریکارەکان لە ویجێت و شاشەی داخستن پیشان بدە.", "appearance": "ڕووکار", "notifications": "ئاگادارکردنەوەکان", "notificationsSubtitle": "پەسەندەکانی پاڵدان", diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 085af93ad9..8e3df4f59a 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Po dokončení zobrazí rozbalené myšlení agenta.", "keepScreenOn": "Udržet obrazovku zapnutou na stránce relace", "keepScreenOnSubtitle": "Udržuje obrazovku zapnutou, dokud relace pracuje.", - "activeAgentsSubtitle": "Zobrazovat počty agentů ve widgetech a na obrazovce uzamčení.", "appearance": "Vzhled", "notifications": "Oznámení", "notificationsSubtitle": "Předvolby push", diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index ceb75b620b..e0fb6278d6 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Dangos meddwl yr asiant wedi'i ehangu pan fydd yn gorffen.", "keepScreenOn": "Cadw'r sgrin ymlaen ar dudalen y sesiwn", "keepScreenOnSubtitle": "Cadw'r sgrin yn effro tra bydd y sesiwn yn gweithio.", - "activeAgentsSubtitle": "Dangos niferoedd asiantau ar eich teclynnau a'r sgrin clo.", "appearance": "Ymddangosiad", "notifications": "Hysbysiadau", "notificationsSubtitle": "Dewisiadau gwthio", diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index d067349bbe..c723d0a594 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Vis agentens tænkning udvidet, når den er færdig.", "keepScreenOn": "Hold skærmen tændt på sessionssiden", "keepScreenOnSubtitle": "Hold skærmen vågen, mens sessionen arbejder.", - "activeAgentsSubtitle": "Vis antal agenter på dine widgets og låseskærmen.", "appearance": "Udseende", "notifications": "Meddelelser", "notificationsSubtitle": "Push-indstillinger", diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 4e85a3ab68..f0403009c1 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Das Denken des Agenten erweitert anzeigen, wenn es fertig ist.", "keepScreenOn": "Bildschirm auf der Sitzungsseite anlassen", "keepScreenOnSubtitle": "Den Bildschirm wach halten, während die Sitzung arbeitet.", - "activeAgentsSubtitle": "Agentenzahlen auf Widgets und dem Sperrbildschirm anzeigen.", "appearance": "Erscheinungsbild", "notifications": "Benachrichtigungen", "notificationsSubtitle": "Push-Einstellungen", diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index 95f1ac9458..770bf5ff01 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Εμφάνιση της σκέψης του πράκτορα σε πλήρη επέκταση όταν ολοκληρωθεί.", "keepScreenOn": "Κρατήστε την οθόνη ενεργή στη σελίδα συνεδρίας", "keepScreenOnSubtitle": "Κρατήστε την οθόνη ανοιχτή όσο η συνεδρία εργάζεται.", - "activeAgentsSubtitle": "Εμφάνιση αριθμού πρακτόρων στα γραφικά στοιχεία και στην οθόνη κλειδώματος.", "appearance": "Εμφάνιση", "notifications": "Ειδοποιήσεις", "notificationsSubtitle": "Προτιμήσεις push", diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 3900299fb3..277d5c7af2 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -146,7 +146,6 @@ "autoExpandThinkingSubtitle": "Show the agent's thinking expanded when it finishes.", "keepScreenOn": "Keep screen on while on session page", "keepScreenOnSubtitle": "Hold the screen awake while the session is working.", - "activeAgentsSubtitle": "Show agent counts on your widgets and Lock Screen.", "appearance": "Appearance", "notifications": "Notifications", "notificationsSubtitle": "Push preferences", diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 431e070e60..78c98180d0 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Mostrar el pensamiento del agente expandido cuando termine.", "keepScreenOn": "Mantener la pantalla encendida en la página de sesión", "keepScreenOnSubtitle": "Mantener la pantalla activa mientras la sesión trabaja.", - "activeAgentsSubtitle": "Muestra el número de agentes en los widgets y la pantalla de bloqueo.", "appearance": "Apariencia", "notifications": "Notificaciones", "notificationsSubtitle": "Preferencias de push", diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index 2fe6e4c891..f5efb3a9df 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Näita agendi mõtlemist laiendatuna, kui see lõpeb.", "keepScreenOn": "Hoia ekraan sees sessioonilehel", "keepScreenOnSubtitle": "Hoia ekraan ärkvel, kui sessioon töötab.", - "activeAgentsSubtitle": "Näita agentide arvu vidinatel ja lukustuskuval.", "appearance": "Välimus", "notifications": "Teavitused", "notificationsSubtitle": "Push-eelistused", diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 7649dd7a62..0b8a26c90b 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Erakutsi agentearen pentsamendua zabalduta amaitzen denean.", "keepScreenOn": "Mantendu pantaila piztuta saio-orrian", "keepScreenOnSubtitle": "Mantendu pantaila esnatuta saioa lanean ari den bitartean.", - "activeAgentsSubtitle": "Erakutsi agenteen kopurua widgetetan eta blokeo-pantailan.", "appearance": "Itxura", "notifications": "Jakinarazpenak", "notificationsSubtitle": "Bultzadazko hobespenak", diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index a88ed04fae..675cae9a90 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "هنگام پایان کار عامل، تفکر او را باز شده نشان بده.", "keepScreenOn": "روشن نگه داشتن صفحه در صفحهٔ نشست", "keepScreenOnSubtitle": "در حالی که نشست کار می‌کند، صفحه را بیدار نگه دار.", - "activeAgentsSubtitle": "تعداد عامل‌ها را در ابزارک‌ها و صفحه قفل نشان بده.", "appearance": "ظاهر", "notifications": "اعلان‌ها", "notificationsSubtitle": "ترجیحات push", diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index 94bc174fe0..e4b1dee68f 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Näytä agentin ajattelu laajennettuna, kun se on valmis.", "keepScreenOn": "Pidä näyttö päällä istuntosivulla", "keepScreenOnSubtitle": "Pidä näyttö hereillä istunnon työskennellessä.", - "activeAgentsSubtitle": "Näytä agenttien määrät widgeteissä ja lukitusnäytöllä.", "appearance": "Ulkoasu", "notifications": "Ilmoitukset", "notificationsSubtitle": "Push-asetukset", diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index 20c6296ebd..c104a1eb83 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Ipakita ang pag-iisip ng agent na naka-expand kapag ito ay natapos.", "keepScreenOn": "Panatilihing naka-on ang screen habang nasa session page", "keepScreenOnSubtitle": "Panatilihing gising ang screen habang gumagawa ang session.", - "activeAgentsSubtitle": "Ipakita ang bilang ng ahente sa iyong mga widget at Lock Screen.", "appearance": "Hitsura", "notifications": "Mga Notipikasyon", "notificationsSubtitle": "Mga kagustuhan sa push", diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index 068fd6e7c0..5cf0f077ae 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -2186,7 +2186,6 @@ "autoExpandThinkingSubtitle": "Afficher la réflexion de l'agent développée lorsqu'elle se termine.", "keepScreenOn": "Garder l'écran allumé sur la page de session", "keepScreenOnSubtitle": "Maintenir l'écran éveillé pendant que la session travaille.", - "activeAgentsSubtitle": "Affiche le nombre d'agents sur vos widgets et l'écran de verrouillage.", "appearance": "Apparence", "notifications": "Notifications", "notificationsSubtitle": "Préférences de push", diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index bbaa640eda..f86727e629 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Taispeáin smaointeoireacht an ghníomhaire leathnaithe nuair a chríochnaíonn sé.", "keepScreenOn": "Coinnigh an scáileán ar siúl ar leathanach an tseisiúin", "keepScreenOnSubtitle": "Coinnigh an scáileán dúisithe agus an seisiún ag obair.", - "activeAgentsSubtitle": "Taispeáin líon na ngníomhaithe ar do ghiuirléidí agus ar an scáileán glasála.", "appearance": "Dealramh", "notifications": "Fógraí", "notificationsSubtitle": "Roghanna brú-fhógraí", diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index 35a873cabd..44528c590b 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Mostra o pensamento do axente expandido cando remate.", "keepScreenOn": "Manter a pantalla acesa na páxina de sesión", "keepScreenOnSubtitle": "Mantén a pantalla esperta mentres a sesión traballa.", - "activeAgentsSubtitle": "Mostra o número de axentes nos trebellos e na pantalla de bloqueo.", "appearance": "Aparencia", "notifications": "Notificacións", "notificationsSubtitle": "Preferencias de notificacións push", diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index c692499b7d..c75136edaf 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "પૂરું થાય ત્યારે એજન્ટનું વિચાર વિસ્તૃત બતાવો.", "keepScreenOn": "સત્ર પૃષ્ઠ પર હોય ત્યારે સ્ક્રીન ચાલુ રાખો", "keepScreenOnSubtitle": "સત્ર કામ કરી રહ્યું હોય ત્યારે સ્ક્રીન જાગતી રાખો.", - "activeAgentsSubtitle": "તમારા વિજેટ્સ અને લોક સ્ક્રીન પર એજન્ટની સંખ્યા બતાવો.", "appearance": "દેખાવ", "notifications": "સૂચનાઓ", "notificationsSubtitle": "પુશ પસંદગીઓ", diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 4436987d39..12b8710d47 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Nuna tunanin wakilin ya fadada idan ya gama.", "keepScreenOn": "Tsare allo a kunne a shafin zama", "keepScreenOnSubtitle": "Rike allo a farke yayin da zama ke aiki.", - "activeAgentsSubtitle": "Nuna adadin wakilai a kan widget da allon kulle.", "appearance": "Kamanni", "notifications": "Sanarwa", "notificationsSubtitle": "Zaɓuɓɓukan turawa", diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index cc3e79065c..24bd97e568 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "הצג את החשיבה של הסוכן מורחבת כשהיא מסתיימת.", "keepScreenOn": "השאר את המסך דלוק בדף ההפעלה", "keepScreenOnSubtitle": "החזק את המסך ער בזמן שההפעלה עובדת.", - "activeAgentsSubtitle": "הצג את מספר הסוכנים בווידג'טים ובמסך הנעילה.", "appearance": "מראה", "notifications": "התראות", "notificationsSubtitle": "העדפות דחיפה", diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 5b3a0f5f02..952b6c6346 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "एजेंट की सोच समाप्त होने पर उसे विस्तारित दिखाएँ।", "keepScreenOn": "सत्र पेज पर स्क्रीन चालू रखें", "keepScreenOnSubtitle": "सत्र के काम करते समय स्क्रीन को जाग्रत रखें।", - "activeAgentsSubtitle": "अपने विजेट और लॉक स्क्रीन पर एजेंट की संख्या दिखाएं।", "appearance": "दिखावट", "notifications": "सूचनाएँ", "notificationsSubtitle": "पुश प्राथमिकताएँ", diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index f84b330d4d..4bfe4d0ae9 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Prikaži agentovo razmišljanje prošireno kada završi.", "keepScreenOn": "Drži zaslon uključen na stranici sesije", "keepScreenOnSubtitle": "Drži zaslon budnim dok sesija radi.", - "activeAgentsSubtitle": "Prikaži broj agenata na widgetima i zaključanom zaslonu.", "appearance": "Izgled", "notifications": "Obavijesti", "notificationsSubtitle": "Push postavke", diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 2d891fcb67..567a3abcc0 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Montre panse ajans lan elaji lè li fini.", "keepScreenOn": "Kenbe ekran an limen sou paj sesyon an", "keepScreenOnSubtitle": "Kenbe ekran an reveye pandan sesyon an ap travay.", - "activeAgentsSubtitle": "Montre kantite ajan sou widgèt ou yo ak ekran fèmen an.", "appearance": "Aparans", "notifications": "Notifikasyon", "notificationsSubtitle": "Preferans pouse", diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index 2718acd105..8b1588af6d 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Az ügynök gondolkodásának kibontott megjelenítése, amikor befejezi.", "keepScreenOn": "Képernyő bekapcsolva tartása a munkamenet oldalán", "keepScreenOnSubtitle": "Tartsa ébren a képernyőt, amíg a munkamenet dolgozik.", - "activeAgentsSubtitle": "Az ügynökök száma megjelenik a modulokon és a lezárási képernyőn.", "appearance": "Megjelenés", "notifications": "Értesítések", "notificationsSubtitle": "Leküldéses beállítások", diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index 9a4c05c1db..f3f9c661c4 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Ցույց տալ գործակալի մտածումը ընդլայնված, երբ այն ավարտվի:", "keepScreenOn": "Էկրանը միացված պահել նիստի էջում", "keepScreenOnSubtitle": "Էկրանն արթուն պահել, մինչ նիստն աշխատում է:", - "activeAgentsSubtitle": "Ցուցադրել գործակալների քանակը վիջեթներում և կողպէկրանին։", "appearance": "Արտաքին տեսք", "notifications": "Ծանուցումներ", "notificationsSubtitle": "Push նախապատվություններ", diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index f9f825ec01..a500119116 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Tampilkan pemikiran agen dalam keadaan diperluas saat selesai.", "keepScreenOn": "Biarkan layar menyala saat di halaman sesi", "keepScreenOnSubtitle": "Jaga layar tetap aktif saat sesi berjalan.", - "activeAgentsSubtitle": "Tampilkan jumlah agen di widget dan Layar Kunci Anda.", "appearance": "Tampilan", "notifications": "Notifikasi", "notificationsSubtitle": "Preferensi push", diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 6555b27610..c5e6e86fd5 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Gosipụta echiche onye ọrụ gbasaa mgbe ọ gwụchara.", "keepScreenOn": "Mee ka enyo nọrọ mgbe nọ na ibe oge", "keepScreenOnSubtitle": "Mee ka enyo nọrọ teta mgbe oge na-arụ ọrụ.", - "activeAgentsSubtitle": "Gosi ọnụọgụ ndị ọrụ na widget gị na Lock Screen.", "appearance": "Ọdịdị", "notifications": "Ọkwa", "notificationsSubtitle": "Mmasị push", diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index b1e7d93a0a..1d34bddf0a 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Sýna hugsun umboðsins stækkaða þegar það lýkur.", "keepScreenOn": "Halda skjá kveiktum á fundarsíðu", "keepScreenOnSubtitle": "Halda skjánum vakandi á meðan fundin er í gangi.", - "activeAgentsSubtitle": "Sýna fjölda kerfa á smáforritum og lásskjá.", "appearance": "Útlit", "notifications": "Tilkynningar", "notificationsSubtitle": "Push-kjörstillingar", diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 0c4956f997..3a03412ac7 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -2256,7 +2256,6 @@ "autoExpandThinkingSubtitle": "Mostra il pensiero dell'agente espanso quando termina.", "keepScreenOn": "Mantieni lo schermo acceso nella pagina della sessione", "keepScreenOnSubtitle": "Tieni lo schermo attivo mentre la sessione lavora.", - "activeAgentsSubtitle": "Mostra il numero di agenti nei widget e nella schermata di blocco.", "appearance": "Aspetto", "notifications": "Notifiche", "notificationsSubtitle": "Preferenze push", diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 4b545b646e..efd3e822a8 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "エージェントの思考が終了したときに展開して表示します。", "keepScreenOn": "セッションページで画面を常時オン", "keepScreenOnSubtitle": "セッションの動作中は画面を起動したままにします。", - "activeAgentsSubtitle": "ウィジェットとロック画面にエージェント数を表示します。", "appearance": "外観", "notifications": "通知", "notificationsSubtitle": "プッシュ設定", diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 382abb8d2d..b644066a82 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "აჩვენე აგენტის ფიქრი გაფართოებულად, როცა დასრულდება.", "keepScreenOn": "ეკრანის ჩართული დატოვება სესიის გვერდზე", "keepScreenOnSubtitle": "შეინახე ეკრანი ჩართული, სანამ სესია მუშაობს.", - "activeAgentsSubtitle": "აჩვენე აგენტების რაოდენობა ვიჯეტებზე და ჩაკეტვის ეკრანზე.", "appearance": "გარეგნობა", "notifications": "შეტყობინებები", "notificationsSubtitle": "Push პარამეტრები", diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 743ca71e1e..5fd40b1cff 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Агент аяқтағанда оның ойлауын ашылған күйде көрсету.", "keepScreenOn": "Сессия бетінде экранды қосулы ұстау", "keepScreenOnSubtitle": "Сессия жұмыс істеп тұрғанда экранды ояу ұстау.", - "activeAgentsSubtitle": "Агент сандарын виджеттерде және құлып экранында көрсету.", "appearance": "Көрініс", "notifications": "Хабарландырулар", "notificationsSubtitle": "Push параметрлері", diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 83af731ecf..7c084da2ad 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "បង្ហាញការគិតរបស់ភ្នាក់ងារពង្រីក នៅពេលវាបញ្ចប់។", "keepScreenOn": "រក្សាអេក្រង់បើក ពេលស្ថិតនៅលើទំព័រសម័យការ", "keepScreenOnSubtitle": "រក្សាអេក្រង់ភ្ញាក់ ពេលសម័យការកំពុងដំណើរការ។", - "activeAgentsSubtitle": "បង្ហាញចំនួនភ្នាក់ងារនៅលើធាតុក្រាហ្វិក និងអេក្រង់ចាក់សោ។", "appearance": "រូបរាង", "notifications": "ការជូនដំណឹង", "notificationsSubtitle": "ចំណូលចិត្តរុញ (push)", diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index c9c9a41795..1c083d7657 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "ಏಜೆಂಟ್ ಮುಗಿಸಿದಾಗ ಅದರ ಯೋಚನೆಯನ್ನು ವಿಸ್ತರಿಸಿ ತೋರಿಸಿ.", "keepScreenOn": "ಸೆಷನ್ ಪುಟದಲ್ಲಿ ಪರದೆಯನ್ನು ಆನ್ ಆಗಿ ಇರಿಸಿ", "keepScreenOnSubtitle": "ಸೆಷನ್ ಕೆಲಸ ಮಾಡುತ್ತಿರುವಾಗ ಪರದೆಯನ್ನು ಎಚ್ಚರವಾಗಿ ಹಿಡಿಯಿರಿ.", - "activeAgentsSubtitle": "ನಿಮ್ಮ ವಿಜೆಟ್‌ಗಳು ಮತ್ತು ಲಾಕ್ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿ ಏಜೆಂಟ್ ಎಣಿಕೆಗಳನ್ನು ತೋರಿಸಿ.", "appearance": "ನೋಟ", "notifications": "ಅಧಿಸೂಚನೆಗಳು", "notificationsSubtitle": "ಪುಶ್ ಆದ್ಯತೆಗಳು", diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 369814b23a..9526dfb3e7 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "에이전트가 완료되면 사고를 펼쳐서 표시합니다.", "keepScreenOn": "세션 페이지에서 화면 켜짐 유지", "keepScreenOnSubtitle": "세션이 작업하는 동안 화면을 깨어 있게 유지합니다.", - "activeAgentsSubtitle": "위젯과 잠금 화면에 에이전트 수를 표시합니다.", "appearance": "모양", "notifications": "알림", "notificationsSubtitle": "푸시 환경설정", diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index e6df3cd89a..3b4de51544 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "ສະແດງການຄິດຂອງເອເຈນໃຫ້ຂະຫຍາຍເມື່ອມັນສຳເລັດ.", "keepScreenOn": "ເປີດໜ້າຈໍໄວ້ໃນຂະນະຢູ່ໜ້າເຊສຊັນ", "keepScreenOnSubtitle": "ຖືໜ້າຈໍໃຫ້ຕື່ນໃນຂະນະທີ່ເຊສຊັນກຳລັງເຮັດວຽກ.", - "activeAgentsSubtitle": "ສະແດງຈຳນວນຕົວແທນຢູ່ວິດເຈັດ ແລະ ໜ້າຈໍລ໊ອກ.", "appearance": "ຮູບລັກສະນະ", "notifications": "ການແຈ້ງເຕືອນ", "notificationsSubtitle": "ການຕັ້ງຄ່າການຜັກດັນ", diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 1de77fad32..1f6db88044 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Parodyti agento mąstymą išskleistą, kai jis baigia.", "keepScreenOn": "Laikyti ekraną įjungtą sesijos puslapyje", "keepScreenOnSubtitle": "Laikyti ekraną įjungtą, kol sesija dirba.", - "activeAgentsSubtitle": "Rodyti agentų skaičius valdikliuose ir užrakto ekrane.", "appearance": "Išvaizda", "notifications": "Pranešimai", "notificationsSubtitle": "Push nuostatos", diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index b331707645..db93325f3a 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Rādīt aģenta domāšanu izvērstu, kad tas beidzas.", "keepScreenOn": "Saglabāt ekrānu ieslēgtu sesijas lapā", "keepScreenOnSubtitle": "Noturēt ekrānu nomodā, kamēr sesija darbojas.", - "activeAgentsSubtitle": "Rādīt aģentu skaitu logrīkos un bloķēšanas ekrānā.", "appearance": "Izskats", "notifications": "Paziņojumi", "notificationsSubtitle": "Push preferences", diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index f2d169a211..0bd5e94de5 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Aseho mivelatra ny fisainan'ny agent rehefa vita.", "keepScreenOn": "Hitehirizana ny efijery mandritra ny fotoam-pivoriana", "keepScreenOnSubtitle": "Tehirizo mifoha ny efijery raha mbola miasa ny fotoam-pivoriana.", - "activeAgentsSubtitle": "Asehoy ny isan'ny mpiasa eo amin'ny widget sy ny efijery mihidy.", "appearance": "Bika", "notifications": "Fampandrenesana", "notificationsSubtitle": "Fika push", diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index c3b2943b2a..e1098abf3d 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Whakaatuhia te whakaaro o te kaiāwhina kia whakawhānuihia ina mutu.", "keepScreenOn": "Kia mārama tonu te mata i te whārangi huinga", "keepScreenOnSubtitle": "Kia mārama tonu te mata i te wā e mahi ana te huinga.", - "activeAgentsSubtitle": "Whakaatu i te maha o ngā kaihoko ki ō taputapu me te mata raka.", "appearance": "Te Āhua", "notifications": "Ngā Pānui", "notificationsSubtitle": "Ngā manakoretanga pana", diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 3a98f3e83a..869c742dca 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Покажи го размислувањето на агентот проширено кога ќе заврши.", "keepScreenOn": "Држи го екранот вклучен на страницата на сесијата", "keepScreenOnSubtitle": "Држи го екранот буден додека сесијата работи.", - "activeAgentsSubtitle": "Прикажувај број на агенти на виџетите и заклучениот екран.", "appearance": "Изглед", "notifications": "Известувања", "notificationsSubtitle": "Преференции за притискање", diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 00e9682071..7632ae7442 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "പൂർത്തിയാകുമ്പോൾ ഏജന്റിന്റെ ചിന്ത വികസിപ്പിച്ച് കാണിക്കുക.", "keepScreenOn": "സെഷൻ പേജിൽ സ്ക്രീൻ ഓണായി നിർത്തുക", "keepScreenOnSubtitle": "സെഷൻ പ്രവർത്തിക്കുമ്പോൾ സ്ക്രീൻ ഉണർന്നിരിക്കുക.", - "activeAgentsSubtitle": "നിങ്ങളുടെ വിജറ്റുകളിലും ലോക്ക് സ്ക്രീനിലും ഏജന്റ് എണ്ണം കാണിക്കുക.", "appearance": "രൂപം", "notifications": "അറിയിപ്പുകൾ", "notificationsSubtitle": "പുഷ് മുൻഗണനകൾ", diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index ccafaf1c42..8e1ea63359 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Дууссаны дараа агентын бодлыг дэлгэсэн байдлаар харуулна.", "keepScreenOn": "Сесс хуудсан дээр дэлгэцийг асаалттай байлгах", "keepScreenOnSubtitle": "Сесс ажиллаж байх үед дэлгэцийг унтраахгүй байлгах.", - "activeAgentsSubtitle": "Агентын тоог виджет болон түгжээний дэлгэц дээр харуулах.", "appearance": "Гадаад байдал", "notifications": "Мэдэгдэлүүд", "notificationsSubtitle": "Түлхэлтийн тохиргоо", diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index bd753c2ffa..2e864796a9 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "एजंट पूर्ण झाल्यावर त्याचा विस्तारित विचार दाखवा.", "keepScreenOn": "सत्र पृष्ठावर असताना स्क्रीन चालू ठेवा", "keepScreenOnSubtitle": "सत्र कार्यरत असताना स्क्रीन जागी ठेवा.", - "activeAgentsSubtitle": "तुमच्या विजेट्स आणि लॉक स्क्रीनवर एजंटची संख्या दाखवा.", "appearance": "स्वरूप", "notifications": "सूचना", "notificationsSubtitle": "पुश प्राधान्ये", diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 19b240c460..f75a32bc14 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Paparkan pemikiran ejen yang dikembangkan apabila ia selesai.", "keepScreenOn": "Kekalkan skrin hidup semasa pada halaman sesi", "keepScreenOnSubtitle": "Kekalkan skrin hidup semasa sesi berfungsi.", - "activeAgentsSubtitle": "Tunjukkan bilangan agen pada widget dan Skrin Kunci anda.", "appearance": "Penampilan", "notifications": "Pemberitahuan", "notificationsSubtitle": "Keutamaan push", diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index 2bc264845c..c16fb359a8 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Uri l-ħsieb tal-aġent espandut meta jispiċċa.", "keepScreenOn": "Żomm l-iskrin mixgħul fil-paġna tas-sessjoni", "keepScreenOnSubtitle": "Żomm l-iskrin imqajjem waqt li s-sessjoni tkun qed taħdem.", - "activeAgentsSubtitle": "Uri l-għadd ta' aġenti fuq il-widgets u l-iskrin imsakkar.", "appearance": "Dehra", "notifications": "Notifiki", "notificationsSubtitle": "Preferenzi tal-push", diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index 7d498ece2c..02eda2f2c6 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "ပြီးဆုံးသောအခါ agent ၏ တွေးခေါ်မှုကို ချဲ့ပြီး ပြပါ။", "keepScreenOn": "ဆက်ရှင်စာမျက်နှာပေါ်တွင် စခရင်ဖွင့်ထားပါ", "keepScreenOnSubtitle": "ဆက်ရှင်အလုပ်လုပ်နေစဉ် စခရင်ကို ဖွင့်ထားပါ။", - "activeAgentsSubtitle": "သင့်ဝိဂျက်များနှင့် လော့ခ်စခရင်ပေါ်တွင် အေးဂျင့်အရေအတွက်ကို ပြပါ။", "appearance": "အသွင်အပြင်", "notifications": "အသိပေးချက်များ", "notificationsSubtitle": "Push နှစ်သက်ချက်များ", diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index dee8ad6546..673beb1da8 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Vis agentens tenking utvidet når den er ferdig.", "keepScreenOn": "Hold skjermen på mens du er på sesjonssiden", "keepScreenOnSubtitle": "Hold skjermen våken mens sesjonen jobber.", - "activeAgentsSubtitle": "Vis antall agenter på widgetene og låseskjermen.", "appearance": "Utseende", "notifications": "Varsler", "notificationsSubtitle": "Push-innstillinger", diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index a61e70e2f5..cc967e15a6 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "समाप्त हुँदा एजेन्टको सोच विस्तार गरेर देखाउनुहोस्।", "keepScreenOn": "सत्र पृष्ठमा रहँदा स्क्रिन खुला राख्नुहोस्", "keepScreenOnSubtitle": "सत्र काम गर्दै गर्दा स्क्रिन जगाएर राख्नुहोस्।", - "activeAgentsSubtitle": "तपाईंका विजेट र लक स्क्रिनमा एजेन्ट संख्या देखाउनुहोस्।", "appearance": "रूप", "notifications": "सूचनाहरू", "notificationsSubtitle": "पुश प्राथमिकताहरू", diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 1a6f4f592c..a54bf55267 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Toon het denken van de agent uitgevouwen wanneer het klaar is.", "keepScreenOn": "Scherm aan houden op de sessiepagina", "keepScreenOnSubtitle": "Houd het scherm wakker terwijl de sessie werkt.", - "activeAgentsSubtitle": "Toon het aantal agents op je widgets en het vergrendelscherm.", "appearance": "Weergave", "notifications": "Meldingen", "notificationsSubtitle": "Pushvoorkeuren", diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index 5ec0e2b0f9..ac8e0d0053 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Yeroo xumuuru, yaada ejentichaa bal'ifame agarsiisi.", "keepScreenOn": "Fuula session irratti yeroo jirtu sareen ifaa eegi", "keepScreenOnSubtitle": "Yeroo session hojiirraa jiru, sareen hiikkaa eegi.", - "activeAgentsSubtitle": "Baay'ina ergamtootaa widgetii keessan fi iskiriinii cufaa irratti agarsiisi.", "appearance": "Bifa", "notifications": "Beeksisota", "notificationsSubtitle": "Filannoo push", diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index fff9f1b9b7..67987a83d3 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "ସମାପ୍ତ ହେଲେ ଏଜେଣ୍ଟର ଚିନ୍ତା ବିସ୍ତାର ହୋଇ ଦେଖାନ୍ତୁ।", "keepScreenOn": "ସେସନ୍ ପୃଷ୍ଠାରେ ଥିବା ସମୟରେ ସ୍କ୍ରିନ୍ ଚାଲୁ ରଖନ୍ତୁ", "keepScreenOnSubtitle": "ସେସନ୍ କାର୍ଯ୍ୟ କରୁଥିବା ସମୟରେ ସ୍କ୍ରିନ୍ ଜାଗ୍ରତ ରଖନ୍ତୁ।", - "activeAgentsSubtitle": "ଆପଣଙ୍କ ୱିଜେଟ ଏବଂ ଲକ ସ୍କ୍ରିନରେ ଏଜେଣ୍ଟ ସଂଖ୍ୟା ଦେଖାନ୍ତୁ।", "appearance": "ରୂପ", "notifications": "ବିଜ୍ଞପ୍ତି", "notificationsSubtitle": "ପୁସ୍ ପସନ୍ଦ", diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 2da0e5f9ec..fac79d4f87 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "ਖਤਮ ਹੋਣ 'ਤੇ ਏਜੰਟ ਦੀ ਸੋਚ ਵਿਸਤਾਰ ਵਿੱਚ ਦਿਖਾਓ।", "keepScreenOn": "ਸੈਸ਼ਨ ਪੰਨੇ 'ਤੇ ਸਕ੍ਰੀਨ ਚਾਲੂ ਰੱਖੋ", "keepScreenOnSubtitle": "ਜਦੋਂ ਸੈਸ਼ਨ ਕੰਮ ਕਰ ਰਿਹਾ ਹੋਵੇ ਸਕ੍ਰੀਨ ਨੂੰ ਜਾਗਦੀ ਰੱਖੋ।", - "activeAgentsSubtitle": "ਆਪਣੇ ਵਿਜੇਟਸ ਅਤੇ ਲਾਕ ਸਕ੍ਰੀਨ ਉੱਤੇ ਏਜੰਟ ਗਿਣਤੀ ਦਿਖਾਓ।", "appearance": "ਦਿੱਖ", "notifications": "ਸੂਚਨਾਵਾਂ", "notificationsSubtitle": "ਪੁਸ਼ ਤਰਜੀਹਾਂ", diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index cae22208c3..789212d4e1 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Pokaż myślenie agenta rozwinięte po zakończeniu.", "keepScreenOn": "Trzymaj ekran włączony na stronie sesji", "keepScreenOnSubtitle": "Utrzymuj ekran włączony, gdy sesja pracuje.", - "activeAgentsSubtitle": "Pokazuj liczbę agentów w widżetach i na ekranie blokady.", "appearance": "Wygląd", "notifications": "Powiadomienia", "notificationsSubtitle": "Preferencje powiadomień push", diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 5ff861860e..1848c3e71d 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "کله چې بشپړ شي د اجنټ فکر پراخ ښودل شوی.", "keepScreenOn": "په ناستې پاڼه کې سکرین فعال وساتئ", "keepScreenOnSubtitle": "پداسې حال کې چې ناسته کار کوي سکرین ویښ وساتئ.", - "activeAgentsSubtitle": "د استازو شمېر په ویجټونو او د بندولو پردې کې وښایه.", "appearance": "بڼه", "notifications": "خبرتیاوې", "notificationsSubtitle": "د پوش ترجیحات", diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index 8d663f6448..fdb445a1e0 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -2256,7 +2256,6 @@ "autoExpandThinkingSubtitle": "Mostrar o raciocínio do agente expandido quando ele terminar.", "keepScreenOn": "Manter a tela ligada na página da sessão", "keepScreenOnSubtitle": "Mantenha a tela ativa enquanto a sessão trabalha.", - "activeAgentsSubtitle": "Mostre a contagem de agentes nos widgets e na tela de bloqueio.", "appearance": "Aparência", "notifications": "Notificações", "notificationsSubtitle": "Preferências de push", diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 795ef77929..d7899b76ff 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Mostrar o raciocínio do agente expandido quando termina.", "keepScreenOn": "Manter o ecrã ligado na página da sessão", "keepScreenOnSubtitle": "Manter o ecrã ativo enquanto a sessão está a trabalhar.", - "activeAgentsSubtitle": "Mostra a contagem de agentes nos widgets e no ecrã de bloqueio.", "appearance": "Aparência", "notifications": "Notificações", "notificationsSubtitle": "Preferências push", diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index eea2d62b24..385a470803 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Afișează gândirea agentului extinsă atunci când termină.", "keepScreenOn": "Menține ecranul pornit pe pagina sesiunii", "keepScreenOnSubtitle": "Menține ecranul activ cât timp sesiunea lucrează.", - "activeAgentsSubtitle": "Afișează numărul de agenți pe widgeturi și pe ecranul de blocare.", "appearance": "Aspect", "notifications": "Notificări", "notificationsSubtitle": "Preferințe push", diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 3d0e6e5514..f5780c9118 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Показывать мышление агента раскрытым по завершении.", "keepScreenOn": "Не выключать экран на странице сеанса", "keepScreenOnSubtitle": "Держать экран включенным, пока сеанс работает.", - "activeAgentsSubtitle": "Показывать количество агентов в виджетах и на экране блокировки.", "appearance": "Внешний вид", "notifications": "Уведомления", "notificationsSubtitle": "Настройки push-уведомлений", diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index dc6766d58f..dde395e880 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "අවසන් වූ විට නියෝජිතයාගේ චින්තනය පුළුල් කර පෙන්වන්න.", "keepScreenOn": "සැසි පිටුවේ සිටින විට තිරය දැල්වෙන්න තබන්න", "keepScreenOnSubtitle": "සැසිය ක්‍රියාත්මක වන අතරතුර තිරය අවදියෙන් තබන්න.", - "activeAgentsSubtitle": "ඔබේ විජට් සහ අගුළු තිරයේ නියෝජිත ගණන පෙන්වන්න.", "appearance": "පෙනුම", "notifications": "දැනුම්දීම්", "notificationsSubtitle": "තෙරපුම් මනාප", diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index 3939581616..47c4cf673b 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Po dokončení zobrazí myslenie agenta rozbalené.", "keepScreenOn": "Ponechať obrazovku zapnutú na stránke relácie", "keepScreenOnSubtitle": "Udržiava obrazovku zapnutú, kým relácia pracuje.", - "activeAgentsSubtitle": "Zobrazovať počty agentov vo widgetoch a na uzamknutej obrazovke.", "appearance": "Vzhľad", "notifications": "Upozornenia", "notificationsSubtitle": "Predvoľby push", diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index e5cc63fdac..0ad9b4176c 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Ko agent konča, prikaži njegovo razmišljanje razširjeno.", "keepScreenOn": "Pusti zaslon prižgan na strani seje", "keepScreenOnSubtitle": "Drži zaslon prižgan, medtem ko seja deluje.", - "activeAgentsSubtitle": "Prikaži število agentov na pripomočkih in zaklenjenem zaslonu.", "appearance": "Videz", "notifications": "Obvestila", "notificationsSubtitle": "Nastavitve potisnih obvestil", diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index e53747aa03..d70c42f4d6 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Muuji fikirka wakiilka oo balaadhan markay dhammeeyaan.", "keepScreenOn": "Shaashadda ha ahaato mid hurdaynaysa bogga fadhiga", "keepScreenOnSubtitle": "Shaashadda u hay oo hurdaynaysa inta fadhigu shaqeynayo.", - "activeAgentsSubtitle": "Muuji tirada wakiillada widget-yadaada iyo shaashadda qufulka.", "appearance": "Muuqaal", "notifications": "Ogaysiisyada", "notificationsSubtitle": "Doorsaarka push", diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index 5a9b1f2556..3e9499f7fa 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Trego të menduarit e agjentit të zgjeruar kur të përfundojë.", "keepScreenOn": "Mbaje ekranin ndezur në faqen e sesionit", "keepScreenOnSubtitle": "Mbaje ekranin zgjuar ndërsa sesioni po punon.", - "activeAgentsSubtitle": "Shfaq numrin e agjentëve në miniaplikacionet dhe ekranin e kyçjes.", "appearance": "Pamja", "notifications": "Njoftimet", "notificationsSubtitle": "Preferencat e shtytjes", diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index c3bf3ff1aa..f87b771f62 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Prikaži prošireno razmišljanje agenta kada završi.", "keepScreenOn": "Zadrži uključen ekran na stranici sesije", "keepScreenOnSubtitle": "Zadrži budan ekran dok sesija radi.", - "activeAgentsSubtitle": "Prikaži broj agenata na vidžetima i zaključanom ekranu.", "appearance": "Izgled", "notifications": "Obaveštenja", "notificationsSubtitle": "Podešavanja push", diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index ba7e3ae8ab..fc0545eede 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Visa agentens tänkande utökat när den är klar.", "keepScreenOn": "Håll skärmen på under sessionen", "keepScreenOnSubtitle": "Håll skärmen vaken medan sessionen arbetar.", - "activeAgentsSubtitle": "Visa antal agenter på widgetar och låsskärmen.", "appearance": "Utseende", "notifications": "Aviseringar", "notificationsSubtitle": "Push-inställningar", diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index 340a476d8a..39d4cd727e 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Onyesha kufikiri kwa wakala kikiwa kimepanuliwa kinapomaliza.", "keepScreenOn": "Weka skrini ikiwa kwenye ukurasa wa kipindi", "keepScreenOnSubtitle": "Weka skrini macho wakati kipindi kinafanya kazi.", - "activeAgentsSubtitle": "Onyesha idadi ya wakala kwenye vijineni na Skrini ya Kufuli.", "appearance": "Muonekano", "notifications": "Arifa", "notificationsSubtitle": "Mapendeleo ya push", diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 1029869b84..6a8c4e6c5d 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "முடிவடையும் போது முகவரின் சிந்தனையை விரிவாகக் காட்டவும்.", "keepScreenOn": "அமர்வு பக்கத்தில் இருக்கும் போது திரையை இயக்கத்தில் வை", "keepScreenOnSubtitle": "அமர்வு வேலை செய்யும் போது திரையை விழித்திருக்க வை.", - "activeAgentsSubtitle": "உங்கள் விட்ஜெட்டுகள் மற்றும் பூட்டுத் திரையில் முகவர் எண்ணிக்கையைக் காட்டு.", "appearance": "தோற்றம்", "notifications": "அறிவிப்புகள்", "notificationsSubtitle": "அழுத்து விருப்பத்தேர்வுகள்", diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index dac27e3e50..04f97a19a1 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "పూర్తయినప్పుడు ఏజెంట్ యొక్క ఆలోచనను విస్తరించి చూపించండి.", "keepScreenOn": "సెషన్ పేజీలో స్క్రీన్ ఆన్లో ఉంచండి", "keepScreenOnSubtitle": "సెషన్ పని చేస్తున్నప్పుడు స్క్రీన్ను మేల్కొని ఉంచండి.", - "activeAgentsSubtitle": "మీ విడ్జెట్‌లు మరియు లాక్ స్క్రీన్‌పై ఏజెంట్ల సంఖ్యను చూపించు.", "appearance": "రూపం", "notifications": "నోటిఫికేషన్లు", "notificationsSubtitle": "పుష్ ప్రాధాన్యతలు", diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index 602a61603a..b31b4352d5 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "แสดงการคิดของเอเจนต์ที่ขยายไว้เมื่อเสร็จสิ้น", "keepScreenOn": "เปิดหน้าจอไว้ขณะอยู่ในหน้าของเซสชัน", "keepScreenOnSubtitle": "ค้างหน้าจอให้ตื่นอยู่ขณะที่เซสชันกำลังทำงาน", - "activeAgentsSubtitle": "แสดงจำนวนเอเจนต์บนวิดเจ็ตและหน้าจอล็อก", "appearance": "ลักษณะที่ปรากฏ", "notifications": "การแจ้งเตือน", "notificationsSubtitle": "การตั้งค่าการแจ้งเตือนแบบพุช", diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index f83e38f307..b3748d2277 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Ajanın düşünmesi bittiğinde genişletilmiş olarak göster.", "keepScreenOn": "Oturum sayfasındayken ekranı açık tut", "keepScreenOnSubtitle": "Oturum çalışırken ekranı uyanık tut.", - "activeAgentsSubtitle": "Aracı sayılarını widget'larda ve kilit ekranında göster.", "appearance": "Görünüm", "notifications": "Bildirimler", "notificationsSubtitle": "Anlık bildirim tercihleri", diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index f2c778aff7..c1ea0d0b8a 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Показувати мислення агента розгорнутим після завершення.", "keepScreenOn": "Тримати екран увімкненим на сторінці сеансу", "keepScreenOnSubtitle": "Тримати екран активним, поки сеанс працює.", - "activeAgentsSubtitle": "Показувати кількість агентів у віджетах і на екрані блокування.", "appearance": "Зовнішній вигляд", "notifications": "Сповіщення", "notificationsSubtitle": "Налаштування push", diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 9b82d9597f..1acf081d85 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "ایجنٹ مکمل ہونے پر اس کی سوچ کو پھیلا کر دکھائیں۔", "keepScreenOn": "سیشن صفحہ پر اسکرین کو آن رکھیں", "keepScreenOnSubtitle": "سیشن کام کرتے وقت اسکرین کو جاگتی رکھیں۔", - "activeAgentsSubtitle": "اپنے ویجٹس اور لاک اسکرین پر ایجنٹ کی تعداد دکھائیں۔", "appearance": "ظاہری شکل", "notifications": "اطلاعیں", "notificationsSubtitle": "پش ترجیحات", diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 265a3e5be8..41b6914520 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Tugaganda agentning fikrlashini kengaytirilgan holda ko'rsatish.", "keepScreenOn": "Seans sahifasida ekranni yoqilgan holda ushlab turish", "keepScreenOnSubtitle": "Seans ishlayotganda ekranni yoqilgan holda ushlab turish.", - "activeAgentsSubtitle": "Agentlar sonini vidjetlarda va qulflash ekranida ko'rsatish.", "appearance": "Ko'rinish", "notifications": "Bildirishnomalar", "notificationsSubtitle": "Push sozlamalari", diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 351d35e1aa..c47f917208 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Hiển thị suy nghĩ của tác nhân được mở rộng khi hoàn tất.", "keepScreenOn": "Giữ màn hình bật khi ở trang phiên", "keepScreenOnSubtitle": "Giữ màn hình bật khi phiên đang hoạt động.", - "activeAgentsSubtitle": "Hiển thị số lượng tác nhân trên tiện ích và Màn hình khóa.", "appearance": "Giao diện", "notifications": "Thông báo", "notificationsSubtitle": "Tùy chọn đẩy", diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index d980f3127a..a76f7e4040 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Fi ironu aṣoju han ti o fẹ̀ nigbati o ba pari.", "keepScreenOn": "Pa iboju mọ́ nigba ti o ba wa lori oju-iwe akoko", "keepScreenOnSubtitle": "Mu iboju ṣiṣẹ nigba ti akoko n ṣiṣẹ.", - "activeAgentsSubtitle": "Fi iye awọn aṣoju han lori awọn wíjẹ́tì àti Ojú-ìwé títìí.", "appearance": "Irisi", "notifications": "Awọn ifitonileti", "notificationsSubtitle": "Awọn ààyò titari", diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index 3fd941a508..9da8ae399f 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "代理完成时展开显示其思考过程。", "keepScreenOn": "在会话页面保持屏幕常亮", "keepScreenOnSubtitle": "会话运行期间保持屏幕唤醒。", - "activeAgentsSubtitle": "在小组件和锁定屏幕上显示代理数量。", "appearance": "外观", "notifications": "通知", "notificationsSubtitle": "推送偏好设置", diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index fd880073aa..941289a140 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "代理完成時,以展開狀態顯示其思考內容。", "keepScreenOn": "在工作階段頁面保持螢幕開啟", "keepScreenOnSubtitle": "工作階段運作時保持螢幕喚醒。", - "activeAgentsSubtitle": "在小工具與鎖定畫面上顯示代理程式數量。", "appearance": "外觀", "notifications": "通知", "notificationsSubtitle": "推播偏好設定", diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index a103a0bf90..785c785642 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -144,7 +144,6 @@ "autoExpandThinkingSubtitle": "Khombisa ukucabanga kwe-agent kuvulekile uma kuqeda.", "keepScreenOn": "Gcina isikrini sivuliwe usekhasini leseshini", "keepScreenOnSubtitle": "Gcina isikrini sivuliwe ngenkathi iseshini isebenza.", - "activeAgentsSubtitle": "Bonisa inani lama-ejenti kuma-widget nakusikrini sokukhiya.", "appearance": "Ukubukeka", "notifications": "Izaziso", "notificationsSubtitle": "Izintandokazi zokudonsa", diff --git a/apps/mobile/src/lib/auth/auth-context.test.tsx b/apps/mobile/src/lib/auth/auth-context.test.tsx index 53180ff0c5..45e4a241f2 100644 --- a/apps/mobile/src/lib/auth/auth-context.test.tsx +++ b/apps/mobile/src/lib/auth/auth-context.test.tsx @@ -203,19 +203,13 @@ vi.mock('@/lib/hooks/use-persisted-agent-model', () => ({ clearAgentModelPreference: vi.fn(), })); -const { - clearKeepScreenOnPreference, - clearReasoningPreference, - clearPrReviewFooterPreference, - clearGlanceablePreference, -} = vi.hoisted(() => ({ - clearKeepScreenOnPreference: vi.fn(), - clearReasoningPreference: vi.fn(), - clearPrReviewFooterPreference: vi.fn(), - clearGlanceablePreference: vi.fn(), -})); +const { clearKeepScreenOnPreference, clearReasoningPreference, clearPrReviewFooterPreference } = + vi.hoisted(() => ({ + clearKeepScreenOnPreference: vi.fn(), + clearReasoningPreference: vi.fn(), + clearPrReviewFooterPreference: vi.fn(), + })); vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ clearKeepScreenOnPreference })); -vi.mock('@/lib/hooks/use-glanceable-preference', () => ({ clearGlanceablePreference })); vi.mock('@/lib/hooks/use-reasoning-preference', () => ({ clearReasoningPreference })); @@ -569,7 +563,6 @@ describe('sign-out teardown ordering', () => { }); expect(clearKeepScreenOnPreference).toHaveBeenCalled(); - expect(clearGlanceablePreference).toHaveBeenCalled(); expect(clearReasoningPreference).toHaveBeenCalled(); expect(clearPrReviewFooterPreference).toHaveBeenCalled(); }); diff --git a/apps/mobile/src/lib/auth/auth-context.tsx b/apps/mobile/src/lib/auth/auth-context.tsx index 7313313a51..b0840a71aa 100644 --- a/apps/mobile/src/lib/auth/auth-context.tsx +++ b/apps/mobile/src/lib/auth/auth-context.tsx @@ -44,7 +44,6 @@ import { } from '@/lib/auth/token-owner'; import { chainSave } from '@/lib/hooks/save-chain'; import { clearAgentModelPreference } from '@/lib/hooks/use-persisted-agent-model'; -import { clearGlanceablePreference } from '@/lib/hooks/use-glanceable-preference'; import { clearKeepScreenOnPreference } from '@/lib/hooks/use-keep-screen-on-preference'; import { clearPrReviewFooterPreference } from '@/lib/hooks/use-pr-review-footer-preference'; import { clearReasoningPreference } from '@/lib/hooks/use-reasoning-preference'; @@ -393,7 +392,6 @@ export function AuthProvider({ children }: { readonly children: ReactNode }) { clearKeepScreenOnPreference(); clearSessionScopedState(); clearPrReviewFooterPreference(); - clearGlanceablePreference(); } finally { queryClient.clear(); setSessionEnded(ended); diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts index 2bd49df932..d3ee8b51e7 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -10,7 +10,6 @@ import { clearActivityKitDeniedIfAvailable, getActivityKitDenied } from '@/glanc import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; import { getLastGlanceableSnapshot, getLocalScopeKey } from '@/lib/glanceable/persist'; -import { readGlanceableEnabled } from '@/lib/glanceable/enabled'; import { forEachSink } from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { i18n } from '@/i18n'; @@ -23,17 +22,13 @@ import { i18n } from '@/i18n'; let alertShown = false; -export async function showActivityKitDisabledAlertOnce(): Promise { +export function showActivityKitDisabledAlertOnce(): void { if (Platform.OS !== 'ios' || alertShown) { return; } if (!getActivityKitDenied()) { return; } - // Never ask for an OS permission the user turned the feature off for. - if (!(await readGlanceableEnabled())) { - return; - } alertShown = true; Alert.alert( i18n.t('glanceable.activityKitDisabledTitle'), @@ -54,9 +49,6 @@ export async function recoverGlanceableActivityKit(): Promise { if (Platform.OS !== 'ios' || !getActivityKitDenied()) { return; } - if (!(await readGlanceableEnabled())) { - return; - } const authEpoch = currentAuthEpoch(); const blankEpoch = getTerminalBlankEpoch(); const scopeKey = getLocalScopeKey(); diff --git a/apps/mobile/src/lib/glanceable/cleanup.test.ts b/apps/mobile/src/lib/glanceable/cleanup.test.ts index b79e34efd5..40bc043b2f 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.test.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.test.ts @@ -174,7 +174,7 @@ describe('cleanup', () => { running: 2, needsInput: 1, idle: 1, - eligibleStartedAt: '2026-08-26T23:00:00.000Z', + needsInputSince: '2026-08-26T23:00:00.000Z', }; _setLastGlanceableSnapshotForTests(seeded); @@ -202,7 +202,7 @@ describe('cleanup', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }); } } finally { @@ -225,7 +225,7 @@ describe('cleanup', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }; _setLastGlanceableSnapshotForTests(terminal); @@ -243,7 +243,7 @@ describe('cleanup', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }); } } finally { diff --git a/apps/mobile/src/lib/glanceable/cleanup.ts b/apps/mobile/src/lib/glanceable/cleanup.ts index 25aac19cb3..18345ea1b7 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.ts @@ -69,7 +69,7 @@ function buildTerminalSnapshot(status: 'signed_out' | 'privacy'): GlanceableAgen running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }; } diff --git a/apps/mobile/src/lib/glanceable/enabled.test.ts b/apps/mobile/src/lib/glanceable/enabled.test.ts deleted file mode 100644 index 78810f8815..0000000000 --- a/apps/mobile/src/lib/glanceable/enabled.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { - parseGlanceableEnabled, - readGlanceableEnabled, - serializeGlanceableEnabled, -} from './enabled'; - -const { getItemAsync } = vi.hoisted(() => ({ getItemAsync: vi.fn() })); -vi.mock('expo-secure-store', () => ({ getItemAsync })); - -describe('parseGlanceableEnabled', () => { - it.each([ - [null, true], - ['true', true], - ['', true], - ['nonsense', true], - ['false', false], - ])('reads %j as %s', (raw, expected) => { - expect(parseGlanceableEnabled(raw)).toBe(expected); - }); - - it('round-trips both states', () => { - expect(parseGlanceableEnabled(serializeGlanceableEnabled(false))).toBe(false); - expect(parseGlanceableEnabled(serializeGlanceableEnabled(true))).toBe(true); - }); -}); - -describe('readGlanceableEnabled', () => { - beforeEach(() => { - getItemAsync.mockReset(); - }); - - it('reads the stored switch', async () => { - getItemAsync.mockResolvedValue('false'); - expect(await readGlanceableEnabled()).toBe(false); - expect(getItemAsync).toHaveBeenCalledWith('glanceable-surfaces-enabled'); - }); - - it('keeps the surfaces on when the read fails', async () => { - getItemAsync.mockRejectedValue(new Error('storage unavailable')); - expect(await readGlanceableEnabled()).toBe(true); - }); -}); diff --git a/apps/mobile/src/lib/glanceable/enabled.ts b/apps/mobile/src/lib/glanceable/enabled.ts deleted file mode 100644 index c6b1955778..0000000000 --- a/apps/mobile/src/lib/glanceable/enabled.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as SecureStore from 'expo-secure-store'; - -import { GLANCEABLE_ENABLED_KEY } from '@/lib/storage-keys'; - -/** - * Master switch for the Active Agents glanceable surfaces, kept free of the - * preference store's toast and Sentry imports so the publisher, the background - * push, and the ActivityKit prompt can read it without loading that graph. - * - * Default-on: only the exact stored string 'false' turns the surfaces off, so a - * missing or unreadable value keeps the behavior the app ships with. The OS - * gates (iOS Live Activities, Android notification permission) stay in force - * above this switch. - */ -export function parseGlanceableEnabled(raw: string | null): boolean { - return raw !== 'false'; -} - -export function serializeGlanceableEnabled(value: boolean): string { - return value ? 'true' : 'false'; -} - -/** - * Disk read for callers with no React state, including the headless background - * push whose process starts with the in-memory default. - */ -export async function readGlanceableEnabled(): Promise { - try { - return parseGlanceableEnabled(await SecureStore.getItemAsync(GLANCEABLE_ENABLED_KEY)); - } catch { - return true; - } -} diff --git a/apps/mobile/src/lib/glanceable/mount.tsx b/apps/mobile/src/lib/glanceable/mount.tsx index a288117bf9..f50680f6a9 100644 --- a/apps/mobile/src/lib/glanceable/mount.tsx +++ b/apps/mobile/src/lib/glanceable/mount.tsx @@ -7,7 +7,6 @@ import { } from '@/lib/active-sessions-live'; import { useAuth } from '@/lib/auth/auth-context'; import { useCurrentUserId } from '@/lib/hooks/use-current-user-id'; -import { useGlanceablePreference } from '@/lib/hooks/use-glanceable-preference'; import { useOrganization } from '@/lib/organization-context'; import { useTRPC } from '@/lib/trpc'; @@ -16,7 +15,7 @@ import { persistGlanceableSink, restorePersistedGlanceable, } from './persist'; -import { getTerminalBlankEpoch, writePrivacySnapshotAndEnd } from './cleanup'; +import { getTerminalBlankEpoch } from './cleanup'; import { GlanceablePublisher } from './publisher'; import { getGlanceableSinks, registerGlanceableSink } from './sink-registry'; @@ -36,7 +35,6 @@ export function GlanceablePublisherMount(): null { const { organizationId, isLoaded } = useOrganization(); const { token } = useAuth(); const { userId } = useCurrentUserId(); - const { glanceableEnabled, hasLoaded: glanceableLoaded } = useGlanceablePreference(); const input = useMemo(() => buildActiveSessionsTrayInput(organizationId), [organizationId]); const queryKey = useMemo(() => trpc.activeSessions.list.queryKey(input), [trpc, input]); @@ -44,17 +42,6 @@ export function GlanceablePublisherMount(): null { const signedIn = token != null; - // The one place the master switch is applied. Turning it off blanks every - // surface and unregisters its push tokens, so the server stops targeting this - // device; the publisher effect below then refuses to subscribe. Blanking also - // runs on a launch that is already off, which clears a surface left behind by - // a build that had no switch. - useEffect(() => { - if (glanceableLoaded && !glanceableEnabled) { - writePrivacySnapshotAndEnd(); - } - }, [glanceableEnabled, glanceableLoaded]); - // Populate the persisted last snapshot once so cleanup/org-fence can see it, // and so the publisher below seeds its revision from the persisted value. const [restored, setRestored] = useState(false); @@ -73,14 +60,7 @@ export function GlanceablePublisherMount(): null { }, []); useEffect(() => { - if ( - !isLoaded || - !signedIn || - userId === undefined || - !restored || - !glanceableLoaded || - !glanceableEnabled - ) { + if (!isLoaded || !signedIn || userId === undefined || !restored) { return undefined; } @@ -121,18 +101,7 @@ export function GlanceablePublisherMount(): null { unsubscribe(); publisher.dispose(); }; - }, [ - queryClient, - queryKey, - targetHash, - isLoaded, - signedIn, - userId, - organizationId, - restored, - glanceableEnabled, - glanceableLoaded, - ]); + }, [queryClient, queryKey, targetHash, isLoaded, signedIn, userId, organizationId, restored]); return null; } diff --git a/apps/mobile/src/lib/glanceable/presentation.ts b/apps/mobile/src/lib/glanceable/presentation.ts index 91cf3198e6..ad8b60003d 100644 --- a/apps/mobile/src/lib/glanceable/presentation.ts +++ b/apps/mobile/src/lib/glanceable/presentation.ts @@ -40,23 +40,27 @@ const COUNT_ORDER: readonly { key: GlanceableCountKey; kind: GlanceableCountKind { key: 'glanceable.idle', kind: 'idle' }, ]; -/** Every non-zero count in rank order (expanded, medium, large, spoken). */ +/** + * All three counts in rank order, zeros included. + * + * A zero row still draws: dropping it would move every remaining row as work + * changes state, and a surface the user only glances at must not reflow. The + * surfaces show these rows only while some work exists — a snapshot with three + * zeros carries the `empty` status and draws its status line instead. + */ export function glanceableCountLines(snapshot: GlanceableAgentsSnapshot): GlanceableCountLine[] { - const lines: GlanceableCountLine[] = []; - for (const { key, kind } of COUNT_ORDER) { - const count = snapshot[kind]; - if (count > 0) { - lines.push({ key, kind, count }); - } - } - return lines; + return COUNT_ORDER.map(({ key, kind }) => ({ key, kind, count: snapshot[kind] })); } -/** The single ranked count for compact surfaces; null when nothing is eligible. */ +/** + * The single ranked count for compact surfaces; null when nothing is eligible. + * Zero rows are skipped here: one number on the Dynamic Island must be a + * number worth showing. + */ export function primaryGlanceableCount( snapshot: GlanceableAgentsSnapshot ): GlanceableCountLine | null { - return glanceableCountLines(snapshot)[0] ?? null; + return glanceableCountLines(snapshot).find(line => line.count > 0) ?? null; } export type GlanceableSurfaceFlags = { @@ -101,7 +105,9 @@ export function glanceableSpokenLabelKeys( const status = resolveGlanceableStatus(snapshot, flags); const parts: string[] = []; if (status === 'happy' || status === 'stale') { - for (const { key } of glanceableCountLines(snapshot)) { + // Zeros draw on the surfaces to hold the layout still, but "0 Working" is + // only noise to a screen reader, so the spoken label keeps the real counts. + for (const { key } of glanceableCountLines(snapshot).filter(line => line.count > 0)) { parts.push(key); } } else { @@ -123,7 +129,7 @@ export function glanceableSpokenLabel( parts.push(translate(GLANCEABLE_STATUS_COPY_KEY[status])); } if (status === 'happy' || status === 'stale') { - for (const { key, count } of glanceableCountLines(snapshot)) { + for (const { key, count } of glanceableCountLines(snapshot).filter(line => line.count > 0)) { parts.push(`${count} ${translate(key)}`); } } diff --git a/apps/mobile/src/lib/glanceable/publisher.test.ts b/apps/mobile/src/lib/glanceable/publisher.test.ts index 5dcc0f34fa..0845573591 100644 --- a/apps/mobile/src/lib/glanceable/publisher.test.ts +++ b/apps/mobile/src/lib/glanceable/publisher.test.ts @@ -176,7 +176,7 @@ describe('GlanceablePublisher', () => { idle: expectedCount, }); } - expect(lastSnapshot(calls, 'publish').eligibleStartedAt).toBeNull(); + expect(lastSnapshot(calls, 'publish').needsInputSince).toBeNull(); publisher.dispose(); }); diff --git a/apps/mobile/src/lib/glanceable/publisher.ts b/apps/mobile/src/lib/glanceable/publisher.ts index 6b2e136cb1..13709d2d8d 100644 --- a/apps/mobile/src/lib/glanceable/publisher.ts +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -5,6 +5,7 @@ import { GLANCEABLE_TERMINAL_MS, type GlanceableAgentsSnapshot, type GlanceableAgentsSnapshotStatus, + type GlanceableSessionRow, isEligibleGlanceableWork, shouldDiscardGlanceableRevision, } from '@kilocode/app-shared/glanceable-agents-snapshot'; @@ -60,7 +61,7 @@ export function withStatus( ...snapshot, revision: snapshot.revision + 1, status: expired ? 'expired' : 'stale', - ...(expired ? { running: 0, needsInput: 0, idle: 0, eligibleStartedAt: null } : {}), + ...(expired ? { running: 0, needsInput: 0, idle: 0, needsInputSince: null } : {}), }; } const updatedAt = new Date(now).toISOString(); @@ -101,7 +102,7 @@ export class GlanceablePublisher { } /** Cache success: derive the next snapshot from the current session rows. */ - handleSessions(sessions: readonly { status: string }[], ctx: GlanceablePublisherContext): void { + handleSessions(sessions: readonly GlanceableSessionRow[], ctx: GlanceablePublisherContext): void { if (this.isGated()) { return; } @@ -115,7 +116,6 @@ export class GlanceablePublisher { organizationId: ctx.organizationId, now, previousRevision: this.current?.revision ?? 0, - previousEligibleStartedAt: this.current?.eligibleStartedAt ?? null, }); if (isEligibleGlanceableWork(snapshot)) { diff --git a/apps/mobile/src/lib/hooks/use-glanceable-preference.ts b/apps/mobile/src/lib/hooks/use-glanceable-preference.ts deleted file mode 100644 index 916dc13db6..0000000000 --- a/apps/mobile/src/lib/hooks/use-glanceable-preference.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useSyncExternalStore } from 'react'; - -import { parseGlanceableEnabled, serializeGlanceableEnabled } from '@/lib/glanceable/enabled'; -import { createSecureStorePreference } from '@/lib/hooks/secure-store-preference'; -import { GLANCEABLE_ENABLED_KEY } from '@/lib/storage-keys'; - -/** Reactive view of the Active Agents master switch; see `glanceable/enabled`. */ -const store = createSecureStorePreference({ - key: GLANCEABLE_ENABLED_KEY, - defaultValue: true, - parse: parseGlanceableEnabled, - serialize: serializeGlanceableEnabled, -}); - -export function clearGlanceablePreference() { - store.clear(); -} - -function setGlanceableEnabled(value: boolean) { - store.set(value); -} - -export function useGlanceablePreference() { - const glanceableEnabled = useSyncExternalStore(store.subscribe, store.get); - const hasLoaded = useSyncExternalStore(store.subscribe, store.getHasLoaded); - return { glanceableEnabled, hasLoaded, setGlanceableEnabled }; -} diff --git a/apps/mobile/src/lib/notification-path.test.ts b/apps/mobile/src/lib/notification-path.test.ts index 421b0f0377..20cc0700c2 100644 --- a/apps/mobile/src/lib/notification-path.test.ts +++ b/apps/mobile/src/lib/notification-path.test.ts @@ -69,7 +69,7 @@ describe('notificationPathForData', () => { idle: 0, updatedAt: '2026-01-01T00:00:00.000Z', expiresAt: '2026-01-01T08:00:00.000Z', - eligibleStartedAt: '2026-01-01T00:00:00.000Z', + needsInputSince: '2026-01-01T00:00:00.000Z', }) ).toBe('/(app)/(tabs)/(2_agents)'); }); diff --git a/apps/mobile/src/lib/notifications.test.ts b/apps/mobile/src/lib/notifications.test.ts index 661cc8c117..ed2c83d4ba 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -20,11 +20,7 @@ import { registerGlanceableSink, unregisterGlanceableSink, } from '@/lib/glanceable/sink-registry'; -import { - ACTIVE_USER_ID_KEY, - GLANCEABLE_ENABLED_KEY, - ORGANIZATION_STORAGE_KEY, -} from '@/lib/storage-keys'; +import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; import { _setGlanceableSinksLoaderForTests, applyGlanceablePushData, @@ -432,7 +428,7 @@ function glanceableSnapshot( running: 1, needsInput: 0, idle: 0, - eligibleStartedAt: '2026-01-01T00:00:00.000Z', + needsInputSince: '2026-01-01T00:00:00.000Z', ...overrides, }; } @@ -549,28 +545,6 @@ describe('applyGlanceablePushData', () => { unregisterGlanceableSink(sink); }); - it('drops a remote snapshot while the Active Agents switch is off', async () => { - mocks.getItemAsync.mockImplementation((key: string) => { - if (key === GLANCEABLE_ENABLED_KEY) { - return 'false'; - } - return key === ACTIVE_USER_ID_KEY ? 'u1' : 'org-9'; - }); - _setLastGlanceableSnapshotForTests(glanceableSnapshot({ scopeKey: SCOPE_KEY, revision: 1 })); - const sink = makeFakeSink(); - registerGlanceableSink(sink); - - const result = await applyGlanceablePushData( - activeGlanceablePush({ scopeKey: SCOPE_KEY, updatedAt: '2026-01-03T00:00:00.000Z' }) - ); - - expect(result).toBe(false); - expect(sink.publish).not.toHaveBeenCalled(); - expect(sink.startOrUpdate).not.toHaveBeenCalled(); - - unregisterGlanceableSink(sink); - }); - it('applies a newer remote snapshot and re-registers under the selected organization', async () => { _setLastGlanceableSnapshotForTests( glanceableSnapshot({ @@ -621,7 +595,7 @@ describe('applyGlanceablePushData', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }) ); @@ -643,7 +617,7 @@ describe('applyGlanceablePushData', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }) ); @@ -673,7 +647,7 @@ describe('applyGlanceablePushData', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }) ); await applyGlanceablePushData( @@ -860,7 +834,7 @@ describe('glanceable publication storage fences', () => { updatedAt: '2026-01-02T00:00:00.000Z', running, status: running === 0 ? 'empty' : 'happy', - eligibleStartedAt: running === 0 ? null : '2026-01-01T00:00:00.000Z', + needsInputSince: running === 0 ? null : '2026-01-01T00:00:00.000Z', }) ); await read.started; @@ -1235,7 +1209,7 @@ describe('cold iOS background delivery', () => { updatedAt: '2026-01-02T00:00:01.000Z', status: 'empty', running: 0, - eligibleStartedAt: null, + needsInputSince: null, }); await vi.advanceTimersByTimeAsync(0); expect(native.policies).toEqual([]); @@ -1258,7 +1232,7 @@ describe('cold iOS background delivery', () => { updatedAt: '2026-01-02T00:00:03.000Z', status: 'empty', running: 0, - eligibleStartedAt: null, + needsInputSince: null, }) ).resolves.toBe(0); @@ -1282,7 +1256,7 @@ describe('cold iOS background delivery', () => { updatedAt: '2026-01-02T00:00:01.000Z', status: 'empty', running: 0, - eligibleStartedAt: null, + needsInputSince: null, }); const rejected = expect(applying).rejects.toThrow(); await vi.advanceTimersByTimeAsync(0); @@ -1310,7 +1284,7 @@ describe('cold iOS background delivery', () => { updatedAt: '2026-01-02T00:00:02.000Z', status: 'empty', running: 0, - eligibleStartedAt: null, + needsInputSince: null, }) ).resolves.toBe(0); expect(native.policies).toEqual(['immediate']); @@ -1355,7 +1329,7 @@ describe('cold iOS background delivery', () => { return { success: true }; }); const background = await loadColdBackground(); - const applying = background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null }); + const applying = background.deliver({ status: 'empty', running: 0, needsInputSince: null }); await vi.advanceTimersByTimeAsync(0); expect(native.exists).toBe(true); read.resolve(); @@ -1383,7 +1357,7 @@ describe('cold iOS background delivery', () => { const result = await background.deliver({ status: 'empty', running: 0, - eligibleStartedAt: null, + needsInputSince: null, }); completed = true; return result; @@ -1403,14 +1377,14 @@ describe('cold iOS background delivery', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }); expect(rows.has('scope-token')).toBe(true); }); it('immediately dismisses an ended adopted handle and rejects old-scope work after privacy', async () => { const background = await loadColdBackground(); - expect(await background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null })).toBe( + expect(await background.deliver({ status: 'empty', running: 0, needsInputSince: null })).toBe( 0 ); expect(native.dismissAt).toBe(Date.now() + 8000); @@ -1432,7 +1406,7 @@ describe('cold iOS background delivery', () => { const end = deferred(); native.endRead = end.promise; const background = await loadColdBackground(); - const applying = background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null }); + const applying = background.deliver({ status: 'empty', running: 0, needsInputSince: null }); await vi.advanceTimersByTimeAsync(0); background.blank.writeSignedOutSnapshotAndEnd(); end.resolve(); @@ -1449,7 +1423,7 @@ describe('cold iOS background delivery', () => { rows.set(native.token, { kind: 'ios_activity', organizationId: 'org-9' }); mocks.unregisterActivityToken.mockRejectedValueOnce(new Error('network unavailable')); const background = await loadColdBackground(); - expect(await background.deliver({ status: 'empty', running: 0, eligibleStartedAt: null })).toBe( + expect(await background.deliver({ status: 'empty', running: 0, needsInputSince: null })).toBe( 0 ); await background.cleanup.awaitActivityCleanupSettled(); diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index 8e363c7089..8abc51d8b6 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -27,7 +27,6 @@ import { import { captureEvent } from '@/lib/analytics/posthog'; import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; -import { readGlanceableEnabled } from '@/lib/glanceable/enabled'; import { getLastGlanceableSnapshot, getLocalScopeKey, @@ -134,12 +133,6 @@ export async function applyGlanceablePushData( return false; } - // The headless process starts with the in-memory default, so read the switch - // from disk. Dropping the push leaves the surfaces the in-app blank produced. - if (!(await readGlanceableEnabled())) { - return false; - } - const organizationId = await getSelectedOrganizationId(); const userId = await getActiveUserId(); if ( diff --git a/apps/mobile/src/lib/storage-keys.ts b/apps/mobile/src/lib/storage-keys.ts index a447e72424..f45849d6b6 100644 --- a/apps/mobile/src/lib/storage-keys.ts +++ b/apps/mobile/src/lib/storage-keys.ts @@ -38,7 +38,6 @@ export const TRUSTED_HOSTS_KEY = 'trusted-hosts'; export const PR_REVIEW_FOOTER_KEY = 'pr-review-footer-enabled'; /** Master switch for the glanceable Active Agents surfaces (widgets, Live Activity, * Android ongoing). Off blanks every surface and unregisters its push tokens. */ -export const GLANCEABLE_ENABLED_KEY = 'glanceable-surfaces-enabled'; /** SQLCipher database key for the encrypted persistence store (DEC-01). */ export const PERSIST_DB_KEY = 'persist-db-key'; /** diff --git a/apps/web/src/lib/active-sessions-list.ts b/apps/web/src/lib/active-sessions-list.ts index 8da46f1b06..5e40080833 100644 --- a/apps/web/src/lib/active-sessions-list.ts +++ b/apps/web/src/lib/active-sessions-list.ts @@ -32,6 +32,14 @@ export const activeSessionSchema = z.object({ * the column is NULL or the row was never enriched. */ lastActivityAt: z.string().optional(), + /** + * When this session's status last changed, from + * `cli_sessions_v2.status_updated_at`, normalized to ISO 8601. Omitted when + * the column is NULL, unparseable, or the row was never enriched. The + * glanceable snapshot reads it to report how long the longest-waiting agent + * has needed input, and Hermes only parses the ISO form. + */ + statusUpdatedAt: z.string().optional(), /** * Capabilities advertised by the CLI connection that owns this session. * Omitted when the owning connection's latest heartbeat did not include a @@ -94,6 +102,7 @@ type EnrichmentRow = { title: string | null; organization_id: string | null; last_activity_at: string | null; + status_updated_at: string | null; total_cost_microdollars: number | null; // Session's own stored PR link, aliased so it never collides with the // cache keys below. @@ -162,6 +171,18 @@ export function resolveActiveSessionStatus( return liveStatus; } +/** + * Normalize a raw `timestamptz` text to ISO 8601, or null when it will not + * parse. Hermes rejects the Postgres form (`2026-09-02 17:28:02.242039+00`), + * so a field a React Native client passes to `Date` must be converted here. + * The older timestamp fields stay raw: their consumers already handle the + * Postgres form and changing them would be a wire change with no reader. + */ +function toIsoTimestamp(value: string): string | null { + const at = Date.parse(value); + return Number.isNaN(at) ? null : new Date(at).toISOString(); +} + function mapEnrichedHeartbeatSession( session: ActiveSession, row: EnrichmentRow | undefined @@ -189,6 +210,11 @@ function mapEnrichedHeartbeatSession( if (row.last_activity_at != null) { mapped.lastActivityAt = row.last_activity_at; } + const statusUpdatedAt = + row.status_updated_at == null ? null : toIsoTimestamp(row.status_updated_at); + if (statusUpdatedAt !== null) { + mapped.statusUpdatedAt = statusUpdatedAt; + } if (row.total_cost_microdollars != null) { mapped.totalCostMicrodollars = row.total_cost_microdollars; } @@ -219,6 +245,11 @@ function mapCloudCandidateRow(row: CloudCandidateRow): ActiveSession { if (row.last_activity_at != null) { mapped.lastActivityAt = row.last_activity_at; } + const statusUpdatedAt = + row.status_updated_at == null ? null : toIsoTimestamp(row.status_updated_at); + if (statusUpdatedAt !== null) { + mapped.statusUpdatedAt = statusUpdatedAt; + } if (row.total_cost_microdollars != null) { mapped.totalCostMicrodollars = row.total_cost_microdollars; } @@ -320,6 +351,7 @@ export async function listActiveSessions({ title: cli_sessions_v2.title, organization_id: cli_sessions_v2.organization_id, last_activity_at: cli_sessions_v2.last_activity_at, + status_updated_at: cli_sessions_v2.status_updated_at, total_cost_microdollars: cli_sessions_v2.total_cost_microdollars, session_pr_platform: cli_sessions_v2.platform, session_pr_url: cli_sessions_v2.pr_url, @@ -415,6 +447,7 @@ export async function listActiveSessions({ git_url: cli_sessions_v2.git_url, git_branch: cli_sessions_v2.git_branch, last_activity_at: cli_sessions_v2.last_activity_at, + status_updated_at: cli_sessions_v2.status_updated_at, total_cost_microdollars: cli_sessions_v2.total_cost_microdollars, cloud_agent_session_id: cli_sessions_v2.cloud_agent_session_id, session_pr_platform: cli_sessions_v2.platform, diff --git a/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts b/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts index f4595a9f40..b5b986c1a6 100644 --- a/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts +++ b/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts @@ -32,6 +32,7 @@ describe('buildGlanceableSnapshotForUser', () => { status: 'question', title: 'Another secret', connectionId: 'conn-2', + statusUpdatedAt: '2026-08-27T10:00:00.000Z', }, ]; mockedListActiveSessions.mockResolvedValue({ sessions }); @@ -54,5 +55,29 @@ describe('buildGlanceableSnapshotForUser', () => { expect(snapshot.status).toBe('happy'); expect(snapshot.running).toBe(1); expect(snapshot.needsInput).toBe(1); + // A timestamp is the one session-derived value the snapshot may carry. + expect(snapshot.needsInputSince).toBe('2026-08-27T10:00:00.000Z'); + }); + + it('reports no wait when nothing needs input', async () => { + mockedListActiveSessions.mockResolvedValue({ + sessions: [ + { + id: 'ses_raw_3', + status: 'busy', + title: 'Running', + connectionId: 'conn-3', + statusUpdatedAt: '2026-08-27T10:00:00.000Z', + }, + ], + }); + + const snapshot = await buildGlanceableSnapshotForUser({ + userId: 'oauth/user-1', + organizationId: null, + }); + + expect(snapshot.running).toBe(1); + expect(snapshot.needsInputSince).toBeNull(); }); }); diff --git a/packages/app-shared/src/glanceable-agents-snapshot.test.ts b/packages/app-shared/src/glanceable-agents-snapshot.test.ts index 5571e549ce..413bcddf13 100644 --- a/packages/app-shared/src/glanceable-agents-snapshot.test.ts +++ b/packages/app-shared/src/glanceable-agents-snapshot.test.ts @@ -6,6 +6,7 @@ import { countGlanceableSessions, GLANCEABLE_SNAPSHOT_EXPIRY_MS, isEligibleGlanceableWork, + oldestNeedsInputSince, shouldDiscardGlanceableRevision, } from './glanceable-agents-snapshot'; @@ -88,40 +89,59 @@ describe('buildGlanceableSnapshot', () => { ); }); - it('keeps revision monotonic and eligibleStartedAt while work stays eligible', () => { + it('keeps revision monotonic and reports the oldest wait from the rows', () => { + const waitedLonger = new Date(NOW - 600_000).toISOString(); const first = buildGlanceableSnapshot({ - sessions: [{ status: 'busy' }], + sessions: [{ status: 'question', statusUpdatedAt: waitedLonger }], userId: 'u1', organizationId: null, now: NOW, }); const second = buildGlanceableSnapshot({ - sessions: [{ status: 'busy' }, { status: 'question' }], + sessions: [ + { status: 'busy' }, + { status: 'question', statusUpdatedAt: waitedLonger }, + { status: 'permission', statusUpdatedAt: new Date(NOW - 1000).toISOString() }, + ], userId: 'u1', organizationId: null, now: NOW + 5000, previousRevision: first.revision, - previousEligibleStartedAt: first.eligibleStartedAt, }); expect(second.revision).toBe(first.revision + 1); - expect(second.eligibleStartedAt).toBe(first.eligibleStartedAt); - expect(second.needsInput).toBe(1); + expect(second.needsInput).toBe(2); + // Read from the rows every build, so a later revision still reports the + // oldest wait rather than a value latched at the first eligible emit. + expect(second.needsInputSince).toBe(waitedLonger); }); - it('clears eligibleStartedAt when no session is connected', () => { + it('clears needsInputSince when no session is connected', () => { const snapshot = buildGlanceableSnapshot({ sessions: [{ status: 'completed' }], userId: 'u1', organizationId: null, now: NOW, previousRevision: 3, - previousEligibleStartedAt: new Date(NOW - 60_000).toISOString(), }); expect(snapshot.status).toBe('empty'); - expect(snapshot.eligibleStartedAt).toBeNull(); + expect(snapshot.needsInputSince).toBeNull(); expect(snapshot.revision).toBe(4); }); + it('reports no wait while work runs but nothing needs input', () => { + const snapshot = buildGlanceableSnapshot({ + sessions: [ + { status: 'busy', statusUpdatedAt: new Date(NOW - 900_000).toISOString() }, + { status: 'idle', statusUpdatedAt: new Date(NOW - 900_000).toISOString() }, + ], + userId: 'u1', + organizationId: null, + now: NOW, + }); + expect(snapshot.status).toBe('happy'); + expect(snapshot.needsInputSince).toBeNull(); + }); + it('sets organizationBound only when organizationId is a string', () => { const personal = buildGlanceableSnapshot({ sessions: [], @@ -224,3 +244,45 @@ describe('isEligibleGlanceableWork and revision discard', () => { expect(shouldDiscardGlanceableRevision(newerAtEqualRevision, current)).toBe(false); }); }); + +describe('oldestNeedsInputSince', () => { + const at = (ms: number) => new Date(NOW - ms).toISOString(); + + it('returns the earliest wait among the needs-input rows', () => { + expect( + oldestNeedsInputSince([ + { status: 'question', statusUpdatedAt: at(60_000) }, + { status: 'retry', statusUpdatedAt: at(600_000) }, + { status: 'permission', statusUpdatedAt: at(120_000) }, + ]) + ).toBe(at(600_000)); + }); + + it('ignores a row that does not need input, however old', () => { + expect( + oldestNeedsInputSince([ + { status: 'busy', statusUpdatedAt: at(9_000_000) }, + { status: 'idle', statusUpdatedAt: at(8_000_000) }, + { status: 'question', statusUpdatedAt: at(1000) }, + ]) + ).toBe(at(1000)); + }); + + it('skips a missing or unparseable timestamp instead of reporting now', () => { + expect(oldestNeedsInputSince([{ status: 'question' }])).toBeNull(); + expect( + oldestNeedsInputSince([{ status: 'question', statusUpdatedAt: 'not a date' }]) + ).toBeNull(); + expect( + oldestNeedsInputSince([ + { status: 'question', statusUpdatedAt: 'not a date' }, + { status: 'question', statusUpdatedAt: at(300_000) }, + ]) + ).toBe(at(300_000)); + }); + + it('returns null when nothing needs input', () => { + expect(oldestNeedsInputSince([{ status: 'busy', statusUpdatedAt: at(1000) }])).toBeNull(); + expect(oldestNeedsInputSince([])).toBeNull(); + }); +}); diff --git a/packages/app-shared/src/glanceable-agents-snapshot.ts b/packages/app-shared/src/glanceable-agents-snapshot.ts index 7f31e36dce..f115bf9463 100644 --- a/packages/app-shared/src/glanceable-agents-snapshot.ts +++ b/packages/app-shared/src/glanceable-agents-snapshot.ts @@ -52,8 +52,13 @@ export const glanceableAgentsSnapshotSchema = z.object({ needsInput: z.number().int().min(0), /** Sessions connected but doing nothing. */ idle: z.number().int().min(0), - /** ISO 8601 timestamp or null; binds the elapsed-time display. */ - eligibleStartedAt: z.string().nullable(), + /** + * ISO 8601 timestamp or null: when the longest-waiting needs-input session + * entered that state. Null when nothing needs input, or when no row carried + * a status timestamp. Only needs-input carries a duration, because a wait is + * the one interval the user can act on — see `oldestNeedsInputSince`. + */ + needsInputSince: z.string().nullable(), }); export type GlanceableAgentsSnapshot = z.infer; @@ -64,6 +69,16 @@ export type GlanceableCounts = { idle: number; }; +/** One session row, as both producers read it from the active-sessions list. */ +export type GlanceableSessionRow = { + status: string; + /** ISO 8601; when this session's status last changed. Absent on old rows. */ + statusUpdatedAt?: string; +}; + +/** Statuses that mean the agent waits on the user and cannot go on alone. */ +const NEEDS_INPUT_STATUSES = new Set(['question', 'permission', 'retry']); + /** * Map session rows to the three glanceable counts. `busy` → running, * `question`/`permission`/`retry` → needs-input, `idle` → idle, and any @@ -74,7 +89,9 @@ export type GlanceableCounts = { * disconnects while that session was waiting on an answer, and the CLI writes * it while backing off after a provider error. */ -export function countGlanceableSessions(sessions: readonly { status: string }[]): GlanceableCounts { +export function countGlanceableSessions( + sessions: readonly GlanceableSessionRow[] +): GlanceableCounts { let running = 0; let needsInput = 0; let idle = 0; @@ -99,6 +116,32 @@ export function countGlanceableSessions(sessions: readonly { status: string }[]) return { running, needsInput, idle }; } +/** + * The earliest `statusUpdatedAt` among the needs-input sessions, or null when + * none waits or none carried a usable timestamp. + * + * The counts are aggregates, so a single duration can only honestly describe + * the oldest wait: it is a floor on how long the user has kept an agent + * blocked. A row with a missing or unparseable timestamp is skipped rather + * than treated as waiting since now, which would understate the wait. + */ +export function oldestNeedsInputSince(sessions: readonly GlanceableSessionRow[]): string | null { + let oldest: number | null = null; + let oldestIso: string | null = null; + for (const session of sessions) { + if (!NEEDS_INPUT_STATUSES.has(session.status) || session.statusUpdatedAt === undefined) { + continue; + } + const at = Date.parse(session.statusUpdatedAt); + if (Number.isNaN(at) || (oldest !== null && at >= oldest)) { + continue; + } + oldest = at; + oldestIso = session.statusUpdatedAt; + } + return oldestIso; +} + // FNV-1a 32-bit over UTF-16 code units (two bytes each). Deterministic across // Node and Hermes and not reversible to the input, so the raw ids never appear // in the key. @@ -129,13 +172,12 @@ export function buildOpaqueScopeKey(input: { } export type BuildGlanceableSnapshotInput = { - sessions: readonly { status: string }[]; + sessions: readonly GlanceableSessionRow[]; userId: string; organizationId: string | null; /** Epoch milliseconds. */ now: number; previousRevision?: number; - previousEligibleStartedAt?: string | null; accountEpoch?: number; /** Overrides the happy/empty derivation for waiting, stale, expired, signed_out, privacy. */ status?: GlanceableAgentsSnapshotStatus; @@ -143,9 +185,8 @@ export type BuildGlanceableSnapshotInput = { /** * Build a snapshot from the current session rows. Revision increases by one - * on every build. `eligibleStartedAt` keeps the previous value while work - * stays eligible, starts at `now` when work becomes eligible, and is null - * otherwise. + * on every build. `needsInputSince` comes straight from the rows, so it needs + * no carry-forward across revisions: it is data, not a latch. */ export function buildGlanceableSnapshot( input: BuildGlanceableSnapshotInput @@ -156,7 +197,6 @@ export function buildGlanceableSnapshot( const eligible = counts.running + counts.needsInput + counts.idle > 0; const now = input.now; const updatedAt = new Date(now).toISOString(); - const eligibleStartedAt = eligible ? (input.previousEligibleStartedAt ?? updatedAt) : null; return { schemaVersion: GLANCEABLE_SNAPSHOT_SCHEMA_VERSION, @@ -170,7 +210,7 @@ export function buildGlanceableSnapshot( running: counts.running, needsInput: counts.needsInput, idle: counts.idle, - eligibleStartedAt, + needsInputSince: oldestNeedsInputSince(input.sessions), }; } diff --git a/packages/notifications/src/push-data.ts b/packages/notifications/src/push-data.ts index a170dd2e37..fe3200c18d 100644 --- a/packages/notifications/src/push-data.ts +++ b/packages/notifications/src/push-data.ts @@ -94,7 +94,7 @@ export const pushDataSchema = z.discriminatedUnion('type', [ idle: z.number().int().min(0), updatedAt: z.string(), expiresAt: z.string(), - eligibleStartedAt: z.string().nullable(), + needsInputSince: z.string().nullable(), }), ]); @@ -102,12 +102,12 @@ export type PushData = z.infer; /** * The raw content-state the Active Agents Live Activity renders. The server - * pushes exactly this shape (counts + status + the safe eligible-start + * pushes exactly this shape (counts + status + the safe needs-input wait * timestamp) and the widget extension renders it directly with inlined English * copy. It must never carry a title, session id, repository name, organization * name, generated text, or a raw account id. */ export type GlanceableLiveActivityContentState = Pick< Extract, - 'status' | 'running' | 'needsInput' | 'idle' | 'eligibleStartedAt' + 'status' | 'running' | 'needsInput' | 'idle' | 'needsInputSince' >; diff --git a/packages/notifications/src/push-presentation.test.ts b/packages/notifications/src/push-presentation.test.ts index 6b126e611b..0e16302ef0 100644 --- a/packages/notifications/src/push-presentation.test.ts +++ b/packages/notifications/src/push-presentation.test.ts @@ -31,7 +31,7 @@ const variants = [ idle: 0, updatedAt: '2026-01-01T00:00:00.000Z', expiresAt: '2026-01-01T08:00:00.000Z', - eligibleStartedAt: '2026-01-01T00:00:00.000Z', + needsInputSince: '2026-01-01T00:00:00.000Z', }, ] as const; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e28e3e993b..f72d0a0c25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -634,6 +634,9 @@ importers: '@kilocode/kilo-chat-hooks': injected: true devDependencies: + '@expo/plist': + specifier: 0.8.1 + version: 0.8.1 '@sentry/cli': specifier: 'catalog:' version: 3.6.2 diff --git a/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts b/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts index 2d6f9f30c3..4e66ef3f54 100644 --- a/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts +++ b/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts @@ -153,7 +153,6 @@ function setup(options: { beforeCommit?: () => Promise; refreshError?: Err sessions, now: Date.now(), previousRevision: prior?.revision, - previousEligibleStartedAt: prior?.eligibleStartedAt, }); previous.set(organizationId, snapshot); return { type: 'active_agents_glanceable', ...snapshot }; @@ -273,12 +272,12 @@ describe('committed cloud eligibility refresh', () => { running: status === 'busy' ? 1 : 0, needsInput: status === 'retry' ? 1 : 0, }, - { status: 'empty', running: 0, needsInput: 0, idle: 0, eligibleStartedAt: null }, + { status: 'empty', running: 0, needsInput: 0, idle: 0, needsInputSince: null }, ]); } ); - it('retains the eligible interval when nonterminal retry work refreshes', async () => { + it('reports the wait only once nonterminal retry work needs input', async () => { fixture = setup(); await fixture.seed(); await fixture.consume(); @@ -288,9 +287,11 @@ describe('committed cloud eligibility refresh', () => { .set({ status: 'retry', updated_at: new Date().toISOString() }) .where(eq(cli_sessions_v2.session_id, cliSessionId)); await fixture.consume(); + // The wait reaches the wire from the row's own `status_updated_at`, so + // running work carries none and the retry carries the seeded timestamp. expect(fixture.messages.map(message => message.data)).toMatchObject([ - { running: 1, eligibleStartedAt: occurredAt }, - { running: 0, needsInput: 1, eligibleStartedAt: occurredAt }, + { running: 1, needsInputSince: null }, + { running: 0, needsInput: 1, needsInputSince: occurredAt }, ]); }); diff --git a/services/notifications/src/lib/glanceable-delivery.test.ts b/services/notifications/src/lib/glanceable-delivery.test.ts index ae6a997ab2..9aadc76701 100644 --- a/services/notifications/src/lib/glanceable-delivery.test.ts +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -40,7 +40,7 @@ const snapshot: ActiveAgentsGlanceable = { idle: 0, updatedAt: '2026-08-27T10:00:00.000Z', expiresAt: '2026-08-27T18:00:00.000Z', - eligibleStartedAt: '2026-08-27T09:00:00.000Z', + needsInputSince: '2026-08-27T09:00:00.000Z', }; function fakeDeps(overrides: Partial = {}): { @@ -300,7 +300,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { needsInput: 0, updatedAt: new Date(Date.now()).toISOString(), expiresAt: new Date(Date.now() + 28_800_000).toISOString(), - eligibleStartedAt: new Date(Date.now()).toISOString(), + needsInputSince: new Date(Date.now()).toISOString(), ...overrides, }; } @@ -326,7 +326,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const busy = service.refreshGlanceableSessions(personalRefresh); await started.promise; vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); - current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); await createService().refreshGlanceableSessions(personalRefresh); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:01:00.000Z')); release.resolve(); @@ -335,13 +335,13 @@ describe('NotificationsService.refreshGlanceableSessions', () => { { status: 'empty', running: 0, - eligibleStartedAt: null, + needsInputSince: null, updatedAt: '2026-08-27T10:00:01.000Z', }, { status: 'empty', running: 0, - eligibleStartedAt: null, + needsInputSince: null, updatedAt: '2026-08-27T10:00:01.000Z', }, ]); @@ -384,17 +384,17 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); const busy = service.refreshGlanceableSessions(personalRefresh); await started.promise; - current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); await createService().refreshGlanceableSessions(personalRefresh); release.resolve(); await busy; expect(messages.map(message => message.data)).toMatchObject([ - { status: 'empty', running: 0, eligibleStartedAt: null }, - { status: 'empty', running: 0, eligibleStartedAt: null }, + { status: 'empty', running: 0, needsInputSince: null }, + { status: 'empty', running: 0, needsInputSince: null }, ]); }); - it('retains the eligible start through retry and reconstructed worker and DO instances', async () => { + it('keeps the revision monotonic across retry and reconstructed worker and DO instances', async () => { let current = freshSnapshot(); const { service, createService, messages } = setupService({ response: () => Response.json(current), @@ -411,13 +411,15 @@ describe('NotificationsService.refreshGlanceableSessions', () => { .filter(message => message.to === 'ExponentPushToken[ios]') .map(message => message.data) ).toMatchObject([ - { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 1 }, - { idle: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 2 }, - { needsInput: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z', revision: 3 }, + { running: 2, needsInputSince: '2026-08-27T10:00:00.000Z', revision: 1 }, + // The wait is read from the rows on every build, so each delivery carries + // its own snapshot's value instead of one latched at the first emit. + { idle: 1, needsInputSince: '2026-08-27T10:10:00.000Z', revision: 2 }, + { needsInput: 1, needsInputSince: '2026-08-27T10:20:00.000Z', revision: 3 }, ]); }); - it('clears on authoritative empty and prevents an older empty read resetting the new interval', async () => { + it('fences an older empty read behind the newer authoritative reads', async () => { const started = Promise.withResolvers(); const release = Promise.withResolvers(); let current = freshSnapshot(); @@ -434,7 +436,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }, }); await createService().refreshGlanceableSessions(personalRefresh); - current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); deferNext = true; const oldIdle = createService().refreshGlanceableSessions(personalRefresh); await started.promise; @@ -452,14 +454,14 @@ describe('NotificationsService.refreshGlanceableSessions', () => { .filter(message => message.to === 'ExponentPushToken[ios]') .map(message => message.data) ).toMatchObject([ - { status: 'happy', eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, - { status: 'empty', eligibleStartedAt: null }, - { status: 'happy', eligibleStartedAt: '2026-08-27T10:10:00.000Z' }, - { idle: 1, eligibleStartedAt: '2026-08-27T10:10:00.000Z' }, + { status: 'happy', needsInputSince: '2026-08-27T10:00:00.000Z' }, + { status: 'empty', needsInputSince: null }, + { status: 'happy', needsInputSince: '2026-08-27T10:10:00.000Z' }, + { idle: 1, needsInputSince: '2026-08-27T10:20:00.000Z' }, ]); }); - it('keeps user and organization intervals separate while another scope has a deferred read', async () => { + it('keeps user and organization scopes separate while another scope has a deferred read', async () => { const started = Promise.withResolvers(); const release = Promise.withResolvers(); let first = true; @@ -496,15 +498,15 @@ describe('NotificationsService.refreshGlanceableSessions', () => { .filter(message => message.to === 'ExponentPushToken[ios]') .map(message => message.data) ).toMatchObject([ - { scopeKey: 'usr_1:org-1', eligibleStartedAt: '2026-08-27T10:01:00.000Z' }, - { scopeKey: 'usr_1:org-2', eligibleStartedAt: '2026-08-27T10:02:00.000Z' }, - { scopeKey: 'usr_2:personal', eligibleStartedAt: '2026-08-27T10:03:00.000Z' }, - { scopeKey: 'usr_1:personal', eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, - { scopeKey: 'usr_1:personal', eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { scopeKey: 'usr_1:org-1', needsInputSince: '2026-08-27T10:01:00.000Z' }, + { scopeKey: 'usr_1:org-2', needsInputSince: '2026-08-27T10:02:00.000Z' }, + { scopeKey: 'usr_2:personal', needsInputSince: '2026-08-27T10:03:00.000Z' }, + { scopeKey: 'usr_1:personal', needsInputSince: '2026-08-27T10:00:00.000Z' }, + { scopeKey: 'usr_1:personal', needsInputSince: '2026-08-27T10:04:00.000Z' }, ]); }); - it('preserves the interval after snapshot and delivery failures instead of clearing or replacing it', async () => { + it('recovers delivery after snapshot and delivery failures', async () => { let current = freshSnapshot(); let unavailable = false; const { createService, messages } = setupService({ @@ -524,16 +526,16 @@ describe('NotificationsService.refreshGlanceableSessions', () => { .filter(message => message.to === 'ExponentPushToken[ios]') .map(message => message.data) ).toMatchObject([ - { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, - { idle: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { running: 2, needsInputSince: '2026-08-27T10:00:00.000Z' }, + { idle: 1, needsInputSince: '2026-08-27T10:10:00.000Z' }, ]); }); - it('does not clear an interval from a non-authoritative zero-count response', async () => { + it('delivers nothing from a non-authoritative zero-count response', async () => { let current = freshSnapshot(); const { createService, messages } = setupService({ response: () => Response.json(current) }); await createService().refreshGlanceableSessions(personalRefresh); - current = freshSnapshot({ status: 'stale', running: 0, eligibleStartedAt: null }); + current = freshSnapshot({ status: 'stale', running: 0, needsInputSince: null }); await createService().refreshGlanceableSessions(personalRefresh); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); current = freshSnapshot({ running: 0, idle: 1 }); @@ -543,8 +545,8 @@ describe('NotificationsService.refreshGlanceableSessions', () => { .filter(message => message.to === 'ExponentPushToken[ios]') .map(message => message.data) ).toMatchObject([ - { running: 2, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, - { idle: 1, eligibleStartedAt: '2026-08-27T10:00:00.000Z' }, + { running: 2, needsInputSince: '2026-08-27T10:00:00.000Z' }, + { idle: 1, needsInputSince: '2026-08-27T10:10:00.000Z' }, ]); }); @@ -562,7 +564,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { it('ends empty work and starts later eligible work without mobile token cleanup', async () => { const pem = await generateTestPrivateKeyPem(); - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -582,7 +584,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); @@ -594,7 +596,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { ]); expect(JSON.parse(apns[1].aps['content-state'].props)).toMatchObject({ idle: 1, - eligibleStartedAt: '2026-08-27T10:00:01.000Z', + needsInputSince: '2026-08-27T10:00:01.000Z', }); expect([...activityRows.keys()]).toEqual(['scope-token']); }); @@ -610,7 +612,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { ], apnsStatus: token => (token === 'failed-token' ? 503 : 200), response: () => - Response.json(freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null })), + Response.json(freshSnapshot({ status: 'empty', running: 0, needsInputSince: null })), }); await service.refreshGlanceableSessions(personalRefresh); expect([...activityRows.keys()]).toEqual(['scope-token', 'failed-token']); @@ -623,7 +625,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const started = Promise.withResolvers(); const release = Promise.withResolvers(); let first = true; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, activityRows, activities, liveActivityProps } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -683,7 +685,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const started = Promise.withResolvers(); const release = Promise.withResolvers(); let first = true; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const renewedRow = { id: renewal === 'version' ? `row-${withPushToStart ? 1 : 0}` : 'renewed-row', kind: 'ios_activity' as const, @@ -746,7 +748,8 @@ describe('NotificationsService.refreshGlanceableSessions', () => { running: 0, needsInput: 0, idle: 1, - eligibleStartedAt: '2026-08-27T10:00:01.000Z', + // Forwarded from this refresh's snapshot, not latched at the earlier one. + needsInputSince: '2026-08-27T10:00:02.000Z', }, ]); const liveToken = withPushToStart ? 'started-2' : 'live-activity'; @@ -774,7 +777,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { started.resolve(); await release.promise; }; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows, liveActivityProps, messages } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -801,7 +804,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { running: 0, needsInput: 1, idle: 0, - eligibleStartedAt: '2026-08-27T10:00:01.000Z', + needsInputSince: '2026-08-27T10:00:01.000Z', }, ]); expect([...activityRows.keys()]).toEqual(['scope-token']); @@ -820,7 +823,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const started = Promise.withResolvers(); const release = Promise.withResolvers(); let first = true; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows, liveActivityProps } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -888,7 +891,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { async unusableKey => { const pem = await generateTestPrivateKeyPem(); let configured = false; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows, liveActivityProps } = setupService({ privateKey: async () => (configured ? pem : unusableKey), iosTokens: [ @@ -912,7 +915,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { it('recovers after a delivered end loses its HTTP response across coordinator reconstruction', async () => { const pem = await generateTestPrivateKeyPem(); - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows, activities, liveActivityProps } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -945,7 +948,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { current = freshSnapshot({ running: 0, idle: 1 }); await createService().refreshGlanceableSessions(personalRefresh); expect(liveActivityProps()).toMatchObject([ - { running: 0, needsInput: 0, idle: 1, eligibleStartedAt: '2026-08-27T10:00:01.000Z' }, + { running: 0, needsInput: 0, idle: 1, needsInputSince: '2026-08-27T10:00:02.000Z' }, ]); expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ ['old-activity', 'end'], @@ -959,7 +962,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { async withPushToStart => { const pem = await generateTestPrivateKeyPem(); let rejected = true; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const iosTokens: IosActivityToken[] = [{ token: 'old-activity', kind: 'ios_activity' }]; if (withPushToStart) iosTokens.push({ token: 'scope-token', kind: 'ios_push_to_start' }); const { createService, apns, activityRows, liveActivityProps } = setupService({ @@ -974,7 +977,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { current = freshSnapshot({ running: 0, needsInput: 1 }); await createService().refreshGlanceableSessions(personalRefresh); expect(liveActivityProps()).toMatchObject([ - { running: 0, needsInput: 1, eligibleStartedAt: '2026-08-27T10:00:01.000Z' }, + { running: 0, needsInput: 1, needsInputSince: '2026-08-27T10:00:01.000Z' }, ]); expect(apns.map(({ token, aps }) => [token, aps.event])).toEqual([ ['old-activity', 'end'], @@ -986,7 +989,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { it('starts fresh work after an unregistered end target across reconstruction', async () => { const pem = await generateTestPrivateKeyPem(); - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows, activities, liveActivityProps } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -1032,7 +1035,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const rejectedIndex = rejectedAttempt === 'older' ? 1 : 2; let requests = 0; let responses = 0; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows, activities, liveActivityProps } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -1106,7 +1109,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const release = Promise.withResolvers(); let first = true; let rejected = true; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, liveActivityProps } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -1143,7 +1146,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const release = Promise.withResolvers(); let first = true; let rejected = false; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows, liveActivityProps } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -1201,7 +1204,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { it('retries a rejected end without starting an empty activity', async () => { const pem = await generateTestPrivateKeyPem(); let rejected = true; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows, liveActivityProps } = setupService({ privateKey: async () => pem, iosTokens: [ @@ -1228,7 +1231,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { const started = Promise.withResolvers(); const release = Promise.withResolvers(); let first = true; - let current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + let current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); const { createService, apns, activityRows } = setupService({ response: () => Response.json(current), privateKey: async () => { @@ -1273,14 +1276,14 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); const busy = createService().refreshGlanceableSessions(personalRefresh); await started.promise; - current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); await createService().refreshGlanceableSessions(personalRefresh); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); release.resolve(); await busy; expect(apns.map(request => request.aps.event)).toEqual(['end', 'update']); expect(apns.map(request => JSON.parse(request.aps['content-state'].props))).toMatchObject([ - { status: 'empty', running: 0, eligibleStartedAt: null }, + { status: 'empty', running: 0, needsInputSince: null }, { status: 'happy', running: 2 }, ]); expect(apns[1].aps.timestamp).toBeLessThan(apns[0].aps.timestamp); @@ -1323,7 +1326,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); const busy = createService().refreshGlanceableSessions(personalRefresh); await started.promise; - current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); await createService().refreshGlanceableSessions(personalRefresh); vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); release.resolve(); @@ -1345,14 +1348,14 @@ describe('NotificationsService.refreshGlanceableSessions', () => { running: 0, needsInput: 0, idle: 0, - eligibleStartedAt: null, + needsInputSince: null, }, }, ] ); expect(messages.map(message => message.data)).toMatchObject([ - { status: 'empty', running: 0, eligibleStartedAt: null }, - { status: 'empty', running: 0, eligibleStartedAt: null }, + { status: 'empty', running: 0, needsInputSince: null }, + { status: 'empty', running: 0, needsInputSince: null }, ]); }); @@ -1379,7 +1382,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { }); const busy = createService().refreshGlanceableSessions(personalRefresh); await started.promise; - current = freshSnapshot({ status: 'empty', running: 0, eligibleStartedAt: null }); + current = freshSnapshot({ status: 'empty', running: 0, needsInputSince: null }); await createService().refreshGlanceableSessions(personalRefresh); release.resolve(); await busy; @@ -1388,7 +1391,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { messages .filter(message => message.to === `ExponentPushToken[${platform}]`) .map(message => message.data) - ).toMatchObject([{ status: 'empty', running: 0, eligibleStartedAt: null }]); + ).toMatchObject([{ status: 'empty', running: 0, needsInputSince: null }]); } ); @@ -1488,7 +1491,7 @@ describe('NotificationsService.refreshGlanceableSessions', () => { () => Response.json({ ...snapshot, running: -1 }), () => Response.json({ ...snapshot, updatedAt: 'invalid-date' }), () => Response.json({ ...snapshot, expiresAt: 'invalid-date' }), - () => Response.json({ ...snapshot, eligibleStartedAt: 'invalid-date' }), + () => Response.json({ ...snapshot, needsInputSince: 'invalid-date' }), ])('rejects an unusable snapshot without poisoning the next refresh', async response => { let currentResponse = response; const { service, createService, messages } = setupService({ @@ -1499,8 +1502,8 @@ describe('NotificationsService.refreshGlanceableSessions', () => { currentResponse = () => Response.json(snapshot); await createService().refreshGlanceableSessions(personalRefresh); expect(messages.map(message => message.data)).toMatchObject([ - { running: 2, eligibleStartedAt: '2026-08-27T09:00:00.000Z' }, - { running: 2, eligibleStartedAt: '2026-08-27T09:00:00.000Z' }, + { running: 2, needsInputSince: '2026-08-27T09:00:00.000Z' }, + { running: 2, needsInputSince: '2026-08-27T09:00:00.000Z' }, ]); }); @@ -1633,7 +1636,7 @@ describe('toGlanceableContentState', () => { running: 2, needsInput: 1, idle: 0, - eligibleStartedAt: '2026-08-27T09:00:00.000Z', + needsInputSince: '2026-08-27T09:00:00.000Z', }); }); diff --git a/services/notifications/src/lib/glanceable-delivery.ts b/services/notifications/src/lib/glanceable-delivery.ts index fc5bf1825b..0a95f6a766 100644 --- a/services/notifications/src/lib/glanceable-delivery.ts +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -58,7 +58,7 @@ export function toGlanceableContentState( running: snapshot.running, needsInput: snapshot.needsInput, idle: snapshot.idle, - eligibleStartedAt: snapshot.eligibleStartedAt, + needsInputSince: snapshot.needsInputSince, }; return { name: ACTIVE_AGENTS_LIVE_ACTIVITY_NAME, diff --git a/services/notifications/src/lib/glanceable-refresh.ts b/services/notifications/src/lib/glanceable-refresh.ts index e688b9f372..848a90f6ad 100644 --- a/services/notifications/src/lib/glanceable-refresh.ts +++ b/services/notifications/src/lib/glanceable-refresh.ts @@ -11,11 +11,12 @@ const refreshStateSchema = z.object({ revision: z.number().int().positive(), updatedAt: z.string().datetime(), apnsTimestampSeconds: z.number().int().nonnegative(), - eligibleStartedAt: z.string().datetime().nullable(), }); +// `needsInputSince` comes from the session rows on every build, so no eligible +// interval is carried across revisions and only the dates are validated here. const snapshotTimestampsSchema = refreshStateSchema - .pick({ updatedAt: true, eligibleStartedAt: true }) - .extend({ expiresAt: z.string().datetime() }); + .pick({ updatedAt: true }) + .extend({ expiresAt: z.string().datetime(), needsInputSince: z.string().datetime().nullable() }); /** The user DO owns these records; no ordering or interval state lives in a Worker instance. */ export async function refreshGlanceableSnapshot( @@ -40,7 +41,6 @@ export async function refreshGlanceableSnapshot( Math.floor(now / 1000), (previous?.apnsTimestampSeconds ?? 0) + 1 ), - eligibleStartedAt: previous?.eligibleStartedAt ?? null, }; await tx.put(key, next); return next; @@ -49,17 +49,12 @@ export async function refreshGlanceableSnapshot( const snapshot = await deps.buildSnapshot(scope.userId, scope.organizationId); // Only the authoritative happy/empty result can change an eligible interval. if (snapshot === null || (snapshot.status !== 'happy' && snapshot.status !== 'empty')) return; - // The shared wire schema accepts strings; validate dates before persisting the interval. + // The shared wire schema accepts strings; validate the dates before delivery. snapshotTimestampsSchema.parse(snapshot); const committed = await storage.transaction(async tx => { const current = refreshStateSchema.parse(await tx.get(key)); if (current.revision !== request.revision) return null; - const eligibleStartedAt = - snapshot.running + snapshot.needsInput + snapshot.idle > 0 - ? (current.eligibleStartedAt ?? snapshot.eligibleStartedAt ?? request.updatedAt) - : null; - await tx.put(key, { ...current, eligibleStartedAt }); return { ...snapshot, revision: request.revision, @@ -69,7 +64,6 @@ export async function refreshGlanceableSnapshot( Date.parse(snapshot.expiresAt) - Date.parse(snapshot.updatedAt) ).toISOString(), - eligibleStartedAt, }; }); if (committed === null) return; From 93f0372ec451dc38a846223bcf2d201faaf238bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 2 Sep 2026 23:24:18 +0200 Subject: [PATCH 31/43] feat(mobile): localize the widget gallery and fix five locales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four pieces, all found while testing the glanceable surfaces in a non-English language. Five languages threw on their first formatted number. `@formatjs`'s `shouldPolyfill` runs the CLDR locale matcher, whose best-fit path constructs `Intl.Locale` for a tag its own locale list does not carry — `zh-Hans`, `zh-Hant`, `ht`, `mi` and `pt-BR` all miss the plural-rules list. Hermes ships no `Intl.Locale`, so the call threw before any polyfill could install and every formatted number, list and duration on the screen fell back to its raw value. `intl-cache` now installs that polyfill before each `shouldPolyfill`. The widget gallery copy is now translated. expo-widgets emits the display name and the description as Swift string literals, which bind to SwiftUI's `LocalizedStringKey` overloads, so `withWidgetLocalizations` writes one `.lproj/Localizable.strings` per language into the extension and adds the Resources build phase that copies them. The strings are native bundle metadata rather than app copy, so they live in `widget-gallery-copy.json` beside the plugin and never go through i18next. The English description is now a sentence, which translates far more cleanly than the colon list it replaces. The baked locale tag is the underscore form. `@expo/ui`'s `locale` modifier applies its value only when `Locale.availableIdentifiers` contains it, and that list spells a script or region subtag with an underscore, so `zh-Hans`, `zh-Hant` and `pt-BR` silently left the relative wait in the device language. `glanceable.privacy` now reads "Open Kilo to see agents" instead of "Agents hidden", which said what was withheld rather than what to do. Verified on the simulator in Arabic: every family mirrors, with the mark on the trailing edge and the rows right-aligned. --- apps/mobile/app.config.ts | 12 +- apps/mobile/plugins/widget-gallery-copy.json | 350 ++++++++++++++++++ .../mobile/plugins/withWidgetLocalizations.js | 114 ++++-- .../glanceable-android/android-sink.test.ts | 2 +- .../src/glanceable-android/register.test.ts | 4 +- .../glanceable-android/widget-props.test.ts | 10 +- .../src/glanceable-ios/ios-sink.test.ts | 4 +- .../src/glanceable-ios/layout-copy.test.ts | 7 + apps/mobile/src/glanceable-ios/layout-copy.ts | 12 +- apps/mobile/src/i18n/locales/af.json | 2 +- apps/mobile/src/i18n/locales/am.json | 2 +- apps/mobile/src/i18n/locales/ar.json | 2 +- apps/mobile/src/i18n/locales/az.json | 2 +- apps/mobile/src/i18n/locales/be.json | 2 +- apps/mobile/src/i18n/locales/bg.json | 2 +- apps/mobile/src/i18n/locales/bn.json | 2 +- apps/mobile/src/i18n/locales/bs.json | 2 +- apps/mobile/src/i18n/locales/ca.json | 2 +- apps/mobile/src/i18n/locales/ckb.json | 2 +- apps/mobile/src/i18n/locales/cs.json | 2 +- apps/mobile/src/i18n/locales/cy.json | 2 +- apps/mobile/src/i18n/locales/da.json | 2 +- apps/mobile/src/i18n/locales/de.json | 2 +- apps/mobile/src/i18n/locales/el.json | 2 +- apps/mobile/src/i18n/locales/en.json | 2 +- apps/mobile/src/i18n/locales/es.json | 2 +- apps/mobile/src/i18n/locales/et.json | 2 +- apps/mobile/src/i18n/locales/eu.json | 2 +- apps/mobile/src/i18n/locales/fa.json | 2 +- apps/mobile/src/i18n/locales/fi.json | 2 +- apps/mobile/src/i18n/locales/fil.json | 2 +- apps/mobile/src/i18n/locales/fr.json | 2 +- apps/mobile/src/i18n/locales/ga.json | 2 +- apps/mobile/src/i18n/locales/gl.json | 2 +- apps/mobile/src/i18n/locales/gu.json | 2 +- apps/mobile/src/i18n/locales/ha.json | 2 +- apps/mobile/src/i18n/locales/he.json | 2 +- apps/mobile/src/i18n/locales/hi.json | 2 +- apps/mobile/src/i18n/locales/hr.json | 2 +- apps/mobile/src/i18n/locales/ht.json | 2 +- apps/mobile/src/i18n/locales/hu.json | 2 +- apps/mobile/src/i18n/locales/hy.json | 2 +- apps/mobile/src/i18n/locales/id.json | 2 +- apps/mobile/src/i18n/locales/ig.json | 2 +- apps/mobile/src/i18n/locales/is.json | 2 +- apps/mobile/src/i18n/locales/it.json | 2 +- apps/mobile/src/i18n/locales/ja.json | 2 +- apps/mobile/src/i18n/locales/ka.json | 2 +- apps/mobile/src/i18n/locales/kk.json | 2 +- apps/mobile/src/i18n/locales/km.json | 2 +- apps/mobile/src/i18n/locales/kn.json | 2 +- apps/mobile/src/i18n/locales/ko.json | 2 +- apps/mobile/src/i18n/locales/lo.json | 2 +- apps/mobile/src/i18n/locales/lt.json | 2 +- apps/mobile/src/i18n/locales/lv.json | 2 +- apps/mobile/src/i18n/locales/mg.json | 2 +- apps/mobile/src/i18n/locales/mi.json | 2 +- apps/mobile/src/i18n/locales/mk.json | 2 +- apps/mobile/src/i18n/locales/ml.json | 2 +- apps/mobile/src/i18n/locales/mn.json | 2 +- apps/mobile/src/i18n/locales/mr.json | 2 +- apps/mobile/src/i18n/locales/ms.json | 2 +- apps/mobile/src/i18n/locales/mt.json | 2 +- apps/mobile/src/i18n/locales/my.json | 2 +- apps/mobile/src/i18n/locales/nb.json | 2 +- apps/mobile/src/i18n/locales/ne.json | 2 +- apps/mobile/src/i18n/locales/nl.json | 2 +- apps/mobile/src/i18n/locales/om.json | 2 +- apps/mobile/src/i18n/locales/or.json | 2 +- apps/mobile/src/i18n/locales/pa.json | 2 +- apps/mobile/src/i18n/locales/pl.json | 2 +- apps/mobile/src/i18n/locales/ps.json | 2 +- apps/mobile/src/i18n/locales/pt-BR.json | 2 +- apps/mobile/src/i18n/locales/pt.json | 2 +- apps/mobile/src/i18n/locales/ro.json | 2 +- apps/mobile/src/i18n/locales/ru.json | 2 +- apps/mobile/src/i18n/locales/si.json | 2 +- apps/mobile/src/i18n/locales/sk.json | 2 +- apps/mobile/src/i18n/locales/sl.json | 2 +- apps/mobile/src/i18n/locales/so.json | 2 +- apps/mobile/src/i18n/locales/sq.json | 2 +- apps/mobile/src/i18n/locales/sr.json | 2 +- apps/mobile/src/i18n/locales/sv.json | 2 +- apps/mobile/src/i18n/locales/sw.json | 2 +- apps/mobile/src/i18n/locales/ta.json | 2 +- apps/mobile/src/i18n/locales/te.json | 2 +- apps/mobile/src/i18n/locales/th.json | 2 +- apps/mobile/src/i18n/locales/tr.json | 2 +- apps/mobile/src/i18n/locales/uk.json | 2 +- apps/mobile/src/i18n/locales/ur.json | 2 +- apps/mobile/src/i18n/locales/uz.json | 2 +- apps/mobile/src/i18n/locales/vi.json | 2 +- apps/mobile/src/i18n/locales/yo.json | 2 +- apps/mobile/src/i18n/locales/zh-Hans.json | 2 +- apps/mobile/src/i18n/locales/zh-Hant.json | 2 +- apps/mobile/src/i18n/locales/zu.json | 2 +- .../src/lib/glanceable/presentation.test.ts | 6 +- apps/mobile/src/lib/intl-cache.test.ts | 6 + apps/mobile/src/lib/intl-cache.ts | 30 +- 99 files changed, 598 insertions(+), 133 deletions(-) create mode 100644 apps/mobile/plugins/widget-gallery-copy.json diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index fed24300ba..999edf5b20 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -1,6 +1,9 @@ import type { ExpoConfig } from 'expo/config'; import { ENV_KEYS, OPTIONAL_ENV_KEYS } from './src/lib/env-keys'; import { SUPPORTED_LANGUAGES } from './src/i18n/languages.ts'; +// The widget gallery's own copy. Native bundle metadata, not app copy — see +// plugins/withWidgetLocalizations.js. +import WIDGET_GALLERY_COPY from './plugins/widget-gallery-copy.json'; import { SENTRY_NATIVE_OPTIONS } from './src/lib/sentry-dsn'; import { UNIVERSAL_LINK_PATH_PATTERNS } from './src/lib/universal-link-paths'; import { @@ -263,7 +266,10 @@ const config: ExpoConfig = { // leaves English-only. This must be registered BEFORE 'expo-widgets': // dangerous mods run in reverse registration order, so the earlier entry // runs last and sees the Info.plist expo-widgets has already written. - ['./plugins/withWidgetLocalizations', { languages: [...SUPPORTED_LANGUAGES] }], + [ + './plugins/withWidgetLocalizations', + { languages: [...SUPPORTED_LANGUAGES], copy: WIDGET_GALLERY_COPY }, + ], // Aggregate "Active Agents" glanceable surfaces: one Live Activity plus Home // Screen and Lock Screen widgets, rendered by src/glanceable-ios. The widget // target reuses the existing app group; no second group is created. @@ -276,8 +282,8 @@ const config: ExpoConfig = { widgets: [ { name: 'ActiveAgentsWidget', - displayName: 'Active Agents', - description: 'Your agents at a glance: needs input, working, idle', + displayName: WIDGET_GALLERY_COPY.en.displayName, + description: WIDGET_GALLERY_COPY.en.description, contentMarginsDisabled: false, // Home Screen: the small square and the medium row. `systemLarge` // is deliberately absent — three counts cannot fill a card that diff --git a/apps/mobile/plugins/widget-gallery-copy.json b/apps/mobile/plugins/widget-gallery-copy.json new file mode 100644 index 0000000000..0e1d2035c0 --- /dev/null +++ b/apps/mobile/plugins/widget-gallery-copy.json @@ -0,0 +1,350 @@ +{ + "af": { + "displayName": "Aktiewe agente", + "description": "Sien watter agente insette nodig het, werk of ledig is" + }, + "am": { + "displayName": "ንቁ ወኪሎች", + "description": "የትኞቹ ወኪሎች ግብዓት እንደሚያስፈልጋቸው፣ እንደሚሠሩ ወይም እንደማይሠሩ ይመልከቱ" + }, + "ar": { + "displayName": "الوكلاء النشطون", + "description": "اطّلع على الوكلاء الذين يتطلبون إدخالًا أو يعملون أو خاملون" + }, + "az": { + "displayName": "Aktiv agentlər", + "description": "Hansı agentlərin giriş tələb etdiyini, işlədiyini və ya boş olduğunu görün" + }, + "be": { + "displayName": "Актыўныя агенты", + "description": "Дазнайцеся, якім агентам патрэбны ўвод, якія працуюць, а якія неактыўныя" + }, + "bg": { + "displayName": "Активни агенти", + "description": "Вижте кои агенти изискват въвеждане, кои работят и кои са неактивни" + }, + "bn": { + "displayName": "সক্রিয় এজেন্ট", + "description": "দেখুন কোন এজেন্টগুলির ইনপুট প্রয়োজন, কোনগুলি কাজ করছে বা নিষ্ক্রিয়" + }, + "bs": { + "displayName": "Aktivni agenti", + "description": "Vidite kojim agentima treba unos, koji rade, a koji su neaktivni" + }, + "ca": { + "displayName": "Agents actius", + "description": "Mira quins agents necessiten dades, quins treballen i quins estan inactius" + }, + "ckb": { + "displayName": "ئەجێنتە چالاکەکان", + "description": "ببینە کام ئەجێنت پێویستی بە داخڵکردن هەیە، کام کار دەکات و کام بێکارە" + }, + "cs": { + "displayName": "Aktivní agenti", + "description": "Podívejte se, kteří agenti potřebují vstup, kteří pracují a kteří jsou nečinní" + }, + "cy": { + "displayName": "Asiantau gweithredol", + "description": "Gwelwch pa asiantau sydd angen mewnbwn, sy'n gweithio neu sy'n segur" + }, + "da": { + "displayName": "Aktive agenter", + "description": "Se hvilke agenter der kræver input, hvilke der arbejder, og hvilke der er inaktive" + }, + "de": { + "displayName": "Aktive Agenten", + "description": "Sieh, welche Agenten eine Eingabe brauchen, welche arbeiten und welche inaktiv sind" + }, + "el": { + "displayName": "Ενεργοί πράκτορες", + "description": "Δείτε ποιοι πράκτορες χρειάζονται δεδομένα, ποιοι εργάζονται και ποιοι είναι αδρανείς" + }, + "en": { + "displayName": "Active Agents", + "description": "See which agents need input, are working, or are idle" + }, + "es": { + "displayName": "Agentes activos", + "description": "Ve qué agentes necesitan datos, cuáles trabajan y cuáles están inactivos" + }, + "et": { + "displayName": "Aktiivsed agendid", + "description": "Vaadake, millised agendid vajavad sisendit, millised töötavad ja millised on jõude" + }, + "eu": { + "displayName": "Agente aktiboak", + "description": "Ikusi zein agentek behar duten sarrera, zein ari diren lanean eta zein dauden geldi" + }, + "fa": { + "displayName": "عامل‌های فعال", + "description": "ببینید کدام عامل‌ها به ورودی نیاز دارند، کدام کار می‌کنند و کدام غیرفعال هستند" + }, + "fi": { + "displayName": "Aktiiviset agentit", + "description": "Näe, mitkä agentit tarvitsevat syötettä, mitkä työskentelevät ja mitkä ovat vapaana" + }, + "fil": { + "displayName": "Mga aktibong agent", + "description": "Tingnan kung aling mga agent ang nangangailangan ng input, gumagana, o nakatengga" + }, + "fr": { + "displayName": "Agents actifs", + "description": "Voyez quels agents attendent une saisie, lesquels travaillent et lesquels sont inactifs" + }, + "ga": { + "displayName": "Gníomhairí gníomhacha", + "description": "Féach cé na gníomhairí a bhfuil ionchur uathu, cé atá ag obair agus cé atá díomhaoin" + }, + "gl": { + "displayName": "Axentes activos", + "description": "Mira que axentes precisan datos, cales traballan e cales están inactivos" + }, + "gu": { + "displayName": "સક્રિય એજન્ટો", + "description": "જુઓ કયા એજન્ટોને ઇનપુટની જરૂર છે, કયા કામ કરે છે અને કયા નિષ્ક્રિય છે" + }, + "ha": { + "displayName": "Wakilai da ke aiki", + "description": "Duba waɗanne wakilai ke buƙatar bayani, waɗanne ke aiki, da waɗanne ba sa aiki" + }, + "he": { + "displayName": "סוכנים פעילים", + "description": "ראה אילו סוכנים זקוקים לקלט, אילו עובדים ואילו בטלים" + }, + "hi": { + "displayName": "सक्रिय एजेंट", + "description": "देखें कि किन एजेंट को इनपुट चाहिए, कौन काम कर रहे हैं और कौन निष्क्रिय हैं" + }, + "hr": { + "displayName": "Aktivni agenti", + "description": "Vidite kojim agentima treba unos, koji rade, a koji su neaktivni" + }, + "ht": { + "displayName": "Ajans aktif yo", + "description": "Wè ki ajans ki bezwen antre, ki ap travay, oswa ki poko fè anyen" + }, + "hu": { + "displayName": "Aktív ügynökök", + "description": "Nézze meg, mely ügynökök várnak bevitelre, melyek dolgoznak és melyek tétlenek" + }, + "hy": { + "displayName": "Ակտիվ գործակալներ", + "description": "Տեսեք, որ գործակալները մուտքագրման կարիք ունեն, որոնք են աշխատում և որոնք են պարապ" + }, + "id": { + "displayName": "Agen aktif", + "description": "Lihat agen mana yang perlu masukan, mana yang bekerja, dan mana yang menganggur" + }, + "ig": { + "displayName": "Ndị ọrụ na-arụ ọrụ", + "description": "Hụ ndị ọrụ chọrọ ntinye, ndị na-arụ ọrụ, na ndị na-anọ nkịtị" + }, + "is": { + "displayName": "Virk umboð", + "description": "Sjáðu hvaða umboð þurfa inntak, hvaða eru að vinna og hvaða eru óvirk" + }, + "it": { + "displayName": "Agenti attivi", + "description": "Guarda quali agenti richiedono un input, quali lavorano e quali sono inattivi" + }, + "ja": { + "displayName": "アクティブなエージェント", + "description": "入力が必要なエージェント、処理中のエージェント、待機中のエージェントを確認できます" + }, + "ka": { + "displayName": "აქტიური აგენტები", + "description": "ნახეთ, რომელ აგენტს სჭირდება მონაცემი, რომელი მუშაობს და რომელი უქმად არის" + }, + "kk": { + "displayName": "Белсенді агенттер", + "description": "Қандай агенттерге енгізу қажет, қайсысы жұмыс істейді және қайсысы бос екенін көріңіз" + }, + "km": { + "displayName": "ភ្នាក់ងារសកម្ម", + "description": "មើលថាភ្នាក់ងារណាត្រូវការការបញ្ចូល ណាកំពុងធ្វើការ និងណាទំនេរ" + }, + "kn": { + "displayName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು", + "description": "ಯಾವ ಏಜೆಂಟ್‌ಗಳಿಗೆ ಇನ್‌ಪುಟ್ ಬೇಕು, ಯಾವುವು ಕೆಲಸ ಮಾಡುತ್ತಿವೆ ಮತ್ತು ಯಾವುವು ನಿಷ್ಕ್ರಿಯವಾಗಿವೆ ಎಂದು ನೋಡಿ" + }, + "ko": { + "displayName": "활성 에이전트", + "description": "입력이 필요한 에이전트, 작업 중인 에이전트, 대기 중인 에이전트를 확인하세요" + }, + "lo": { + "displayName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ", + "description": "ເບິ່ງວ່າຕົວແທນໃດຕ້ອງການການປ້ອນຂໍ້ມູນ, ໃດກຳລັງເຮັດວຽກ ແລະ ໃດຫວ່າງຢູ່" + }, + "lt": { + "displayName": "Aktyvūs agentai", + "description": "Pamatykite, kuriems agentams reikia įvesties, kurie dirba ir kurie neveiklūs" + }, + "lv": { + "displayName": "Aktīvie aģenti", + "description": "Skaties, kuriem aģentiem nepieciešama ievade, kuri strādā un kuri ir dīkstāvē" + }, + "mg": { + "displayName": "Agent mavitrika", + "description": "Jereo izay agent mila fampidirana, izay miasa, ary izay tsy manao na inona na inona" + }, + "mi": { + "displayName": "Ngā māngai hohe", + "description": "Tirohia ko ēhea māngai e hiahia ana ki te whakaurunga, ko ēhea e mahi ana, ko ēhea kāore i te mahi" + }, + "mk": { + "displayName": "Активни агенти", + "description": "Видете кои агенти бараат внес, кои работат и кои се неактивни" + }, + "ml": { + "displayName": "സജീവ ഏജന്റുകൾ", + "description": "ഏതു ഏജന്റുകൾക്ക് ഇൻപുട്ട് വേണം, ഏതു പ്രവർത്തിക്കുന്നു, ഏതു നിഷ്ക്രിയമാണ് എന്നു കാണുക" + }, + "mn": { + "displayName": "Идэвхтэй агентууд", + "description": "Ямар агентад оролт шаардлагатай, аль нь ажиллаж, аль нь чөлөөтэй байгааг харна уу" + }, + "mr": { + "displayName": "सक्रिय एजंट्स", + "description": "कोणत्या एजंट्सना इनपुट हवे, कोणते काम करत आहेत आणि कोणते निष्क्रिय आहेत ते पाहा" + }, + "ms": { + "displayName": "Ejen aktif", + "description": "Lihat ejen yang memerlukan input, yang sedang bekerja, dan yang tidak aktif" + }, + "mt": { + "displayName": "Aġenti attivi", + "description": "Ara liema aġenti jeħtieġu input, liema qed jaħdmu u liema huma weqfin" + }, + "my": { + "displayName": "လုပ်ဆောင်နေသော agent များ", + "description": "မည်သည့် agent သည် ထည့်သွင်းမှု လိုအပ်သည်၊ မည်သည့်သည် လုပ်ဆောင်နေသည်၊ မည်သည့်သည် နားနေသည်ကို ကြည့်ပါ" + }, + "nb": { + "displayName": "Aktive agenter", + "description": "Se hvilke agenter som trenger inndata, hvilke som arbeider, og hvilke som er inaktive" + }, + "ne": { + "displayName": "सक्रिय एजेन्टहरू", + "description": "कुन एजेन्टहरूलाई इनपुट चाहिन्छ, कुन काम गर्दै छन् र कुन निष्क्रिय छन् हेर्नुहोस्" + }, + "nl": { + "displayName": "Actieve agents", + "description": "Zie welke agents invoer nodig hebben, welke werken en welke inactief zijn" + }, + "om": { + "displayName": "Eejentoota hojii irra jiran", + "description": "Eejentoonni kamiin galtee barbaadan, kamiin hojjetan, kamiin dhaabbatan ilaali" + }, + "or": { + "displayName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ", + "description": "କେଉଁ ଏଜେଣ୍ଟଗୁଡ଼ିକୁ ଇନପୁଟ୍ ଆବଶ୍ୟକ, କେଉଁ କାମ କରୁଛନ୍ତି ଓ କେଉଁ ନିଷ୍କ୍ରିୟ ଅଛନ୍ତି ଦେଖନ୍ତୁ" + }, + "pa": { + "displayName": "ਸਰਗਰਮ ਏਜੰਟ", + "description": "ਦੇਖੋ ਕਿ ਕਿਹੜੇ ਏਜੰਟਾਂ ਨੂੰ ਇਨਪੁਟ ਦੀ ਲੋੜ ਹੈ, ਕਿਹੜੇ ਕੰਮ ਕਰ ਰਹੇ ਹਨ ਅਤੇ ਕਿਹੜੇ ਵਿਹਲੇ ਹਨ" + }, + "pl": { + "displayName": "Aktywni agenci", + "description": "Zobacz, którzy agenci potrzebują danych, którzy pracują, a którzy są bezczynni" + }, + "ps": { + "displayName": "فعال اجنټان", + "description": "وګورئ کوم اجنټان ورودي ته اړتیا لري، کوم کار کوي او کوم بې کاره دي" + }, + "pt": { + "displayName": "Agentes ativos", + "description": "Veja que agentes precisam de dados, quais estão a trabalhar e quais estão inativos" + }, + "pt-BR": { + "displayName": "Agentes ativos", + "description": "Veja quais agentes precisam de entrada, quais estão trabalhando e quais estão ociosos" + }, + "ro": { + "displayName": "Agenți activi", + "description": "Vezi ce agenți necesită introducere, care lucrează și care sunt inactivi" + }, + "ru": { + "displayName": "Активные агенты", + "description": "Посмотрите, каким агентам нужен ввод, какие работают, а какие простаивают" + }, + "si": { + "displayName": "සක්‍රිය නියෝජිතයන්", + "description": "කුමන නියෝජිතයන්ට ආදානය අවශ්‍යද, කවුරුන් වැඩ කරනවාද සහ කවුරුන් නිෂ්ක්‍රීයද බලන්න" + }, + "sk": { + "displayName": "Aktívni agenti", + "description": "Pozrite si, ktorí agenti potrebujú vstup, ktorí pracujú a ktorí sú nečinní" + }, + "sl": { + "displayName": "Aktivni agenti", + "description": "Poglejte, kateri agenti potrebujejo vnos, kateri delajo in kateri so nedejavni" + }, + "so": { + "displayName": "Wakiillada firfircoon", + "description": "Arag wakiillada u baahan wax-soo-gal, kuwa shaqaynaya, iyo kuwa firfircooni la'aan" + }, + "sq": { + "displayName": "Agjentët aktivë", + "description": "Shiko cilët agjentë kanë nevojë për të dhëna, cilët punojnë dhe cilët janë të papunë" + }, + "sr": { + "displayName": "Aktivni agenti", + "description": "Vidite kojim agentima treba unos, koji rade, a koji su neaktivni" + }, + "sv": { + "displayName": "Aktiva agenter", + "description": "Se vilka agenter som behöver indata, vilka som arbetar och vilka som är inaktiva" + }, + "sw": { + "displayName": "Mawakala wanaofanya kazi", + "description": "Ona mawakala wanaohitaji maelezo, wanaofanya kazi, na wasiofanya kitu" + }, + "ta": { + "displayName": "செயலில் உள்ள முகவர்கள்", + "description": "எந்த முகவர்களுக்கு உள்ளீடு தேவை, எவை வேலை செய்கின்றன, எவை செயலற்றுள்ளன என்பதைப் பாருங்கள்" + }, + "te": { + "displayName": "చురుకైన ఏజెంట్లు", + "description": "ఏ ఏజెంట్లకు ఇన్‌పుట్ కావాలి, ఏవి పని చేస్తున్నాయి, ఏవి ఖాళీగా ఉన్నాయి అని చూడండి" + }, + "th": { + "displayName": "เอเจนต์ที่กำลังทำงาน", + "description": "ดูว่าเอเจนต์ใดต้องการข้อมูล เอเจนต์ใดกำลังทำงาน และเอเจนต์ใดว่างอยู่" + }, + "tr": { + "displayName": "Etkin ajanlar", + "description": "Hangi ajanların giriş beklediğini, hangilerinin çalıştığını ve hangilerinin boşta olduğunu görün" + }, + "uk": { + "displayName": "Активні агенти", + "description": "Дивіться, яким агентам потрібне введення, які працюють, а які неактивні" + }, + "ur": { + "displayName": "فعال ایجنٹس", + "description": "دیکھیں کن ایجنٹس کو ان پٹ درکار ہے، کون کام کر رہے ہیں اور کون غیر فعال ہیں" + }, + "uz": { + "displayName": "Faol agentlar", + "description": "Qaysi agentlarga kiritish kerak, qaysilari ishlayapti va qaysilari bo'sh ekanini ko'ring" + }, + "vi": { + "displayName": "Tác nhân đang hoạt động", + "description": "Xem tác nhân nào cần dữ liệu, tác nhân nào đang làm việc và tác nhân nào đang rảnh" + }, + "yo": { + "displayName": "Awọn aṣoju to n ṣiṣẹ", + "description": "Wo awọn aṣoju to nilo igbewọle, awọn to n ṣiṣẹ, ati awọn to wa laiṣiṣẹ" + }, + "zh-Hans": { + "displayName": "活动代理", + "description": "查看哪些代理需要输入、哪些正在工作、哪些空闲" + }, + "zh-Hant": { + "displayName": "使用中的代理", + "description": "查看哪些代理需要輸入、哪些正在工作、哪些閒置" + }, + "zu": { + "displayName": "Ama-agent asebenzayo", + "description": "Bona ukuthi ama-agent aphi adinga okokufaka, aphi asebenzayo, futhi aphi angenzi lutho" + } +} diff --git a/apps/mobile/plugins/withWidgetLocalizations.js b/apps/mobile/plugins/withWidgetLocalizations.js index 17d204914a..8a8345f9a4 100644 --- a/apps/mobile/plugins/withWidgetLocalizations.js +++ b/apps/mobile/plugins/withWidgetLocalizations.js @@ -1,43 +1,107 @@ const fs = require('fs'); const path = require('path'); const plist = require('@expo/plist').default; -const { withDangerousMod } = require('expo/config-plugins'); +const { withDangerousMod, withXcodeProject } = require('expo/config-plugins'); -// Declares the app's languages on the widget extension. +// Localizes the widget extension. // // expo-widgets writes the extension's Info.plist with four keys and no // localization list, so iOS treats the extension as English-only. Two things // break: the Live Activity and every widget family lay out left-to-right on an -// Arabic or Hebrew device, and the widget gallery copy cannot localize. The -// main app declares the same list for the same reason — see `CFBundleLocalizations` +// Arabic or Hebrew device, and the widget gallery copy stays English. The main +// app declares the same list for the same reason — see `CFBundleLocalizations` // in app.config.ts. // -// This must run after the `expo-widgets` plugin: dangerous mods run in the -// order they are registered, and expo-widgets rewrites the file. +// The gallery copy is bundle metadata, not app copy: expo-widgets emits +// `.configurationDisplayName("…")` and `.description("…")` as Swift string +// literals, which bind to SwiftUI's `LocalizedStringKey` overloads and resolve +// against `Localizable.strings` in the extension bundle. So the English strings +// are the keys, and this writes one `.lproj/Localizable.strings` per +// language. It never goes through i18next, which is why the translations live in +// `widget-gallery-copy.json` beside this file rather than in the app catalogs. +// +// Both mods must run after the `expo-widgets` plugin, which rewrites the +// Info.plist and creates the target. Mods run in reverse registration order, so +// this plugin is registered BEFORE 'expo-widgets' in app.config.ts. const TARGET_NAME = 'ExpoWidgetsTarget'; -module.exports = function withWidgetLocalizations(config, { languages } = {}) { +/** One `.strings` entry. Only the quote and the backslash need escaping. */ +const stringsLine = (key, value) => + `"${key.replace(/[\\"]/g, '\\$&')}" = "${value.replace(/[\\"]/g, '\\$&')}";`; + +module.exports = function withWidgetLocalizations(config, { languages, copy } = {}) { if (!Array.isArray(languages) || languages.length === 0) { throw new Error('withWidgetLocalizations needs a non-empty `languages` array'); } - return withDangerousMod(config, [ - 'ios', - async modConfig => { - const infoPlistPath = path.join( - modConfig.modRequest.platformProjectRoot, - TARGET_NAME, - 'Info.plist' + const missing = languages.filter(tag => !copy?.[tag]); + if (missing.length > 0) { + throw new Error(`withWidgetLocalizations: no gallery copy for ${missing.join(', ')}`); + } + const english = copy.en; + if (!english) { + throw new Error('withWidgetLocalizations: the gallery copy needs an `en` entry'); + } + + // The build phase that copies the `.lproj` directories into the appex. The + // file references are relative to the project root, so they resolve without + // being added to the target's group. + const withResources = cfg => + withXcodeProject(cfg, projectConfig => { + const project = projectConfig.modResults; + // The uuid, not `pbxTargetByName`: that returns the target body, which + // carries no uuid, and `addBuildPhase` silently falls back to the first + // target — the app — when the uuid is undefined. + const targets = project.pbxNativeTargetSection(); + const targetUuid = Object.keys(targets).find( + key => !key.endsWith('_comment') && targets[key].name === TARGET_NAME ); - if (!fs.existsSync(infoPlistPath)) { - throw new Error(`withWidgetLocalizations: ${infoPlistPath} is missing`); + if (!targetUuid) { + throw new Error( + `withWidgetLocalizations: the ${TARGET_NAME} target is missing — this plugin ran before expo-widgets` + ); } - const parsed = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')); - parsed.CFBundleLocalizations = [...languages]; - // The extension has no .lproj resources, so name the development language - // explicitly; otherwise iOS picks the first entry of the list above. - parsed.CFBundleDevelopmentRegion = 'en'; - fs.writeFileSync(infoPlistPath, plist.build(parsed)); - return modConfig; - }, - ]); + const files = languages.map(tag => `${TARGET_NAME}/${tag}.lproj/Localizable.strings`); + const phase = project.addBuildPhase( + files, + 'PBXResourcesBuildPhase', + 'Resources', + targetUuid, + 'app_extension', + '""' + ); + if (!targets[targetUuid].buildPhases.some(entry => entry.value === phase.uuid)) { + throw new Error( + `withWidgetLocalizations: the Resources phase did not attach to ${TARGET_NAME}` + ); + } + return projectConfig; + }); + + return withResources( + withDangerousMod(config, [ + 'ios', + async modConfig => { + const targetRoot = path.join(modConfig.modRequest.platformProjectRoot, TARGET_NAME); + const infoPlistPath = path.join(targetRoot, 'Info.plist'); + if (!fs.existsSync(infoPlistPath)) { + throw new Error(`withWidgetLocalizations: ${infoPlistPath} is missing`); + } + const parsed = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')); + parsed.CFBundleLocalizations = [...languages]; + parsed.CFBundleDevelopmentRegion = 'en'; + fs.writeFileSync(infoPlistPath, plist.build(parsed)); + + for (const tag of languages) { + const dir = path.join(targetRoot, `${tag}.lproj`); + fs.mkdirSync(dir, { recursive: true }); + const lines = [ + stringsLine(english.displayName, copy[tag].displayName), + stringsLine(english.description, copy[tag].description), + ]; + fs.writeFileSync(path.join(dir, 'Localizable.strings'), `${lines.join('\n')}\n`, 'utf8'); + } + return modConfig; + }, + ]) + ); }; diff --git a/apps/mobile/src/glanceable-android/android-sink.test.ts b/apps/mobile/src/glanceable-android/android-sink.test.ts index 9a15cb1933..c16d3ba08d 100644 --- a/apps/mobile/src/glanceable-android/android-sink.test.ts +++ b/apps/mobile/src/glanceable-android/android-sink.test.ts @@ -417,7 +417,7 @@ describe('androidSink widget publish and end', () => { }); it.each([ - ['privacy', 'Agents hidden'], + ['privacy', 'Open Kilo to see agents'], ['signed_out', 'Sign in to see agents'], ] as const)('cancels both deadlines immediately for %s', async (status, copy) => { androidSink.publish(MIXED); diff --git a/apps/mobile/src/glanceable-android/register.test.ts b/apps/mobile/src/glanceable-android/register.test.ts index e6f1b1e0c1..8014b13140 100644 --- a/apps/mobile/src/glanceable-android/register.test.ts +++ b/apps/mobile/src/glanceable-android/register.test.ts @@ -189,7 +189,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { }); it.each([ - ['privacy', 'Agents hidden'], + ['privacy', 'Open Kilo to see agents'], ['signed_out', 'Sign in to see agents'], ] as const)('preserves the %s blank even after expiry', async (status, copy) => { const stored = snapshotFor([], status); @@ -239,7 +239,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { }); it.each([ - ['privacy', 'Agents hidden'], + ['privacy', 'Open Kilo to see agents'], ['signed_out', 'Sign in to see agents'], ] as const)('reads a native %s blank instead of stale legacy storage', async (status, copy) => { const old = snapshotFor(); diff --git a/apps/mobile/src/glanceable-android/widget-props.test.ts b/apps/mobile/src/glanceable-android/widget-props.test.ts index 6d84fa48bf..8e523b273c 100644 --- a/apps/mobile/src/glanceable-android/widget-props.test.ts +++ b/apps/mobile/src/glanceable-android/widget-props.test.ts @@ -22,7 +22,7 @@ const COPY: Record = { 'glanceable.stale': 'Updates delayed', 'glanceable.expired': 'Status expired', 'glanceable.signedOut': 'Sign in to see agents', - 'glanceable.privacy': 'Agents hidden', + 'glanceable.privacy': 'Open Kilo to see agents', 'glanceable.openAgents': 'Open agents', }; const translate = (key: string): string => COPY[key] ?? key; @@ -87,7 +87,7 @@ describe('buildAndroidWidgetProps', () => { ['stale', [{ status: 'busy' }], 'Updates delayed', 3, true], ['expired', [], 'Status expired', 0, false], ['signed_out', [], 'Sign in to see agents', 0, false], - ['privacy', [], 'Agents hidden', 0, false], + ['privacy', [], 'Open Kilo to see agents', 0, false], ]; for (const [status, sessions, statusLine, counts, showOpenAgents] of cases) { const props = buildAndroidWidgetProps(snapshotFor(sessions, 0, status), {}, translate); @@ -142,7 +142,7 @@ describe('current widget deadline rendering', () => { }); it.each([ - ['privacy', 'Agents hidden'], + ['privacy', 'Open Kilo to see agents'], ['signed_out', 'Sign in to see agents'], ['empty', 'No work in progress'], ['waiting', 'Waiting for agents'], @@ -210,7 +210,7 @@ describe('status precedence and count hiding', () => { ['empty', 'No work in progress'], ['expired', 'Status expired'], ['signed_out', 'Sign in to see agents'], - ['privacy', 'Agents hidden'], + ['privacy', 'Open Kilo to see agents'], ] as const)('hides counts on every Android surface for %s', (status, expected) => { const snapshot = { ...MIXED, status }; const props = buildAndroidWidgetProps(snapshot, {}, translate); @@ -225,7 +225,7 @@ describe('status precedence and count hiding', () => { it.each([ [{ signedOut: true, orgInvalid: true }, 'Sign in to see agents'], - [{ orgInvalid: true }, 'Agents hidden'], + [{ orgInvalid: true }, 'Open Kilo to see agents'], ] as const)('honors auth overrides before stale counts: %j', (flags, expected) => { const snapshot = { ...MIXED, status: 'stale' as const }; const props = buildAndroidWidgetProps(snapshot, flags, translate); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index e5ac92d4b5..ff8ee3a925 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -726,7 +726,7 @@ describe('iosSink widget publish', () => { it.each([ ['signed_out', 'Sign in to see agents'], - ['privacy', 'Agents hidden'], + ['privacy', 'Open Kilo to see agents'], ] as const)('keeps %s copy after a previous active timeline expires', (status, statusLine) => { vi.useFakeTimers(); vi.setSystemTime(NOW); @@ -777,7 +777,7 @@ describe('iosSink widget publish', () => { ['stale', [{ status: 'busy' }], "Can't update now", 3, true], ['expired', [], 'Status expired', 0, false], ['signed_out', [], 'Sign in to see agents', 0, false], - ['privacy', [], 'Agents hidden', 0, false], + ['privacy', [], 'Open Kilo to see agents', 0, false], ]; for (const [status, sessions, statusLine, counts, hasPrimary] of cases) { iosSink.publish(snapshotFor(sessions, 0, status)); diff --git a/apps/mobile/src/glanceable-ios/layout-copy.test.ts b/apps/mobile/src/glanceable-ios/layout-copy.test.ts index dca594ca3e..3a742e95ec 100644 --- a/apps/mobile/src/glanceable-ios/layout-copy.test.ts +++ b/apps/mobile/src/glanceable-ios/layout-copy.test.ts @@ -50,6 +50,13 @@ describe('withGlanceableCopy', () => { expect(JSON.parse(JSON.parse(literal) as string)).toEqual(glanceableLayoutCopy()); }); + it('bakes the locale in the form the SwiftUI modifier accepts', () => { + // `@expo/ui` applies the locale only when `Locale.availableIdentifiers` + // contains the value, and that list writes `zh_Hans`, not `zh-Hans`. A + // hyphen there silently left the wait in the device language. + expect(glanceableLayoutCopy().locale).not.toContain('-'); + }); + it('covers every status the layouts render, plus the language tag', () => { expect(Object.keys(glanceableLayoutCopy()).toSorted()).toEqual([ 'empty', diff --git a/apps/mobile/src/glanceable-ios/layout-copy.ts b/apps/mobile/src/glanceable-ios/layout-copy.ts index b76a4caa8a..b39434f6d9 100644 --- a/apps/mobile/src/glanceable-ios/layout-copy.ts +++ b/apps/mobile/src/glanceable-ios/layout-copy.ts @@ -31,6 +31,16 @@ const COPY_PLACEHOLDER = '__KILO_GLANCEABLE_COPY__'; * language. The layouts feed the tag to SwiftUI's `locale` environment value * so the whole surface speaks one language. * + * The tag is the underscore form, because `@expo/ui`'s `locale` modifier + * applies the value only when `Locale.availableIdentifiers` contains it, and + * that list spells a script or region subtag with an underscore. `zh-Hans`, + * `zh-Hant` and `pt-BR` failed the check and silently left the wait in the + * device language, which is the one thing this tag exists to prevent. A + * numbering-system extension (`ar-u-nu-latn`) fails the same check, so the + * counts stay in Western digits beside an Arabic-Indic wait; formatting the + * wait in JS instead would freeze it, because a pushed content state carries + * only the timestamp. + * * The slot names are the layouts' own field names, and the status slots match * `GlanceableAgentsSnapshot['status']` so a layout can index this by status. */ @@ -46,7 +56,7 @@ export function glanceableLayoutCopy() { running: i18n.t('glanceable.running'), idle: i18n.t('glanceable.idle'), openAgents: i18n.t('glanceable.openAgents'), - locale: i18n.language, + locale: i18n.language.replace('-', '_'), }; } diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 01a96cb127..92a54f21d5 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -3219,7 +3219,7 @@ "stale": "Kan nie nou opdateer nie", "expired": "Status het verval", "signedOut": "Meld aan om agente te sien", - "privacy": "Agente versteek", + "privacy": "Open Kilo om agente te sien", "openAgents": "Maak agente oop", "running": "Werk", "needsInput": "benodig invoer", diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index f4fd9b7d41..94ed5219f7 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -3219,7 +3219,7 @@ "stale": "አሁን ማዘመን አይቻልም", "expired": "የሁኔታው ጊዜ አልፏል", "signedOut": "ወኪሎችን ለማየት ይግቡ", - "privacy": "ወኪሎች ተደብቀዋል", + "privacy": "ወኪሎችን ለማየት Kilo ይክፈቱ", "openAgents": "ወኪሎችን ይክፈቱ", "running": "በመስራት ላይ", "needsInput": "ግብዓት ይፈልጋል", diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index c9f2a98b36..5fec89d51b 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -3307,7 +3307,7 @@ "stale": "يتعذّر التحديث الآن", "expired": "انتهت صلاحية الحالة", "signedOut": "سجّل الدخول لرؤية الوكلاء", - "privacy": "الوكلاء مخفيون", + "privacy": "افتح Kilo لرؤية الوكلاء", "openAgents": "فتح الوكلاء", "running": "جارٍ العمل", "needsInput": "يتطلب إدخالًا", diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index aa00483fe7..a7790db421 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -3219,7 +3219,7 @@ "stale": "Hazırda yeniləmək mümkün deyil", "expired": "Statusun müddəti bitib", "signedOut": "Agentləri görmək üçün daxil olun", - "privacy": "Agentlər gizlədilib", + "privacy": "Agentləri görmək üçün Kilo-nu açın", "openAgents": "Agentləri açın", "running": "İşlənir", "needsInput": "GİRİŞ TƏLƏB OLUNUR", diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index bcd998f942..62a9c2bbb9 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -3263,7 +3263,7 @@ "stale": "Зараз немагчыма абнавіць", "expired": "Тэрмін дзеяння статусу скончыўся", "signedOut": "Увайдзіце, каб бачыць агентаў", - "privacy": "Агенты схаваны", + "privacy": "Адкрыйце Kilo, каб убачыць агентаў", "openAgents": "Адкрыць агентаў", "running": "Працуе", "needsInput": "патрабуецца ўвод", diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 015b525632..88ca31a370 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -3219,7 +3219,7 @@ "stale": "Не може да се актуализира сега", "expired": "Статусът е изтекъл", "signedOut": "Влезте, за да видите агентите", - "privacy": "Агентите са скрити", + "privacy": "Отворете Kilo, за да видите агентите", "openAgents": "Отворете агентите", "running": "Работи", "needsInput": "изисква въвеждане", diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index f23faeef6f..dbea3b14db 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -3219,7 +3219,7 @@ "stale": "এখন আপডেট করা যাচ্ছে না", "expired": "অবস্থার মেয়াদ শেষ হয়েছে", "signedOut": "এজেন্টগুলি দেখতে সাইন ইন করুন", - "privacy": "এজেন্টগুলি লুকানো আছে", + "privacy": "এজেন্ট দেখতে Kilo খুলুন", "openAgents": "এজেন্টগুলি খুলুন", "running": "কাজ চলছে", "needsInput": "ইনপুট প্রয়োজন", diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index ba8899dca5..a52b744201 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -3241,7 +3241,7 @@ "stale": "Trenutno nije moguće ažurirati", "expired": "Status je istekao", "signedOut": "Prijavite se da biste vidjeli agente", - "privacy": "Agenti su skriveni", + "privacy": "Otvorite Kilo da vidite agente", "openAgents": "Otvorite agente", "running": "U toku", "needsInput": "treba unos", diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 63fbcbb197..fc321d63d9 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -3241,7 +3241,7 @@ "stale": "Ara no es pot actualitzar", "expired": "Estat caducat", "signedOut": "Inicia la sessió per veure els agents", - "privacy": "Agents ocults", + "privacy": "Obre Kilo per veure els agents", "openAgents": "Obre els agents", "running": "Treballant", "needsInput": "requereix entrada", diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 68fefa48f1..0d90dea8b2 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -3219,7 +3219,7 @@ "stale": "ئێستا ناتوانرێت نوێ بکرێتەوە", "expired": "دۆخەکە بەسەرچووە", "signedOut": "بچۆ ژوورەوە بۆ بینینی ئەجێنتەکان", - "privacy": "ئەجێنتەکان شاراونەتەوە", + "privacy": "Kilo بکەرەوە بۆ بینینی ئەجێنتەکان", "openAgents": "کردنەوەی ئەجێنتەکان", "running": "کارکردن", "needsInput": "پێویستی بە داخڵکردن", diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 8e3df4f59a..fcd59ddf43 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -3263,7 +3263,7 @@ "stale": "Nyní nelze aktualizovat", "expired": "Platnost stavu vypršela", "signedOut": "Přihlaste se pro zobrazení agentů", - "privacy": "Agenti jsou skrytí", + "privacy": "Otevřete Kilo a zobrazte agenty", "openAgents": "Otevřít agenty", "running": "Pracuji", "needsInput": "vyžaduje vstup", diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index e0fb6278d6..214b302589 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -3307,7 +3307,7 @@ "stale": "Methu diweddaru nawr", "expired": "Mae'r statws wedi dod i ben", "signedOut": "Mewngofnodwch i weld asiantau", - "privacy": "Asiantau wedi'u cuddio", + "privacy": "Agorwch Kilo i weld asiantau", "openAgents": "Agorwch asiantau", "running": "Yn gweithio", "needsInput": "angen mewnbwn", diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index c723d0a594..88e976c883 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -3219,7 +3219,7 @@ "stale": "Kan ikke opdatere nu", "expired": "Status er udløbet", "signedOut": "Log ind for at se agenter", - "privacy": "Agenter er skjult", + "privacy": "Åbn Kilo for at se agenter", "openAgents": "Åbn agenter", "running": "Arbejder", "needsInput": "kræver input", diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index f0403009c1..85ce092266 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -3219,7 +3219,7 @@ "stale": "Aktualisierung derzeit nicht möglich", "expired": "Status abgelaufen", "signedOut": "Melde dich an, um Agenten zu sehen", - "privacy": "Agenten ausgeblendet", + "privacy": "Öffne Kilo, um Agenten zu sehen", "openAgents": "Agenten öffnen", "running": "Wird bearbeitet", "needsInput": "Eingabe erforderlich", diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index 770bf5ff01..3148efc268 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -3219,7 +3219,7 @@ "stale": "Δεν είναι δυνατή η ενημέρωση τώρα", "expired": "Η κατάσταση έληξε", "signedOut": "Συνδεθείτε για να δείτε τους πράκτορες", - "privacy": "Οι πράκτορες είναι κρυφοί", + "privacy": "Άνοιξε το Kilo για να δεις τους πράκτορες", "openAgents": "Ανοίξτε τους πράκτορες", "running": "Εργασία", "needsInput": "χρειάζεται είσοδο", diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 277d5c7af2..0731a718f3 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -3219,7 +3219,7 @@ "stale": "Can't update now", "expired": "Status expired", "signedOut": "Sign in to see agents", - "privacy": "Agents hidden", + "privacy": "Open Kilo to see agents", "openAgents": "Open agents", "running": "Working", "needsInput": "Needs input", diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 78c98180d0..562f07bf8f 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -3241,7 +3241,7 @@ "stale": "No se puede actualizar ahora", "expired": "Estado caducado", "signedOut": "Inicia sesión para ver los agentes", - "privacy": "Agentes ocultos", + "privacy": "Abre Kilo para ver los agentes", "openAgents": "Abrir agentes", "running": "Trabajando", "needsInput": "requiere entrada", diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index f5efb3a9df..46e1085191 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -3219,7 +3219,7 @@ "stale": "Praegu ei saa uuendada", "expired": "Olek on aegunud", "signedOut": "Agentide nägemiseks logige sisse", - "privacy": "Agendid on peidetud", + "privacy": "Agentide nägemiseks ava Kilo", "openAgents": "Avage agendid", "running": "Töötamine", "needsInput": "vajab sisendit", diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 0b8a26c90b..4b72920aa9 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -3219,7 +3219,7 @@ "stale": "Ezin da orain eguneratu", "expired": "Egoera iraungi da", "signedOut": "Hasi saioa agenteak ikusteko", - "privacy": "Agenteak ezkutatuta", + "privacy": "Ireki Kilo agenteak ikusteko", "openAgents": "Ireki agenteak", "running": "Lanean", "needsInput": "sarreraren zain", diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 675cae9a90..168817c134 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -3219,7 +3219,7 @@ "stale": "اکنون به‌روزرسانی ممکن نیست", "expired": "وضعیت منقضی شد", "signedOut": "برای دیدن عامل‌ها وارد شوید", - "privacy": "عامل‌ها پنهان هستند", + "privacy": "برای دیدن عامل‌ها Kilo را باز کنید", "openAgents": "عامل‌ها را باز کنید", "running": "در حال کار", "needsInput": "نیاز به ورودی", diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index e4b1dee68f..c84b7a34dc 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -3219,7 +3219,7 @@ "stale": "Päivitys ei onnistu nyt", "expired": "Tila vanhentunut", "signedOut": "Kirjaudu sisään nähdäksesi agentit", - "privacy": "Agentit piilotettu", + "privacy": "Avaa Kilo nähdäksesi agentit", "openAgents": "Avaa agentit", "running": "Työstetään", "needsInput": "vaatii syötettä", diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index c104a1eb83..d7da7e546c 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -3219,7 +3219,7 @@ "stale": "Hindi makapag-update ngayon", "expired": "Nag-expire ang katayuan", "signedOut": "Mag-sign in para makita ang mga agent", - "privacy": "Nakatago ang mga agent", + "privacy": "Buksan ang Kilo para makita ang mga agent", "openAgents": "Buksan ang mga agent", "running": "Gumagawa", "needsInput": "kailangan ng input", diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index 5cf0f077ae..ac2123c829 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -3241,7 +3241,7 @@ "stale": "Mise à jour impossible pour le moment", "expired": "Statut expiré", "signedOut": "Connectez-vous pour voir les agents", - "privacy": "Agents masqués", + "privacy": "Ouvre Kilo pour voir les agents", "openAgents": "Ouvrir les agents", "running": "En cours", "needsInput": "saisie requise", diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index f86727e629..da8301a4ea 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -3285,7 +3285,7 @@ "stale": "Ní féidir nuashonrú anois", "expired": "Stádas imithe in éag", "signedOut": "Sínigh isteach chun gníomhairí a fheiceáil", - "privacy": "Gníomhairí i bhfolach", + "privacy": "Oscail Kilo chun gníomhairí a fheiceáil", "openAgents": "Oscail gníomhairí", "running": "Ag obair", "needsInput": "teastaíonn ionchur", diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index 44528c590b..d00d0913ca 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -3219,7 +3219,7 @@ "stale": "Non se pode actualizar agora", "expired": "Estado caducado", "signedOut": "Inicia sesión para ver os axentes", - "privacy": "Axentes ocultos", + "privacy": "Abre Kilo para ver os axentes", "openAgents": "Abrir axentes", "running": "Traballando", "needsInput": "precisa entrada", diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index c75136edaf..6f8d4ca9b0 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -3219,7 +3219,7 @@ "stale": "હમણાં અપડેટ કરી શકાતું નથી", "expired": "સ્થિતિની સમયસીમા સમાપ્ત થઈ", "signedOut": "એજન્ટો જોવા માટે સાઇન ઇન કરો", - "privacy": "એજન્ટો છુપાવેલા છે", + "privacy": "એજન્ટો જોવા માટે Kilo ખોલો", "openAgents": "એજન્ટો ખોલો", "running": "કામ થઈ રહ્યું છે", "needsInput": "ઇનપુટ જરૂરી", diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 12b8710d47..2b91541268 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -3219,7 +3219,7 @@ "stale": "Ba a iya sabuntawa yanzu", "expired": "Matsayi ya ƙare", "signedOut": "Shiga don ganin wakilai", - "privacy": "An ɓoye wakilai", + "privacy": "Buɗe Kilo don ganin wakilai", "openAgents": "Buɗe wakilai", "running": "Yana aiki", "needsInput": "yana buƙatar bayani", diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index 24bd97e568..109758c2b6 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -3241,7 +3241,7 @@ "stale": "לא ניתן לעדכן כעת", "expired": "תוקף המצב פג", "signedOut": "היכנס כדי לראות סוכנים", - "privacy": "הסוכנים מוסתרים", + "privacy": "פתח את Kilo כדי לראות סוכנים", "openAgents": "פתח סוכנים", "running": "עובד", "needsInput": "נדרש קלט", diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 952b6c6346..443f1de30b 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -3219,7 +3219,7 @@ "stale": "अभी अपडेट नहीं हो सकता", "expired": "स्थिति की समय सीमा समाप्त हो गई", "signedOut": "एजेंट देखने के लिए साइन इन करें", - "privacy": "एजेंट छिपे हुए हैं", + "privacy": "एजेंट देखने के लिए Kilo खोलें", "openAgents": "एजेंट खोलें", "running": "काम कर रहा है", "needsInput": "इनपुट आवश्यक", diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 4bfe4d0ae9..2e9a82f3eb 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -3241,7 +3241,7 @@ "stale": "Trenutačno nije moguće ažurirati", "expired": "Status je istekao", "signedOut": "Prijavite se da biste vidjeli agente", - "privacy": "Agenti su skriveni", + "privacy": "Otvorite Kilo da vidite agente", "openAgents": "Otvorite agente", "running": "Radim", "needsInput": "treba unos", diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 567a3abcc0..31a36273d5 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -3219,7 +3219,7 @@ "stale": "Pa ka mete ajou kounye a", "expired": "Estati a ekspire", "signedOut": "Konekte pou wè ajans yo", - "privacy": "Ajans yo kache", + "privacy": "Ouvri Kilo pou wè ajans", "openAgents": "Louvri ajans yo", "running": "Ap travay", "needsInput": "bezwen input", diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index 8b1588af6d..698a43e229 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -3219,7 +3219,7 @@ "stale": "Most nem frissíthető", "expired": "Az állapot lejárt", "signedOut": "Jelentkezzen be az ügynökök megtekintéséhez", - "privacy": "Ügynökök elrejtve", + "privacy": "Nyisd meg a Kilót az ügynökök megtekintéséhez", "openAgents": "Ügynökök megnyitása", "running": "Feldolgozás", "needsInput": "bemenetet igényel", diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index f3f9c661c4..9b22285fdb 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -3219,7 +3219,7 @@ "stale": "Այժմ հնարավոր չէ թարմացնել", "expired": "Կարգավիճակի ժամկետը լրացել է", "signedOut": "Մուտք գործեք՝ գործակալներին տեսնելու համար", - "privacy": "Գործակալները թաքցված են", + "privacy": "Բացեք Kilo-ն՝ գործակալները տեսնելու համար", "openAgents": "Բացեք գործակալները", "running": "Մշակվում է", "needsInput": "մուտքագրման կարիք ունի", diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index a500119116..551d310416 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -3219,7 +3219,7 @@ "stale": "Tidak dapat memperbarui sekarang", "expired": "Status kedaluwarsa", "signedOut": "Masuk untuk melihat agen", - "privacy": "Agen disembunyikan", + "privacy": "Buka Kilo untuk melihat agen", "openAgents": "Buka agen", "running": "Mengerjakan", "needsInput": "memerlukan input", diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index c5e6e86fd5..31ec289b58 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -3219,7 +3219,7 @@ "stale": "Enweghị ike imelite ugbu a", "expired": "Oge ọnọdụ agwụla", "signedOut": "Banye iji hụ ndị ọrụ", - "privacy": "Ezochiri ndị ọrụ", + "privacy": "Mepee Kilo ka ị hụ ndị ọrụ", "openAgents": "Mepee ndị ọrụ", "running": "Na-arụ ọrụ", "needsInput": "chọrọ ntinye", diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 1d34bddf0a..54e7bddd56 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -3219,7 +3219,7 @@ "stale": "Ekki hægt að uppfæra núna", "expired": "Staða útrunnin", "signedOut": "Skráðu þig inn til að sjá umboð", - "privacy": "Umboð falin", + "privacy": "Opnaðu Kilo til að sjá umboð", "openAgents": "Opna umboð", "running": "Vinnur", "needsInput": "þarfnast inntaks", diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 3a03412ac7..e7b908fa2b 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -3241,7 +3241,7 @@ "stale": "Impossibile aggiornare ora", "expired": "Stato scaduto", "signedOut": "Accedi per vedere gli agenti", - "privacy": "Agenti nascosti", + "privacy": "Apri Kilo per vedere gli agenti", "openAgents": "Apri agenti", "running": "In corso", "needsInput": "richiede input", diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index efd3e822a8..923bc461ca 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -3219,7 +3219,7 @@ "stale": "現在更新できません", "expired": "ステータスの有効期限が切れました", "signedOut": "エージェントを表示するにはサインインしてください", - "privacy": "エージェントは非表示です", + "privacy": "エージェントを表示するには Kilo を開いてください", "openAgents": "エージェントを開く", "running": "作業中", "needsInput": "入力が必要", diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index b644066a82..fe22965dd5 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -3219,7 +3219,7 @@ "stale": "ახლა განახლება ვერ ხერხდება", "expired": "სტატუსს ვადა გაუვიდა", "signedOut": "შედით აგენტების სანახავად", - "privacy": "აგენტები დამალულია", + "privacy": "აგენტების სანახავად გახსენით Kilo", "openAgents": "აგენტების გახსნა", "running": "მუშავდება", "needsInput": "მოითხოვს შეყვანას", diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 5fd40b1cff..8eb541ddbf 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -3219,7 +3219,7 @@ "stale": "Қазір жаңарту мүмкін емес", "expired": "Күйдің мерзімі өтті", "signedOut": "Агенттерді көру үшін кіріңіз", - "privacy": "Агенттер жасырылған", + "privacy": "Агенттерді көру үшін Kilo ашыңыз", "openAgents": "Агенттерді ашу", "running": "Орындалуда", "needsInput": "енгізу қажет", diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 7c084da2ad..4d9dbe5480 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -3219,7 +3219,7 @@ "stale": "មិនអាចធ្វើបច្ចុប្បន្នភាពឥឡូវនេះបានទេ", "expired": "ស្ថានភាពបានផុតកំណត់", "signedOut": "ចូលដើម្បីមើលភ្នាក់ងារ", - "privacy": "ភ្នាក់ងារត្រូវបានលាក់", + "privacy": "បើក Kilo ដើម្បីមើលភ្នាក់ងារ", "openAgents": "បើកភ្នាក់ងារ", "running": "កំពុងដំណើរការ", "needsInput": "ត្រូវការបញ្ចូល", diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 1c083d7657..ec7b1dae98 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -3219,7 +3219,7 @@ "stale": "ಈಗ ನವೀಕರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ", "expired": "ಸ್ಥಿತಿಯ ಅವಧಿ ಮುಗಿದಿದೆ", "signedOut": "ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೈನ್ ಇನ್ ಮಾಡಿ", - "privacy": "ಏಜೆಂಟ್‌ಗಳನ್ನು ಮರೆಮಾಡಲಾಗಿದೆ", + "privacy": "ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು Kilo ತೆರೆಯಿರಿ", "openAgents": "ಏಜೆಂಟ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ", "running": "ಕೆಲಸ ಮಾಡುತ್ತಿದೆ", "needsInput": "ಇನ್‌ಪುಟ್ ಅಗತ್ಯವಿದೆ", diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 9526dfb3e7..ce7137f3ec 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -3219,7 +3219,7 @@ "stale": "지금 업데이트할 수 없습니다", "expired": "상태 만료됨", "signedOut": "에이전트를 보려면 로그인하세요", - "privacy": "에이전트 숨겨짐", + "privacy": "에이전트를 보려면 Kilo를 여세요", "openAgents": "에이전트 열기", "running": "작업 중", "needsInput": "입력 필요", diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index 3b4de51544..bbe61c4cd3 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -3219,7 +3219,7 @@ "stale": "ບໍ່ສາມາດອັບເດດໄດ້ໃນຕອນນີ້", "expired": "ສະຖານະໝົດອາຍຸແລ້ວ", "signedOut": "ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງຕົວແທນ", - "privacy": "ຕົວແທນຖືກເຊື່ອງໄວ້", + "privacy": "ເປີດ Kilo ເພື່ອເບິ່ງຕົວແທນ", "openAgents": "ເປີດຕົວແທນ", "running": "ກຳລັງດຳເນີນການ", "needsInput": "ຕ້ອງການຂໍ້ມູນເຂົ້າ", diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 1f6db88044..c7855363d5 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -3263,7 +3263,7 @@ "stale": "Dabar nepavyksta atnaujinti", "expired": "Būsenos galiojimas baigėsi", "signedOut": "Prisijunkite, kad matytumėte agentus", - "privacy": "Agentai paslėpti", + "privacy": "Atidarykite Kilo, kad pamatytumėte agentus", "openAgents": "Atidaryti agentus", "running": "Vykdoma", "needsInput": "reikia įvesties", diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index db93325f3a..b8b13d1bb5 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -3241,7 +3241,7 @@ "stale": "Pašlaik nevar atjaunināt", "expired": "Statusa derīgums ir beidzies", "signedOut": "Pieraksties, lai redzētu aģentus", - "privacy": "Aģenti ir paslēpti", + "privacy": "Atveriet Kilo, lai redzētu aģentus", "openAgents": "Atvērt aģentus", "running": "Apstrādā", "needsInput": "nepieciešama ievade", diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index 0bd5e94de5..973680a65b 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -3219,7 +3219,7 @@ "stale": "Tsy afaka manavao izao", "expired": "Lany daty ny sata", "signedOut": "Midira mba hahitana ny agent", - "privacy": "Nafenina ny agent", + "privacy": "Sokafy Kilo hijery ny agent", "openAgents": "Sokafy ny agent", "running": "Miasa", "needsInput": "mila fampidirana", diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index e1098abf3d..23b262959f 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -3219,7 +3219,7 @@ "stale": "Kāore e taea te whakahou ināianei", "expired": "Kua pau te mana o te tūnga", "signedOut": "Takiuru kia kite i ngā māngai", - "privacy": "Kua huna ngā māngai", + "privacy": "Whakatuwherahia Kilo kia kite i ngā māngai", "openAgents": "Whakatuwheratia ngā māngai", "running": "Kei te mahi", "needsInput": "e hiahia ana ki te whakaurunga", diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 869c742dca..b4686fb968 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -3219,7 +3219,7 @@ "stale": "Не може да се ажурира сега", "expired": "Статусот истече", "signedOut": "Најавете се за да ги видите агентите", - "privacy": "Агентите се скриени", + "privacy": "Отворете Kilo за да ги видите агентите", "openAgents": "Отворете ги агентите", "running": "Работи", "needsInput": "бара внес", diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 7632ae7442..e7c3f7a9ba 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -3219,7 +3219,7 @@ "stale": "ഇപ്പോൾ അപ്ഡേറ്റ് ചെയ്യാൻ കഴിയില്ല", "expired": "നില കാലഹരണപ്പെട്ടു", "signedOut": "ഏജന്റുകളെ കാണാൻ സൈൻ ഇൻ ചെയ്യുക", - "privacy": "ഏജന്റുകളെ മറച്ചിരിക്കുന്നു", + "privacy": "ഏജന്റുകളെ കാണാൻ Kilo തുറക്കുക", "openAgents": "ഏജന്റുകളെ തുറക്കുക", "running": "പ്രവർത്തിക്കുന്നു", "needsInput": "ഇൻപുട്ട് ആവശ്യമാണ്", diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index 8e1ea63359..6882b5cf02 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -3219,7 +3219,7 @@ "stale": "Одоо шинэчлэх боломжгүй", "expired": "Төлөвийн хугацаа дууссан", "signedOut": "Агентуудыг харахын тулд нэвтэрнэ үү", - "privacy": "Агентуудыг нуусан", + "privacy": "Агентуудыг харахын тулд Kilo-г онгойлго", "openAgents": "Агентуудыг нээх", "running": "Ажиллаж байна", "needsInput": "оролт шаардлагатай", diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index 2e864796a9..c1f8bded68 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -3219,7 +3219,7 @@ "stale": "आता अद्यतनित करता येत नाही", "expired": "स्थिती कालबाह्य झाली", "signedOut": "एजंट्स पाहण्यासाठी साइन इन करा", - "privacy": "एजंट्स लपवले आहेत", + "privacy": "एजंट पाहण्यासाठी Kilo उघडा", "openAgents": "एजंट्स उघडा", "running": "कार्यरत", "needsInput": "इनपुट आवश्यक", diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index f75a32bc14..7ff9af474e 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -3219,7 +3219,7 @@ "stale": "Tidak dapat mengemas kini sekarang", "expired": "Status tamat tempoh", "signedOut": "Log masuk untuk melihat ejen", - "privacy": "Ejen disembunyikan", + "privacy": "Buka Kilo untuk melihat ejen", "openAgents": "Buka ejen", "running": "Memproses…", "needsInput": "perlu input", diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index c16fb359a8..70e46890b4 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -3285,7 +3285,7 @@ "stale": "Ma jistax jaġġorna bħalissa", "expired": "L-istatus skada", "signedOut": "Idħol biex tara l-aġenti", - "privacy": "Aġenti moħbija", + "privacy": "Iftaħ Kilo biex tara l-aġenti", "openAgents": "Iftaħ l-aġenti", "running": "Qed jaħdem", "needsInput": "jeħtieġ input", diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index 02eda2f2c6..d45e3d7f7d 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -3219,7 +3219,7 @@ "stale": "ယခု အပ်ဒိတ်လုပ်၍ မရပါ", "expired": "အခြေအနေ သက်တမ်းကုန်သွားသည်", "signedOut": "agent များကို ကြည့်ရန် ဝင်ပါ", - "privacy": "agent များကို ဝှက်ထားသည်", + "privacy": "Agent များကို ကြည့်ရန် Kilo ကို ဖွင့်ပါ", "openAgents": "agent များကို ဖွင့်ပါ", "running": "လုပ်ဆောင်နေသည်", "needsInput": "ထည့်သွင်းမှု လိုအပ်သည်", diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index 673beb1da8..e5478cf2c1 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -3219,7 +3219,7 @@ "stale": "Kan ikke oppdatere nå", "expired": "Statusen er utløpt", "signedOut": "Logg inn for å se agenter", - "privacy": "Agenter er skjult", + "privacy": "Åpne Kilo for å se agenter", "openAgents": "Åpne agenter", "running": "Jobber", "needsInput": "trenger innspill", diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index cc967e15a6..db44bcb78c 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -3219,7 +3219,7 @@ "stale": "अहिले अद्यावधिक गर्न सकिँदैन", "expired": "स्थितिको म्याद सकियो", "signedOut": "एजेन्टहरू हेर्न साइन इन गर्नुहोस्", - "privacy": "एजेन्टहरू लुकाइएका छन्", + "privacy": "एजेन्टहरू देख्न Kilo खोल्नुहोस्", "openAgents": "एजेन्टहरू खोल्नुहोस्", "running": "काम गर्दै", "needsInput": "इनपुट चाहिन्छ", diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index a54bf55267..d6c4bd4678 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -3219,7 +3219,7 @@ "stale": "Kan nu niet bijwerken", "expired": "Status verlopen", "signedOut": "Log in om agents te zien", - "privacy": "Agents verborgen", + "privacy": "Open Kilo om agents te zien", "openAgents": "Agents openen", "running": "Bezig", "needsInput": "heeft invoer nodig", diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index ac8e0d0053..6554e83bd0 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -3219,7 +3219,7 @@ "stale": "Amma haaromsuu hin danda'u", "expired": "Yeroon haalaa darbeera", "signedOut": "Eejentoota arguuf seenaa", - "privacy": "Eejentoonni dhokamaniiru", + "privacy": "Eejentoota ilaaluuf Kilo bani", "openAgents": "Eejentoota banaa", "running": "Hojachaa jira", "needsInput": "seensa barbaada", diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 67987a83d3..e56d6839a2 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -3219,7 +3219,7 @@ "stale": "ଏବେ ଅପଡେଟ୍ କରିହେବ ନାହିଁ", "expired": "ସ୍ଥିତିର ଅବଧି ସମାପ୍ତ ହୋଇଛି", "signedOut": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସାଇନ୍ ଇନ୍ କରନ୍ତୁ", - "privacy": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଲୁଚାଯାଇଛି", + "privacy": "ଏଜେଣ୍ଟ ଦେଖିବା ପାଇଁ Kilo ଖୋଲନ୍ତୁ", "openAgents": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଖୋଲନ୍ତୁ", "running": "କାମ କରୁଛି", "needsInput": "ଇନପୁଟ୍ ଆବଶ୍ୟକ", diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index fac79d4f87..680c163a74 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -3219,7 +3219,7 @@ "stale": "ਹੁਣ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ", "expired": "ਸਥਿਤੀ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ", "signedOut": "ਏਜੰਟ ਦੇਖਣ ਲਈ ਸਾਈਨ ਇਨ ਕਰੋ", - "privacy": "ਏਜੰਟ ਲੁਕਾਏ ਗਏ ਹਨ", + "privacy": "ਏਜੰਟ ਦੇਖਣ ਲਈ Kilo ਖੋਲ੍ਹੋ", "openAgents": "ਏਜੰਟ ਖੋਲ੍ਹੋ", "running": "ਕੰਮ ਹੋ ਰਿਹਾ ਹੈ", "needsInput": "ਇਨਪੁੱਟ ਦੀ ਲੋੜ ਹੈ", diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index 789212d4e1..1b75013aea 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -3263,7 +3263,7 @@ "stale": "Nie można teraz zaktualizować", "expired": "Status wygasł", "signedOut": "Zaloguj się, aby zobaczyć agentów", - "privacy": "Agenci ukryci", + "privacy": "Otwórz Kilo, aby zobaczyć agentów", "openAgents": "Otwórz agentów", "running": "Pracuję", "needsInput": "wymaga danych", diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 1848c3e71d..82e7709516 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -3219,7 +3219,7 @@ "stale": "اوس تازه کول ناشوني دي", "expired": "د حالت اعتبار پای ته رسېدلی", "signedOut": "د اجنټانو د لیدلو لپاره ننوزئ", - "privacy": "اجنټان پټ دي", + "privacy": "اجنټان لیدلو لپاره Kilo پرانیزئ", "openAgents": "اجنټان پرانیزئ", "running": "روان", "needsInput": "ورودی ته اړتیا لري", diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index fdb445a1e0..875cb0f4fd 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -3241,7 +3241,7 @@ "stale": "Não é possível atualizar agora", "expired": "Status expirado", "signedOut": "Entre para ver os agentes", - "privacy": "Agentes ocultos", + "privacy": "Abra o Kilo para ver os agentes", "openAgents": "Abrir agentes", "running": "Trabalhando", "needsInput": "requer entrada", diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index d7899b76ff..0192694ce4 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -3241,7 +3241,7 @@ "stale": "Não é possível atualizar agora", "expired": "Estado expirado", "signedOut": "Inicie sessão para ver os agentes", - "privacy": "Agentes ocultos", + "privacy": "Abre o Kilo para ver os agentes", "openAgents": "Abrir agentes", "running": "A trabalhar", "needsInput": "requer entrada", diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 385a470803..17463e4068 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -3241,7 +3241,7 @@ "stale": "Nu se poate actualiza acum", "expired": "Stare expirată", "signedOut": "Autentifică-te pentru a vedea agenții", - "privacy": "Agenți ascunși", + "privacy": "Deschide Kilo pentru a vedea agenții", "openAgents": "Deschide agenții", "running": "Se procesează", "needsInput": "necesită introducere", diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index f5780c9118..69deac959e 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -3263,7 +3263,7 @@ "stale": "Сейчас не удается обновить", "expired": "Статус устарел", "signedOut": "Войдите, чтобы видеть агентов", - "privacy": "Агенты скрыты", + "privacy": "Откройте Kilo, чтобы увидеть агентов", "openAgents": "Открыть агентов", "running": "Работаю...", "needsInput": "требует ввода", diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index dde395e880..d9be04fff8 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -3219,7 +3219,7 @@ "stale": "දැන් යාවත්කාලීන කළ නොහැක", "expired": "තත්ත්වය කල් ඉකුත් වී ඇත", "signedOut": "නියෝජිතයන් බැලීමට පුරනය වන්න", - "privacy": "නියෝජිතයන් සඟවා ඇත", + "privacy": "නියෝජිතයන් බැලීමට Kilo විවෘත කරන්න", "openAgents": "නියෝජිතයන් විවෘත කරන්න", "running": "වැඩ කරමින්", "needsInput": "ආදානය අවශ්යයි", diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index 47c4cf673b..0c36227171 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -3263,7 +3263,7 @@ "stale": "Teraz sa nedá aktualizovať", "expired": "Platnosť stavu vypršala", "signedOut": "Prihláste sa na zobrazenie agentov", - "privacy": "Agenti sú skrytí", + "privacy": "Otvorte Kilo a zobrazte agentov", "openAgents": "Otvoriť agentov", "running": "Pracuje sa", "needsInput": "vyžaduje vstup", diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index 0ad9b4176c..aced43ff0d 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -3263,7 +3263,7 @@ "stale": "Trenutno ni mogoče posodobiti", "expired": "Stanje je poteklo", "signedOut": "Prijavite se za ogled agentov", - "privacy": "Agenti so skriti", + "privacy": "Odpri Kilo za ogled agentov", "openAgents": "Odprite agente", "running": "Delam", "needsInput": "potrebuje vnos", diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index d70c42f4d6..a18c8dc26a 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -3219,7 +3219,7 @@ "stale": "Hadda lama cusboonaysiin karo", "expired": "Xaaladdu way dhacday", "signedOut": "Soo gal si aad u aragto wakiillada", - "privacy": "Wakiillada waa la qariyay", + "privacy": "Fur Kilo si aad wakiillada u aragto", "openAgents": "Fur wakiillada", "running": "Waa shaqaynayaa", "needsInput": "u baahan wax-soo-gal", diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index 3e9499f7fa..55db2c7793 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -3219,7 +3219,7 @@ "stale": "Nuk mund të përditësohet tani", "expired": "Statusi ka skaduar", "signedOut": "Identifikohuni për të parë agjentët", - "privacy": "Agjentët janë fshehur", + "privacy": "Hap Kilo për të shikuar agjentët", "openAgents": "Hapni agjentët", "running": "Duke punuar", "needsInput": "ka nevojë për të dhëna", diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index f87b771f62..d53cb0fe91 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -3241,7 +3241,7 @@ "stale": "Ažuriranje trenutno nije moguće", "expired": "Status je istekao", "signedOut": "Prijavite se da biste videli agente", - "privacy": "Agenti su skriveni", + "privacy": "Otvorite Kilo da vidite agente", "openAgents": "Otvorite agente", "running": "Radim…", "needsInput": "zahteva unos", diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index fc0545eede..68c4a6a7b4 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -3219,7 +3219,7 @@ "stale": "Kan inte uppdatera nu", "expired": "Statusen har gått ut", "signedOut": "Logga in för att se agenter", - "privacy": "Agenter dolda", + "privacy": "Öppna Kilo för att se agenter", "openAgents": "Öppna agenter", "running": "Arbetar", "needsInput": "kräver indata", diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index 39d4cd727e..c630bce5fc 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -3219,7 +3219,7 @@ "stale": "Haiwezi kusasisha sasa", "expired": "Muda wa hali umeisha", "signedOut": "Ingia ili uone mawakala", - "privacy": "Mawakala wamefichwa", + "privacy": "Fungua Kilo ili kuona mawakala", "openAgents": "Fungua mawakala", "running": "Inafanya kazi", "needsInput": "inahitaji mchango", diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 6a8c4e6c5d..870faf20f0 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -3219,7 +3219,7 @@ "stale": "இப்போது புதுப்பிக்க முடியவில்லை", "expired": "நிலை காலாவதியானது", "signedOut": "முகவர்களைக் காண உள்நுழையவும்", - "privacy": "முகவர்கள் மறைக்கப்பட்டுள்ளனர்", + "privacy": "முகவர்களைப் பார்க்க Kilo திறக்கவும்", "openAgents": "முகவர்களைத் திறக்கவும்", "running": "வேலை செய்கிறது", "needsInput": "உள்ளீடு தேவை", diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 04f97a19a1..ddf5575a49 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -3219,7 +3219,7 @@ "stale": "ఇప్పుడు నవీకరించలేము", "expired": "స్థితి గడువు ముగిసింది", "signedOut": "ఏజెంట్లను చూడటానికి సైన్ ఇన్ చేయండి", - "privacy": "ఏజెంట్లు దాచబడ్డారు", + "privacy": "ఏజెంట్లను చూడటానికి Kilo తెరవండి", "openAgents": "ఏజెంట్లను తెరవండి", "running": "పని జరుగుతోంది", "needsInput": "ఇన్పుట్ అవసరం", diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index b31b4352d5..7178250c74 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -3219,7 +3219,7 @@ "stale": "ไม่สามารถอัปเดตได้ในขณะนี้", "expired": "สถานะหมดอายุ", "signedOut": "ลงชื่อเข้าใช้เพื่อดูเอเจนต์", - "privacy": "ซ่อนเอเจนต์อยู่", + "privacy": "เปิด Kilo เพื่อดูเอเจนต์", "openAgents": "เปิดเอเจนต์", "running": "กำลังทำงาน", "needsInput": "ต้องป้อนข้อมูล", diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index b3748d2277..33150d863a 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -3219,7 +3219,7 @@ "stale": "Şu anda güncellenemiyor", "expired": "Durumun süresi doldu", "signedOut": "Ajanları görmek için oturum açın", - "privacy": "Ajanlar gizli", + "privacy": "Ajanları görmek için Kilo'yu açın", "openAgents": "Ajanları açın", "running": "Çalışıyor", "needsInput": "Girdi gerekli", diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index c1ea0d0b8a..87e90e3f62 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -3263,7 +3263,7 @@ "stale": "Зараз не вдається оновити", "expired": "Термін дії статусу минув", "signedOut": "Увійдіть, щоб бачити агентів", - "privacy": "Агентів приховано", + "privacy": "Відкрийте Kilo, щоб побачити агентів", "openAgents": "Відкрити агентів", "running": "Працює", "needsInput": "потребує вводу", diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 1acf081d85..220ed37d31 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -3219,7 +3219,7 @@ "stale": "ابھی اپڈیٹ نہیں ہو سکتا", "expired": "حالت کی میعاد ختم ہو گئی", "signedOut": "ایجنٹس دیکھنے کے لیے سائن ان کریں", - "privacy": "ایجنٹس چھپے ہوئے ہیں", + "privacy": "ایجنٹس دیکھنے کے لیے Kilo کھولیں", "openAgents": "ایجنٹس کھولیں", "running": "کام جاری ہے", "needsInput": "ان پٹ درکار", diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 41b6914520..15f4df58ea 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -3219,7 +3219,7 @@ "stale": "Hozir yangilab bo'lmaydi", "expired": "Holat muddati tugadi", "signedOut": "Agentlarni ko'rish uchun tizimga kiring", - "privacy": "Agentlar yashirilgan", + "privacy": "Agentlarni ko'rish uchun Kilo'ni oching", "openAgents": "Agentlarni oching", "running": "Ishlamoqda", "needsInput": "kiritish kerak", diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index c47f917208..09c7839999 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -3219,7 +3219,7 @@ "stale": "Hiện không thể cập nhật", "expired": "Trạng thái đã hết hạn", "signedOut": "Đăng nhập để xem tác nhân", - "privacy": "Đã ẩn tác nhân", + "privacy": "Mở Kilo để xem tác nhân", "openAgents": "Mở tác nhân", "running": "Đang xử lý", "needsInput": "cần nhập", diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index a76f7e4040..26ece66ab6 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -3219,7 +3219,7 @@ "stale": "Ko le ṣe imudojuiwọn bayi", "expired": "Ipo ti pari akoko", "signedOut": "Wọle lati ri awọn aṣoju", - "privacy": "Awọn aṣoju wa ni ipamọ", + "privacy": "Ṣi Kilo lati ri awọn aṣoju", "openAgents": "Ṣii awọn aṣoju", "running": "Nṣiṣẹ́", "needsInput": "nilo igbewọle", diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index 9da8ae399f..6f9b50b982 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -3219,7 +3219,7 @@ "stale": "暂时无法更新", "expired": "状态已过期", "signedOut": "请登录以查看代理", - "privacy": "代理已隐藏", + "privacy": "打开 Kilo 以查看代理", "openAgents": "打开代理", "running": "工作中", "needsInput": "需要输入", diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index 941289a140..772e63e103 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -3219,7 +3219,7 @@ "stale": "目前無法更新", "expired": "狀態已過期", "signedOut": "請登入以查看代理", - "privacy": "代理已隱藏", + "privacy": "開啟 Kilo 以查看代理", "openAgents": "開啟代理", "running": "處理中", "needsInput": "需要輸入", diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 785c785642..4ee399aa09 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -3219,7 +3219,7 @@ "stale": "Akukwazi ukubuyekeza manje", "expired": "Isimo siphelelwe yisikhathi", "signedOut": "Ngena ngemvume ukuze ubone ama-agent", - "privacy": "Ama-agent afihliwe", + "privacy": "Vula i-Kilo ukubona ama-agent", "openAgents": "Vula ama-agent", "running": "Iyasebenza", "needsInput": "idinga okokufaka", diff --git a/apps/mobile/src/lib/glanceable/presentation.test.ts b/apps/mobile/src/lib/glanceable/presentation.test.ts index 28436a5ce8..57ae4a8be8 100644 --- a/apps/mobile/src/lib/glanceable/presentation.test.ts +++ b/apps/mobile/src/lib/glanceable/presentation.test.ts @@ -170,7 +170,7 @@ describe('numeric spoken label', () => { 'glanceable.stale': 'Updates delayed', 'glanceable.expired': 'Status expired', 'glanceable.signedOut': 'Sign in to see agents', - 'glanceable.privacy': 'Agents hidden', + 'glanceable.privacy': 'Open Kilo to see agents', 'glanceable.openAgents': 'Open agents', }; const translate = (key: string): string => copy[key] ?? key; @@ -204,7 +204,7 @@ describe('numeric spoken label', () => { ['empty', 'No work in progress, Open agents'], ['expired', 'Status expired, Open agents'], ['signed_out', 'Sign in to see agents, Open agents'], - ['privacy', 'Agents hidden, Open agents'], + ['privacy', 'Open Kilo to see agents, Open agents'], ] as const)('hides numeric counts when the status is %s', (status, expected) => { expect(glanceableSpokenLabel({ ...mixed, status }, {}, translate)).toBe(expected); }); @@ -215,7 +215,7 @@ describe('numeric spoken label', () => { 'Sign in to see agents, Open agents' ); expect(glanceableSpokenLabel(stale, { orgInvalid: true }, translate)).toBe( - 'Agents hidden, Open agents' + 'Open Kilo to see agents, Open agents' ); }); }); diff --git a/apps/mobile/src/lib/intl-cache.test.ts b/apps/mobile/src/lib/intl-cache.test.ts index 8e264c032e..68baeb7f36 100644 --- a/apps/mobile/src/lib/intl-cache.test.ts +++ b/apps/mobile/src/lib/intl-cache.test.ts @@ -54,6 +54,12 @@ describe('intl-cache', () => { PluralRules: nativeIntl.PluralRules, }); + // First, deliberately: a tag no `@formatjs` locale list carries — the + // others are `zh-Hans`, `zh-Hant`, `ht` and `pt-BR`. `shouldPolyfill` + // cannot match one by lookup, so it falls through to the CLDR best-fit + // matcher, which constructs `Intl.Locale`. This is the call that used to + // throw, before any other had installed that polyfill. + expect(numberFormat('mi', {}).format(1234.5)).toMatch(/1/); expect(relativeTimeFormat('de', { numeric: 'auto' }).format(-5, 'minute')).toBe( 'vor 5 Minuten' ); diff --git a/apps/mobile/src/lib/intl-cache.ts b/apps/mobile/src/lib/intl-cache.ts index 54fe9c9b39..daa2c86614 100644 --- a/apps/mobile/src/lib/intl-cache.ts +++ b/apps/mobile/src/lib/intl-cache.ts @@ -36,7 +36,6 @@ let usesListFormatPolyfill = false; let usesRelativeTimePolyfill = false; let usesDurationFormatPolyfill = false; let usesSegmenterPolyfill = false; -let usesLocalePolyfill = false; /** * The tag every formatter is built with. @@ -65,16 +64,36 @@ function localeDataLanguage(locale: string): SupportedLanguage { return isSupportedLanguage(base) ? base : 'en'; } +/** + * Install the `Intl.Locale` polyfill. Every `ensure*` below calls this before + * its own `shouldPolyfill(locale)`, and it must stay that way. + * + * `shouldPolyfill` runs the CLDR locale matcher, whose best-fit path + * constructs `Intl.Locale` for a tag the package's locale list does not carry: + * `zh-Hans`, `zh-Hant`, `ht`, `mi` and `pt-BR` all miss the plural-rules list. + * Hermes ships no `Intl.Locale`, so the first formatter call in one of those + * languages threw "undefined cannot be used as a constructor" before any + * polyfill could install, and every formatted number, list and duration on the + * screen fell back to its raw value. + */ function ensureLocale(): void { - if (!usesLocalePolyfill && shouldPolyfillLocale()) { - require('@formatjs/intl-locale/polyfill-force.js'); - usesLocalePolyfill = true; + if (!shouldPolyfillLocale()) { + return; } + // The class, not `polyfill-force`. That entry point assigns onto whichever + // `Intl` was global the first time it ran, and a second `require` is a cache + // hit that assigns nothing, so a later `Intl` would keep no `Locale` at all. + // Assigning here depends on the current `Intl` alone, and `shouldPolyfill` + // already returns false once a usable `Intl.Locale` is in place. + // eslint-disable-next-line typescript-eslint/no-require-imports, unicorn/prefer-module -- the polyfill is a lazy native-weight load, like every other one here + const { Locale } = require('@formatjs/intl-locale') as { Locale: unknown }; + Object.defineProperty(Intl, 'Locale', { value: Locale, configurable: true, writable: true }); } // Hermes ships without Intl.PluralRules, and the NumberFormat and // RelativeTimeFormat polyfills construct one. function ensurePluralRules(language: SupportedLanguage): void { + ensureLocale(); if (!usesPluralRulesPolyfill && shouldPolyfillPluralRules(language)) { require('@formatjs/intl-pluralrules/polyfill-force.js'); usesPluralRulesPolyfill = true; @@ -88,6 +107,7 @@ function ensurePluralRules(language: SupportedLanguage): void { function ensureNumberFormat(locale: string): void { const language = localeDataLanguage(locale); ensurePluralRules(language); + ensureLocale(); if (!usesNumberFormatPolyfill && shouldPolyfillNumberFormat(locale)) { require('@formatjs/intl-numberformat/polyfill-force.js'); usesNumberFormatPolyfill = true; @@ -100,6 +120,7 @@ function ensureNumberFormat(locale: string): void { function ensureListFormat(locale: string): void { const language = localeDataLanguage(locale); + ensureLocale(); if (!usesListFormatPolyfill && shouldPolyfillListFormat(locale)) { require('@formatjs/intl-listformat/polyfill-force.js'); usesListFormatPolyfill = true; @@ -113,6 +134,7 @@ function ensureListFormat(locale: string): void { function ensureRelativeTimeFormat(locale: string): void { const language = localeDataLanguage(locale); ensureNumberFormat(locale); + ensureLocale(); if (!usesRelativeTimePolyfill && shouldPolyfillRelativeTimeFormat(language)) { require('@formatjs/intl-relativetimeformat/polyfill-force.js'); usesRelativeTimePolyfill = true; From a8a24e824d36de6366a9128a5860c3a2b92b7411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 2 Sep 2026 23:35:10 +0200 Subject: [PATCH 32/43] fix(session-ingest): update the glanceable expectations to the state model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The aggregate carries three counts — running, needs-input, idle — and these tests still expected a fourth, `reconnecting`. A retry now counts as a wait, because the surfaces draw one orange state for "the agent is waiting on you", and an idle agent counts as work rather than an empty aggregate, because it is the third, white row. --- .../src/dos/UserConnectionDO.test.ts | 28 ++++++++++--------- .../src/ingest/metadata.test.ts | 14 ++++++---- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/services/session-ingest/src/dos/UserConnectionDO.test.ts b/services/session-ingest/src/dos/UserConnectionDO.test.ts index 1b712c5d63..4e0d1475ac 100644 --- a/services/session-ingest/src/dos/UserConnectionDO.test.ts +++ b/services/session-ingest/src/dos/UserConnectionDO.test.ts @@ -601,11 +601,15 @@ describe('UserConnectionDO', () => { await flushAsync(); } expect(messages.map(message => message.data)).toMatchObject([ - { status: 'happy', running: 1, needsInput: 0, reconnecting: 0 }, - { status: 'happy', running: 0, needsInput: 0, reconnecting: 1 }, - { status: 'happy', running: 0, needsInput: 1, reconnecting: 0 }, - { status: 'happy', running: 1, needsInput: 0, reconnecting: 0 }, - { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + { status: 'happy', running: 1, needsInput: 0, idle: 0 }, + // The retry and the question deliver the same counts, because one + // orange state covers both, but each still delivers: the coordinator + // resends on a root status change, not on a count change. + { status: 'happy', running: 0, needsInput: 1, idle: 0 }, + { status: 'happy', running: 0, needsInput: 1, idle: 0 }, + { status: 'happy', running: 1, needsInput: 0, idle: 0 }, + // Idle is a count, not an empty aggregate. + { status: 'happy', running: 0, needsInput: 0, idle: 1 }, ]); expect(messages.every(message => message._contentAvailable && !message.body)).toBe(true); expect( @@ -649,9 +653,9 @@ describe('UserConnectionDO', () => { ]); await flushAsync(); expect(messages.map(message => message.data)).toMatchObject([ - { running: 1, needsInput: 0, reconnecting: 1 }, - { running: 0, needsInput: 1, reconnecting: 1 }, - { running: 1, needsInput: 0, reconnecting: 1 }, + { running: 1, needsInput: 1, idle: 0 }, + { running: 0, needsInput: 2, idle: 0 }, + { running: 1, needsInput: 1, idle: 0 }, ]); }); @@ -674,9 +678,7 @@ describe('UserConnectionDO', () => { expect(restored.getActiveSessions()).toMatchObject([{ id: 's1', status: 'retry' }]); sendHeartbeat(restored, cliWs, [makeSession('s1', 'retry')]); await flushAsync(); - expect(messages.map(message => message.data)).toMatchObject([ - { running: 0, reconnecting: 1 }, - ]); + expect(messages.map(message => message.data)).toMatchObject([{ running: 0, needsInput: 1 }]); }); it('delivers an empty aggregate when a root disappears from the heartbeat', async () => { @@ -688,7 +690,7 @@ describe('UserConnectionDO', () => { await flushAsync(); expect(messages.map(message => message.data)).toMatchObject([ { running: 1 }, - { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + { status: 'empty', running: 0, needsInput: 0, idle: 0 }, ]); }); @@ -712,7 +714,7 @@ describe('UserConnectionDO', () => { await disconnect; await flushAsync(); expect(messages.map(message => message.data)).toMatchObject([ - { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }, + { status: 'empty', running: 0, needsInput: 0, idle: 0 }, ]); } ); diff --git a/services/session-ingest/src/ingest/metadata.test.ts b/services/session-ingest/src/ingest/metadata.test.ts index 48074e850f..d38186cc44 100644 --- a/services/session-ingest/src/ingest/metadata.test.ts +++ b/services/session-ingest/src/ingest/metadata.test.ts @@ -493,11 +493,15 @@ describe('applyMetadataChanges', () => { describe('glanceable aggregate refresh', () => { it.each([ - ['idle', 'busy', { status: 'happy', running: 1, needsInput: 0, reconnecting: 0 }], - ['busy', 'retry', { status: 'happy', running: 0, needsInput: 0, reconnecting: 1 }], - ['question', 'busy', { status: 'happy', running: 1, needsInput: 0, reconnecting: 0 }], - ['permission', 'idle', { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }], - ['busy', 'idle', { status: 'empty', running: 0, needsInput: 0, reconnecting: 0 }], + ['idle', 'busy', { status: 'happy', running: 1, needsInput: 0, idle: 0 }], + // Reconnecting is not its own count: the surfaces draw one orange state + // for "the agent is waiting on you", and a retry is a wait. + ['busy', 'retry', { status: 'happy', running: 0, needsInput: 1, idle: 0 }], + ['question', 'busy', { status: 'happy', running: 1, needsInput: 0, idle: 0 }], + // An idle agent is connected, so it is work to show, not an empty + // aggregate: the surfaces draw it as the third, white row. + ['permission', 'idle', { status: 'happy', running: 0, needsInput: 0, idle: 1 }], + ['busy', 'idle', { status: 'happy', running: 0, needsInput: 0, idle: 1 }], ] as const)( 'delivers persisted cloud status %s → %s without attention or stream clients', async (initialStatus, status, expected) => { From 71f1c996328d755d971944cb5c9dd84826bb5fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 01:38:25 +0200 Subject: [PATCH 33/43] fix(mobile): draw the counts in the language's own digits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The layout stringifies its counts itself, because a pushed content state carries raw numbers and the widget process has no formatter, so a Persian row drew "1" beside the "۲۵ دقیقه" SwiftUI had formatted. The copy bake now carries the language's ten digits and the layout maps its own through them, which covers every surface including a background push. The table is empty for a language that writes the plain ten, which is most of them — Arabic included, because Arabic-Indic digits belong to a region, not to the language. It is fa, ps, ckb, my, ne, bn and mr that differ. The mapping keeps `split('')` rather than a spread or `replaceAll`: both of those render nothing in the widget process, which is why the lint rule is disabled on that line. --- .../active-agents-live-activity.tsx | 18 +++++++++++++++-- .../glanceable-ios/active-agents-widget.tsx | 20 ++++++++++++++++--- .../src/glanceable-ios/layout-copy.test.ts | 7 +++++++ apps/mobile/src/glanceable-ios/layout-copy.ts | 17 ++++++++++++++++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx index 9d5d71928c..568f46a729 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx @@ -56,6 +56,20 @@ const layout: LiveActivityComponent = props => { // somehow missing it, which is what the widget process would have used anyway. const locale = COPY.locale ?? 'en'; + // The counts are stringified here, not formatted: a pushed content state + // carries raw numbers and this process has no formatter. `COPY.digits` is the + // language's own ten, empty when it writes them the way `String` already + // does, so an Arabic count reads "١" beside the "٢٦ د" SwiftUI formats. + const digits = COPY.digits ?? ''; + const count = (value: number) => + digits.length === 10 + ? // eslint-disable-next-line unicorn/prefer-spread -- `replaceAll` and a spread both failed in the widget process; this form is the one verified on device + String(value) + .split('') + .map(character => digits[Number(character)] ?? character) + .join('') + : String(value); + const status = props.status ?? 'empty'; const statusLine = status === 'happy' ? null : COPY[status]; @@ -93,7 +107,7 @@ const layout: LiveActivityComponent = props => { // number worth showing. const primary = countLines.find(line => line.count > 0) ?? null; const hasCounts = primary !== null; - const primaryCount = String(primary === null ? 0 : primary.count); + const primaryCount = count(primary === null ? 0 : primary.count); // Only the needs-input row carries a duration, and only the oldest wait: a // blocked agent is the one interval the user can act on. Working and idle // durations tell the user nothing they can use. @@ -148,7 +162,7 @@ const layout: LiveActivityComponent = props => { primaryForeground, ]} > - {String(line.count)} + {count(line.count)} Reac // somehow missing it, which is what the widget process would have used anyway. const locale = COPY.locale ?? 'en'; + // The counts are stringified here, not formatted: a pushed content state + // carries raw numbers and this process has no formatter. `COPY.digits` is the + // language's own ten, empty when it writes them the way `String` already + // does, so an Arabic count reads "١" beside the "٢٦ د" SwiftUI formats. + const digits = COPY.digits ?? ''; + const count = (value: number) => + digits.length === 10 + ? // eslint-disable-next-line unicorn/prefer-spread -- `replaceAll` and a spread both failed in the widget process; this form is the one verified on device + String(value) + .split('') + .map(character => digits[Number(character)] ?? character) + .join('') + : String(value); + const family = widgetEnvironment.widgetFamily; const counts = props.countLines ?? []; const primaryLabel = props.primaryLabel ?? null; @@ -135,7 +149,7 @@ const layout: (props: WidgetProps, widgetEnvironment: WidgetEnvironment) => Reac primaryForeground, ]} > - {String(line.count)} + {count(line.count)} Reac ...a11y, ]} > - {hasCounts ? String(primaryCount) : '—'} + {hasCounts ? count(primaryCount) : '—'} ); @@ -193,7 +207,7 @@ const layout: (props: WidgetProps, widgetEnvironment: WidgetEnvironment) => Reac if (family === 'accessoryInline') { const label = hasCounts - ? `${primaryCount}${primaryLabel !== null ? ` ${primaryLabel}` : ''}` + ? `${count(primaryCount)}${primaryLabel !== null ? ` ${primaryLabel}` : ''}` : (statusLine ?? ''); return ( { expect(JSON.parse(JSON.parse(literal) as string)).toEqual(glanceableLayoutCopy()); }); + it('bakes no digit table for a language that writes the plain ten', () => { + // English is `latn`, so the layout's own `String` is already right and the + // empty table tells it to skip the mapping. + expect(glanceableLayoutCopy().digits).toBe(''); + }); + it('bakes the locale in the form the SwiftUI modifier accepts', () => { // `@expo/ui` applies the locale only when `Locale.availableIdentifiers` // contains the value, and that list writes `zh_Hans`, not `zh-Hans`. A @@ -59,6 +65,7 @@ describe('withGlanceableCopy', () => { it('covers every status the layouts render, plus the language tag', () => { expect(Object.keys(glanceableLayoutCopy()).toSorted()).toEqual([ + 'digits', 'empty', 'expired', 'idle', diff --git a/apps/mobile/src/glanceable-ios/layout-copy.ts b/apps/mobile/src/glanceable-ios/layout-copy.ts index b39434f6d9..93506e201b 100644 --- a/apps/mobile/src/glanceable-ios/layout-copy.ts +++ b/apps/mobile/src/glanceable-ios/layout-copy.ts @@ -1,5 +1,6 @@ import { i18n } from '@/i18n'; import { GLANCEABLE_STATUS_COPY_KEY } from '@/lib/glanceable/presentation'; +import { numberFormat } from '@/lib/intl-cache'; /** * Translated copy for the stringified `'widget'` layouts. @@ -57,6 +58,7 @@ export function glanceableLayoutCopy() { idle: i18n.t('glanceable.idle'), openAgents: i18n.t('glanceable.openAgents'), locale: i18n.language.replace('-', '_'), + digits: glanceableDigits(), }; } @@ -71,6 +73,21 @@ export function glanceableLayoutCopy() { * quotes, so `JSON.stringify` produces a correctly escaped source literal for * copy that contains an apostrophe. */ +/** + * The active language's ten digits, or an empty string when it writes them the + * way the layout already does. + * + * The layout stringifies its counts itself, because a pushed content state + * carries raw numbers and the widget process has no formatter, so an Arabic + * row drew "1" beside a wait SwiftUI had formatted as "٢٦ د". Baking the digits + * lets the layout map its own — one table, every surface, push included. + */ +function glanceableDigits(): string { + const formatter = numberFormat(i18n.language, { useGrouping: false }); + const digits = Array.from({ length: 10 }, (_, digit) => formatter.format(digit)).join(''); + return digits === '0123456789' ? '' : digits; +} + export function withGlanceableCopy(layout: T): T { // eslint-disable-next-line anti-slop/no-runtime-typeof -- the two representations are the contract; see above if (typeof layout !== 'string') { From 8d324e10aa5b0ee015fd1854fca7cac576cefedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 02:28:15 +0200 Subject: [PATCH 34/43] feat(mobile): add an in-app switch for the Live Activity The only way to stop the Active Agents Live Activity was the per-app switch in Settings, which is all-or-nothing for the device. The notifications screen now carries its own switch, first on the screen because the Live Activity is the surface the user sees without opening the app. The row mirrors ActivityKit rather than competing with it. `expo-widgets` is patched to expose `areActivitiesEnabled`, which it only ever used internally, so the row can show the real state: off and disabled with an Open Settings button when iOS has Live Activities off, because our switch cannot turn that back on. It is re-read on every foreground, since Settings is the only place to change it. The switch lives in a React-Native-free holder so the sink can read it without pulling native imports into its test graph; the hook owns the SecureStore round trip and mirrors each change into it. Turning it off ends the activity already on the Lock Screen, not just the next start. The widget families are deliberately not covered: placing one is the opt-in and removing it is the opt-out. The cold-launch alert is gone. It fired from the Agents tab on every launch after ActivityKit refused a start, asking the user to undo a choice they had just made. Two stale mocks for the master switch removed earlier are deleted with it. --- .../(tabs)/(2_agents)/index.mounted.test.tsx | 19 +- .../src/app/(app)/(tabs)/(2_agents)/index.tsx | 6 +- .../notifications-screen.mounted.test.tsx | 25 ++- .../src/components/notifications-screen.tsx | 72 ++++++- .../preferences-screen.mounted.test.tsx | 7 - .../src/glanceable-ios/ios-sink.test.ts | 19 ++ apps/mobile/src/glanceable-ios/ios-sink.ts | 11 +- apps/mobile/src/glanceable-ios/register.ts | 16 ++ .../src/glanceable-ios/system-switch.ts | 26 +++ apps/mobile/src/i18n/locales/af.json | 3 +- apps/mobile/src/i18n/locales/am.json | 3 +- apps/mobile/src/i18n/locales/ar.json | 3 +- apps/mobile/src/i18n/locales/az.json | 3 +- apps/mobile/src/i18n/locales/be.json | 3 +- apps/mobile/src/i18n/locales/bg.json | 3 +- apps/mobile/src/i18n/locales/bn.json | 3 +- apps/mobile/src/i18n/locales/bs.json | 3 +- apps/mobile/src/i18n/locales/ca.json | 3 +- apps/mobile/src/i18n/locales/ckb.json | 3 +- apps/mobile/src/i18n/locales/cs.json | 3 +- apps/mobile/src/i18n/locales/cy.json | 3 +- apps/mobile/src/i18n/locales/da.json | 3 +- apps/mobile/src/i18n/locales/de.json | 3 +- apps/mobile/src/i18n/locales/el.json | 3 +- apps/mobile/src/i18n/locales/en.json | 3 +- apps/mobile/src/i18n/locales/es.json | 3 +- apps/mobile/src/i18n/locales/et.json | 3 +- apps/mobile/src/i18n/locales/eu.json | 3 +- apps/mobile/src/i18n/locales/fa.json | 3 +- apps/mobile/src/i18n/locales/fi.json | 3 +- apps/mobile/src/i18n/locales/fil.json | 3 +- apps/mobile/src/i18n/locales/fr.json | 3 +- apps/mobile/src/i18n/locales/ga.json | 3 +- apps/mobile/src/i18n/locales/gl.json | 3 +- apps/mobile/src/i18n/locales/gu.json | 3 +- apps/mobile/src/i18n/locales/ha.json | 3 +- apps/mobile/src/i18n/locales/he.json | 3 +- apps/mobile/src/i18n/locales/hi.json | 3 +- apps/mobile/src/i18n/locales/hr.json | 3 +- apps/mobile/src/i18n/locales/ht.json | 3 +- apps/mobile/src/i18n/locales/hu.json | 3 +- apps/mobile/src/i18n/locales/hy.json | 3 +- apps/mobile/src/i18n/locales/id.json | 3 +- apps/mobile/src/i18n/locales/ig.json | 3 +- apps/mobile/src/i18n/locales/is.json | 3 +- apps/mobile/src/i18n/locales/it.json | 3 +- apps/mobile/src/i18n/locales/ja.json | 3 +- apps/mobile/src/i18n/locales/ka.json | 3 +- apps/mobile/src/i18n/locales/kk.json | 3 +- apps/mobile/src/i18n/locales/km.json | 3 +- apps/mobile/src/i18n/locales/kn.json | 3 +- apps/mobile/src/i18n/locales/ko.json | 3 +- apps/mobile/src/i18n/locales/lo.json | 3 +- apps/mobile/src/i18n/locales/lt.json | 3 +- apps/mobile/src/i18n/locales/lv.json | 3 +- apps/mobile/src/i18n/locales/mg.json | 3 +- apps/mobile/src/i18n/locales/mi.json | 3 +- apps/mobile/src/i18n/locales/mk.json | 3 +- apps/mobile/src/i18n/locales/ml.json | 3 +- apps/mobile/src/i18n/locales/mn.json | 3 +- apps/mobile/src/i18n/locales/mr.json | 3 +- apps/mobile/src/i18n/locales/ms.json | 3 +- apps/mobile/src/i18n/locales/mt.json | 3 +- apps/mobile/src/i18n/locales/my.json | 3 +- apps/mobile/src/i18n/locales/nb.json | 3 +- apps/mobile/src/i18n/locales/ne.json | 3 +- apps/mobile/src/i18n/locales/nl.json | 3 +- apps/mobile/src/i18n/locales/om.json | 3 +- apps/mobile/src/i18n/locales/or.json | 3 +- apps/mobile/src/i18n/locales/pa.json | 3 +- apps/mobile/src/i18n/locales/pl.json | 3 +- apps/mobile/src/i18n/locales/ps.json | 3 +- apps/mobile/src/i18n/locales/pt-BR.json | 3 +- apps/mobile/src/i18n/locales/pt.json | 3 +- apps/mobile/src/i18n/locales/ro.json | 3 +- apps/mobile/src/i18n/locales/ru.json | 3 +- apps/mobile/src/i18n/locales/si.json | 3 +- apps/mobile/src/i18n/locales/sk.json | 3 +- apps/mobile/src/i18n/locales/sl.json | 3 +- apps/mobile/src/i18n/locales/so.json | 3 +- apps/mobile/src/i18n/locales/sq.json | 3 +- apps/mobile/src/i18n/locales/sr.json | 3 +- apps/mobile/src/i18n/locales/sv.json | 3 +- apps/mobile/src/i18n/locales/sw.json | 3 +- apps/mobile/src/i18n/locales/ta.json | 3 +- apps/mobile/src/i18n/locales/te.json | 3 +- apps/mobile/src/i18n/locales/th.json | 3 +- apps/mobile/src/i18n/locales/tr.json | 3 +- apps/mobile/src/i18n/locales/uk.json | 3 +- apps/mobile/src/i18n/locales/ur.json | 3 +- apps/mobile/src/i18n/locales/uz.json | 3 +- apps/mobile/src/i18n/locales/vi.json | 3 +- apps/mobile/src/i18n/locales/yo.json | 3 +- apps/mobile/src/i18n/locales/zh-Hans.json | 3 +- apps/mobile/src/i18n/locales/zh-Hant.json | 3 +- apps/mobile/src/i18n/locales/zu.json | 3 +- .../mobile/src/lib/auth/auth-context.test.tsx | 3 + apps/mobile/src/lib/auth/auth-context.tsx | 2 + apps/mobile/src/lib/auth/credentials.test.ts | 4 +- .../src/lib/glanceable/activity-kit-prompt.ts | 29 +-- .../glanceable/live-activity-switch.test.ts | 41 ++++ .../lib/glanceable/live-activity-switch.ts | 38 ++++ .../lib/hooks/use-live-activity-preference.ts | 48 +++++ apps/mobile/src/lib/storage-keys.ts | 1 + patches/expo-widgets@57.0.11.patch | 192 ++++++++++++++---- pnpm-lock.yaml | 10 +- 106 files changed, 644 insertions(+), 186 deletions(-) create mode 100644 apps/mobile/src/glanceable-ios/system-switch.ts create mode 100644 apps/mobile/src/lib/glanceable/live-activity-switch.test.ts create mode 100644 apps/mobile/src/lib/glanceable/live-activity-switch.ts create mode 100644 apps/mobile/src/lib/hooks/use-live-activity-preference.ts diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx index 7442940d54..99cda6c210 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx @@ -415,27 +415,20 @@ describe('Agents ActivityKit Settings recovery', () => { }); } - it('recovers on direct Settings return without refocusing or repeating the alert', async () => { + it('recovers on direct Settings return without refocusing, and never alerts', async () => { activityKit.denied = true; const renderer = mountRoute(); await flushMicrotasks(); expect(surface.activity).toBeNull(); - expect(lastAlertButtons()).toEqual([ - { text: 'Cancel', style: 'cancel' }, - { text: 'Open Settings', onPress: expect.any(Function) }, - ]); + // This screen used to alert on every cold launch, asking the user to undo a + // choice they had just made. The state now lives on the notifications + // screen, where they went to set it. + expect(alertMock).not.toHaveBeenCalled(); - act(() => { - lastAlertButtons() - ?.find(button => button.text === 'Open Settings') - ?.onPress?.(); - }); - expect(activityKit.settingsOpen).toBe(true); changeAppState('background'); changeAppState('active'); await flushMicrotasks(); expect(surface.activity).toBeNull(); - expect(alertMock.mock.calls).toHaveLength(1); changeAppState('background'); activityKit.available = true; @@ -446,7 +439,7 @@ describe('Agents ActivityKit Settings recovery', () => { await flushMicrotasks(); expect(surface.activity).toEqual(snapshot); - expect(alertMock.mock.calls).toHaveLength(1); + expect(alertMock).not.toHaveBeenCalled(); act(() => { renderer.unmount(); }); diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx index 9dcf628b12..175f89d012 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx @@ -12,10 +12,7 @@ import { type GitHubInstallReturnOutcome, subscribeToGitHubInstallReturnOutcome, } from '@/lib/github-install-return'; -import { - recoverGlanceableActivityKit, - showActivityKitDisabledAlertOnce, -} from '@/lib/glanceable/activity-kit-prompt'; +import { recoverGlanceableActivityKit } from '@/lib/glanceable/activity-kit-prompt'; import { trpcClient } from '@/lib/trpc'; export type GitHubInstallOutcomeAlertButton = { @@ -144,7 +141,6 @@ export default function AgentSessionList() { // changing route focus, so also retry recovery when the app becomes active. useFocusEffect( useCallback(() => { - showActivityKitDisabledAlertOnce(); void recoverGlanceableActivityKit(); const subscription = AppState.addEventListener('change', state => { if (state === 'active') { diff --git a/apps/mobile/src/components/notifications-screen.mounted.test.tsx b/apps/mobile/src/components/notifications-screen.mounted.test.tsx index a89f8f1349..2f1bd84cd3 100644 --- a/apps/mobile/src/components/notifications-screen.mounted.test.tsx +++ b/apps/mobile/src/components/notifications-screen.mounted.test.tsx @@ -30,13 +30,35 @@ vi.mock('@/lib/hooks/use-kiloclaw-tab-visible', () => ({ useKiloClawTabVisible, })); +const { openSettings, setLiveActivityEnabled, liveActivityEnabled, systemAllowsLiveActivities } = + vi.hoisted(() => ({ + openSettings: vi.fn(), + setLiveActivityEnabled: vi.fn(), + liveActivityEnabled: vi.fn(() => true), + systemAllowsLiveActivities: vi.fn(() => true), + })); + vi.mock('react-native', () => ({ View: 'View', Switch: 'Switch', Pressable: 'Pressable', ActivityIndicator: 'ActivityIndicator', Alert: { alert: vi.fn() }, - Linking: { openSettings: vi.fn() }, + Linking: { openSettings: openSettings }, + Platform: { OS: 'ios' }, +})); +// The Live Activity row: the preference is SecureStore-backed and the system +// switch is a native read, so both are stubbed the way every other native +// dependency on this screen is. +vi.mock('@/lib/hooks/use-live-activity-preference', () => ({ + useLiveActivityPreference: () => ({ + liveActivityEnabled: liveActivityEnabled(), + hasLoaded: true, + setLiveActivityEnabled, + }), +})); +vi.mock('@/glanceable-ios/system-switch', () => ({ + liveActivitiesAllowedBySystem: () => systemAllowsLiveActivities(), })); vi.mock('expo-notifications', () => ({ PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' }, @@ -54,6 +76,7 @@ vi.mock('@/components/ui/icons', () => ({ MessageSquare: 'MessageSquare', RefreshCw: 'RefreshCw', ShieldAlert: 'ShieldAlert', + Smartphone: 'Smartphone', Sparkles: 'Sparkles', Wallet: 'Wallet', })); diff --git a/apps/mobile/src/components/notifications-screen.tsx b/apps/mobile/src/components/notifications-screen.tsx index c8338a526f..1e4218c788 100644 --- a/apps/mobile/src/components/notifications-screen.tsx +++ b/apps/mobile/src/components/notifications-screen.tsx @@ -17,15 +17,17 @@ import { MessageSquare, RefreshCw, ShieldAlert, + Smartphone, Sparkles, Wallet, } from '@/components/ui/icons'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ActivityIndicator, Alert, Linking, Pressable, Switch, View } from 'react-native'; +import { ActivityIndicator, Alert, Linking, Platform, Pressable, Switch, View } from 'react-native'; import { toast } from 'sonner-native'; import { deriveMasterGateLeadingPresentation } from '@/components/notifications-master-gate'; +import { liveActivitiesAllowedBySystem } from '@/glanceable-ios/system-switch'; import { ScreenHeader } from '@/components/screen-header'; import { TabScreenScrollView } from '@/components/tab-screen'; import { Skeleton } from '@/components/ui/skeleton'; @@ -48,6 +50,7 @@ import { nextMutationGeneration, } from '@/lib/hooks/mutation-generations'; import { useKiloClawTabVisible } from '@/lib/hooks/use-kiloclaw-tab-visible'; +import { useLiveActivityPreference } from '@/lib/hooks/use-live-activity-preference'; import { getResolvedLanguage } from '@/lib/hooks/use-language-preference'; import { useThemeColors } from '@/lib/hooks/use-theme-colors'; import { @@ -311,12 +314,27 @@ export function NotificationsScreen() { const notificationsEnabled = permissionGranted && serverRegistered; const showEnableCta = deriveShowEnableCta(notificationsEnabled); + const { + liveActivityEnabled, + hasLoaded: liveActivityLoaded, + setLiveActivityEnabled, + } = useLiveActivityPreference(); + // ActivityKit's own switch. Read on mount and again on every foreground, + // because the only way to change it is to leave for Settings and come back. + const [systemAllowsLiveActivities, setSystemAllowsLiveActivities] = useState( + liveActivitiesAllowedBySystem + ); + // Off in Settings, or the stored preference has not been read yet: either way + // the switch must not accept a change it cannot honor. + const liveActivityRowDisabled = !liveActivityLoaded || !systemAllowsLiveActivities; + // Re-check permission on foreground resume const { isActive } = useAppLifecycle(); const wasActiveRef = useRef(isActive); useEffect(() => { if (!wasActiveRef.current && isActive) { void queryClient.invalidateQueries({ queryKey: permissionQueryKey }); + setSystemAllowsLiveActivities(liveActivitiesAllowedBySystem()); } wasActiveRef.current = isActive; }, [isActive, queryClient]); @@ -541,6 +559,58 @@ export function NotificationsScreen() { contentContainerClassName="px-6 gap-6 pt-4" showsVerticalScrollIndicator={false} > + {/* Live Activity. First on the screen because it is the surface the + user sees without opening the app, and it must not sit below seven + category rows. iOS only: ActivityKit has no Android counterpart. */} + {Platform.OS === 'ios' && ( + + + {t('notifications.liveActivities')} + + + + + {/* Disabled cue is the muted title, not row opacity — the same + pattern as CategoryRow below. */} + + {t('glanceable.channelName')} + + + {systemAllowsLiveActivities + ? t('notifications.liveActivitySubtitle') + : t('glanceable.activityKitDisabledBody')} + + + + + {/* Our switch cannot turn ActivityKit's back on, so the row offers + the only thing that can instead of pretending otherwise. */} + {!systemAllowsLiveActivities && ( + void Linking.openSettings()} + accessibilityRole="button" + accessibilityLabel={t('common.openSettings')} + className="items-center rounded-lg bg-primary py-2.5 active:opacity-80" + > + + {t('common.openSettings')} + + + )} + + )} + {/* Master gate */} diff --git a/apps/mobile/src/components/preferences-screen.mounted.test.tsx b/apps/mobile/src/components/preferences-screen.mounted.test.tsx index ad84edd478..beb7a4d1c6 100644 --- a/apps/mobile/src/components/preferences-screen.mounted.test.tsx +++ b/apps/mobile/src/components/preferences-screen.mounted.test.tsx @@ -77,13 +77,6 @@ vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ setKeepScreenOn: vi.fn(), }), })); -vi.mock('@/lib/hooks/use-glanceable-preference', () => ({ - useGlanceablePreference: () => ({ - glanceableEnabled: true, - hasLoaded: true, - setGlanceableEnabled: vi.fn(), - }), -})); vi.mock('@/lib/hooks/use-pr-review-footer-preference', () => ({ usePrReviewFooterPreference: () => ({ prReviewFooter: true, diff --git a/apps/mobile/src/glanceable-ios/ios-sink.test.ts b/apps/mobile/src/glanceable-ios/ios-sink.test.ts index ff8ee3a925..d323472836 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -7,6 +7,10 @@ import { } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; +import { + _resetLiveActivitySwitchForTests, + setLiveActivityEnabledValue, +} from '@/lib/glanceable/live-activity-switch'; import { writeSignedOutSnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { GlanceablePublisher } from '@/lib/glanceable/publisher'; import { @@ -166,6 +170,7 @@ function snapshotFor( } beforeEach(() => { + _resetLiveActivitySwitchForTests(); _resetIosSinkForTests(); subscriptions.clear(); mockState.startError = null; @@ -202,6 +207,20 @@ describe('iosSink start and update', () => { publisher.dispose(); }); + it('starts nothing while the in-app switch is off, and starts once it is on', () => { + setLiveActivityEnabledValue(false); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + expect(mockState.started).toEqual([]); + // The widget families are not covered by this switch: they are opt-in by + // placement, so publish still writes their timeline. + iosSink.publish(snapshotFor([{ status: 'busy' }], 0)); + expect(mockState.snapshots.at(-1)).toMatchObject({ primaryCount: 1 }); + + setLiveActivityEnabledValue(true); + iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 1), CTX); + expect(mockState.started.length).toBe(1); + }); + it('starts once and updates the same activity on a newer revision', () => { iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); iosSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 1), CTX); diff --git a/apps/mobile/src/glanceable-ios/ios-sink.ts b/apps/mobile/src/glanceable-ios/ios-sink.ts index 557295112d..85ad2c919d 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -8,6 +8,7 @@ import { after, type LiveActivity } from 'expo-widgets'; import { i18n } from '@/i18n'; import { getGlanceableDelivery, type GlanceableSink } from '@/lib/glanceable/sink-registry'; +import { getLiveActivityEnabled } from '@/lib/glanceable/live-activity-switch'; import { ActiveAgentsLiveActivity } from './active-agents-live-activity'; import { ActiveAgentsWidget } from './active-agents-widget'; @@ -294,7 +295,15 @@ export const iosSink: GlanceableSink = { }, startOrUpdate(snapshot, ctx) { - if (activityKitDeniedState || !isEligibleGlanceableWork(snapshot) || !refreshActivity()) { + // The in-app switch is checked first: it is the one the user set here, and + // honoring it costs no native call. ActivityKit's own switch still decides + // the rest, and `start` remains the authority on it. + if ( + !getLiveActivityEnabled() || + activityKitDeniedState || + !isEligibleGlanceableWork(snapshot) || + !refreshActivity() + ) { return; } diff --git a/apps/mobile/src/glanceable-ios/register.ts b/apps/mobile/src/glanceable-ios/register.ts index 57d337d830..1ab06723b3 100644 --- a/apps/mobile/src/glanceable-ios/register.ts +++ b/apps/mobile/src/glanceable-ios/register.ts @@ -1,5 +1,9 @@ import { i18n } from '@/i18n'; import { registerGlanceableSink } from '@/lib/glanceable/sink-registry'; +import { + getLiveActivityEnabled, + subscribeLiveActivityEnabled, +} from '@/lib/glanceable/live-activity-switch'; import { refreshActiveAgentsLiveActivityCopy } from './active-agents-live-activity'; import { refreshActiveAgentsWidgetCopy } from './active-agents-widget'; @@ -26,3 +30,15 @@ i18n.on('languageChanged', () => { refreshActiveAgentsLiveActivityCopy(); refreshActiveAgentsWidgetCopy(); }); + +// Turning the in-app switch off must clear the activity already on the Lock +// Screen, not just stop the next start. `startOrUpdate` holds the guard for +// everything after this. +let liveActivityAllowed = getLiveActivityEnabled(); +subscribeLiveActivityEnabled(() => { + const next = getLiveActivityEnabled(); + if (liveActivityAllowed && !next) { + iosSink.endImmediate(); + } + liveActivityAllowed = next; +}); diff --git a/apps/mobile/src/glanceable-ios/system-switch.ts b/apps/mobile/src/glanceable-ios/system-switch.ts new file mode 100644 index 0000000000..53ecf0b58a --- /dev/null +++ b/apps/mobile/src/glanceable-ios/system-switch.ts @@ -0,0 +1,26 @@ +import type * as ExpoWidgets from 'expo-widgets'; +import { Platform } from 'react-native'; + +/** + * The per-app "Live Activities" switch in Settings. + * + * ActivityKit refuses `start` when it is off, so the app used to learn the + * state only from a failed start. Reading it directly lets the notifications + * screen show the truth before anything is attempted. + */ +export function liveActivitiesAllowedBySystem(): boolean { + if (Platform.OS !== 'ios') { + return false; + } + try { + // Lazy require keeps expo-widgets' native module out of the settings + // screen's import graph, the same reason the sink registry defers Sentry. + // eslint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires, unicorn/prefer-module -- lazy native load + const { areLiveActivitiesEnabled } = require('expo-widgets') as typeof ExpoWidgets; + return areLiveActivitiesEnabled(); + } catch { + // An older binary without the patched native function: assume allowed and + // let `start` be the authority, which is the behavior that shipped before. + return true; + } +} diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 92a54f21d5..30ec662846 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Kennisgewings", + "liveActivities": "Live-aktiwiteite", + "liveActivitySubtitle": "Wys aktiewe agente op die sluitskerm", "push": "Druk", "enabled": "Kennisgewings geaktiveer", "onDescription": "Drukkennisgewings is aan vir hierdie toestel.", @@ -3225,7 +3227,6 @@ "needsInput": "benodig invoer", "idle": "Onaktief", "channelName": "Aktiewe agente", - "activityKitDisabledTitle": "Regstreekse Aktiwiteite is af", "activityKitDisabledBody": "Skakel Regstreekse Aktiwiteite in Instellings aan om Aktiewe Agente op die Sluitskerm te sien." } } diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 94ed5219f7..ba1257dbff 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "ማሳወቂያዎች", + "liveActivities": "የቀጥታ እንቅስቃሴዎች", + "liveActivitySubtitle": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ አሳይ", "push": "ግፋ", "enabled": "ማሳወቂያዎች ነቅተዋል", "onDescription": "የግፋ ማሳወቂያዎች ለዚህ መሣሪያ በርተዋል።", @@ -3225,7 +3227,6 @@ "needsInput": "ግብዓት ይፈልጋል", "idle": "በእረፍት", "channelName": "ንቁ ወኪሎች", - "activityKitDisabledTitle": "የቀጥታ እንቅስቃሴዎች ጠፍተዋል", "activityKitDisabledBody": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ ለማየት በቅንብሮች ውስጥ የቀጥታ እንቅስቃሴዎችን ያብሩ።" } } diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index 5fec89d51b..94fe2dcd6b 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "الإشعارات", + "liveActivities": "الأنشطة المباشرة", + "liveActivitySubtitle": "اعرض الوكلاء النشطين على شاشة القفل", "push": "فوري", "enabled": "الإشعارات ممكّنة", "onDescription": "الإشعارات الفورية قيد التشغيل لهذا الجهاز.", @@ -3313,7 +3315,6 @@ "needsInput": "يتطلب إدخالًا", "idle": "خامل", "channelName": "الوكلاء النشطون", - "activityKitDisabledTitle": "الأنشطة المباشرة متوقفة", "activityKitDisabledBody": "فعّل الأنشطة المباشرة في الإعدادات لرؤية الوكلاء النشطين على شاشة القفل." } } diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index a7790db421..96108f8291 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Bildirişlər", + "liveActivities": "Canlı fəaliyyətlər", + "liveActivitySubtitle": "Aktiv agentləri kilid ekranında göstərin", "push": "Push", "enabled": "Bildirişlər aktivdir", "onDescription": "Push bildirişləri bu cihaz üçün açıqdır.", @@ -3225,7 +3227,6 @@ "needsInput": "GİRİŞ TƏLƏB OLUNUR", "idle": "Boşda", "channelName": "Aktiv agentlər", - "activityKitDisabledTitle": "Canlı fəaliyyətlər söndürülüb", "activityKitDisabledBody": "Kilid ekranında aktiv agentləri görmək üçün Parametrlərdə Canlı fəaliyyətləri aktivləşdirin." } } diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 62a9c2bbb9..896b645e8a 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -628,6 +628,8 @@ }, "notifications": { "title": "Апавяшчэнні", + "liveActivities": "Жывыя актыўнасці", + "liveActivitySubtitle": "Паказваць актыўных агентаў на экране блакіроўкі", "push": "Push", "enabled": "Апавяшчэнні ўключаны", "onDescription": "Push-апавяшчэнні ўключаны для гэтай прылады.", @@ -3269,7 +3271,6 @@ "needsInput": "патрабуецца ўвод", "idle": "Чакае", "channelName": "Актыўныя агенты", - "activityKitDisabledTitle": "Жывыя дзеянні выключаны", "activityKitDisabledBody": "Уключыце «Жывыя дзеянні» ў «Наладах», каб бачыць актыўных агентаў на экране блакіроўкі." } } diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 88ca31a370..23b3ae6255 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Известия", + "liveActivities": "Живи активности", + "liveActivitySubtitle": "Показвай активните агенти на заключения екран", "push": "Push", "enabled": "Известията са активирани", "onDescription": "Push известията са включени за това устройство.", @@ -3225,7 +3227,6 @@ "needsInput": "изисква въвеждане", "idle": "Неактивен", "channelName": "Активни агенти", - "activityKitDisabledTitle": "Дейностите на живо са изключени", "activityKitDisabledBody": "Включете Дейности на живо в Настройки, за да виждате активните агенти на заключения екран." } } diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index dbea3b14db..b15d822c5b 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "বিজ্ঞপ্তি", + "liveActivities": "লাইভ অ্যাক্টিভিটি", + "liveActivitySubtitle": "লক স্ক্রিনে সক্রিয় এজেন্ট দেখান", "push": "পুশ", "enabled": "বিজ্ঞপ্তি সক্রিয়", "onDescription": "এই ডিভাইসে পুশ বিজ্ঞপ্তি চালু আছে।", @@ -3225,7 +3227,6 @@ "needsInput": "ইনপুট প্রয়োজন", "idle": "নিষ্ক্রিয়", "channelName": "সক্রিয় এজেন্ট", - "activityKitDisabledTitle": "সরাসরি কার্যকলাপ বন্ধ আছে", "activityKitDisabledBody": "লক স্ক্রিনে সক্রিয় এজেন্টগুলি দেখতে সেটিংসে সরাসরি কার্যকলাপ চালু করুন।" } } diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index a52b744201..a8aa07124f 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -621,6 +621,8 @@ }, "notifications": { "title": "Obavijesti", + "liveActivities": "Aktivnosti uživo", + "liveActivitySubtitle": "Prikaži aktivne agente na zaključanom ekranu", "push": "Push", "enabled": "Obavijesti omogućene", "onDescription": "Push obavijesti su uključene za ovaj uređaj.", @@ -3247,7 +3249,6 @@ "needsInput": "treba unos", "idle": "Neaktivan", "channelName": "Aktivni agenti", - "activityKitDisabledTitle": "Aktivnosti uživo su isključene", "activityKitDisabledBody": "Uključite aktivnosti uživo u Postavkama da biste vidjeli aktivne agente na zaključanom ekranu." } } diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index fc321d63d9..f7086ca7d7 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -621,6 +621,8 @@ }, "notifications": { "title": "Notificacions", + "liveActivities": "Activitats en directe", + "liveActivitySubtitle": "Mostra els agents actius a la pantalla de bloqueig", "push": "Push", "enabled": "Notificacions activades", "onDescription": "Les notificacions push estan activades per a aquest dispositiu.", @@ -3247,7 +3249,6 @@ "needsInput": "requereix entrada", "idle": "Inactiu", "channelName": "Agents actius", - "activityKitDisabledTitle": "Les activitats en directe estan desactivades", "activityKitDisabledBody": "Activa les activitats en directe a Configuració per veure els agents actius a la pantalla de bloqueig." } } diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 0d90dea8b2..2b9d8a5ea7 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "ئاگادارکردنەوەکان", + "liveActivities": "چالاکییە ڕاستەوخۆکان", + "liveActivitySubtitle": "ئەجێنتە چالاکەکان لەسەر شاشەی داخستن پیشان بدە", "push": "پاڵدان", "enabled": "ئاگادارکردنەوەکان چالاک کراون", "onDescription": "ئاگادارکردنەوەکانی پاڵدان بۆ ئەم ئامێرە چالاکن.", @@ -3225,7 +3227,6 @@ "needsInput": "پێویستی بە داخڵکردن", "idle": "بێکار", "channelName": "ئەجێنتە چالاکەکان", - "activityKitDisabledTitle": "چالاکییە ڕاستەوخۆکان ناچالاکن", "activityKitDisabledBody": "چالاکییە ڕاستەوخۆکان لە ڕێکخستنەکان چالاک بکە بۆ بینینی ئەجێنتە چالاکەکان لە شاشەی قوفڵ." } } diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index fcd59ddf43..01d0b462f2 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -628,6 +628,8 @@ }, "notifications": { "title": "Oznámení", + "liveActivities": "Živé aktivity", + "liveActivitySubtitle": "Zobrazovat aktivní agenty na uzamčené obrazovce", "push": "Push", "enabled": "Oznámení povolena", "onDescription": "Push oznámení jsou pro toto zařízení zapnutá.", @@ -3269,7 +3271,6 @@ "needsInput": "vyžaduje vstup", "idle": "Nečinný", "channelName": "Aktivní agenti", - "activityKitDisabledTitle": "Živé aktivity jsou vypnuté", "activityKitDisabledBody": "Zapněte Živé aktivity v Nastavení, abyste viděli aktivní agenty na zamknuté obrazovce." } } diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index 214b302589..92db905c45 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -642,6 +642,8 @@ }, "notifications": { "title": "Hysbysiadau", + "liveActivities": "Gweithgareddau byw", + "liveActivitySubtitle": "Dangos asiantau gweithredol ar y sgrin clo", "push": "Gwthio", "enabled": "Hysbysiadau wedi'u galluogi", "onDescription": "Mae hysbysiadau gwthio ymlaen ar gyfer y ddyfais hon.", @@ -3313,7 +3315,6 @@ "needsInput": "angen mewnbwn", "idle": "Segur", "channelName": "Asiantau gweithredol", - "activityKitDisabledTitle": "Mae Gweithgareddau Byw wedi'u diffodd", "activityKitDisabledBody": "Trowch Weithgareddau Byw ymlaen yn Gosodiadau i weld Asiantau gweithredol ar y Sgrin Glo." } } diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index 88e976c883..d93874a746 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Meddelelser", + "liveActivities": "Live-aktiviteter", + "liveActivitySubtitle": "Vis aktive agenter på låseskærmen", "push": "Push", "enabled": "Meddelelser aktiveret", "onDescription": "Push-meddelelser er til for denne enhed.", @@ -3225,7 +3227,6 @@ "needsInput": "kræver input", "idle": "Inaktiv", "channelName": "Aktive agenter", - "activityKitDisabledTitle": "Liveaktiviteter er slået fra", "activityKitDisabledBody": "Slå Liveaktiviteter til i Indstillinger for at se aktive agenter på låseskærmen." } } diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 85ce092266..1a6f6c0fa8 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "Benachrichtigungen", + "liveActivities": "Live-Aktivitäten", + "liveActivitySubtitle": "Aktive Agenten auf dem Sperrbildschirm anzeigen", "push": "Push", "enabled": "Benachrichtigungen aktiviert", "onDescription": "Push-Benachrichtigungen sind für dieses Gerät aktiviert.", @@ -3225,7 +3227,6 @@ "needsInput": "Eingabe erforderlich", "idle": "Inaktiv", "channelName": "Aktive Agenten", - "activityKitDisabledTitle": "Live-Aktivitäten sind deaktiviert", "activityKitDisabledBody": "Aktiviere Live-Aktivitäten in den Einstellungen, um aktive Agenten auf dem Sperrbildschirm zu sehen." } } diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index 3148efc268..f238fa4705 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Ειδοποιήσεις", + "liveActivities": "Ζωντανές δραστηριότητες", + "liveActivitySubtitle": "Εμφάνιση ενεργών πρακτόρων στην οθόνη κλειδώματος", "push": "Push", "enabled": "Οι ειδοποιήσεις είναι ενεργοποιημένες", "onDescription": "Οι push ειδοποιήσεις είναι ενεργές για αυτή τη συσκευή.", @@ -3225,7 +3227,6 @@ "needsInput": "χρειάζεται είσοδο", "idle": "Αδρανής", "channelName": "Ενεργοί πράκτορες", - "activityKitDisabledTitle": "Οι Ζωντανές δραστηριότητες είναι απενεργοποιημένες", "activityKitDisabledBody": "Ενεργοποιήστε τις Ζωντανές δραστηριότητες στις Ρυθμίσεις για να δείτε τους ενεργούς πράκτορες στην Οθόνη κλειδώματος." } } diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 0731a718f3..80fd2ef963 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Notifications", + "liveActivities": "Live Activities", + "liveActivitySubtitle": "Show active agents on the Lock Screen", "push": "Push", "enabled": "Notifications enabled", "onDescription": "Push notifications are on for this device.", @@ -3225,7 +3227,6 @@ "needsInput": "Needs input", "idle": "Idle", "channelName": "Active agents", - "activityKitDisabledTitle": "Live Activities are off", "activityKitDisabledBody": "Turn on Live Activities in Settings to see Active Agents on the Lock Screen." } } diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 562f07bf8f..f57260fc0e 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -183,6 +183,8 @@ "securityFindingsSubtitle": "nuevos hallazgos y recordatorios de SLA" }, "title": "Notificaciones", + "liveActivities": "Actividades en directo", + "liveActivitySubtitle": "Muestra los agentes activos en la pantalla bloqueada", "push": "Push", "enabled": "Notificaciones activadas", "onDescription": "Las notificaciones push están activadas para este dispositivo.", @@ -3247,7 +3249,6 @@ "needsInput": "requiere entrada", "idle": "Inactivo", "channelName": "Agentes activos", - "activityKitDisabledTitle": "Las actividades en directo están desactivadas", "activityKitDisabledBody": "Activa las actividades en directo en Ajustes para ver los agentes activos en la pantalla de bloqueo." } } diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index 46e1085191..77d88c2365 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Teavitused", + "liveActivities": "Reaalajas tegevused", + "liveActivitySubtitle": "Näita aktiivseid agente lukustuskuval", "push": "Push", "enabled": "Teavitused on lubatud", "onDescription": "Push-teavitused on selle seadme jaoks sisse lülitatud.", @@ -3225,7 +3227,6 @@ "needsInput": "vajab sisendit", "idle": "Ooterežiimis", "channelName": "Aktiivsed agendid", - "activityKitDisabledTitle": "Reaalajas tegevused on välja lülitatud", "activityKitDisabledBody": "Lülitage seadetes reaalajas tegevused sisse, et näha aktiivseid agente lukustuskuval." } } diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 4b72920aa9..733b02025a 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Jakinarazpenak", + "liveActivities": "Zuzeneko jarduerak", + "liveActivitySubtitle": "Erakutsi agente aktiboak blokeo-pantailan", "push": "Bultzadazkoak", "enabled": "Jakinarazpenak gaituta", "onDescription": "Bultzadazko jakinarazpenak piztuta daude gailu honetan.", @@ -3225,7 +3227,6 @@ "needsInput": "sarreraren zain", "idle": "Geldirik", "channelName": "Agente aktiboak", - "activityKitDisabledTitle": "Zuzeneko jarduerak desaktibatuta daude", "activityKitDisabledBody": "Aktibatu Zuzeneko jarduerak Ezarpenetan, Agente aktiboak Blokeo-pantailan ikusteko." } } diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 168817c134..662fb60b01 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "اعلان‌ها", + "liveActivities": "فعالیت‌های زنده", + "liveActivitySubtitle": "نمایش عامل‌های فعال در صفحه قفل", "push": "Push", "enabled": "اعلان‌ها فعال‌اند", "onDescription": "اعلان‌های push برای این دستگاه روشن‌اند.", @@ -3225,7 +3227,6 @@ "needsInput": "نیاز به ورودی", "idle": "غیرفعال", "channelName": "عامل‌های فعال", - "activityKitDisabledTitle": "فعالیت‌های زنده خاموش هستند", "activityKitDisabledBody": "برای دیدن عامل‌های فعال در صفحه قفل، فعالیت‌های زنده را در تنظیمات روشن کنید." } } diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index c84b7a34dc..0f890003f0 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Ilmoitukset", + "liveActivities": "Livetoiminnot", + "liveActivitySubtitle": "Näytä aktiiviset agentit lukitusnäytöllä", "push": "Push", "enabled": "Ilmoitukset käytössä", "onDescription": "Push-ilmoitukset ovat päällä tälle laitteelle.", @@ -3225,7 +3227,6 @@ "needsInput": "vaatii syötettä", "idle": "Vapaalla", "channelName": "Aktiiviset agentit", - "activityKitDisabledTitle": "Live-aktiviteetit ovat pois päältä", "activityKitDisabledBody": "Ota live-aktiviteetit käyttöön Asetuksissa, niin näet aktiiviset agentit lukitulla näytöllä." } } diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index d7da7e546c..25ffcf32bc 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Mga Notipikasyon", + "liveActivities": "Mga live na aktibidad", + "liveActivitySubtitle": "Ipakita ang mga aktibong agent sa Lock Screen", "push": "Push", "enabled": "Pinagana ang mga notipikasyon", "onDescription": "Naka-on ang push notifications para sa device na ito.", @@ -3225,7 +3227,6 @@ "needsInput": "kailangan ng input", "idle": "Idle", "channelName": "Mga aktibong agent", - "activityKitDisabledTitle": "Naka-off ang Mga Live na Aktibidad", "activityKitDisabledBody": "I-on ang Mga Live na Aktibidad sa Mga setting para makita ang Mga aktibong agent sa Naka-lock na Screen." } } diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index ac2123c829..47ae596434 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -163,6 +163,8 @@ "securityFindingsSubtitle": "nouveaux résultats et rappels SLA" }, "title": "Notifications", + "liveActivities": "Activités en direct", + "liveActivitySubtitle": "Afficher les agents actifs sur l’écran verrouillé", "push": "Push", "enabled": "Notifications activées", "onDescription": "Les notifications push sont activées pour cet appareil.", @@ -3247,7 +3249,6 @@ "needsInput": "saisie requise", "idle": "Inactif", "channelName": "Agents actifs", - "activityKitDisabledTitle": "Les activités en direct sont désactivées", "activityKitDisabledBody": "Activez les activités en direct dans Réglages pour voir les agents actifs sur l'écran verrouillé." } } diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index da8301a4ea..1d4434bfce 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -635,6 +635,8 @@ }, "notifications": { "title": "Fógraí", + "liveActivities": "Gníomhaíochtaí beo", + "liveActivitySubtitle": "Taispeáin gníomhairí gníomhacha ar an scáileán glasáilte", "push": "Brú", "enabled": "Fógraí cumasaithe", "onDescription": "Tá brú-fhógraí ar siúl don ghléas seo.", @@ -3291,7 +3293,6 @@ "needsInput": "teastaíonn ionchur", "idle": "Díomhaoin", "channelName": "Gníomhairí gníomhacha", - "activityKitDisabledTitle": "Tá Gníomhaíochtaí Beo as", "activityKitDisabledBody": "Cumasaigh Gníomhaíochtaí Beo sna Socruithe chun Gníomhairí gníomhacha a fheiceáil ar an Scáileán Glasála." } } diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index d00d0913ca..038191af6a 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Notificacións", + "liveActivities": "Actividades en directo", + "liveActivitySubtitle": "Amosa os axentes activos na pantalla de bloqueo", "push": "Push", "enabled": "Notificacións activadas", "onDescription": "As notificacións push están activadas para este dispositivo.", @@ -3225,7 +3227,6 @@ "needsInput": "precisa entrada", "idle": "Inactivo", "channelName": "Axentes activos", - "activityKitDisabledTitle": "As actividades en directo están desactivadas", "activityKitDisabledBody": "Activa as actividades en directo en Configuración para ver os axentes activos na pantalla de bloqueo." } } diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index 6f8d4ca9b0..7871cc044b 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "સૂચનાઓ", + "liveActivities": "લાઇવ પ્રવૃત્તિઓ", + "liveActivitySubtitle": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો બતાવો", "push": "પુશ", "enabled": "સૂચનાઓ સક્ષમ", "onDescription": "આ ઉપકરણ માટે પુશ સૂચનાઓ ચાલુ છે.", @@ -3225,7 +3227,6 @@ "needsInput": "ઇનપુટ જરૂરી", "idle": "નિષ્ક્રિય", "channelName": "સક્રિય એજન્ટો", - "activityKitDisabledTitle": "લાઇવ પ્રવૃત્તિઓ બંધ છે", "activityKitDisabledBody": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો જોવા માટે સેટિંગ્સમાં લાઇવ પ્રવૃત્તિઓ ચાલુ કરો." } } diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 2b91541268..0fe1858fa3 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Sanarwa", + "liveActivities": "Ayyukan kai tsaye", + "liveActivitySubtitle": "Nuna wakilai masu aiki a allon kulle", "push": "Turawa", "enabled": "Sanarwa suna aiki", "onDescription": "Sanarwar turawa suna aiki ga wannan na'ura.", @@ -3225,7 +3227,6 @@ "needsInput": "yana buƙatar bayani", "idle": "Rashin aiki", "channelName": "Wakilai da ke aiki", - "activityKitDisabledTitle": "Ayyukan Kai Tsaye suna a kashe", "activityKitDisabledBody": "Kunna Ayyukan Kai Tsaye a cikin Saituna don ganin Wakilai da ke Aiki a kan Allon Kulle." } } diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index 109758c2b6..427c7ea24a 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "התראות", + "liveActivities": "פעילויות בזמן אמת", + "liveActivitySubtitle": "הצג סוכנים פעילים במסך הנעילה", "push": "דחיפה", "enabled": "התראות מופעלות", "onDescription": "התראות דחיפה מופעלות עבור מכשיר זה.", @@ -3247,7 +3249,6 @@ "needsInput": "נדרש קלט", "idle": "בטל", "channelName": "סוכנים פעילים", - "activityKitDisabledTitle": "פעילויות בזמן אמת כבויות", "activityKitDisabledBody": "הפעל פעילויות בזמן אמת בהגדרות כדי לראות סוכנים פעילים במסך הנעילה." } } diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 443f1de30b..a41a56f9f3 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "सूचनाएँ", + "liveActivities": "लाइव गतिविधियाँ", + "liveActivitySubtitle": "लॉक स्क्रीन पर सक्रिय एजेंट दिखाएँ", "push": "पुश", "enabled": "सूचनाएँ सक्षम", "onDescription": "इस डिवाइस के लिए पुश सूचनाएँ चालू हैं।", @@ -3225,7 +3227,6 @@ "needsInput": "इनपुट आवश्यक", "idle": "निष्क्रिय", "channelName": "सक्रिय एजेंट", - "activityKitDisabledTitle": "लाइव ऐक्टिविटी बंद हैं", "activityKitDisabledBody": "लॉक स्क्रीन पर सक्रिय एजेंट देखने के लिए सेटिंग में लाइव ऐक्टिविटी चालू करें।" } } diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 2e9a82f3eb..a78ed27e6b 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -621,6 +621,8 @@ }, "notifications": { "title": "Obavijesti", + "liveActivities": "Aktivnosti uživo", + "liveActivitySubtitle": "Prikaži aktivne agente na zaključanom zaslonu", "push": "Push", "enabled": "Obavijesti omogućene", "onDescription": "Push obavijesti su uključene za ovaj uređaj.", @@ -3247,7 +3249,6 @@ "needsInput": "treba unos", "idle": "Neaktivan", "channelName": "Aktivni agenti", - "activityKitDisabledTitle": "Aktivnosti uživo su isključene", "activityKitDisabledBody": "Uključite Aktivnosti uživo u Postavkama kako biste vidjeli aktivne agente na zaključanom zaslonu." } } diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 31a36273d5..8a9d063235 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Notifikasyon", + "liveActivities": "Aktivite an dirèk", + "liveActivitySubtitle": "Montre ajans aktif yo sou ekran vewouye a", "push": "Pouse", "enabled": "Notifikasyon aktive", "onDescription": "Notifikasyon pouse louvri pou aparèy sa a.", @@ -3225,7 +3227,6 @@ "needsInput": "bezwen input", "idle": "Anchaj", "channelName": "Ajans aktif yo", - "activityKitDisabledTitle": "Aktivite an dirèk yo fèmen", "activityKitDisabledBody": "Aktive Aktivite an dirèk nan Paramèt pou wè Ajans aktif yo sou Ekran bloke a." } } diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index 698a43e229..030380d8e4 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Értesítések", + "liveActivities": "Élő tevékenységek", + "liveActivitySubtitle": "Aktív ügynökök megjelenítése a zárolási képernyőn", "push": "Leküldés", "enabled": "Értesítések engedélyezve", "onDescription": "A leküldéses értesítések be vannak kapcsolva ehhez az eszközhöz.", @@ -3225,7 +3227,6 @@ "needsInput": "bemenetet igényel", "idle": "Tétlen", "channelName": "Aktív ügynökök", - "activityKitDisabledTitle": "Az Élő tevékenységek ki vannak kapcsolva", "activityKitDisabledBody": "Kapcsolja be az Élő tevékenységeket a Beállításokban, hogy az aktív ügynökök megjelenjenek a zárolási képernyőn." } } diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index 9b22285fdb..92139a6a54 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Ծանուցումներ", + "liveActivities": "Ուղիղ գործողություններ", + "liveActivitySubtitle": "Ցուցադրել ակտիվ գործակալները կողպէկրանին", "push": "Push", "enabled": "Ծանուցումները միացված են", "onDescription": "Push ծանուցումները միացված են այս սարքի համար:", @@ -3225,7 +3227,6 @@ "needsInput": "մուտքագրման կարիք ունի", "idle": "Պարապ", "channelName": "Ակտիվ գործակալներ", - "activityKitDisabledTitle": "Ուղիղ ակտիվություններն անջատված են", "activityKitDisabledBody": "Միացրեք «Ուղիղ ակտիվություններ»-ը Կարգավորումներում՝ ակտիվ գործակալներին կողպման էկրանին տեսնելու համար։" } } diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index 551d310416..00ee15e059 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "Notifikasi", + "liveActivities": "Aktivitas langsung", + "liveActivitySubtitle": "Tampilkan agen aktif di Layar Terkunci", "push": "Push", "enabled": "Notifikasi diaktifkan", "onDescription": "Notifikasi push aktif untuk perangkat ini.", @@ -3225,7 +3227,6 @@ "needsInput": "memerlukan input", "idle": "Idle", "channelName": "Agen aktif", - "activityKitDisabledTitle": "Aktivitas Langsung nonaktif", "activityKitDisabledBody": "Aktifkan Aktivitas Langsung di Pengaturan untuk melihat Agen Aktif di Layar Terkunci." } } diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 31ec289b58..21fc0f6fc1 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Ọkwa", + "liveActivities": "Ọrụ ndụ", + "liveActivitySubtitle": "Gosi ndị ọrụ na-arụ ọrụ na Lock Screen", "push": "Push", "enabled": "Ọkwa agbanyela", "onDescription": "Ọkwa push dị maka ngwaọrụ a.", @@ -3225,7 +3227,6 @@ "needsInput": "chọrọ ntinye", "idle": "Ọrụ na-agaghị", "channelName": "Ndị ọrụ na-arụ ọrụ", - "activityKitDisabledTitle": "Agbanyụrụ Ihe Omume Dị Ndụ", "activityKitDisabledBody": "Gbanye Ihe Omume Dị Ndụ na Ntọala iji hụ Ndị ọrụ na-arụ ọrụ na Ihuenyo Mkpọchi." } } diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 54e7bddd56..9f89db0fa9 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Tilkynningar", + "liveActivities": "Beinar aðgerðir", + "liveActivitySubtitle": "Sýna virk umboð á lásskjánum", "push": "Push", "enabled": "Tilkynningar virkjaðar", "onDescription": "Push-tilkynningar eru kveiktar á þessu tæki.", @@ -3225,7 +3227,6 @@ "needsInput": "þarfnast inntaks", "idle": "Í bið", "channelName": "Virk umboð", - "activityKitDisabledTitle": "Slökkt er á Beinni virkni", "activityKitDisabledBody": "Kveiktu á Beinni virkni í Stillingum til að sjá Virk umboð á Lásskjánum." } } diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index e7b908fa2b..829eaea32a 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -163,6 +163,8 @@ "securityFindingsSubtitle": "nuovi risultati e promemoria SLA" }, "title": "Notifiche", + "liveActivities": "Attività in tempo reale", + "liveActivitySubtitle": "Mostra gli agenti attivi nella schermata di blocco", "push": "Push", "enabled": "Notifiche attivate", "onDescription": "Le notifiche push sono attive per questo dispositivo.", @@ -3247,7 +3249,6 @@ "needsInput": "richiede input", "idle": "Inattivo", "channelName": "Agenti attivi", - "activityKitDisabledTitle": "Le attività in tempo reale sono disattivate", "activityKitDisabledBody": "Attiva le attività in tempo reale in Impostazioni per vedere gli agenti attivi sulla schermata di blocco." } } diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 923bc461ca..a64d515d93 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "通知", + "liveActivities": "ライブアクティビティ", + "liveActivitySubtitle": "ロック画面に稼働中のエージェントを表示", "push": "プッシュ", "enabled": "通知が有効です", "onDescription": "このデバイスではプッシュ通知がオンです。", @@ -3225,7 +3227,6 @@ "needsInput": "入力が必要", "idle": "アイドル", "channelName": "アクティブなエージェント", - "activityKitDisabledTitle": "ライブアクティビティはオフです", "activityKitDisabledBody": "ロック画面にアクティブなエージェントを表示するには、設定でライブアクティビティをオンにしてください。" } } diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index fe22965dd5..9998871384 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "შეტყობინებები", + "liveActivities": "ცოცხალი აქტივობები", + "liveActivitySubtitle": "აქტიური აგენტების ჩვენება ჩაკეტილ ეკრანზე", "push": "Push", "enabled": "შეტყობინებები ჩართულია", "onDescription": "Push შეტყობინებები ჩართულია ამ მოწყობილობაზე.", @@ -3225,7 +3227,6 @@ "needsInput": "მოითხოვს შეყვანას", "idle": "უქმე", "channelName": "აქტიური აგენტები", - "activityKitDisabledTitle": "ცოცხალი აქტივობები გამორთულია", "activityKitDisabledBody": "ჩართეთ ცოცხალი აქტივობები პარამეტრებში, რათა დაბლოკვის ეკრანზე აქტიური აგენტები ნახოთ." } } diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 8eb541ddbf..982db80d99 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Хабарландырулар", + "liveActivities": "Тікелей әрекеттер", + "liveActivitySubtitle": "Белсенді агенттерді құлып экранында көрсету", "push": "Push", "enabled": "Хабарландырулар қосылған", "onDescription": "Бұл құрылғы үшін push хабарландырулар қосулы.", @@ -3225,7 +3227,6 @@ "needsInput": "енгізу қажет", "idle": "Бос тұр", "channelName": "Белсенді агенттер", - "activityKitDisabledTitle": "Тікелей әрекеттер өшірулі", "activityKitDisabledBody": "Құлыптау экранында белсенді агенттерді көру үшін Параметрлерде тікелей әрекеттерді қосыңыз." } } diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 4d9dbe5480..327959c10b 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "ការជូនដំណឹង", + "liveActivities": "សកម្មភាពផ្ទាល់", + "liveActivitySubtitle": "បង្ហាញភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ", "push": "Push", "enabled": "ការជូនដំណឹងត្រូវបានបើក", "onDescription": "ការជូនដំណឹងរុញបានបើកសម្រាប់ឧបករណ៍នេះ។", @@ -3225,7 +3227,6 @@ "needsInput": "ត្រូវការបញ្ចូល", "idle": "ទំនេរ", "channelName": "ភ្នាក់ងារសកម្ម", - "activityKitDisabledTitle": "សកម្មភាពបន្តផ្ទាល់ត្រូវបានបិទ", "activityKitDisabledBody": "បើកសកម្មភាពបន្តផ្ទាល់នៅក្នុងការកំណត់ ដើម្បីមើលភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ។" } } diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index ec7b1dae98..160f871b4c 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "ಅಧಿಸೂಚನೆಗಳು", + "liveActivities": "ಲೈವ್ ಚಟುವಟಿಕೆಗಳು", + "liveActivitySubtitle": "ಲಾಕ್ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ತೋರಿಸಿ", "push": "ಪುಶ್", "enabled": "ಅಧಿಸೂಚನೆಗಳು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ", "onDescription": "ಈ ಸಾಧನಕ್ಕೆ ಪುಶ್ ಅಧಿಸೂಚನೆಗಳು ಆನ್ ಆಗಿವೆ.", @@ -3225,7 +3227,6 @@ "needsInput": "ಇನ್‌ಪುಟ್ ಅಗತ್ಯವಿದೆ", "idle": "ನಿಷ್ಕ್ರಿಯ", "channelName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು", - "activityKitDisabledTitle": "ನೇರ ಚಟುವಟಿಕೆಗಳು ಆಫ್ ಆಗಿವೆ", "activityKitDisabledBody": "ಲಾಕ್ ಪರದೆಯಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ನೇರ ಚಟುವಟಿಕೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ." } } diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index ce7137f3ec..04467574fb 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "알림", + "liveActivities": "실시간 활동", + "liveActivitySubtitle": "잠금 화면에 활성 에이전트 표시", "push": "푸시", "enabled": "알림 활성화됨", "onDescription": "이 기기에서 푸시 알림이 켜져 있습니다.", @@ -3225,7 +3227,6 @@ "needsInput": "입력 필요", "idle": "유휴", "channelName": "활성 에이전트", - "activityKitDisabledTitle": "실시간 현황이 꺼져 있습니다", "activityKitDisabledBody": "잠금 화면에서 활성 에이전트를 보려면 설정에서 실시간 현황을 켜세요." } } diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index bbe61c4cd3..4ebc991c0a 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "ການແຈ້ງເຕືອນ", + "liveActivities": "ກິດຈະກຳສົດ", + "liveActivitySubtitle": "ສະແດງຕົວແທນທີ່ເຮັດວຽກຢູ່ໜ້າຈໍລັອກ", "push": "ການຜັກດັນ", "enabled": "ເປີດການແຈ້ງເຕືອນແລ້ວ", "onDescription": "ການແຈ້ງເຕືອນແບບຜັກດັນເປີດຢູ່ສຳລັບອຸປະກອນນີ້.", @@ -3225,7 +3227,6 @@ "needsInput": "ຕ້ອງການຂໍ້ມູນເຂົ້າ", "idle": "ບໍ່ຫຍຸ້ງ", "channelName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ", - "activityKitDisabledTitle": "ກິດຈະກຳສົດປິດຢູ່", "activityKitDisabledBody": "ເປີດກິດຈະກຳສົດໃນການຕັ້ງຄ່າ ເພື່ອເບິ່ງຕົວແທນທີ່ກຳລັງເຮັດວຽກໃນໜ້າຈໍລັອກ." } } diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index c7855363d5..4171d5dfb7 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -628,6 +628,8 @@ }, "notifications": { "title": "Pranešimai", + "liveActivities": "Tiesioginės veiklos", + "liveActivitySubtitle": "Rodyti aktyvius agentus užrakinimo ekrane", "push": "Push", "enabled": "Pranešimai įjungti", "onDescription": "Push pranešimai šiame įrenginyje įjungti.", @@ -3269,7 +3271,6 @@ "needsInput": "reikia įvesties", "idle": "Neaktyvus", "channelName": "Aktyvūs agentai", - "activityKitDisabledTitle": "Tiesioginės veiklos išjungtos", "activityKitDisabledBody": "Nustatymuose įjunkite tiesiogines veiklas, kad užrakinimo ekrane matytumėte aktyvius agentus." } } diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index b8b13d1bb5..0cfb086aae 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -621,6 +621,8 @@ }, "notifications": { "title": "Paziņojumi", + "liveActivities": "Tiešās aktivitātes", + "liveActivitySubtitle": "Rādīt aktīvos aģentus bloķēšanas ekrānā", "push": "Push", "enabled": "Paziņojumi iespējoti", "onDescription": "Push paziņojumi šai ierīcei ir ieslēgti.", @@ -3247,7 +3249,6 @@ "needsInput": "nepieciešama ievade", "idle": "Dīkstāvē", "channelName": "Aktīvie aģenti", - "activityKitDisabledTitle": "Tiešraides aktivitātes ir izslēgtas", "activityKitDisabledBody": "Ieslēdz tiešraides aktivitātes iestatījumos, lai bloķēšanas ekrānā redzētu aktīvos aģentus." } } diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index 973680a65b..30509ea914 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Fampandrenesana", + "liveActivities": "Hetsika mivantana", + "liveActivitySubtitle": "Asehoy ny agent miasa eo amin’ny efijery mihidy", "push": "Push", "enabled": "Alefa ny fampandrenesana", "onDescription": "Miasa amin'ity fitaovana ity ny fampandrenesana push.", @@ -3225,7 +3227,6 @@ "needsInput": "mila fampidirana", "idle": "Tsy mihetsika", "channelName": "Agent mavitrika", - "activityKitDisabledTitle": "Tsy mandeha ny Hetsika Mivantana", "activityKitDisabledBody": "Alefaso ao amin'ny Fikirana ny Hetsika Mivantana mba hahitana ny Agent Mavitrika eo amin'ny Efijery Fihidy." } } diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index 23b262959f..ae0ca3bf06 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Ngā Pānui", + "liveActivities": "Ngā mahi ora", + "liveActivitySubtitle": "Whakaatu i ngā māngai kaha ki te Mata Raka", "push": "Pana", "enabled": "Kua whakahohea ngā pānui", "onDescription": "Kei te kā ngā pānui pana mō tēnei pūrere.", @@ -3225,7 +3227,6 @@ "needsInput": "e hiahia ana ki te whakaurunga", "idle": "Kore mahi", "channelName": "Ngā māngai hohe", - "activityKitDisabledTitle": "Kua whakawetohia ngā Mahi Mataora", "activityKitDisabledBody": "Whakakāngia ngā Mahi Mataora i Ngā tautuhinga kia kite i ngā Māngai Hohe i te Mata Maukati." } } diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index b4686fb968..08e73b5d65 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Известувања", + "liveActivities": "Активности во живо", + "liveActivitySubtitle": "Прикажувај активни агенти на заклучениот екран", "push": "Притискање", "enabled": "Известувањата се овозможени", "onDescription": "Притиснатите известувања се вклучени за овој уред.", @@ -3225,7 +3227,6 @@ "needsInput": "бара внес", "idle": "Неактивен", "channelName": "Активни агенти", - "activityKitDisabledTitle": "Активностите во живо се исклучени", "activityKitDisabledBody": "Вклучете Активности во живо во Поставки за да ги видите активните агенти на заклучениот екран." } } diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index e7c3f7a9ba..e26a867df0 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "അറിയിപ്പുകൾ", + "liveActivities": "ലൈവ് ആക്റ്റിവിറ്റികൾ", + "liveActivitySubtitle": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണിക്കുക", "push": "പുഷ്", "enabled": "അറിയിപ്പുകൾ പ്രവർത്തനക്ഷമമാക്കി", "onDescription": "ഈ ഉപകരണത്തിനായി പുഷ് അറിയിപ്പുകൾ ഓണാണ്.", @@ -3225,7 +3227,6 @@ "needsInput": "ഇൻപുട്ട് ആവശ്യമാണ്", "idle": "നിഷ്ക്രിയം", "channelName": "സജീവ ഏജന്റുകൾ", - "activityKitDisabledTitle": "തത്സമയ പ്രവർത്തനങ്ങൾ ഓഫാണ്", "activityKitDisabledBody": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണാൻ ക്രമീകരണങ്ങളിൽ തത്സമയ പ്രവർത്തനങ്ങൾ പ്രവർത്തനക്ഷമമാക്കുക." } } diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index 6882b5cf02..f708308912 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Мэдэгдэлүүд", + "liveActivities": "Шууд үйл ажиллагаа", + "liveActivitySubtitle": "Идэвхтэй агентуудыг түгжээний дэлгэцэд харуулах", "push": "Түлхэлт", "enabled": "Мэдэгдэл идэвхжсэн", "onDescription": "Энэ төхөөрөмжид түлхэлтийн мэдэгдэл асна.", @@ -3225,7 +3227,6 @@ "needsInput": "оролт шаардлагатай", "idle": "Сул зогсож", "channelName": "Идэвхтэй агентууд", - "activityKitDisabledTitle": "Шууд үйл ажиллагаа унтраалттай байна", "activityKitDisabledBody": "Түгжээтэй дэлгэц дээр Идэвхтэй агентуудыг харахын тулд Тохиргоо хэсэгт Шууд үйл ажиллагааг асаана уу." } } diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index c1f8bded68..f431723b15 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "सूचना", + "liveActivities": "लाइव्ह क्रियाकलाप", + "liveActivitySubtitle": "लॉक स्क्रीनवर सक्रिय एजंट दाखवा", "push": "पुश", "enabled": "सूचना सक्षम केल्या", "onDescription": "या डिव्हाइससाठी पुश सूचना चालू आहेत.", @@ -3225,7 +3227,6 @@ "needsInput": "इनपुट आवश्यक", "idle": "निष्क्रिय", "channelName": "सक्रिय एजंट्स", - "activityKitDisabledTitle": "थेट क्रियाकलाप बंद आहेत", "activityKitDisabledBody": "लॉक स्क्रीनवर सक्रिय एजंट्स पाहण्यासाठी सेटिंग्जमध्ये थेट क्रियाकलाप सुरू करा." } } diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 7ff9af474e..99166caa09 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Pemberitahuan", + "liveActivities": "Aktiviti langsung", + "liveActivitySubtitle": "Tunjukkan ejen aktif pada Skrin Kunci", "push": "Push", "enabled": "Pemberitahuan didayakan", "onDescription": "Pemberitahuan push dihidupkan untuk peranti ini.", @@ -3225,7 +3227,6 @@ "needsInput": "perlu input", "idle": "Melahu", "channelName": "Ejen aktif", - "activityKitDisabledTitle": "Aktiviti Langsung dimatikan", "activityKitDisabledBody": "Hidupkan Aktiviti Langsung dalam Tetapan untuk melihat Ejen Aktif pada Skrin Kunci." } } diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index 70e46890b4..a58cdb286c 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -635,6 +635,8 @@ }, "notifications": { "title": "Notifiki", + "liveActivities": "Attivitajiet diretti", + "liveActivitySubtitle": "Uri l-aġenti attivi fuq l-iskrin imsakkar", "push": "Push", "enabled": "Notifiki attivati", "onDescription": "In-notifiki push huma mixgħula għal dan l-apparat.", @@ -3291,7 +3293,6 @@ "needsInput": "jeħtieġ input", "idle": "Idle", "channelName": "Aġenti attivi", - "activityKitDisabledTitle": "L-Attivitajiet Diretti huma mitfija", "activityKitDisabledBody": "Ixgħel l-Attivitajiet Diretti fis-Settings biex tara l-Aġenti Attivi fuq l-Iskrin Imsakkar." } } diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index d45e3d7f7d..e0005ae576 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "အသိပေးချက်များ", + "liveActivities": "တိုက်ရိုက် လှုပ်ရှားမှုများ", + "liveActivitySubtitle": "လော့ခ်စခရင်တွင် လုပ်ဆောင်နေသော agent များကို ပြပါ", "push": "Push", "enabled": "အကြောင်းကြားချက်များ ဖွင့်ထားသည်", "onDescription": "ဤစက်အတွက် push အကြောင်းကြားချက်များ ဖွင့်ထားသည်။", @@ -3225,7 +3227,6 @@ "needsInput": "ထည့်သွင်းမှု လိုအပ်သည်", "idle": "နားနေသည်", "channelName": "လုပ်ဆောင်နေသော agent များ", - "activityKitDisabledTitle": "တိုက်ရိုက်လှုပ်ရှားမှုများ ပိတ်ထားသည်", "activityKitDisabledBody": "သော့ခတ်မျက်နှာပြင်တွင် လုပ်ဆောင်နေသော agent များကို ကြည့်ရန် ဆက်တင်များတွင် တိုက်ရိုက်လှုပ်ရှားမှုများကို ဖွင့်ပါ။" } } diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index e5478cf2c1..1861bd1110 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Varsler", + "liveActivities": "Sanntidsaktiviteter", + "liveActivitySubtitle": "Vis aktive agenter på låseskjermen", "push": "Push", "enabled": "Varsler aktivert", "onDescription": "Push-varsler er på for denne enheten.", @@ -3225,7 +3227,6 @@ "needsInput": "trenger innspill", "idle": "Ledig", "channelName": "Aktive agenter", - "activityKitDisabledTitle": "Oppdateringer i sanntid er av", "activityKitDisabledBody": "Slå på Oppdateringer i sanntid i Innstillinger for å se Aktive agenter på låst skjerm." } } diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index db44bcb78c..6bc3290f65 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "सूचनाहरू", + "liveActivities": "लाइभ गतिविधिहरू", + "liveActivitySubtitle": "लक स्क्रिनमा सक्रिय एजेन्टहरू देखाउनुहोस्", "push": "पुश", "enabled": "सूचनाहरू सक्षम गरियो", "onDescription": "यो यन्त्रको लागि पुश सूचनाहरू सक्रिय छन्।", @@ -3225,7 +3227,6 @@ "needsInput": "इनपुट चाहिन्छ", "idle": "निष्क्रिय", "channelName": "सक्रिय एजेन्टहरू", - "activityKitDisabledTitle": "प्रत्यक्ष गतिविधिहरू बन्द छन्", "activityKitDisabledBody": "लक स्क्रिनमा सक्रिय एजेन्टहरू हेर्न सेटिङ्समा प्रत्यक्ष गतिविधिहरू चालू गर्नुहोस्।" } } diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index d6c4bd4678..36173deb9b 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -183,6 +183,8 @@ "securityFindingsSubtitle": "nieuwe bevindingen en SLA-herinneringen" }, "title": "Meldingen", + "liveActivities": "Live activiteiten", + "liveActivitySubtitle": "Actieve agents tonen op het toegangsscherm", "push": "Push", "enabled": "Meldingen ingeschakeld", "onDescription": "Pushmeldingen staan aan voor dit apparaat.", @@ -3225,7 +3227,6 @@ "needsInput": "heeft invoer nodig", "idle": "Inactief", "channelName": "Actieve agents", - "activityKitDisabledTitle": "Liveactiviteiten staan uit", "activityKitDisabledBody": "Schakel liveactiviteiten in via Instellingen om actieve agents op het toegangsscherm te zien." } } diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index 6554e83bd0..f876ade8a9 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Beeksisota", + "liveActivities": "Sochoota kallattii", + "liveActivitySubtitle": "Eejentoota hojjetan gaaffii cufaa irratti agarsiisi", "push": "Push", "enabled": "Beeksisni dandeesame", "onDescription": "Beeksisni push meeshaa kanaaf jira.", @@ -3225,7 +3227,6 @@ "needsInput": "seensa barbaada", "idle": "Hojii irraa boqachaa", "channelName": "Eejentoota hojii irra jiran", - "activityKitDisabledTitle": "Sochiiwwan Kallattii cufamaniiru", "activityKitDisabledBody": "Eejentoota hojii irra jiran Iskiriinii Qulfii irratti arguuf, Qindaa'ina keessatti Sochiiwwan Kallattii banaa." } } diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index e56d6839a2..81db0eb214 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "ବିଜ୍ଞପ୍ତି", + "liveActivities": "ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ", + "liveActivitySubtitle": "ଲକ୍ ସ୍କ୍ରିନରେ ସକ୍ରିୟ ଏଜେଣ୍ଟ ଦେଖାନ୍ତୁ", "push": "ପୁସ୍", "enabled": "ବିଜ୍ଞପ୍ତି ସକ୍ଷମ ହେଲା", "onDescription": "ଏହି ଉପକରଣ ପାଇଁ ପୁସ୍ ବିଜ୍ଞପ୍ତି ଚାଲୁ ଅଛି।", @@ -3225,7 +3227,6 @@ "needsInput": "ଇନପୁଟ୍ ଆବଶ୍ୟକ", "idle": "ନିଷ୍କ୍ରିୟ", "channelName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ", - "activityKitDisabledTitle": "ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ବନ୍ଦ ଅଛି", "activityKitDisabledBody": "ଲକ୍ ସ୍କ୍ରିନ୍‌ରେ ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସେଟିଂସ୍‌ରେ ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ଚାଲୁ କରନ୍ତୁ।" } } diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 680c163a74..09fda15734 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "ਸੂਚਨਾਵਾਂ", + "liveActivities": "ਲਾਈਵ ਸਰਗਰਮੀਆਂ", + "liveActivitySubtitle": "ਲਾਕ ਸਕ੍ਰੀਨ ਉੱਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦਿਖਾਓ", "push": "ਪੁਸ਼", "enabled": "ਸੂਚਨਾਵਾਂ ਸਮਰੱਥ ਹਨ", "onDescription": "ਇਸ ਡਿਵਾਈਸ ਲਈ ਪੁਸ਼ ਸੂਚਨਾਵਾਂ ਚਾਲੂ ਹਨ।", @@ -3225,7 +3227,6 @@ "needsInput": "ਇਨਪੁੱਟ ਦੀ ਲੋੜ ਹੈ", "idle": "ਸੁਸਤ", "channelName": "ਸਰਗਰਮ ਏਜੰਟ", - "activityKitDisabledTitle": "ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਬੰਦ ਹਨ", "activityKitDisabledBody": "ਲਾਕ ਸਕ੍ਰੀਨ 'ਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦੇਖਣ ਲਈ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਚਾਲੂ ਕਰੋ।" } } diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index 1b75013aea..a9e3db06dd 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "Powiadomienia", + "liveActivities": "Aktywności na żywo", + "liveActivitySubtitle": "Pokazuj aktywnych agentów na ekranie blokady", "push": "Push", "enabled": "Powiadomienia włączone", "onDescription": "Powiadomienia push są włączone dla tego urządzenia.", @@ -3269,7 +3271,6 @@ "needsInput": "wymaga danych", "idle": "Bezczynny", "channelName": "Aktywni agenci", - "activityKitDisabledTitle": "Wydarzenia na żywo są wyłączone", "activityKitDisabledBody": "Włącz wydarzenia na żywo w Ustawieniach, aby widzieć aktywnych agentów na ekranie blokady." } } diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 82e7709516..59a92c34f1 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "خبرتیاوې", + "liveActivities": "ژوندۍ فعالیتونه", + "liveActivitySubtitle": "فعال اجنټان په لاک سکرین کې وښایه", "push": "پوش", "enabled": "خبرتیاوې فعالې شوې", "onDescription": "پوش خبرتیاوې د دې وسیلې لپاره فعالې دي.", @@ -3225,7 +3227,6 @@ "needsInput": "ورودی ته اړتیا لري", "idle": "بې کاره", "channelName": "فعال اجنټان", - "activityKitDisabledTitle": "ژوندي فعالیتونه بند دي", "activityKitDisabledBody": "په قلف شوې پرده کې د فعالو اجنټانو د لیدلو لپاره په ترتیباتو کې ژوندي فعالیتونه فعال کړئ." } } diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index 875cb0f4fd..df0392d0b4 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -147,6 +147,8 @@ "security": "Descobertas de segurança" }, "title": "Notificações", + "liveActivities": "Atividades ao vivo", + "liveActivitySubtitle": "Mostrar agentes ativos na tela bloqueada", "push": "Push", "enabled": "Notificações ativadas", "onDescription": "As notificações push estão ativadas para este dispositivo.", @@ -3247,7 +3249,6 @@ "needsInput": "requer entrada", "idle": "Ocioso", "channelName": "Agentes ativos", - "activityKitDisabledTitle": "As Atividades ao Vivo estão desativadas", "activityKitDisabledBody": "Ative as Atividades ao Vivo em Ajustes para ver os agentes ativos na Tela Bloqueada." } } diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 0192694ce4..6fd8e81001 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -621,6 +621,8 @@ }, "notifications": { "title": "Notificações", + "liveActivities": "Atividades em direto", + "liveActivitySubtitle": "Mostrar agentes ativos no ecrã bloqueado", "push": "Push", "enabled": "Notificações ativadas", "onDescription": "As notificações push estão ativas para este dispositivo.", @@ -3247,7 +3249,6 @@ "needsInput": "requer entrada", "idle": "Inativo", "channelName": "Agentes ativos", - "activityKitDisabledTitle": "As Atividades em tempo real estão desativadas", "activityKitDisabledBody": "Ative as Atividades em tempo real nas Definições para ver os agentes ativos no Ecrã bloqueado." } } diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 17463e4068..5f910dcecc 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -621,6 +621,8 @@ }, "notifications": { "title": "Notificări", + "liveActivities": "Activități live", + "liveActivitySubtitle": "Afișează agenții activi pe ecranul blocat", "push": "Push", "enabled": "Notificări activate", "onDescription": "Notificările push sunt activate pentru acest dispozitiv.", @@ -3247,7 +3249,6 @@ "needsInput": "necesită introducere", "idle": "Inactiv", "channelName": "Agenți activi", - "activityKitDisabledTitle": "Activitățile live sunt dezactivate", "activityKitDisabledBody": "Activează Activități live în Setări pentru a vedea Agenții activi pe ecranul de blocare." } } diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 69deac959e..a2c7968fa1 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "Уведомления", + "liveActivities": "Живые активности", + "liveActivitySubtitle": "Показывать активных агентов на экране блокировки", "push": "Push", "enabled": "Уведомления включены", "onDescription": "Push-уведомления включены для этого устройства.", @@ -3269,7 +3271,6 @@ "needsInput": "требует ввода", "idle": "Неактивен", "channelName": "Активные агенты", - "activityKitDisabledTitle": "Эфир активности выключен", "activityKitDisabledBody": "Включите Эфир активности в Настройках, чтобы видеть активных агентов на экране блокировки." } } diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index d9be04fff8..661f5aefc5 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "දැනුම්දීම්", + "liveActivities": "සජීවී ක්‍රියාකාරකම්", + "liveActivitySubtitle": "අගුළු තිරයේ සක්‍රීය නියෝජිතයන් පෙන්වන්න", "push": "තෙරපුම", "enabled": "දැනුම්දීම් සක්‍රීය කර ඇත", "onDescription": "මෙම උපාංගය සඳහා තෙරපුම් දැනුම්දීම් ක්‍රියාත්මකයි.", @@ -3225,7 +3227,6 @@ "needsInput": "ආදානය අවශ්යයි", "idle": "නිශ්චල", "channelName": "සක්‍රිය නියෝජිතයන්", - "activityKitDisabledTitle": "සජීවී ක්‍රියාකාරකම් අක්‍රියයි", "activityKitDisabledBody": "අගුළු තිරයේ සක්‍රිය නියෝජිතයන් බැලීමට සැකසුම් තුළ සජීවී ක්‍රියාකාරකම් සක්‍රිය කරන්න." } } diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index 0c36227171..a198952d40 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -628,6 +628,8 @@ }, "notifications": { "title": "Upozornenia", + "liveActivities": "Živé aktivity", + "liveActivitySubtitle": "Zobrazovať aktívnych agentov na uzamknutej obrazovke", "push": "Push", "enabled": "Upozornenia povolené", "onDescription": "Push upozornenia sú pre toto zariadenie zapnuté.", @@ -3269,7 +3271,6 @@ "needsInput": "vyžaduje vstup", "idle": "Nečinný", "channelName": "Aktívni agenti", - "activityKitDisabledTitle": "Živé aktivity sú vypnuté", "activityKitDisabledBody": "Zapnite živé aktivity v Nastaveniach, aby sa aktívni agenti zobrazovali na zamknutej obrazovke." } } diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index aced43ff0d..0afed798ad 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -628,6 +628,8 @@ }, "notifications": { "title": "Obvestila", + "liveActivities": "Aktivnosti v živo", + "liveActivitySubtitle": "Prikaži aktivne agente na zaklenjenem zaslonu", "push": "Potisno", "enabled": "Obvestila omogočena", "onDescription": "Potisna obvestila so vklopljena za to napravo.", @@ -3269,7 +3271,6 @@ "needsInput": "potrebuje vnos", "idle": "Nedejavno", "channelName": "Aktivni agenti", - "activityKitDisabledTitle": "Dejavnosti v živo so izklopljene", "activityKitDisabledBody": "V Nastavitvah vklopite Dejavnosti v živo, da bodo Aktivni agenti prikazani na zaklenjenem zaslonu." } } diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index a18c8dc26a..0458b74992 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Ogaysiisyada", + "liveActivities": "Hawlaha tooska ah", + "liveActivitySubtitle": "Ku muuji wakiillada firfircoon Shaashadda Qufulka", "push": "Push", "enabled": "Ogaysiisyadu waa daaran", "onDescription": "Ogaysiisyada push waxay u daaran aaladdan.", @@ -3225,7 +3227,6 @@ "needsInput": "u baahan wax-soo-gal", "idle": "Firfircooni la'aan", "channelName": "Wakiillada firfircoon", - "activityKitDisabledTitle": "Hawlaha Tooska ah waa daman", "activityKitDisabledBody": "Ku daar Hawlaha Tooska ah Dejinta si aad Wakiillada firfircoon ugu aragto Shaashadda Qufulka." } } diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index 55db2c7793..f9ae4c64b4 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Njoftimet", + "liveActivities": "Aktivitete të drejtpërdrejta", + "liveActivitySubtitle": "Shfaq agjentët aktivë në ekranin e kyçur", "push": "Shtytje", "enabled": "Njoftimet të aktivizuara", "onDescription": "Njoftimet shtytëse janë ndezur për këtë pajisje.", @@ -3225,7 +3227,6 @@ "needsInput": "ka nevojë për të dhëna", "idle": "I papunë", "channelName": "Agjentët aktivë", - "activityKitDisabledTitle": "Aktivitetet e drejtpërdrejta janë çaktivizuar", "activityKitDisabledBody": "Aktivizoni Aktivitetet e drejtpërdrejta te Cilësimet për të parë Agjentët aktivë në Ekranin e kyçjes." } } diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index d53cb0fe91..253ae3a837 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -621,6 +621,8 @@ }, "notifications": { "title": "Obaveštenja", + "liveActivities": "Aktivnosti uživo", + "liveActivitySubtitle": "Prikaži aktivne agente na zaključanom ekranu", "push": "Push", "enabled": "Obaveštenja omogućena", "onDescription": "Push obaveštenja su uključena za ovaj uređaj.", @@ -3247,7 +3249,6 @@ "needsInput": "zahteva unos", "idle": "Neaktivan", "channelName": "Aktivni agenti", - "activityKitDisabledTitle": "Aktivnosti uživo su isključene", "activityKitDisabledBody": "Uključite Aktivnosti uživo u Podešavanjima da biste videli aktivne agente na zaključanom ekranu." } } diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index 68c4a6a7b4..7394579515 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Aviseringar", + "liveActivities": "Liveaktiviteter", + "liveActivitySubtitle": "Visa aktiva agenter på låsskärmen", "push": "Push", "enabled": "Aviseringar aktiverade", "onDescription": "Pushaviseringar är på för den här enheten.", @@ -3225,7 +3227,6 @@ "needsInput": "kräver indata", "idle": "Inaktiv", "channelName": "Aktiva agenter", - "activityKitDisabledTitle": "Liveaktiviteter är avstängda", "activityKitDisabledBody": "Aktivera liveaktiviteter i Inställningar för att se Aktiva agenter på låsskärmen." } } diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index c630bce5fc..6445b1b090 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Arifa", + "liveActivities": "Shughuli za moja kwa moja", + "liveActivitySubtitle": "Onyesha mawakala hai kwenye Skrini ya Kufunga", "push": "Push", "enabled": "Arifa zimewashwa", "onDescription": "Arifa za push zimewashwa kwa kifaa hiki.", @@ -3225,7 +3227,6 @@ "needsInput": "inahitaji mchango", "idle": "Hakikazi", "channelName": "Mawakala wanaofanya kazi", - "activityKitDisabledTitle": "Shughuli za Moja kwa Moja zimezimwa", "activityKitDisabledBody": "Washa Shughuli za Moja kwa Moja katika Mipangilio ili uone Mawakala wanaofanya kazi kwenye Skrini Iliyofungwa." } } diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 870faf20f0..66fbca7b90 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "அறிவிப்புகள்", + "liveActivities": "நேரடி செயல்பாடுகள்", + "liveActivitySubtitle": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காட்டு", "push": "அழுத்து", "enabled": "அறிவிப்புகள் இயக்கப்பட்டது", "onDescription": "இந்த சாதனத்திற்கு அழுத்து அறிவிப்புகள் இயக்கத்தில் உள்ளன.", @@ -3225,7 +3227,6 @@ "needsInput": "உள்ளீடு தேவை", "idle": "செயலற்று", "channelName": "செயலில் உள்ள முகவர்கள்", - "activityKitDisabledTitle": "நேரலைச் செயல்பாடுகள் முடக்கப்பட்டுள்ளன", "activityKitDisabledBody": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காண அமைப்புகளில் நேரலைச் செயல்பாடுகளை இயக்கவும்." } } diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index ddf5575a49..606201cbf8 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "నోటిఫికేషన్లు", + "liveActivities": "ప్రత్యక్ష కార్యకలాపాలు", + "liveActivitySubtitle": "లాక్ స్క్రీన్‌లో క్రియాశీల ఏజెంట్లను చూపించు", "push": "పుష్", "enabled": "నోటిఫికేషన్లు ప్రారంభించబడ్డాయి", "onDescription": "ఈ పరికరానికి పుష్ నోటిఫికేషన్లు ఆన్లో ఉన్నాయి.", @@ -3225,7 +3227,6 @@ "needsInput": "ఇన్పుట్ అవసరం", "idle": "నిష్క్రియం", "channelName": "చురుకైన ఏజెంట్లు", - "activityKitDisabledTitle": "ప్రత్యక్ష కార్యకలాపాలు ఆఫ్‌లో ఉన్నాయి", "activityKitDisabledBody": "లాక్ స్క్రీన్‌పై చురుకైన ఏజెంట్లను చూడటానికి సెట్టింగ్‌లలో ప్రత్యక్ష కార్యకలాపాలను ఆన్ చేయండి." } } diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index 7178250c74..33a3f1b0ab 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "การแจ้งเตือน", + "liveActivities": "กิจกรรมสด", + "liveActivitySubtitle": "แสดงเอเจนต์ที่ทำงานอยู่บนหน้าจอล็อก", "push": "พุช", "enabled": "เปิดการแจ้งเตือนแล้ว", "onDescription": "เปิดการแจ้งเตือนแบบพุชสำหรับอุปกรณ์นี้", @@ -3225,7 +3227,6 @@ "needsInput": "ต้องป้อนข้อมูล", "idle": "ว่าง", "channelName": "เอเจนต์ที่กำลังทำงาน", - "activityKitDisabledTitle": "กิจกรรมสดปิดอยู่", "activityKitDisabledBody": "เปิดกิจกรรมสดในการตั้งค่าเพื่อดูเอเจนต์ที่กำลังทำงานบนหน้าจอล็อค" } } diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index 33150d863a..1e7c15d32b 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "Bildirimler", + "liveActivities": "Canlı etkinlikler", + "liveActivitySubtitle": "Etkin ajanları Kilit Ekranı’nda göster", "push": "Anlık", "enabled": "Bildirimler etkin", "onDescription": "Anlık bildirimler bu cihaz için açık.", @@ -3225,7 +3227,6 @@ "needsInput": "Girdi gerekli", "idle": "Boşta", "channelName": "Etkin ajanlar", - "activityKitDisabledTitle": "Canlı Etkinlikler kapalı", "activityKitDisabledBody": "Etkin ajanları Kilit Ekranı'nda görmek için Ayarlar'dan Canlı Etkinlikler'i açın." } } diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 87e90e3f62..3470eaa563 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "Сповіщення", + "liveActivities": "Живі активності", + "liveActivitySubtitle": "Показувати активних агентів на екрані блокування", "push": "Push", "enabled": "Сповіщення увімкнено", "onDescription": "Push-сповіщення увімкнено для цього пристрою.", @@ -3269,7 +3271,6 @@ "needsInput": "потребує вводу", "idle": "Неактивний", "channelName": "Активні агенти", - "activityKitDisabledTitle": "Дії наживо вимкнено", "activityKitDisabledBody": "Увімкніть «Дії наживо» в «Параметрах», щоб бачити активних агентів на замкненому екрані." } } diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 220ed37d31..bd6d4e29e4 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "اطلاعیں", + "liveActivities": "لائیو سرگرمیاں", + "liveActivitySubtitle": "لاک اسکرین پر فعال ایجنٹس دکھائیں", "push": "پش", "enabled": "اطلاعیں فعال ہیں", "onDescription": "پش اطلاعیں اس آلے کے لیے آن ہیں۔", @@ -3225,7 +3227,6 @@ "needsInput": "ان پٹ درکار", "idle": "غیر فعال", "channelName": "فعال ایجنٹس", - "activityKitDisabledTitle": "لائیو سرگرمیاں بند ہیں", "activityKitDisabledBody": "لاک اسکرین پر فعال ایجنٹس دیکھنے کے لیے ترتیبات میں لائیو سرگرمیاں فعال کریں۔" } } diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 15f4df58ea..97ad6a4193 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Bildirishnomalar", + "liveActivities": "Jonli faoliyatlar", + "liveActivitySubtitle": "Faol agentlarni qulflash ekranida ko‘rsatish", "push": "Push", "enabled": "Bildirishnomalar yoqilgan", "onDescription": "Push bildirishnomalari bu qurilmada yoqilgan.", @@ -3225,7 +3227,6 @@ "needsInput": "kiritish kerak", "idle": "Kutmoqda", "channelName": "Faol agentlar", - "activityKitDisabledTitle": "Jonli faoliyatlar o'chirilgan", "activityKitDisabledBody": "Qulflangan ekranda Faol agentlarni ko'rish uchun Sozlamalarda Jonli faoliyatlarni yoqing." } } diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 09c7839999..b8e1c1c2bb 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "Thông báo", + "liveActivities": "Hoạt động trực tiếp", + "liveActivitySubtitle": "Hiển thị tác nhân đang hoạt động trên Màn hình khóa", "push": "Đẩy", "enabled": "Đã bật thông báo", "onDescription": "Thông báo đẩy đang bật cho thiết bị này.", @@ -3225,7 +3227,6 @@ "needsInput": "cần nhập", "idle": "Không hoạt động", "channelName": "Tác nhân đang hoạt động", - "activityKitDisabledTitle": "Hoạt động trực tiếp đang tắt", "activityKitDisabledBody": "Bật Hoạt động trực tiếp trong Cài đặt để xem các tác nhân đang hoạt động trên Màn hình khóa." } } diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index 26ece66ab6..d163054894 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Awọn ifitonileti", + "liveActivities": "Awọn iṣẹ laaye", + "liveActivitySubtitle": "Fi awọn aṣoju to n ṣiṣẹ han lori Iboju Titiipa", "push": "Titari", "enabled": "Awọn ifitonileti mu ṣiṣẹ", "onDescription": "Awọn ifitonileti titari wa ni lori ẹ̀rọ yii.", @@ -3225,7 +3227,6 @@ "needsInput": "nilo igbewọle", "idle": "Ìsinmi", "channelName": "Awọn aṣoju to n ṣiṣẹ", - "activityKitDisabledTitle": "Awọn Iṣẹ Lọwọlọwọ wa ni pipa", "activityKitDisabledBody": "Tan Awọn Iṣẹ Lọwọlọwọ ninu Eto lati ri Awọn aṣoju to n ṣiṣẹ lori Iboju Titiipa." } } diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index 6f9b50b982..b8fcaac0e6 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "通知", + "liveActivities": "实时活动", + "liveActivitySubtitle": "在锁定屏幕上显示活跃代理", "push": "推送", "enabled": "通知已启用", "onDescription": "此设备的推送通知已开启。", @@ -3225,7 +3227,6 @@ "needsInput": "需要输入", "idle": "空闲", "channelName": "活动代理", - "activityKitDisabledTitle": "实时活动已关闭", "activityKitDisabledBody": "请在“设置”中开启“实时活动”,以在锁定屏幕上查看活动代理。" } } diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index 772e63e103..82b5f53030 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -160,6 +160,8 @@ }, "notifications": { "title": "通知", + "liveActivities": "即時動態", + "liveActivitySubtitle": "在鎖定畫面上顯示使用中的代理", "push": "推播", "enabled": "已啟用通知", "onDescription": "此裝置的推播通知已開啟。", @@ -3225,7 +3227,6 @@ "needsInput": "需要輸入", "idle": "閒置", "channelName": "使用中的代理", - "activityKitDisabledTitle": "即時動態已關閉", "activityKitDisabledBody": "請在「設定」中開啟「即時動態」,以在鎖定畫面上查看使用中的代理。" } } diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 4ee399aa09..aa0f1b97e0 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -614,6 +614,8 @@ }, "notifications": { "title": "Izaziso", + "liveActivities": "Imisebenzi ebukhoma", + "liveActivitySubtitle": "Bonisa ama-agent asebenzayo kuSikrini Sokukhiya", "push": "Ukudonsa", "enabled": "Izaziso zivuliwe", "onDescription": "Izaziso zokudonsa zivuliwe kule divayisi.", @@ -3225,7 +3227,6 @@ "needsInput": "idinga okokufaka", "idle": "Banga", "channelName": "Ama-agent asebenzayo", - "activityKitDisabledTitle": "Imisebenzi Ebukhoma ivaliwe", "activityKitDisabledBody": "Vula Imisebenzi Ebukhoma ku-Izilungiselelo ukuze ubone Ama-agent asebenzayo Esikrinini Esikhiyiwe." } } diff --git a/apps/mobile/src/lib/auth/auth-context.test.tsx b/apps/mobile/src/lib/auth/auth-context.test.tsx index 45e4a241f2..9681c95e5a 100644 --- a/apps/mobile/src/lib/auth/auth-context.test.tsx +++ b/apps/mobile/src/lib/auth/auth-context.test.tsx @@ -210,6 +210,9 @@ const { clearKeepScreenOnPreference, clearReasoningPreference, clearPrReviewFoot clearPrReviewFooterPreference: vi.fn(), })); vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ clearKeepScreenOnPreference })); +vi.mock('@/lib/hooks/use-live-activity-preference', () => ({ + clearLiveActivityPreference: vi.fn(), +})); vi.mock('@/lib/hooks/use-reasoning-preference', () => ({ clearReasoningPreference })); diff --git a/apps/mobile/src/lib/auth/auth-context.tsx b/apps/mobile/src/lib/auth/auth-context.tsx index b0840a71aa..81965603f9 100644 --- a/apps/mobile/src/lib/auth/auth-context.tsx +++ b/apps/mobile/src/lib/auth/auth-context.tsx @@ -45,6 +45,7 @@ import { import { chainSave } from '@/lib/hooks/save-chain'; import { clearAgentModelPreference } from '@/lib/hooks/use-persisted-agent-model'; import { clearKeepScreenOnPreference } from '@/lib/hooks/use-keep-screen-on-preference'; +import { clearLiveActivityPreference } from '@/lib/hooks/use-live-activity-preference'; import { clearPrReviewFooterPreference } from '@/lib/hooks/use-pr-review-footer-preference'; import { clearReasoningPreference } from '@/lib/hooks/use-reasoning-preference'; import { clearSessionScopedState } from '@/lib/auth/session-scoped-state'; @@ -390,6 +391,7 @@ export function AuthProvider({ children }: { readonly children: ReactNode }) { clearAgentModelPreference(); clearReasoningPreference(); clearKeepScreenOnPreference(); + clearLiveActivityPreference(); clearSessionScopedState(); clearPrReviewFooterPreference(); } finally { diff --git a/apps/mobile/src/lib/auth/credentials.test.ts b/apps/mobile/src/lib/auth/credentials.test.ts index 354551e2b8..f96267aa88 100644 --- a/apps/mobile/src/lib/auth/credentials.test.ts +++ b/apps/mobile/src/lib/auth/credentials.test.ts @@ -61,8 +61,8 @@ vi.mock('@/lib/hooks/use-persisted-agent-model', () => ({ clearAgentModelPrefere vi.mock('@/lib/hooks/use-keep-screen-on-preference', () => ({ clearKeepScreenOnPreference: vi.fn(), })); -vi.mock('@/lib/hooks/use-glanceable-preference', () => ({ - clearGlanceablePreference: vi.fn(), +vi.mock('@/lib/hooks/use-live-activity-preference', () => ({ + clearLiveActivityPreference: vi.fn(), })); vi.mock('@/lib/hooks/use-pr-review-footer-preference', () => ({ clearPrReviewFooterPreference: vi.fn(), diff --git a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts index d3ee8b51e7..5ed2fe2ec5 100644 --- a/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -1,5 +1,5 @@ import * as SecureStore from 'expo-secure-store'; -import { Alert, Linking, Platform } from 'react-native'; +import { Platform } from 'react-native'; import { buildOpaqueScopeKey, @@ -12,33 +12,6 @@ import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; import { getLastGlanceableSnapshot, getLocalScopeKey } from '@/lib/glanceable/persist'; import { forEachSink } from '@/lib/glanceable/sink-registry'; import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; -import { i18n } from '@/i18n'; - -/** - * The one in-app Open Settings alert for an ActivityKit-unavailable surface. - * Shown at most once per process when the Agents tab regains focus — never - * auto-alerted from the publisher, so the publisher stays a pure state machine. - */ - -let alertShown = false; - -export function showActivityKitDisabledAlertOnce(): void { - if (Platform.OS !== 'ios' || alertShown) { - return; - } - if (!getActivityKitDenied()) { - return; - } - alertShown = true; - Alert.alert( - i18n.t('glanceable.activityKitDisabledTitle'), - i18n.t('glanceable.activityKitDisabledBody'), - [ - { text: i18n.t('common.cancel'), style: 'cancel' }, - { text: i18n.t('common.openSettings'), onPress: () => void Linking.openSettings() }, - ] - ); -} /** * Recover a once-denied ActivityKit surface after verifying the stored identity diff --git a/apps/mobile/src/lib/glanceable/live-activity-switch.test.ts b/apps/mobile/src/lib/glanceable/live-activity-switch.test.ts new file mode 100644 index 0000000000..0bb8e37b53 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/live-activity-switch.test.ts @@ -0,0 +1,41 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { + _resetLiveActivitySwitchForTests, + getLiveActivityEnabled, + setLiveActivityEnabledValue, + subscribeLiveActivityEnabled, +} from './live-activity-switch'; + +afterEach(() => { + _resetLiveActivitySwitchForTests(); +}); + +describe('live activity switch', () => { + it('defaults to on, which is what the app does before the disk read lands', () => { + expect(getLiveActivityEnabled()).toBe(true); + }); + + it('notifies only on a change, so a re-read cannot end a running activity', () => { + const listener = vi.fn<() => void>(); + subscribeLiveActivityEnabled(listener); + + setLiveActivityEnabledValue(true); + expect(listener).not.toHaveBeenCalled(); + + setLiveActivityEnabledValue(false); + expect(listener).toHaveBeenCalledTimes(1); + expect(getLiveActivityEnabled()).toBe(false); + + setLiveActivityEnabledValue(false); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('stops notifying an unsubscribed listener', () => { + const listener = vi.fn<() => void>(); + const unsubscribe = subscribeLiveActivityEnabled(listener); + unsubscribe(); + setLiveActivityEnabledValue(false); + expect(listener).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/mobile/src/lib/glanceable/live-activity-switch.ts b/apps/mobile/src/lib/glanceable/live-activity-switch.ts new file mode 100644 index 0000000000..75b77bff94 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/live-activity-switch.ts @@ -0,0 +1,38 @@ +/** + * The in-app Live Activity switch, as a value the sink can read. + * + * `use-live-activity-preference` owns the SecureStore round trip and pushes + * every change here. This module holds only the current answer, and imports + * nothing, so the sink's test graph stays free of React Native. + */ + +let enabled = true; +const listeners = new Set<() => void>(); + +/** Defaults to on, which is what the app does before the disk read lands. */ +export function getLiveActivityEnabled(): boolean { + return enabled; +} + +export function setLiveActivityEnabledValue(next: boolean): void { + if (next === enabled) { + return; + } + enabled = next; + for (const listener of listeners) { + listener(); + } +} + +export function subscribeLiveActivityEnabled(listener: () => void): () => void { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +} + +/** Test-only: restore the shipped default between cases. */ +export function _resetLiveActivitySwitchForTests(): void { + enabled = true; + listeners.clear(); +} diff --git a/apps/mobile/src/lib/hooks/use-live-activity-preference.ts b/apps/mobile/src/lib/hooks/use-live-activity-preference.ts new file mode 100644 index 0000000000..c7d4121555 --- /dev/null +++ b/apps/mobile/src/lib/hooks/use-live-activity-preference.ts @@ -0,0 +1,48 @@ +import { useSyncExternalStore } from 'react'; + +import { setLiveActivityEnabledValue } from '@/lib/glanceable/live-activity-switch'; +import { createSecureStorePreference } from '@/lib/hooks/secure-store-preference'; +import { LIVE_ACTIVITY_KEY } from '@/lib/storage-keys'; + +/** + * The in-app escape hatch for the Active Agents Live Activity. + * + * Separate from the per-app switch in Settings, which ActivityKit owns: this + * one lets someone keep Live Activities for every other app and stop only + * Kilo's. Both must allow it for the activity to start — see `ios-sink`. + * + * Default-on: only the exact stored string 'false' turns it off, so a missing + * or unreadable value keeps the behavior the app ships with. + */ +function parseLiveActivityEnabled(raw: string | null): boolean { + return raw !== 'false'; +} + +const store = createSecureStorePreference({ + key: LIVE_ACTIVITY_KEY, + defaultValue: true, + parse: parseLiveActivityEnabled, + serialize: value => (value ? 'true' : 'false'), +}); + +// Mirror the persisted value into the React-Native-free holder the sink reads. +// Subscribing also starts the disk read, so the mirror is correct from the +// first emit rather than from the first render of the settings screen. +store.subscribe(() => { + setLiveActivityEnabledValue(store.get()); +}); +setLiveActivityEnabledValue(store.get()); + +export function clearLiveActivityPreference() { + store.clear(); +} + +function setLiveActivityEnabled(value: boolean) { + store.set(value); +} + +export function useLiveActivityPreference() { + const liveActivityEnabled = useSyncExternalStore(store.subscribe, store.get); + const hasLoaded = useSyncExternalStore(store.subscribe, store.getHasLoaded); + return { liveActivityEnabled, hasLoaded, setLiveActivityEnabled }; +} diff --git a/apps/mobile/src/lib/storage-keys.ts b/apps/mobile/src/lib/storage-keys.ts index f45849d6b6..29d79ca960 100644 --- a/apps/mobile/src/lib/storage-keys.ts +++ b/apps/mobile/src/lib/storage-keys.ts @@ -31,6 +31,7 @@ export const LOGIN_EMAIL_DRAFT_KEY = 'login-email-draft'; /** Login SSO-recovery banner draft, persisted before an RTL language reload. */ export const LOGIN_SSO_RECOVERY_DRAFT_KEY = 'login-sso-recovery-draft'; export const KEEP_SCREEN_ON_KEY = 'keep-session-screen-on'; +export const LIVE_ACTIVITY_KEY = 'live-activity-enabled'; /** Return key in the agent composer sends/start instead of inserting a newline. */ export const RETURN_SENDS_MESSAGE_KEY = 'return-sends-message'; /** Revocable per-host list of markdown link hosts that open without an Alert. */ diff --git a/patches/expo-widgets@57.0.11.patch b/patches/expo-widgets@57.0.11.patch index 29e859b15d..0e026ca8d8 100644 --- a/patches/expo-widgets@57.0.11.patch +++ b/patches/expo-widgets@57.0.11.patch @@ -1,15 +1,54 @@ +diff --git a/build/ExpoWidgets.d.ts b/build/ExpoWidgets.d.ts +index 7f139d2..500bdd5 100644 +--- a/build/ExpoWidgets.d.ts ++++ b/build/ExpoWidgets.d.ts +@@ -3,6 +3,7 @@ import type { ExpoWidgetsEvents, NativeLiveActivity, NativeLiveActivityFactory, + declare const ExpoWidgetsModule: { + widgetsDirectory: string; + reloadAllWidgets(): void; ++ areLiveActivitiesEnabled(): boolean; + Widget: typeof NativeWidgetObject; + LiveActivityFactory: typeof NativeLiveActivityFactory; + LiveActivity: typeof NativeLiveActivity; +diff --git a/build/ExpoWidgets.ios.d.ts b/build/ExpoWidgets.ios.d.ts +index 7df2b6c..17dacae 100644 +--- a/build/ExpoWidgets.ios.d.ts ++++ b/build/ExpoWidgets.ios.d.ts +@@ -3,6 +3,7 @@ import type { ExpoWidgetsEvents, NativeLiveActivity, NativeLiveActivityFactory, + declare class ExpoWidgetsModule extends NativeModule { + widgetsDirectory: string; + reloadAllWidgets(): void; ++ areLiveActivitiesEnabled(): boolean; + readonly Widget: typeof NativeWidgetObject; + readonly LiveActivityFactory: typeof NativeLiveActivityFactory; + readonly LiveActivity: typeof NativeLiveActivity; +diff --git a/build/ExpoWidgets.js b/build/ExpoWidgets.js +index 03721af..06cecf8 100644 +--- a/build/ExpoWidgets.js ++++ b/build/ExpoWidgets.js +@@ -31,6 +31,7 @@ class LiveActivityFactoryStub { + const ExpoWidgetsModule = { + widgetsDirectory: '', + reloadAllWidgets() { }, ++ areLiveActivitiesEnabled() { return false; }, + Widget: WidgetStub, + LiveActivityFactory: LiveActivityFactoryStub, + LiveActivity: LiveActivityStub, diff --git a/build/Widgets.d.ts b/build/Widgets.d.ts -index 6886a8f..fdcfa44 100644 +index 6886a8f..bf9d21d 100644 --- a/build/Widgets.d.ts +++ b/build/Widgets.d.ts -@@ -34,4 +34,6 @@ export declare class LiveActivity { +@@ -33,6 +33,8 @@ export declare class LiveActivity { + /** @hidden */ private nativeLiveActivity; constructor(nativeLiveActivity: NativeLiveActivity); + /** Native identity and current state, including an external end. */ + getInfo(): ReturnType; /** * Updates the Live Activity's content. The UI reflects the new properties immediately. -@@ -76,6 +78,7 @@ export declare class LiveActivityFactory { + * @param props The updated content properties. +@@ -75,8 +77,9 @@ export declare class LiveActivityFactory { + start(props: T, url?: string): LiveActivity; /** * Returns all currently active instances of this Live Activity type. + * Set includeEnded for privacy dismissal of native-retained terminal instances. @@ -18,11 +57,24 @@ index 6886a8f..fdcfa44 100644 + getInstances(includeEnded?: boolean): LiveActivity[]; } /** + * Creates a dismissal policy that removes the Live Activity at the specified time within a four-hour window. +@@ -116,4 +119,9 @@ export declare function addPushToStartTokenListener(listener: ExpoWidgetsEvents[ + * The contents of this directory are accessible by both the main app and widgets. + */ + export declare const widgetsDirectory: string; ++/** ++ * Whether the per-app "Live Activities" switch in Settings is on. False on ++ * every platform and OS version that cannot run one. ++ */ ++export declare function areLiveActivitiesEnabled(): boolean; + //# sourceMappingURL=Widgets.d.ts.map +\ No newline at end of file diff --git a/build/Widgets.js b/build/Widgets.js -index 6cd5127..3ca28e5 100644 +index 6cd5127..02a6e56 100644 --- a/build/Widgets.js +++ b/build/Widgets.js -@@ -48,4 +48,8 @@ export class LiveActivity { +@@ -47,6 +47,10 @@ export class LiveActivity { + constructor(nativeLiveActivity) { this.nativeLiveActivity = nativeLiveActivity; } + /** Native identity and current state, including an external end. */ @@ -31,7 +83,9 @@ index 6cd5127..3ca28e5 100644 + } /** * Updates the Live Activity's content. The UI reflects the new properties immediately. -@@ -108,8 +112,9 @@ export class LiveActivityFactory { + * @param props The updated content properties. +@@ -107,10 +111,11 @@ export class LiveActivityFactory { + } /** * Returns all currently active instances of this Live Activity type. + * Set includeEnded for privacy dismissal of native-retained terminal instances. @@ -43,11 +97,26 @@ index 6cd5127..3ca28e5 100644 + .getInstances(includeEnded) .map((instance) => new LiveActivity(instance)); } + } +@@ -160,4 +165,11 @@ export function addPushToStartTokenListener(listener) { + * The contents of this directory are accessible by both the main app and widgets. + */ + export const widgetsDirectory = ExpoWidgetsModule.widgetsDirectory; ++/** ++ * Whether the per-app "Live Activities" switch in Settings is on. False on ++ * every platform and OS version that cannot run one. ++ */ ++export function areLiveActivitiesEnabled() { ++ return ExpoWidgetsModule.areLiveActivitiesEnabled(); ++} + //# sourceMappingURL=Widgets.js.map +\ No newline at end of file diff --git a/build/Widgets.types.d.ts b/build/Widgets.types.d.ts index f623ece..e6a5a03 100644 --- a/build/Widgets.types.d.ts +++ b/build/Widgets.types.d.ts -@@ -254,7 +254,8 @@ export declare class NativeLiveActivityFactory extends SharedObject { +@@ -253,9 +253,10 @@ export declare class NativeWidgetObject extends SharedObject { + export declare class NativeLiveActivityFactory extends SharedObject { constructor(name: string, layout: string); start(props: string, url?: string): NativeLiveActivity; - getInstances(): NativeLiveActivity[]; @@ -57,11 +126,13 @@ index f623ece..e6a5a03 100644 + getInfo(): { id: string; state: 'active' | 'stale' | 'ended' | 'dismissed' }; update(props: string): Promise; end(dismissalPolicy?: string, afterDate?: number, state?: string, contentDate?: number): Promise; + getPushToken(): Promise; diff --git a/ios/LiveActivity.swift b/ios/LiveActivity.swift index c4b5bcc..b444df0 100644 --- a/ios/LiveActivity.swift +++ b/ios/LiveActivity.swift -@@ -6,4 +6,35 @@ final class LiveActivity: SharedObject { +@@ -5,6 +5,37 @@ final class LiveActivity: SharedObject { + let id: String let name: String private var pushTokenObserverTask: Task? + // Native identity survives JS wrapper release/recreation, but not process exit. @@ -95,26 +166,29 @@ index c4b5bcc..b444df0 100644 + } + return ["id": id, "state": value] + } -- -+ + init(id: String, name: String) { -@@ -27,5 +58,5 @@ final class LiveActivity: SharedObject { + self.id = id +@@ -26,7 +57,7 @@ final class LiveActivity: SharedObject { + func end(dismissalPolicy: LiveActivityDismissalPolicy?, afterDate: Date?, props: String?, contentDate: Date?) async throws { guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() } -- -+ + - guard let activity = Activity.activities.first(where: { $0.id == id }) else { + guard let activity = Self.currentActivities().first(where: { $0.id == id }) else { throw LiveActivityNotFoundException(id) } -@@ -48,5 +79,5 @@ final class LiveActivity: SharedObject { + +@@ -47,7 +78,7 @@ final class LiveActivity: SharedObject { + func getPushToken() throws -> String? { guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() } -- -+ + - guard let activity = Activity.activities.first(where: { $0.id == id }) else { + guard let activity = Self.currentActivities().first(where: { $0.id == id }) else { throw LiveActivityNotFoundException(id) } -@@ -59,4 +90,7 @@ final class LiveActivity: SharedObject { + +@@ -58,6 +89,9 @@ final class LiveActivity: SharedObject { + @available(iOS 16.1, *) func observePushTokenUpdates(for activity: Activity, pushNotificationsEnabled: Bool) { + Self.activitiesLock.withLock { @@ -122,19 +196,19 @@ index c4b5bcc..b444df0 100644 + } guard pushNotificationsEnabled else { return + } diff --git a/ios/LiveActivityFactory.swift b/ios/LiveActivityFactory.swift index caf1e05..821fc64 100644 --- a/ios/LiveActivityFactory.swift +++ b/ios/LiveActivityFactory.swift -@@ -40,9 +40,13 @@ final class LiveActivityFactory: SharedObject { +@@ -39,11 +39,15 @@ final class LiveActivityFactory: SharedObject { + } } -- -+ + - func getInstances() throws -> [LiveActivity] { + func getInstances(includeEnded: Bool = false) throws -> [LiveActivity] { guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() } -- -+ + - return Activity.activities.map { activity in - LiveActivity(id: activity.id, name: name) + return LiveActivity.currentActivities().filter { @@ -145,22 +219,37 @@ index caf1e05..821fc64 100644 + return instance } } + } diff --git a/ios/WidgetsModule.swift b/ios/WidgetsModule.swift -index 678a7d1..c182a7a 100644 +index 678a7d1..bc1aae1 100644 --- a/ios/WidgetsModule.swift +++ b/ios/WidgetsModule.swift -@@ -91,10 +91,14 @@ public final class WidgetsModule: Module { - } -- +@@ -63,6 +63,14 @@ public final class WidgetsModule: Module { + WidgetCenter.shared.reloadAllTimelines() + } + ++ // The per-app "Live Activities" switch in Settings. `start` already refuses ++ // when it is off; this lets JavaScript show the state instead of inferring ++ // it from a failed start. ++ Function("areLiveActivitiesEnabled") { () -> Bool in ++ guard #available(iOS 16.2, *) else { return false } ++ return ActivityAuthorizationInfo().areActivitiesEnabled ++ } + + Class("Widget", WidgetObject.self) { + Constructor { (name: String, layout: String) in + WidgetObject(name: name, layout: layout) +@@ -90,12 +98,16 @@ public final class WidgetsModule: Module { + try liveActivity.start(props: props, url: url) + } + - Function("getInstances") { (liveActivity: LiveActivityFactory) in - try liveActivity.getInstances() + Function("getInstances") { (liveActivity: LiveActivityFactory, includeEnded: Bool?) in + try liveActivity.getInstances(includeEnded: includeEnded ?? false) } } -- -+ + Class("LiveActivity", LiveActivity.self) { + Function("getInfo") { (instance: LiveActivity) in + try instance.getInfo() @@ -168,14 +257,29 @@ index 678a7d1..c182a7a 100644 + AsyncFunction("update") { (instance: LiveActivity, props: String) in try await instance.update(props: props) + } +diff --git a/src/ExpoWidgets.ts b/src/ExpoWidgets.ts +index a72c639..4f14fee 100644 +--- a/src/ExpoWidgets.ts ++++ b/src/ExpoWidgets.ts +@@ -50,6 +50,9 @@ class LiveActivityFactoryStub { + const ExpoWidgetsModule = { + widgetsDirectory: '', + reloadAllWidgets(): void {}, ++ areLiveActivitiesEnabled(): boolean { ++ return false; ++ }, + Widget: WidgetStub as typeof NativeWidgetObject, + LiveActivityFactory: LiveActivityFactoryStub as typeof NativeLiveActivityFactory, + LiveActivity: LiveActivityStub as typeof NativeLiveActivity, diff --git a/src/Widgets.ts b/src/Widgets.ts -index f5bbbba..741d73d 100644 +index f5bbbba..a23a95a 100644 --- a/src/Widgets.ts +++ b/src/Widgets.ts -@@ -79,4 +79,9 @@ export class LiveActivity { +@@ -78,6 +78,11 @@ export class LiveActivity { + this.nativeLiveActivity = nativeLiveActivity; } -- -+ + + /** Native identity and current state, including an external end. */ + getInfo(): ReturnType { + return this.nativeLiveActivity.getInfo(); @@ -183,7 +287,9 @@ index f5bbbba..741d73d 100644 + /** * Updates the Live Activity's content. The UI reflects the new properties immediately. -@@ -160,8 +165,9 @@ export class LiveActivityFactory { + * @param props The updated content properties. +@@ -159,10 +164,11 @@ export class LiveActivityFactory { + /** * Returns all currently active instances of this Live Activity type. + * Set includeEnded for privacy dismissal of native-retained terminal instances. @@ -195,19 +301,33 @@ index f5bbbba..741d73d 100644 + .getInstances(includeEnded) .map((instance) => new LiveActivity(instance)); } + } +@@ -231,3 +237,11 @@ export function addPushToStartTokenListener( + * The contents of this directory are accessible by both the main app and widgets. + */ + export const widgetsDirectory = ExpoWidgetsModule.widgetsDirectory; ++ ++/** ++ * Whether the per-app "Live Activities" switch in Settings is on. False on ++ * every platform and OS version that cannot run one. ++ */ ++export function areLiveActivitiesEnabled(): boolean { ++ return ExpoWidgetsModule.areLiveActivitiesEnabled(); ++} diff --git a/src/Widgets.types.ts b/src/Widgets.types.ts index 37a1b14..a1134c3 100644 --- a/src/Widgets.types.ts +++ b/src/Widgets.types.ts -@@ -283,8 +283,9 @@ export declare class NativeLiveActivityFactory extends SharedObject { +@@ -282,10 +282,11 @@ export declare class NativeWidgetObject extends SharedObject { + export declare class NativeLiveActivityFactory extends SharedObject { constructor(name: string, layout: string); start(props: string, url?: string): NativeLiveActivity; - getInstances(): NativeLiveActivity[]; + getInstances(includeEnded?: boolean): NativeLiveActivity[]; } -- -+ + export declare class NativeLiveActivity extends SharedObject { + getInfo(): { id: string; state: 'active' | 'stale' | 'ended' | 'dismissed' }; update(props: string): Promise; end( + dismissalPolicy?: string, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f72d0a0c25..9501e80b4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,7 +154,7 @@ packageExtensionsChecksum: sha256-1pgKZxx87NNMe1poF5N5u5kZB/qlEyILBQPxofM1shE= patchedDependencies: expo-router@57.0.15: 616f8a79932a3ebc7d31343b53b3fc425a14bfb89bb86cdafa614cefa4e603ed expo-server-sdk: 7850520582b5b394397b35d1ea195192fe78589d8a6a748fe15177b818c4ed0b - expo-widgets@57.0.11: 0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7 + expo-widgets@57.0.11: 4d6fc2097496a5a9f303a95f326ce929fd9ef7893948b647fcd914e9ba2a120c react-native-appsflyer@6.18.0: 82df99378c830e774b0f01796d8be595da114d1d13393d85ddd47d565c5c2aab importers: @@ -557,7 +557,7 @@ importers: version: 57.0.2(expo@57.0.15)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-widgets: specifier: 57.0.11 - version: 57.0.11(patch_hash=0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 57.0.11(patch_hash=4d6fc2097496a5a9f303a95f326ce929fd9ef7893948b647fcd914e9ba2a120c)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) i18next: specifier: ^26.4.0 version: 26.4.0(typescript@6.0.3) @@ -28214,7 +28214,7 @@ snapshots: optionalDependencies: '@babel/runtime': 7.29.7 expo: 57.0.15(@babel/core@7.29.7)(@expo/metro-runtime@57.0.12)(bufferutil@4.1.0)(expo-router@57.0.15)(expo-widgets@57.0.11)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@5.9.3)(utf-8-validate@6.0.6) - expo-widgets: 57.0.11(patch_hash=0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + expo-widgets: 57.0.11(patch_hash=4d6fc2097496a5a9f303a95f326ce929fd9ef7893948b647fcd914e9ba2a120c)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) transitivePeerDependencies: - '@babel/core' - supports-color @@ -30549,7 +30549,7 @@ snapshots: expo: 57.0.15(@babel/core@7.29.7)(@expo/metro-runtime@57.0.12)(bufferutil@4.1.0)(expo-router@57.0.15)(expo-widgets@57.0.11)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react-native: 0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-widgets@57.0.11(patch_hash=0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-widgets@57.0.11(patch_hash=4d6fc2097496a5a9f303a95f326ce929fd9ef7893948b647fcd914e9ba2a120c)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/plist': 0.8.1 '@expo/ui': 57.0.12(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -30563,7 +30563,7 @@ snapshots: - react-dom - react-native-worklets - expo-widgets@57.0.11(patch_hash=0daac50dfa73b2b7e11951f954e0fe23524367319bd887137ef3ea83e41c55f7)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + expo-widgets@57.0.11(patch_hash=4d6fc2097496a5a9f303a95f326ce929fd9ef7893948b647fcd914e9ba2a120c)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: '@expo/plist': 0.8.1 '@expo/ui': 57.0.12(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo@57.0.15)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) From 007225cbf626e01fa6da6640fadde3f52cfd01d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 02:54:24 +0200 Subject: [PATCH 35/43] feat(mobile): bring the Android glanceable surfaces to parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Android widget drew three identical grey rows of text. It carried none of the state vocabulary the iOS surfaces settled on, no mark, English-only picker copy, and no way to turn the Live Update off inside the app. Each row now leads with its state: filled orange for needs-input, filled green for working, an outline ring for idle. The shapes differ as well as the colors, so the three states stay apart for a user who cannot tell orange from green. The colors come from the app's own palette rather than a widget-local one — a Home Screen card that does not match the app it opens reads as a different product — and the Kilo mark sits beside them. Three sizes: a narrow cell shows the ranked count, a short one runs the states in a row, a tall one stacks them under the mark. The default placement is now 4x2 so the layout that shows all three states is the one a user gets. Counts are drawn in the language's own digits. Unlike the iOS widget extension these surfaces render in the app's own runtime, so `Intl` is already there and the formatter is injected rather than baked into a layout. The widget picker is translated into all 87 languages. The library already wraps the description in a string resource and writes the label straight into the receiver, so the label becomes a resource reference and a new plugin writes both keys into one `values-b+` folder per language. The copy comes from widget-gallery-copy.json, the file the iOS gallery already reads, so the two pickers cannot drift. The notifications screen's Live Activity row is now on both platforms, named the way each OS names the surface: a Live Activity on iOS, a Live Update on Android. Android reads the notification permission as its system state and leaves the Open Settings button to the master gate directly below, which enables the same permission. Turning the switch off ends the Live Update already in the shade. The widget is deliberately not gated: placing one is the opt-in and removing it is the opt-out. --- apps/mobile/app.config.ts | 11 + .../plugins/withActiveAgentsAndroidWidget.js | 16 +- .../plugins/withAndroidWidgetLocalizations.js | 118 ++++++++ .../src/components/notifications-screen.tsx | 119 ++++---- .../active-agents-widget.test.ts | 26 +- .../active-agents-widget.tsx | 261 ++++++++++++++---- .../glanceable-android/android-sink.test.ts | 13 +- .../src/glanceable-android/android-sink.ts | 25 +- .../src/glanceable-android/count-format.ts | 14 + .../src/glanceable-android/register.test.ts | 21 +- .../mobile/src/glanceable-android/register.ts | 23 +- .../src/glanceable-android/widget-config.json | 8 +- .../glanceable-android/widget-props.test.ts | 14 +- .../src/glanceable-android/widget-props.ts | 65 +++-- apps/mobile/src/i18n/locales/af.json | 2 + apps/mobile/src/i18n/locales/am.json | 2 + apps/mobile/src/i18n/locales/ar.json | 2 + apps/mobile/src/i18n/locales/az.json | 2 + apps/mobile/src/i18n/locales/be.json | 2 + apps/mobile/src/i18n/locales/bg.json | 2 + apps/mobile/src/i18n/locales/bn.json | 2 + apps/mobile/src/i18n/locales/bs.json | 2 + apps/mobile/src/i18n/locales/ca.json | 2 + apps/mobile/src/i18n/locales/ckb.json | 2 + apps/mobile/src/i18n/locales/cs.json | 2 + apps/mobile/src/i18n/locales/cy.json | 2 + apps/mobile/src/i18n/locales/da.json | 2 + apps/mobile/src/i18n/locales/de.json | 2 + apps/mobile/src/i18n/locales/el.json | 2 + apps/mobile/src/i18n/locales/en.json | 2 + apps/mobile/src/i18n/locales/es.json | 2 + apps/mobile/src/i18n/locales/et.json | 2 + apps/mobile/src/i18n/locales/eu.json | 2 + apps/mobile/src/i18n/locales/fa.json | 2 + apps/mobile/src/i18n/locales/fi.json | 2 + apps/mobile/src/i18n/locales/fil.json | 2 + apps/mobile/src/i18n/locales/fr.json | 2 + apps/mobile/src/i18n/locales/ga.json | 2 + apps/mobile/src/i18n/locales/gl.json | 2 + apps/mobile/src/i18n/locales/gu.json | 2 + apps/mobile/src/i18n/locales/ha.json | 2 + apps/mobile/src/i18n/locales/he.json | 2 + apps/mobile/src/i18n/locales/hi.json | 2 + apps/mobile/src/i18n/locales/hr.json | 2 + apps/mobile/src/i18n/locales/ht.json | 2 + apps/mobile/src/i18n/locales/hu.json | 2 + apps/mobile/src/i18n/locales/hy.json | 2 + apps/mobile/src/i18n/locales/id.json | 2 + apps/mobile/src/i18n/locales/ig.json | 2 + apps/mobile/src/i18n/locales/is.json | 2 + apps/mobile/src/i18n/locales/it.json | 2 + apps/mobile/src/i18n/locales/ja.json | 2 + apps/mobile/src/i18n/locales/ka.json | 2 + apps/mobile/src/i18n/locales/kk.json | 2 + apps/mobile/src/i18n/locales/km.json | 2 + apps/mobile/src/i18n/locales/kn.json | 2 + apps/mobile/src/i18n/locales/ko.json | 2 + apps/mobile/src/i18n/locales/lo.json | 2 + apps/mobile/src/i18n/locales/lt.json | 2 + apps/mobile/src/i18n/locales/lv.json | 2 + apps/mobile/src/i18n/locales/mg.json | 2 + apps/mobile/src/i18n/locales/mi.json | 2 + apps/mobile/src/i18n/locales/mk.json | 2 + apps/mobile/src/i18n/locales/ml.json | 2 + apps/mobile/src/i18n/locales/mn.json | 2 + apps/mobile/src/i18n/locales/mr.json | 2 + apps/mobile/src/i18n/locales/ms.json | 2 + apps/mobile/src/i18n/locales/mt.json | 2 + apps/mobile/src/i18n/locales/my.json | 2 + apps/mobile/src/i18n/locales/nb.json | 2 + apps/mobile/src/i18n/locales/ne.json | 2 + apps/mobile/src/i18n/locales/nl.json | 2 + apps/mobile/src/i18n/locales/om.json | 2 + apps/mobile/src/i18n/locales/or.json | 2 + apps/mobile/src/i18n/locales/pa.json | 2 + apps/mobile/src/i18n/locales/pl.json | 2 + apps/mobile/src/i18n/locales/ps.json | 2 + apps/mobile/src/i18n/locales/pt-BR.json | 2 + apps/mobile/src/i18n/locales/pt.json | 2 + apps/mobile/src/i18n/locales/ro.json | 2 + apps/mobile/src/i18n/locales/ru.json | 2 + apps/mobile/src/i18n/locales/si.json | 2 + apps/mobile/src/i18n/locales/sk.json | 2 + apps/mobile/src/i18n/locales/sl.json | 2 + apps/mobile/src/i18n/locales/so.json | 2 + apps/mobile/src/i18n/locales/sq.json | 2 + apps/mobile/src/i18n/locales/sr.json | 2 + apps/mobile/src/i18n/locales/sv.json | 2 + apps/mobile/src/i18n/locales/sw.json | 2 + apps/mobile/src/i18n/locales/ta.json | 2 + apps/mobile/src/i18n/locales/te.json | 2 + apps/mobile/src/i18n/locales/th.json | 2 + apps/mobile/src/i18n/locales/tr.json | 2 + apps/mobile/src/i18n/locales/uk.json | 2 + apps/mobile/src/i18n/locales/ur.json | 2 + apps/mobile/src/i18n/locales/uz.json | 2 + apps/mobile/src/i18n/locales/vi.json | 2 + apps/mobile/src/i18n/locales/yo.json | 2 + apps/mobile/src/i18n/locales/zh-Hans.json | 2 + apps/mobile/src/i18n/locales/zh-Hant.json | 2 + apps/mobile/src/i18n/locales/zu.json | 2 + 101 files changed, 740 insertions(+), 168 deletions(-) create mode 100644 apps/mobile/plugins/withAndroidWidgetLocalizations.js create mode 100644 apps/mobile/src/glanceable-android/count-format.ts diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 999edf5b20..00b5aa53ef 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -302,6 +302,17 @@ const config: ExpoConfig = { ], // Local Expo module for Android Live Updates (no-op until slice `and`). './plugins/withActiveAgentsLiveUpdate', + // Translates the Android widget-picker entry, which the widget library + // leaves English-only. Registered BEFORE the widget plugin for the same + // reason as the iOS pair above: mods run in reverse registration order. + [ + './plugins/withAndroidWidgetLocalizations', + { + widgetName: 'ActiveAgentsWidget', + languages: [...SUPPORTED_LANGUAGES], + copy: WIDGET_GALLERY_COPY, + }, + ], // No-op until slice `and` writes src/glanceable-android/widget-config.json. './plugins/withActiveAgentsAndroidWidget', // Registered only when GOOGLE_IOS_CLIENT_ID is set — a guard for checkouts diff --git a/apps/mobile/plugins/withActiveAgentsAndroidWidget.js b/apps/mobile/plugins/withActiveAgentsAndroidWidget.js index f44353d81a..8b33f71154 100644 --- a/apps/mobile/plugins/withActiveAgentsAndroidWidget.js +++ b/apps/mobile/plugins/withActiveAgentsAndroidWidget.js @@ -1,10 +1,19 @@ const fs = require('fs'); const path = require('path'); +const GALLERY_COPY = require('./widget-gallery-copy.json'); + // Wraps react-native-android-widget so its config plugin only applies once the // Android widget actually exists. The widget config file is created by slice // `and` (level 3) at apps/mobile/src/glanceable-android/widget-config.json; // before then this plugin is a no-op, so level 2 prebuilds are unaffected. +// +// The gallery label and description come from widget-gallery-copy.json, the +// same file the iOS gallery reads, so the two pickers never drift. The label is +// passed as a resource reference because the library writes it straight into +// the receiver; withAndroidWidgetLocalizations creates that resource and its 86 +// translations. The description is passed as text because the library already +// wraps it in a string resource of its own. const WIDGET_CONFIG_PATH = path.resolve(__dirname, '../src/glanceable-android/widget-config.json'); function loadAndroidWidgetsPlugin() { @@ -23,5 +32,10 @@ module.exports = function withActiveAgentsAndroidWidget(config) { if (widgets.length === 0) { return config; } - return loadAndroidWidgetsPlugin()(config, { widgets }); + const described = widgets.map(widget => ({ + ...widget, + label: `@string/widget_${widget.name.toLowerCase()}_label`, + description: GALLERY_COPY.en.description, + })); + return loadAndroidWidgetsPlugin()(config, { widgets: described }); }; diff --git a/apps/mobile/plugins/withAndroidWidgetLocalizations.js b/apps/mobile/plugins/withAndroidWidgetLocalizations.js new file mode 100644 index 0000000000..12c15a9518 --- /dev/null +++ b/apps/mobile/plugins/withAndroidWidgetLocalizations.js @@ -0,0 +1,118 @@ +const fs = require('fs'); +const path = require('path'); + +const { withDangerousMod, withStringsXml } = require('expo/config-plugins'); + +// Localizes the Android widget-picker entry. +// +// react-native-android-widget writes one `android:label` straight into the +// receiver and wraps the description in a single `values/strings.xml` entry, so +// the picker stays English on every device. Android resolves both through the +// resource system, which means the only thing missing is a `values-` +// folder per language holding the same two keys. +// +// The copy is bundle metadata, not app copy, so it lives in +// `widget-gallery-copy.json` beside this file — the same file the iOS gallery +// reads — and never goes through i18next. +// +// This must be registered BEFORE './plugins/withActiveAgentsAndroidWidget': +// mods run in reverse registration order, so the earlier entry runs last and +// sees the resources that plugin has already written. + +/** The library derives both resource names from the widget name, lowercased. */ +const stringName = (widgetName, suffix) => `widget_${widgetName.toLowerCase()}_${suffix}`; + +/** + * Android resource qualifier for a BCP 47 tag. + * + * The `b+` form is the only one that carries a script (`zh-Hans`), and it has + * been supported since API 24 — below the app's minimum. The legacy `-r` form + * cannot express a script at all, so everything uses `b+` for one rule. + */ +const localeQualifier = tag => `b+${tag.replace(/-/g, '+')}`; + +/** + * Escape one string resource value. + * + * `&` and `<` are XML; the apostrophe, the quote and the backslash are Android's + * own string escapes; a leading `@` or `?` would otherwise read as a resource + * reference. + */ +const escapeValue = value => + value + .replace(/&/g, '&') + .replace(/ + [ + '', + '', + ...entries.map(([name, value]) => ` ${escapeValue(value)}`), + '', + '', + ].join('\n'); + +module.exports = function withAndroidWidgetLocalizations(config, options) { + const languages = options?.languages ?? []; + const copy = options?.copy ?? {}; + const widgetName = options?.widgetName; + if (!widgetName || languages.length === 0) { + throw new Error('withAndroidWidgetLocalizations requires `widgetName` and `languages`.'); + } + const labelName = stringName(widgetName, 'label'); + const descriptionName = stringName(widgetName, 'description'); + + // The default resources. The label is a plain string the receiver references + // as `@string/…` (see widget-config.json); the description already exists, + // written by the library, so only the label is added here. + const withDefaults = withStringsXml(config, cfg => { + const english = copy.en; + if (!english) { + throw new Error('withAndroidWidgetLocalizations requires English gallery copy.'); + } + const resources = cfg.modResults.resources; + resources.string = resources.string ?? []; + const existing = resources.string.find(entry => entry.$?.name === labelName); + if (existing) { + existing._ = english.displayName; + } else { + resources.string.push({ $: { name: labelName }, _: english.displayName }); + } + return cfg; + }); + + return withDangerousMod(withDefaults, [ + 'android', + async cfg => { + const resPath = path.join( + cfg.modRequest.platformProjectRoot, + 'app', + 'src', + 'main', + 'res' + ); + if (!fs.existsSync(resPath)) { + throw new Error(`withAndroidWidgetLocalizations: no res directory at ${resPath}`); + } + for (const tag of languages) { + const translated = copy[tag]; + if (tag === 'en' || !translated) { + continue; + } + const folder = path.join(resPath, `values-${localeQualifier(tag)}`); + fs.mkdirSync(folder, { recursive: true }); + fs.writeFileSync( + path.join(folder, 'kilo_widget_strings.xml'), + stringsXml([ + [labelName, translated.displayName], + [descriptionName, translated.description], + ]), + 'utf8' + ); + } + return cfg; + }, + ]); +}; diff --git a/apps/mobile/src/components/notifications-screen.tsx b/apps/mobile/src/components/notifications-screen.tsx index 1e4218c788..7a53d3c998 100644 --- a/apps/mobile/src/components/notifications-screen.tsx +++ b/apps/mobile/src/components/notifications-screen.tsx @@ -66,6 +66,18 @@ import { readTrpcErrorField } from '@/lib/trpc-error'; import { cn } from '@/lib/utils'; const permissionQueryKey = ['notificationPermission'] as const; + +/** + * The glanceable row's subtitle: what it promises while the OS allows it, and + * what to do about it when the OS does not. Each platform names its own surface + * and its own setting. + */ +function glanceableSubtitleKey(allowed: boolean, isIos: boolean): string { + if (!allowed) { + return isIos ? 'glanceable.activityKitDisabledBody' : 'notifications.disabledMessage'; + } + return isIos ? 'notifications.liveActivitySubtitle' : 'notifications.liveUpdateSubtitle'; +} const deviceTokenQueryKey = ['devicePushToken'] as const; /** @@ -324,9 +336,15 @@ export function NotificationsScreen() { const [systemAllowsLiveActivities, setSystemAllowsLiveActivities] = useState( liveActivitiesAllowedBySystem ); - // Off in Settings, or the stored preference has not been read yet: either way - // the switch must not accept a change it cannot honor. - const liveActivityRowDisabled = !liveActivityLoaded || !systemAllowsLiveActivities; + const isIos = Platform.OS === 'ios'; + // The OS switch that governs the glanceable surface: ActivityKit's own on + // iOS, the notification permission on Android, which is what a Live Update + // posts through. + const systemAllowsGlanceable = isIos ? systemAllowsLiveActivities : permissionGranted; + // Off in Settings, or a state the screen has not read yet: either way the + // switch must not accept a change it cannot honor. + const liveActivityRowDisabled = + !liveActivityLoaded || !systemAllowsGlanceable || (!isIos && permissionLoading); // Re-check permission on foreground resume const { isActive } = useAppLifecycle(); @@ -559,57 +577,56 @@ export function NotificationsScreen() { contentContainerClassName="px-6 gap-6 pt-4" showsVerticalScrollIndicator={false} > - {/* Live Activity. First on the screen because it is the surface the + {/* The glanceable surface. First on the screen because it is what the user sees without opening the app, and it must not sit below seven - category rows. iOS only: ActivityKit has no Android counterpart. */} - {Platform.OS === 'ios' && ( - - - {t('notifications.liveActivities')} - - - - - {/* Disabled cue is the muted title, not row opacity — the same - pattern as CategoryRow below. */} - - {t('glanceable.channelName')} - - - {systemAllowsLiveActivities - ? t('notifications.liveActivitySubtitle') - : t('glanceable.activityKitDisabledBody')} - - - - - {/* Our switch cannot turn ActivityKit's back on, so the row offers - the only thing that can instead of pretending otherwise. */} - {!systemAllowsLiveActivities && ( - void Linking.openSettings()} - accessibilityRole="button" - accessibilityLabel={t('common.openSettings')} - className="items-center rounded-lg bg-primary py-2.5 active:opacity-80" + category rows. Each platform names it the way its own OS does: + a Live Activity on iOS, a Live Update on Android. */} + + + {isIos ? t('notifications.liveActivities') : t('notifications.liveUpdates')} + + + + + {/* Disabled cue is the muted title, not row opacity — the same + pattern as CategoryRow below. */} + - - {t('common.openSettings')} - - - )} + {t('glanceable.channelName')} + + + {t(glanceableSubtitleKey(systemAllowsGlanceable, isIos))} + + + - )} + {/* Our switch cannot turn ActivityKit's back on, so the row offers + the only thing that can instead of pretending otherwise. Android + needs no button here: the master gate below already enables the + same permission. */} + {isIos && !systemAllowsLiveActivities && ( + void Linking.openSettings()} + accessibilityRole="button" + accessibilityLabel={t('common.openSettings')} + className="items-center rounded-lg bg-primary py-2.5 active:opacity-80" + > + + {t('common.openSettings')} + + + )} + {/* Master gate */} diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts index 10c8419498..d83d40ed2a 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts +++ b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts @@ -4,6 +4,8 @@ import { } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { describe, expect, it, vi } from 'vitest'; +import { darkColors, lightColors } from '@/lib/hooks/theme-colors.generated'; + import { renderActiveAgentsWidget } from './active-agents-widget'; import { buildAndroidWidgetProps } from './widget-props'; @@ -12,6 +14,7 @@ import { buildAndroidWidgetProps } from './widget-props'; vi.mock('react-native-android-widget', () => ({ FlexWidget: (props: Record) => ({ kind: 'FlexWidget', props }), TextWidget: (props: Record) => ({ kind: 'TextWidget', props }), + ImageWidget: (props: Record) => ({ kind: 'ImageWidget', props }), requestWidgetUpdate: () => undefined, })); @@ -97,8 +100,10 @@ describe('renderActiveAgentsWidget', () => { expect(rep.light).toBeDefined(); expect(rep.dark).toBeDefined(); expect(rep.light).not.toBe(rep.dark); - expect(rep.light.props.style?.backgroundColor).toBe('#FFFFFF'); - expect(rep.dark.props.style?.backgroundColor).toBe('#0B0F19'); + // The app's own palette, not a widget-local one: a card that does not match + // the app it opens reads as a different product. + expect(rep.light.props.style?.backgroundColor).toBe(lightColors.background); + expect(rep.dark.props.style?.backgroundColor).toBe(darkColors.background); }); it('shows only the primary count at a small width', () => { @@ -110,7 +115,7 @@ describe('renderActiveAgentsWidget', () => { const rep = render(props, 120); const text = collectText(rep.light); - expect(text).toEqual(['1 Needs input']); + expect(text).toEqual(['1', 'Needs input']); }); it('shows every count, zeros included, and the Open agents affordance at a wide width', () => { @@ -123,14 +128,23 @@ describe('renderActiveAgentsWidget', () => { const text = collectText(rep.light); // The zero row draws so the rows hold still as work moves between states. - expect(text).toEqual(['1 Needs input', '1 Working', '0 Idle', 'Open agents']); + expect(text).toEqual(['1', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']); }); it.each([ - { width: 120, visibleText: ['2 Needs input'] }, + { width: 120, visibleText: ['2', 'Needs input'] }, { width: 250, - visibleText: ['2 Needs input', '4 Working', '3 Idle', 'Updates delayed', 'Open agents'], + visibleText: [ + '2', + 'Needs input', + '4', + 'Working', + '3', + 'Idle', + 'Updates delayed', + 'Open agents', + ], }, ])( 'speaks stale numeric counts and keeps the deep link at width $width', diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.tsx b/apps/mobile/src/glanceable-android/active-agents-widget.tsx index 5de7dd7766..6e815f841f 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.tsx +++ b/apps/mobile/src/glanceable-android/active-agents-widget.tsx @@ -2,33 +2,57 @@ 'use no memo'; +// Metro turns a static image import into the asset id the widget host resolves, +// the same value `require` would give. Imported rather than required so vitest +// can stand in for the binary. +import LOGO from '../../assets/images/logo-widget.png'; import { FlexWidget, type HexColor, + ImageWidget, TextWidget, type WidgetInfo, type WidgetRepresentation, } from 'react-native-android-widget'; +import { type GlanceableCountKind } from '@/lib/glanceable/presentation'; +import { darkColors, lightColors } from '@/lib/hooks/theme-colors.generated'; + import { type AndroidWidgetProps } from './widget-props'; export const WIDGET_NAME = 'ActiveAgentsWidget'; -/** Below this width (dp) the widget shows only the primary count. */ -const COMPACT_MAX_WIDTH_DP = 150; +/** Below this width (dp) only the primary count fits beside the mark. */ +const COMPACT_MAX_WIDTH_DP = 170; +/** Below this height (dp) the three rows cannot stack, so they run in a row. */ +const ROW_MAX_HEIGHT_DP = 90; + -type Palette = { background: HexColor; primary: HexColor; muted: HexColor }; +type Palette = { + background: HexColor; + foreground: HexColor; + muted: HexColor; + /** Three states, three colors — the same vocabulary the iOS surfaces draw. */ + needsInput: HexColor; + running: HexColor; +}; +// The app's own palette, not a widget-local one: a Home Screen card that does +// not match the app it opens reads as a different product. const LIGHT: Palette = { - background: '#FFFFFF', - primary: '#111827', - muted: '#6B7280', + background: lightColors.background, + foreground: lightColors.foreground, + muted: lightColors.mutedForeground, + needsInput: lightColors.warn, + running: lightColors.good, }; const DARK: Palette = { - background: '#0B0F19', - primary: '#F9FAFB', - muted: '#9CA3AF', + background: darkColors.background, + foreground: darkColors.foreground, + muted: darkColors.mutedForeground, + needsInput: darkColors.warn, + running: darkColors.good, }; // This function is evaluated only through `renderActiveAgentsWidget` and the @@ -37,46 +61,184 @@ const DARK: Palette = { // the source. Translated copy arrives through `props`; the English fallbacks // below only render while the gallery placeholder has no snapshot props. -function isCompact(info: WidgetInfo): boolean { - return info.width < COMPACT_MAX_WIDTH_DP; +type Size = 'compact' | 'row' | 'stack'; + +function sizeOf(info: WidgetInfo): Size { + if (info.width < COMPACT_MAX_WIDTH_DP) { + return 'compact'; + } + return info.height < ROW_MAX_HEIGHT_DP ? 'row' : 'stack'; } -function compactText(props: AndroidWidgetProps): string { - if (props.primaryLabel === null) { - return props.statusLine ?? ''; +function dotColor(kind: GlanceableCountKind, palette: Palette): HexColor { + if (kind === 'needsInput') { + return palette.needsInput; } - return `${props.primaryCount} ${props.primaryLabel}`; + return kind === 'running' ? palette.running : palette.foreground; } -function countRows(props: AndroidWidgetProps, color: HexColor) { - return props.countLines.map(line => ( - - )); + ); } -/** Compact widths show the primary count; wider cells show every non-zero count. */ -function renderPrimaryArea(props: AndroidWidgetProps, palette: Palette, compact: boolean) { - if (compact) { - return ( +function logo(size: number) { + return ; +} + +/** + * One count line: marker, count, label. Only the label color ranks the rows, + * because a second font size in a three-row list reads as a mistake. + */ +// eslint-disable-next-line max-params -- one line, its rank, and the two style inputs +function countRow( + line: AndroidWidgetProps['countLines'][number], + isPrimary: boolean, + palette: Palette, + fontSize: number +) { + return ( + + {stateDot(line.kind, palette, fontSize < 14 ? 9 : 10)} + + + ); +} + +/** Narrow cells: the mark, the ranked marker, and the one count worth a glance. */ +function renderCompact(props: AndroidWidgetProps, palette: Palette) { + if (props.primaryKind === null) { + return ( + ); } + return countRow( + { + label: props.primaryLabel ?? '', + kind: props.primaryKind, + count: props.primaryCount, + }, + true, + palette, + 15 + ); +} + +function statusText(props: AndroidWidgetProps, palette: Palette) { + return ( + + ); +} + +function renderCounts(props: AndroidWidgetProps, palette: Palette, size: Size) { if (props.countLines.length === 0) { - return null; + return statusText(props, palette); + } + const primaryLabel = props.primaryLabel; + const rows = props.countLines.map(line => + countRow(line, line.label === primaryLabel, palette, size === 'row' ? 13 : 15) + ); + const stacked = ( + + {rows} + + ); + // Stale carries counts and a warning at once. Only the tall cell has a line + // to spare for it; the short row would have to drop a count to fit it. + if (size !== 'stack' || props.statusLine === null) { + return stacked; } - return countRows(props, palette.primary); + return ( + + {stacked} + {statusText(props, palette)} + + ); } -function renderSurface(props: AndroidWidgetProps, palette: Palette, compact: boolean) { +function renderSurface(props: AndroidWidgetProps, palette: Palette, size: Size) { + const body = + size === 'compact' ? renderCompact(props, palette) : renderCounts(props, palette, size); + // Short cells put the mark beside the counts; a tall cell stacks the mark on + // top and lets the counts sit at the bottom, the same composition as the iOS + // small family. + if (size === 'stack') { + return ( + + {logo(26)} + {body} + {props.showOpenAgents ? ( + + ) : ( + + )} + + ); + } return ( - - {renderPrimaryArea(props, palette, compact)} - {!compact && props.statusLine !== null ? ( - - ) : null} - - {!compact && props.showOpenAgents ? ( - - ) : null} + {logo(size === 'compact' ? 22 : 28)} + {body} ); } /** * Distinct light and dark layouts through the library's theme callback. Narrow - * widths show only the primary count; wider cells show every non-zero count. + * cells show only the ranked count; short cells run the three states in a row; + * a tall cell stacks them under the mark. */ export function renderActiveAgentsWidget( props: AndroidWidgetProps, info: WidgetInfo ): WidgetRepresentation { - const compact = isCompact(info); + const size = sizeOf(info); return { - light: renderSurface(props, LIGHT, compact), - dark: renderSurface(props, DARK, compact), + light: renderSurface(props, LIGHT, size), + dark: renderSurface(props, DARK, size), }; } diff --git a/apps/mobile/src/glanceable-android/android-sink.test.ts b/apps/mobile/src/glanceable-android/android-sink.test.ts index c16d3ba08d..f29cfccd4c 100644 --- a/apps/mobile/src/glanceable-android/android-sink.test.ts +++ b/apps/mobile/src/glanceable-android/android-sink.test.ts @@ -83,6 +83,7 @@ vi.mock('react-native', () => ({ vi.mock('react-native-android-widget', () => ({ FlexWidget: () => null, TextWidget: () => null, + ImageWidget: () => null, requestWidgetUpdate: (...args: unknown[]) => mocks.requestWidgetUpdate(...args), })); @@ -399,7 +400,7 @@ describe('androidSink widget publish and end', () => { expect.objectContaining({ widgetName: 'ActiveAgentsWidget' }) ); expect(getCurrentWidgetProps()?.statusLine).toBeNull(); - expect(getCurrentWidgetProps()?.primaryCount).toBe(1); + expect(getCurrentWidgetProps()?.primaryCount).toBe('1'); }); it('publishes the stale warning and retained counts through the native bridge', async () => { @@ -457,7 +458,7 @@ describe('androidSink widget publish and end', () => { androidSink.endImmediate(); expect(mocks.native.end).toHaveBeenCalledTimes(1); expect(getCurrentWidgetProps()).not.toBeNull(); - expect(getCurrentWidgetProps()?.primaryCount).toBe(1); + expect(getCurrentWidgetProps()?.primaryCount).toBe('1'); }); it('ends the ongoing notification without removing widget delivery', async () => { @@ -480,11 +481,11 @@ describe('androidSink widget publish and end', () => { expect(vi.getTimerCount()).toBe(0); vi.setSystemTime(NOW + 28_799_999); - expect(getCurrentWidgetProps()?.primaryCount).toBe(1); + expect(getCurrentWidgetProps()?.primaryCount).toBe('1'); vi.setSystemTime(NOW + 28_800_000); expect(getCurrentWidgetProps()?.statusLine).toBe('Status expired'); expect(getCurrentWidgetProps()?.countLines).toEqual([]); - expect(getCurrentWidgetProps()?.primaryCount).toBe(0); + expect(getCurrentWidgetProps()?.primaryCount).toBe('0'); expect(getCurrentWidgetProps()?.showOpenAgents).toBe(false); } ); @@ -502,7 +503,7 @@ describe('androidSink widget publish and end', () => { androidSink.endImmediate(); expect(mocks.getWidgetDeadline()).toBe(NOW + 28_860_000); vi.setSystemTime(NOW + 28_800_000); - expect(getCurrentWidgetProps()?.primaryCount).toBe(1); + expect(getCurrentWidgetProps()?.primaryCount).toBe('1'); expect(mocks.getNotification()).toBeNull(); }); @@ -511,7 +512,7 @@ describe('androidSink widget publish and end', () => { vi.setSystemTime(NOW + 60_000); androidSink.publish({ ...MIXED, status: 'stale', revision: 2 }); expect(mocks.getWidgetDeadline()).toBe(NOW + 28_800_000); - expect(getCurrentWidgetProps()?.primaryCount).toBe(2); + expect(getCurrentWidgetProps()?.primaryCount).toBe('2'); vi.setSystemTime(NOW + 28_800_000); expect(getCurrentWidgetProps()?.countLines).toEqual([]); }); diff --git a/apps/mobile/src/glanceable-android/android-sink.ts b/apps/mobile/src/glanceable-android/android-sink.ts index ab86e8e66a..ca898ea2e2 100644 --- a/apps/mobile/src/glanceable-android/android-sink.ts +++ b/apps/mobile/src/glanceable-android/android-sink.ts @@ -6,6 +6,7 @@ import { import { requestWidgetUpdate } from 'react-native-android-widget'; import { i18n } from '@/i18n'; +import { getLiveActivityEnabled } from '@/lib/glanceable/live-activity-switch'; import { getGlanceableDelivery, type GlanceableSink, @@ -13,6 +14,7 @@ import { } from '@/lib/glanceable/sink-registry'; import { renderActiveAgentsWidget, WIDGET_NAME } from './active-agents-widget'; +import { formatGlanceableCount } from './count-format'; import { end as endLiveUpdate, setWidgetSnapshot, @@ -51,7 +53,7 @@ let terminalExpiresAt: number | null = null; export function getCurrentWidgetProps(): AndroidWidgetProps | null { return lastWidgetSnapshot === null ? null - : buildCurrentWidgetProps(lastWidgetSnapshot, translate); + : buildCurrentWidgetProps(lastWidgetSnapshot, translate, formatGlanceableCount); } function renderWidgetNow(props: AndroidWidgetProps): void { @@ -86,7 +88,10 @@ async function tryStartOrUpdate( snapshot: GlanceableAgentsSnapshot, ctx: GlanceableSinkContext ): Promise { - if (!hasCurrentWork(snapshot)) { + // The in-app switch is checked first: it is the one the user set here, and + // honoring it costs no native call. The notification permission still decides + // the rest. The widget is deliberately not gated — placing one is the opt-in. + if (!getLiveActivityEnabled() || !hasCurrentWork(snapshot)) { pending = null; return; } @@ -94,9 +99,9 @@ async function tryStartOrUpdate( return; } const title = translate(NOTIFICATION_TITLE_KEY); - const text = buildOngoingNotificationText(snapshot, {}, translate); + const text = buildOngoingNotificationText(snapshot, {}, translate, formatGlanceableCount); const openAgentsLabel = translate(OPEN_AGENTS_LABEL_KEY); - const compactText = buildCompactNotificationText(snapshot, {}); + const compactText = buildCompactNotificationText(snapshot, {}, formatGlanceableCount); if (notificationActive) { updateLiveUpdate(title, text, openAgentsLabel, compactText); @@ -134,15 +139,15 @@ async function tryStartOrUpdate( /** Retry a pending start after permission turns granted. Caller owns the check. */ function retryPendingStart(): void { const p = pending; - if (p === null || notificationActive || !hasCurrentWork(p.snapshot)) { + if (p === null || notificationActive || !getLiveActivityEnabled() || !hasCurrentWork(p.snapshot)) { return; } const title = translate(NOTIFICATION_TITLE_KEY); startLiveUpdate( title, - buildOngoingNotificationText(p.snapshot, {}, translate), + buildOngoingNotificationText(p.snapshot, {}, translate, formatGlanceableCount), translate(OPEN_AGENTS_LABEL_KEY), - buildCompactNotificationText(p.snapshot, {}) + buildCompactNotificationText(p.snapshot, {}, formatGlanceableCount) ); notificationActive = true; terminalExpiresAt = null; @@ -171,7 +176,7 @@ export const androidSink: GlanceableSink = { publish(snapshot) { lastWidgetSnapshot = snapshot; setWidgetSnapshot(snapshot); - const props = buildCurrentWidgetProps(snapshot, translate); + const props = buildCurrentWidgetProps(snapshot, translate, formatGlanceableCount); renderWidgetNow(props); const eligible = hasCurrentWork(snapshot); if (eligible) { @@ -198,10 +203,10 @@ export const androidSink: GlanceableSink = { updateLiveUpdate( translate(NOTIFICATION_TITLE_KEY), eligible - ? buildOngoingNotificationText(snapshot, {}, translate) + ? buildOngoingNotificationText(snapshot, {}, translate, formatGlanceableCount) : (props.statusLine ?? translate('glanceable.empty')), translate(OPEN_AGENTS_LABEL_KEY), - eligible ? buildCompactNotificationText(snapshot, {}) : null, + eligible ? buildCompactNotificationText(snapshot, {}, formatGlanceableCount) : null, terminalExpiresAt === null ? 0 : Math.max(1, terminalExpiresAt - Date.now()) ); revision = snapshot.revision; diff --git a/apps/mobile/src/glanceable-android/count-format.ts b/apps/mobile/src/glanceable-android/count-format.ts new file mode 100644 index 0000000000..a17db393fa --- /dev/null +++ b/apps/mobile/src/glanceable-android/count-format.ts @@ -0,0 +1,14 @@ +import { i18n } from '@/i18n'; +import { numberFormat } from '@/lib/intl-cache'; + +/** + * Draw a count in the active language's own digits. + * + * Unlike the iOS widget extension, the Android surfaces render in the app's own + * JS runtime, so `Intl` is already there and no digit table has to be baked into + * a layout. Grouping is off: these counts never reach four figures, and a + * separator in a two-character number is only noise. + */ +export function formatGlanceableCount(value: number): string { + return numberFormat(i18n.language, { useGrouping: false }).format(value); +} diff --git a/apps/mobile/src/glanceable-android/register.test.ts b/apps/mobile/src/glanceable-android/register.test.ts index 8014b13140..282af87de1 100644 --- a/apps/mobile/src/glanceable-android/register.test.ts +++ b/apps/mobile/src/glanceable-android/register.test.ts @@ -38,6 +38,7 @@ vi.mock('react-native-android-widget', () => ({ requestWidgetUpdate: vi.fn().mockResolvedValue(undefined), FlexWidget: () => null, TextWidget: () => null, + ImageWidget: () => null, })); const NOW = 1_750_000_000_000; @@ -143,7 +144,9 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { const handler = await registerAfterRestart(snapshotFor()); const rendered = await runWidgetTask(handler, width); const expected = - width === 120 ? ['2 Needs input'] : ['2 Needs input', '2 Working', '0 Idle', 'Open agents']; + width === 120 + ? ['2', 'Needs input'] + : ['2', 'Needs input', '2', 'Working', '0', 'Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -210,7 +213,9 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { const rendered = await runWidgetTask(handler, width); const expected = - width === 120 ? ['1 Working'] : ['0 Needs input', '1 Working', '0 Idle', 'Open agents']; + width === 120 + ? ['1', 'Working'] + : ['0', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -233,7 +238,9 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { const rendered = await runWidgetTask(handler, width); const expected = - width === 120 ? ['1 Working'] : ['0 Needs input', '1 Working', '0 Idle', 'Open agents']; + width === 120 + ? ['1', 'Working'] + : ['0', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); }); @@ -263,8 +270,8 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { const handler = await registerAfterRestart(null); const current = await runWidgetTask(handler, width); - expect(collectText(current.light)).toContain('2 Needs input'); - expect(collectText(current.dark)).toContain('2 Needs input'); + expect(collectText(current.light)).toEqual(expect.arrayContaining(['2', 'Needs input'])); + expect(collectText(current.dark)).toEqual(expect.arrayContaining(['2', 'Needs input'])); expect(mocks.getDeadline()).toBe(expiresAt); vi.setSystemTime(expiresAt); @@ -312,7 +319,9 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { read.resolve(JSON.stringify(stored)); const rendered = await rendering; const expected = - width === 120 ? ['1 Working'] : ['0 Needs input', '1 Working', '0 Idle', 'Open agents']; + width === 120 + ? ['1', 'Working'] + : ['0', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); diff --git a/apps/mobile/src/glanceable-android/register.ts b/apps/mobile/src/glanceable-android/register.ts index 812fb43474..2c74b810df 100644 --- a/apps/mobile/src/glanceable-android/register.ts +++ b/apps/mobile/src/glanceable-android/register.ts @@ -5,11 +5,16 @@ import { } from 'react-native-android-widget'; import { i18n } from '@/i18n'; +import { + getLiveActivityEnabled, + subscribeLiveActivityEnabled, +} from '@/lib/glanceable/live-activity-switch'; import { getLastGlanceableSnapshot, restorePersistedGlanceable } from '@/lib/glanceable/persist'; import { registerGlanceableSink } from '@/lib/glanceable/sink-registry'; import { renderActiveAgentsWidget } from './active-agents-widget'; import { androidSink, getCurrentWidgetProps, handleAppStateActive } from './android-sink'; +import { formatGlanceableCount } from './count-format'; import { getStoredWidgetSnapshot, setWidgetSnapshot } from './live-update'; import { buildCurrentWidgetProps, buildGenericWidgetProps } from './widget-props'; @@ -26,6 +31,18 @@ AppState.addEventListener('change', state => { } }); +// Turning the in-app switch off must clear the Live Update already in the +// shade, not just stop the next start. `startOrUpdate` holds the guard for +// everything after this. +let liveUpdateAllowed = getLiveActivityEnabled(); +subscribeLiveActivityEnabled(() => { + const next = getLiveActivityEnabled(); + if (liveUpdateAllowed && !next) { + androidSink.endImmediate(); + } + liveUpdateAllowed = next; +}); + function translate(key: string): string { return i18n.t(key); } @@ -37,7 +54,7 @@ registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { // queued this task when newer work or a privacy blank replaces its deadline. const stored = getStoredWidgetSnapshot(); let props = - stored === null ? getCurrentWidgetProps() : buildCurrentWidgetProps(stored, translate); + stored === null ? getCurrentWidgetProps() : buildCurrentWidgetProps(stored, translate, formatGlanceableCount); if (props === null) { // Migrate the existing mirror when this installation has no native snapshot yet. await restorePersistedGlanceable(); @@ -47,8 +64,8 @@ registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { } props = snapshot === null - ? buildGenericWidgetProps(translate) - : buildCurrentWidgetProps(snapshot, translate); + ? buildGenericWidgetProps(translate, formatGlanceableCount) + : buildCurrentWidgetProps(snapshot, translate, formatGlanceableCount); // A live publish during restoration owns the widget. props = getCurrentWidgetProps() ?? props; } diff --git a/apps/mobile/src/glanceable-android/widget-config.json b/apps/mobile/src/glanceable-android/widget-config.json index 39be56193c..0fe7bb4fa3 100644 --- a/apps/mobile/src/glanceable-android/widget-config.json +++ b/apps/mobile/src/glanceable-android/widget-config.json @@ -2,14 +2,12 @@ "widgets": [ { "name": "ActiveAgentsWidget", - "label": "Active agents", - "description": "Shows your active agents at a glance.", "minWidth": "110dp", "minHeight": "40dp", - "targetCellWidth": 2, - "targetCellHeight": 1, + "targetCellWidth": 4, + "targetCellHeight": 2, "maxResizeWidth": "360dp", - "maxResizeHeight": "120dp", + "maxResizeHeight": "220dp", "resizeMode": "horizontal|vertical" } ] diff --git a/apps/mobile/src/glanceable-android/widget-props.test.ts b/apps/mobile/src/glanceable-android/widget-props.test.ts index 8e523b273c..48687d05d8 100644 --- a/apps/mobile/src/glanceable-android/widget-props.test.ts +++ b/apps/mobile/src/glanceable-android/widget-props.test.ts @@ -53,11 +53,12 @@ describe('buildAndroidWidgetProps', () => { it('ranks the compact primary count and keeps all expanded numeric counts', () => { const props = buildAndroidWidgetProps(MIXED, {}, translate); expect(props.primaryLabel).toBe('Needs input'); - expect(props.primaryCount).toBe(2); + expect(props.primaryCount).toBe('2'); + expect(props.primaryKind).toBe('needsInput'); expect(props.countLines).toEqual([ - { label: 'Needs input', count: 2 }, - { label: 'Working', count: 4 }, - { label: 'Idle', count: 3 }, + { label: 'Needs input', kind: 'needsInput', count: '2' }, + { label: 'Working', kind: 'running', count: '4' }, + { label: 'Idle', kind: 'idle', count: '3' }, ]); }); @@ -113,6 +114,7 @@ describe('buildAndroidWidgetProps', () => { 'countLines', 'openAgentsLabel', 'primaryCount', + 'primaryKind', 'primaryLabel', 'showOpenAgents', 'statusLine', @@ -217,7 +219,7 @@ describe('status precedence and count hiding', () => { expect(props.statusLine).toBe(expected); expect(props.countLines).toEqual([]); expect(props.primaryLabel).toBeNull(); - expect(props.primaryCount).toBe(0); + expect(props.primaryCount).toBe('0'); expect(props.showOpenAgents).toBe(false); expect(buildOngoingNotificationText(snapshot, {}, translate)).toBe(expected); expect(buildCompactNotificationText(snapshot, {})).toBeNull(); @@ -232,7 +234,7 @@ describe('status precedence and count hiding', () => { expect(props.statusLine).toBe(expected); expect(props.countLines).toEqual([]); expect(props.primaryLabel).toBeNull(); - expect(props.primaryCount).toBe(0); + expect(props.primaryCount).toBe('0'); expect(props.showOpenAgents).toBe(false); expect(buildOngoingNotificationText(snapshot, flags, translate)).toBe(expected); expect(buildCompactNotificationText(snapshot, flags)).toBeNull(); diff --git a/apps/mobile/src/glanceable-android/widget-props.ts b/apps/mobile/src/glanceable-android/widget-props.ts index 2bfe423954..4547a9c4a5 100644 --- a/apps/mobile/src/glanceable-android/widget-props.ts +++ b/apps/mobile/src/glanceable-android/widget-props.ts @@ -1,6 +1,7 @@ import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { + type GlanceableCountKind, glanceableCountLines, glanceableSpokenLabel, glanceableStatusCopyKey, @@ -9,8 +10,18 @@ import { resolveGlanceableStatus, } from '@/lib/glanceable/presentation'; -/** One translated count line for an Android surface. */ -type AndroidWidgetCount = { label: string; count: number }; +/** One translated count line for an Android surface. `kind` picks dot and color. */ +type AndroidWidgetCount = { label: string; kind: GlanceableCountKind; count: string }; + +/** + * Format a count in the active language's own digits. + * + * The default writes them the way `String` does, which is what the 80 languages + * with Latin default digits need; the app injects an `Intl` formatter so fa, ps, + * ckb, my, ne, bn, and mr read in their own numerals. Injected rather than + * imported so this module stays free of i18n and of React Native. + */ +export type GlanceableCountFormat = (value: number) => string; /** * The props the Android widget renders. The builder below is the only producer, @@ -24,8 +35,10 @@ export type AndroidWidgetProps = { countLines: AndroidWidgetCount[]; /** Top-ranked count label for compact widths; null when no eligible work. */ primaryLabel: string | null; - /** Top-ranked count value for compact widths; 0 when no eligible work. */ - primaryCount: number; + /** Top-ranked count state for compact widths; null when no eligible work. */ + primaryKind: GlanceableCountKind | null; + /** Top-ranked count value for compact widths; formatted "0" when none. */ + primaryCount: string; /** Translated "Open agents" affordance. */ openAgentsLabel: string; /** True for happy and stale — the only statuses that show counts. */ @@ -35,10 +48,12 @@ export type AndroidWidgetProps = { }; /** Build the Android widget props from a snapshot, surface flags, and a translator. */ +// eslint-disable-next-line max-params -- snapshot, flags, and the two injected formatters export function buildAndroidWidgetProps( snapshot: GlanceableAgentsSnapshot, flags: GlanceableSurfaceFlags, - translate: (key: string) => string + translate: (key: string) => string, + formatCount: GlanceableCountFormat = String ): AndroidWidgetProps { const status = resolveGlanceableStatus(snapshot, flags); const statusKey = glanceableStatusCopyKey(snapshot, flags); @@ -49,10 +64,12 @@ export function buildAndroidWidgetProps( statusLine: statusKey === null ? null : translate(statusKey), countLines: (showCounts ? glanceableCountLines(snapshot) : []).map(line => ({ label: translate(line.key), - count: line.count, + kind: line.kind, + count: formatCount(line.count), })), primaryLabel: primary === null ? null : translate(primary.key), - primaryCount: primary === null ? 0 : primary.count, + primaryKind: primary === null ? null : primary.kind, + primaryCount: formatCount(primary === null ? 0 : primary.count), openAgentsLabel: translate('glanceable.openAgents'), showOpenAgents: showCounts, accessibilityLabel: glanceableSpokenLabel(snapshot, flags, translate), @@ -62,22 +79,24 @@ export function buildAndroidWidgetProps( /** Every redraw checks the data deadline, including a task queued by an older alarm. */ export function buildCurrentWidgetProps( snapshot: GlanceableAgentsSnapshot, - translate: (key: string) => string + translate: (key: string) => string, + formatCount: GlanceableCountFormat = String ): AndroidWidgetProps { const expiresAt = Date.parse(snapshot.expiresAt); if ( (snapshot.status === 'happy' || snapshot.status === 'stale') && (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) ) { - return buildExpiredWidgetProps(snapshot, translate); + return buildExpiredWidgetProps(snapshot, translate, formatCount); } - return buildAndroidWidgetProps(snapshot, {}, translate); + return buildAndroidWidgetProps(snapshot, {}, translate, formatCount); } /** Zero-count expired props: the single future redraw hides counts at expiresAt. */ function buildExpiredWidgetProps( snapshot: GlanceableAgentsSnapshot, - translate: (key: string) => string + translate: (key: string) => string, + formatCount: GlanceableCountFormat ): AndroidWidgetProps { return buildAndroidWidgetProps( { @@ -89,18 +108,23 @@ function buildExpiredWidgetProps( needsInputSince: null, }, {}, - translate + translate, + formatCount ); } /** Gallery placeholder: empty copy and no counts, with no snapshot behind it. */ -export function buildGenericWidgetProps(translate: (key: string) => string): AndroidWidgetProps { +export function buildGenericWidgetProps( + translate: (key: string) => string, + formatCount: GlanceableCountFormat = String +): AndroidWidgetProps { const empty = translate('glanceable.empty'); return { statusLine: empty, countLines: [], primaryLabel: null, - primaryCount: 0, + primaryKind: null, + primaryCount: formatCount(0), openAgentsLabel: translate('glanceable.openAgents'), showOpenAgents: false, accessibilityLabel: empty, @@ -111,10 +135,12 @@ export function buildGenericWidgetProps(translate: (key: string) => string): And * Ongoing notification: every ranked count, with a warning when stale, otherwise * the locked status copy. Never a title, organization name, or id. */ +// eslint-disable-next-line max-params -- snapshot, flags, and the two injected formatters export function buildOngoingNotificationText( snapshot: GlanceableAgentsSnapshot, flags: GlanceableSurfaceFlags, - translate: (key: string) => string + translate: (key: string) => string, + formatCount: GlanceableCountFormat = String ): string { const status = resolveGlanceableStatus(snapshot, flags); if (status === 'happy' || status === 'stale') { @@ -122,7 +148,9 @@ export function buildOngoingNotificationText( // "0 Working" in a notification line is only noise. const lines = glanceableCountLines(snapshot).filter(line => line.count > 0); if (lines.length > 0) { - const counts = lines.map(line => `${line.count} ${translate(line.key)}`).join(', '); + const counts = lines + .map(line => `${formatCount(line.count)} ${translate(line.key)}`) + .join(', '); return status === 'stale' ? `${translate('glanceable.stale')}, ${counts}` : counts; } } @@ -132,12 +160,13 @@ export function buildOngoingNotificationText( /** The promoted chip shows only the primary number; the full text keeps all labels. */ export function buildCompactNotificationText( snapshot: GlanceableAgentsSnapshot, - flags: GlanceableSurfaceFlags + flags: GlanceableSurfaceFlags, + formatCount: GlanceableCountFormat = String ): string | null { const status = resolveGlanceableStatus(snapshot, flags); if (status !== 'happy' && status !== 'stale') { return null; } const primary = primaryGlanceableCount(snapshot); - return primary === null ? null : String(primary.count); + return primary === null ? null : formatCount(primary.count); } diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 30ec662846..de4d0caa13 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -616,6 +616,8 @@ "title": "Kennisgewings", "liveActivities": "Live-aktiwiteite", "liveActivitySubtitle": "Wys aktiewe agente op die sluitskerm", + "liveUpdates": "Lewendige opdaterings", + "liveUpdateSubtitle": "Wys aktiewe agente in jou kennisgewings", "push": "Druk", "enabled": "Kennisgewings geaktiveer", "onDescription": "Drukkennisgewings is aan vir hierdie toestel.", diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index ba1257dbff..da5179b8f7 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -616,6 +616,8 @@ "title": "ማሳወቂያዎች", "liveActivities": "የቀጥታ እንቅስቃሴዎች", "liveActivitySubtitle": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ አሳይ", + "liveUpdates": "የቀጥታ ዝማኔዎች", + "liveUpdateSubtitle": "ንቁ ወኪሎችን በማሳወቂያዎችዎ ውስጥ አሳይ", "push": "ግፋ", "enabled": "ማሳወቂያዎች ነቅተዋል", "onDescription": "የግፋ ማሳወቂያዎች ለዚህ መሣሪያ በርተዋል።", diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index 94fe2dcd6b..4a3343fb6f 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -162,6 +162,8 @@ "title": "الإشعارات", "liveActivities": "الأنشطة المباشرة", "liveActivitySubtitle": "اعرض الوكلاء النشطين على شاشة القفل", + "liveUpdates": "التحديثات المباشرة", + "liveUpdateSubtitle": "اعرض الوكلاء النشطين في إشعاراتك", "push": "فوري", "enabled": "الإشعارات ممكّنة", "onDescription": "الإشعارات الفورية قيد التشغيل لهذا الجهاز.", diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 96108f8291..dd2aabe82e 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -616,6 +616,8 @@ "title": "Bildirişlər", "liveActivities": "Canlı fəaliyyətlər", "liveActivitySubtitle": "Aktiv agentləri kilid ekranında göstərin", + "liveUpdates": "Canlı yeniləmələr", + "liveUpdateSubtitle": "Aktiv agentləri bildirişlərinizdə göstərin", "push": "Push", "enabled": "Bildirişlər aktivdir", "onDescription": "Push bildirişləri bu cihaz üçün açıqdır.", diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 896b645e8a..c0cc1539b2 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -630,6 +630,8 @@ "title": "Апавяшчэнні", "liveActivities": "Жывыя актыўнасці", "liveActivitySubtitle": "Паказваць актыўных агентаў на экране блакіроўкі", + "liveUpdates": "Абнаўленні ў рэальным часе", + "liveUpdateSubtitle": "Паказваць актыўных агентаў ва ўведамленнях", "push": "Push", "enabled": "Апавяшчэнні ўключаны", "onDescription": "Push-апавяшчэнні ўключаны для гэтай прылады.", diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 23b3ae6255..396233beac 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -616,6 +616,8 @@ "title": "Известия", "liveActivities": "Живи активности", "liveActivitySubtitle": "Показвай активните агенти на заключения екран", + "liveUpdates": "Актуализации на живо", + "liveUpdateSubtitle": "Показвай активните агенти в известията", "push": "Push", "enabled": "Известията са активирани", "onDescription": "Push известията са включени за това устройство.", diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index b15d822c5b..36181b0ca1 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -616,6 +616,8 @@ "title": "বিজ্ঞপ্তি", "liveActivities": "লাইভ অ্যাক্টিভিটি", "liveActivitySubtitle": "লক স্ক্রিনে সক্রিয় এজেন্ট দেখান", + "liveUpdates": "লাইভ আপডেট", + "liveUpdateSubtitle": "আপনার বিজ্ঞপ্তিতে সক্রিয় এজেন্ট দেখান", "push": "পুশ", "enabled": "বিজ্ঞপ্তি সক্রিয়", "onDescription": "এই ডিভাইসে পুশ বিজ্ঞপ্তি চালু আছে।", diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index a8aa07124f..e08c4be55d 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -623,6 +623,8 @@ "title": "Obavijesti", "liveActivities": "Aktivnosti uživo", "liveActivitySubtitle": "Prikaži aktivne agente na zaključanom ekranu", + "liveUpdates": "Ažuriranja uživo", + "liveUpdateSubtitle": "Prikaži aktivne agente u obavijestima", "push": "Push", "enabled": "Obavijesti omogućene", "onDescription": "Push obavijesti su uključene za ovaj uređaj.", diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index f7086ca7d7..df62a13c41 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -623,6 +623,8 @@ "title": "Notificacions", "liveActivities": "Activitats en directe", "liveActivitySubtitle": "Mostra els agents actius a la pantalla de bloqueig", + "liveUpdates": "Actualitzacions en directe", + "liveUpdateSubtitle": "Mostra els agents actius a les notificacions", "push": "Push", "enabled": "Notificacions activades", "onDescription": "Les notificacions push estan activades per a aquest dispositiu.", diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 2b9d8a5ea7..48de0bd596 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -616,6 +616,8 @@ "title": "ئاگادارکردنەوەکان", "liveActivities": "چالاکییە ڕاستەوخۆکان", "liveActivitySubtitle": "ئەجێنتە چالاکەکان لەسەر شاشەی داخستن پیشان بدە", + "liveUpdates": "نوێکردنەوەی ڕاستەوخۆ", + "liveUpdateSubtitle": "ئاژانسە چالاکەکان لە ئاگادارکردنەوەکانتدا پیشان بدە", "push": "پاڵدان", "enabled": "ئاگادارکردنەوەکان چالاک کراون", "onDescription": "ئاگادارکردنەوەکانی پاڵدان بۆ ئەم ئامێرە چالاکن.", diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 01d0b462f2..4cb02ce010 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -630,6 +630,8 @@ "title": "Oznámení", "liveActivities": "Živé aktivity", "liveActivitySubtitle": "Zobrazovat aktivní agenty na uzamčené obrazovce", + "liveUpdates": "Živé aktualizace", + "liveUpdateSubtitle": "Zobrazovat aktivní agenty v oznámeních", "push": "Push", "enabled": "Oznámení povolena", "onDescription": "Push oznámení jsou pro toto zařízení zapnutá.", diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index 92db905c45..5e0b9cb57d 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -644,6 +644,8 @@ "title": "Hysbysiadau", "liveActivities": "Gweithgareddau byw", "liveActivitySubtitle": "Dangos asiantau gweithredol ar y sgrin clo", + "liveUpdates": "Diweddariadau byw", + "liveUpdateSubtitle": "Dangos asiantau gweithredol yn eich hysbysiadau", "push": "Gwthio", "enabled": "Hysbysiadau wedi'u galluogi", "onDescription": "Mae hysbysiadau gwthio ymlaen ar gyfer y ddyfais hon.", diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index d93874a746..ac3aaf7094 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -616,6 +616,8 @@ "title": "Meddelelser", "liveActivities": "Live-aktiviteter", "liveActivitySubtitle": "Vis aktive agenter på låseskærmen", + "liveUpdates": "Liveopdateringer", + "liveUpdateSubtitle": "Vis aktive agenter i dine notifikationer", "push": "Push", "enabled": "Meddelelser aktiveret", "onDescription": "Push-meddelelser er til for denne enhed.", diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 1a6f6c0fa8..cd77f495c7 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -162,6 +162,8 @@ "title": "Benachrichtigungen", "liveActivities": "Live-Aktivitäten", "liveActivitySubtitle": "Aktive Agenten auf dem Sperrbildschirm anzeigen", + "liveUpdates": "Live-Updates", + "liveUpdateSubtitle": "Aktive Agents in deinen Benachrichtigungen anzeigen", "push": "Push", "enabled": "Benachrichtigungen aktiviert", "onDescription": "Push-Benachrichtigungen sind für dieses Gerät aktiviert.", diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index f238fa4705..58c91f52a0 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -616,6 +616,8 @@ "title": "Ειδοποιήσεις", "liveActivities": "Ζωντανές δραστηριότητες", "liveActivitySubtitle": "Εμφάνιση ενεργών πρακτόρων στην οθόνη κλειδώματος", + "liveUpdates": "Ζωντανές ενημερώσεις", + "liveUpdateSubtitle": "Εμφάνιση ενεργών πρακτόρων στις ειδοποιήσεις σας", "push": "Push", "enabled": "Οι ειδοποιήσεις είναι ενεργοποιημένες", "onDescription": "Οι push ειδοποιήσεις είναι ενεργές για αυτή τη συσκευή.", diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 80fd2ef963..29096d6c53 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -616,6 +616,8 @@ "title": "Notifications", "liveActivities": "Live Activities", "liveActivitySubtitle": "Show active agents on the Lock Screen", + "liveUpdates": "Live Updates", + "liveUpdateSubtitle": "Show active agents in your notifications", "push": "Push", "enabled": "Notifications enabled", "onDescription": "Push notifications are on for this device.", diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index f57260fc0e..dfd5232cc1 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -185,6 +185,8 @@ "title": "Notificaciones", "liveActivities": "Actividades en directo", "liveActivitySubtitle": "Muestra los agentes activos en la pantalla bloqueada", + "liveUpdates": "Actualizaciones en vivo", + "liveUpdateSubtitle": "Muestra los agentes activos en tus notificaciones", "push": "Push", "enabled": "Notificaciones activadas", "onDescription": "Las notificaciones push están activadas para este dispositivo.", diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index 77d88c2365..bb09705b0e 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -616,6 +616,8 @@ "title": "Teavitused", "liveActivities": "Reaalajas tegevused", "liveActivitySubtitle": "Näita aktiivseid agente lukustuskuval", + "liveUpdates": "Reaalajas värskendused", + "liveUpdateSubtitle": "Näita aktiivseid agente teavitustes", "push": "Push", "enabled": "Teavitused on lubatud", "onDescription": "Push-teavitused on selle seadme jaoks sisse lülitatud.", diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 733b02025a..3faa867924 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -616,6 +616,8 @@ "title": "Jakinarazpenak", "liveActivities": "Zuzeneko jarduerak", "liveActivitySubtitle": "Erakutsi agente aktiboak blokeo-pantailan", + "liveUpdates": "Zuzeneko eguneratzeak", + "liveUpdateSubtitle": "Erakutsi agente aktiboak zure jakinarazpenetan", "push": "Bultzadazkoak", "enabled": "Jakinarazpenak gaituta", "onDescription": "Bultzadazko jakinarazpenak piztuta daude gailu honetan.", diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 662fb60b01..8bb0f9a367 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -616,6 +616,8 @@ "title": "اعلان‌ها", "liveActivities": "فعالیت‌های زنده", "liveActivitySubtitle": "نمایش عامل‌های فعال در صفحه قفل", + "liveUpdates": "به‌روزرسانی‌های زنده", + "liveUpdateSubtitle": "نمایش عامل‌های فعال در اعلان‌های شما", "push": "Push", "enabled": "اعلان‌ها فعال‌اند", "onDescription": "اعلان‌های push برای این دستگاه روشن‌اند.", diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index 0f890003f0..ab9ab25c17 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -616,6 +616,8 @@ "title": "Ilmoitukset", "liveActivities": "Livetoiminnot", "liveActivitySubtitle": "Näytä aktiiviset agentit lukitusnäytöllä", + "liveUpdates": "Reaaliaikaiset päivitykset", + "liveUpdateSubtitle": "Näytä aktiiviset agentit ilmoituksissa", "push": "Push", "enabled": "Ilmoitukset käytössä", "onDescription": "Push-ilmoitukset ovat päällä tälle laitteelle.", diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index 25ffcf32bc..f5f5f3e127 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -616,6 +616,8 @@ "title": "Mga Notipikasyon", "liveActivities": "Mga live na aktibidad", "liveActivitySubtitle": "Ipakita ang mga aktibong agent sa Lock Screen", + "liveUpdates": "Live na update", + "liveUpdateSubtitle": "Ipakita ang mga aktibong agent sa iyong mga notification", "push": "Push", "enabled": "Pinagana ang mga notipikasyon", "onDescription": "Naka-on ang push notifications para sa device na ito.", diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index 47ae596434..9174a3ab0a 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -165,6 +165,8 @@ "title": "Notifications", "liveActivities": "Activités en direct", "liveActivitySubtitle": "Afficher les agents actifs sur l’écran verrouillé", + "liveUpdates": "Mises à jour en direct", + "liveUpdateSubtitle": "Afficher les agents actifs dans vos notifications", "push": "Push", "enabled": "Notifications activées", "onDescription": "Les notifications push sont activées pour cet appareil.", diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index 1d4434bfce..555f13a60a 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -637,6 +637,8 @@ "title": "Fógraí", "liveActivities": "Gníomhaíochtaí beo", "liveActivitySubtitle": "Taispeáin gníomhairí gníomhacha ar an scáileán glasáilte", + "liveUpdates": "Nuashonruithe beo", + "liveUpdateSubtitle": "Taispeáin gníomhairí gníomhacha i d'fhógraí", "push": "Brú", "enabled": "Fógraí cumasaithe", "onDescription": "Tá brú-fhógraí ar siúl don ghléas seo.", diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index 038191af6a..e873574382 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -616,6 +616,8 @@ "title": "Notificacións", "liveActivities": "Actividades en directo", "liveActivitySubtitle": "Amosa os axentes activos na pantalla de bloqueo", + "liveUpdates": "Actualizacións en directo", + "liveUpdateSubtitle": "Mostrar os axentes activos nas notificacións", "push": "Push", "enabled": "Notificacións activadas", "onDescription": "As notificacións push están activadas para este dispositivo.", diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index 7871cc044b..171d78dd55 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -616,6 +616,8 @@ "title": "સૂચનાઓ", "liveActivities": "લાઇવ પ્રવૃત્તિઓ", "liveActivitySubtitle": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો બતાવો", + "liveUpdates": "લાઇવ અપડેટ્સ", + "liveUpdateSubtitle": "તમારી સૂચનાઓમાં સક્રિય એજન્ટ બતાવો", "push": "પુશ", "enabled": "સૂચનાઓ સક્ષમ", "onDescription": "આ ઉપકરણ માટે પુશ સૂચનાઓ ચાલુ છે.", diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 0fe1858fa3..729aba29de 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -616,6 +616,8 @@ "title": "Sanarwa", "liveActivities": "Ayyukan kai tsaye", "liveActivitySubtitle": "Nuna wakilai masu aiki a allon kulle", + "liveUpdates": "Sabuntawa kai tsaye", + "liveUpdateSubtitle": "Nuna wakilai masu aiki a cikin sanarwarka", "push": "Turawa", "enabled": "Sanarwa suna aiki", "onDescription": "Sanarwar turawa suna aiki ga wannan na'ura.", diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index 427c7ea24a..4a72ba9bed 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -162,6 +162,8 @@ "title": "התראות", "liveActivities": "פעילויות בזמן אמת", "liveActivitySubtitle": "הצג סוכנים פעילים במסך הנעילה", + "liveUpdates": "עדכונים חיים", + "liveUpdateSubtitle": "הצג סוכנים פעילים בהתראות שלך", "push": "דחיפה", "enabled": "התראות מופעלות", "onDescription": "התראות דחיפה מופעלות עבור מכשיר זה.", diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index a41a56f9f3..6095149cd3 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -162,6 +162,8 @@ "title": "सूचनाएँ", "liveActivities": "लाइव गतिविधियाँ", "liveActivitySubtitle": "लॉक स्क्रीन पर सक्रिय एजेंट दिखाएँ", + "liveUpdates": "लाइव अपडेट", + "liveUpdateSubtitle": "अपनी सूचनाओं में सक्रिय एजेंट दिखाएँ", "push": "पुश", "enabled": "सूचनाएँ सक्षम", "onDescription": "इस डिवाइस के लिए पुश सूचनाएँ चालू हैं।", diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index a78ed27e6b..fbe9856c63 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -623,6 +623,8 @@ "title": "Obavijesti", "liveActivities": "Aktivnosti uživo", "liveActivitySubtitle": "Prikaži aktivne agente na zaključanom zaslonu", + "liveUpdates": "Ažuriranja uživo", + "liveUpdateSubtitle": "Prikaži aktivne agente u obavijestima", "push": "Push", "enabled": "Obavijesti omogućene", "onDescription": "Push obavijesti su uključene za ovaj uređaj.", diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 8a9d063235..75050afa67 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -616,6 +616,8 @@ "title": "Notifikasyon", "liveActivities": "Aktivite an dirèk", "liveActivitySubtitle": "Montre ajans aktif yo sou ekran vewouye a", + "liveUpdates": "Mizajou an dirèk", + "liveUpdateSubtitle": "Montre ajan aktif yo nan notifikasyon ou yo", "push": "Pouse", "enabled": "Notifikasyon aktive", "onDescription": "Notifikasyon pouse louvri pou aparèy sa a.", diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index 030380d8e4..9bf89ce072 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -616,6 +616,8 @@ "title": "Értesítések", "liveActivities": "Élő tevékenységek", "liveActivitySubtitle": "Aktív ügynökök megjelenítése a zárolási képernyőn", + "liveUpdates": "Élő frissítések", + "liveUpdateSubtitle": "Aktív ügynökök megjelenítése az értesítésekben", "push": "Leküldés", "enabled": "Értesítések engedélyezve", "onDescription": "A leküldéses értesítések be vannak kapcsolva ehhez az eszközhöz.", diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index 92139a6a54..a00cf5a267 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -616,6 +616,8 @@ "title": "Ծանուցումներ", "liveActivities": "Ուղիղ գործողություններ", "liveActivitySubtitle": "Ցուցադրել ակտիվ գործակալները կողպէկրանին", + "liveUpdates": "Ուղիղ թարմացումներ", + "liveUpdateSubtitle": "Ցուցադրել ակտիվ գործակալները ձեր ծանուցումներում", "push": "Push", "enabled": "Ծանուցումները միացված են", "onDescription": "Push ծանուցումները միացված են այս սարքի համար:", diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index 00ee15e059..ef3bce76d4 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -162,6 +162,8 @@ "title": "Notifikasi", "liveActivities": "Aktivitas langsung", "liveActivitySubtitle": "Tampilkan agen aktif di Layar Terkunci", + "liveUpdates": "Pembaruan langsung", + "liveUpdateSubtitle": "Tampilkan agen aktif di notifikasi Anda", "push": "Push", "enabled": "Notifikasi diaktifkan", "onDescription": "Notifikasi push aktif untuk perangkat ini.", diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 21fc0f6fc1..89e4743826 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -616,6 +616,8 @@ "title": "Ọkwa", "liveActivities": "Ọrụ ndụ", "liveActivitySubtitle": "Gosi ndị ọrụ na-arụ ọrụ na Lock Screen", + "liveUpdates": "Mmelite ndụ", + "liveUpdateSubtitle": "Gosi ndị ọrụ na-arụ ọrụ na ọkwa gị", "push": "Push", "enabled": "Ọkwa agbanyela", "onDescription": "Ọkwa push dị maka ngwaọrụ a.", diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 9f89db0fa9..475d494540 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -616,6 +616,8 @@ "title": "Tilkynningar", "liveActivities": "Beinar aðgerðir", "liveActivitySubtitle": "Sýna virk umboð á lásskjánum", + "liveUpdates": "Beinar uppfærslur", + "liveUpdateSubtitle": "Sýna virka fulltrúa í tilkynningunum þínum", "push": "Push", "enabled": "Tilkynningar virkjaðar", "onDescription": "Push-tilkynningar eru kveiktar á þessu tæki.", diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 829eaea32a..a9e9d9a1a7 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -165,6 +165,8 @@ "title": "Notifiche", "liveActivities": "Attività in tempo reale", "liveActivitySubtitle": "Mostra gli agenti attivi nella schermata di blocco", + "liveUpdates": "Aggiornamenti live", + "liveUpdateSubtitle": "Mostra gli agenti attivi nelle notifiche", "push": "Push", "enabled": "Notifiche attivate", "onDescription": "Le notifiche push sono attive per questo dispositivo.", diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index a64d515d93..387715c7f0 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -162,6 +162,8 @@ "title": "通知", "liveActivities": "ライブアクティビティ", "liveActivitySubtitle": "ロック画面に稼働中のエージェントを表示", + "liveUpdates": "ライブアップデート", + "liveUpdateSubtitle": "通知にアクティブなエージェントを表示", "push": "プッシュ", "enabled": "通知が有効です", "onDescription": "このデバイスではプッシュ通知がオンです。", diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 9998871384..c23f4c862f 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -616,6 +616,8 @@ "title": "შეტყობინებები", "liveActivities": "ცოცხალი აქტივობები", "liveActivitySubtitle": "აქტიური აგენტების ჩვენება ჩაკეტილ ეკრანზე", + "liveUpdates": "ცოცხალი განახლებები", + "liveUpdateSubtitle": "აქტიური აგენტების ჩვენება შეტყობინებებში", "push": "Push", "enabled": "შეტყობინებები ჩართულია", "onDescription": "Push შეტყობინებები ჩართულია ამ მოწყობილობაზე.", diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 982db80d99..bc4b27cc08 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -616,6 +616,8 @@ "title": "Хабарландырулар", "liveActivities": "Тікелей әрекеттер", "liveActivitySubtitle": "Белсенді агенттерді құлып экранында көрсету", + "liveUpdates": "Тікелей жаңартулар", + "liveUpdateSubtitle": "Хабарландыруларда белсенді агенттерді көрсету", "push": "Push", "enabled": "Хабарландырулар қосылған", "onDescription": "Бұл құрылғы үшін push хабарландырулар қосулы.", diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 327959c10b..da8a804217 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -616,6 +616,8 @@ "title": "ការជូនដំណឹង", "liveActivities": "សកម្មភាពផ្ទាល់", "liveActivitySubtitle": "បង្ហាញភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ", + "liveUpdates": "ការធ្វើបច្ចុប្បន្នភាពផ្ទាល់", + "liveUpdateSubtitle": "បង្ហាញភ្នាក់ងារសកម្មនៅក្នុងការជូនដំណឹងរបស់អ្នក", "push": "Push", "enabled": "ការជូនដំណឹងត្រូវបានបើក", "onDescription": "ការជូនដំណឹងរុញបានបើកសម្រាប់ឧបករណ៍នេះ។", diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 160f871b4c..86fff381ce 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -616,6 +616,8 @@ "title": "ಅಧಿಸೂಚನೆಗಳು", "liveActivities": "ಲೈವ್ ಚಟುವಟಿಕೆಗಳು", "liveActivitySubtitle": "ಲಾಕ್ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ತೋರಿಸಿ", + "liveUpdates": "ನೇರ ನವೀಕರಣಗಳು", + "liveUpdateSubtitle": "ನಿಮ್ಮ ಅಧಿಸೂಚನೆಗಳಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ತೋರಿಸಿ", "push": "ಪುಶ್", "enabled": "ಅಧಿಸೂಚನೆಗಳು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ", "onDescription": "ಈ ಸಾಧನಕ್ಕೆ ಪುಶ್ ಅಧಿಸೂಚನೆಗಳು ಆನ್ ಆಗಿವೆ.", diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 04467574fb..9d3bcf8e2e 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -162,6 +162,8 @@ "title": "알림", "liveActivities": "실시간 활동", "liveActivitySubtitle": "잠금 화면에 활성 에이전트 표시", + "liveUpdates": "실시간 업데이트", + "liveUpdateSubtitle": "알림에 활성 에이전트 표시", "push": "푸시", "enabled": "알림 활성화됨", "onDescription": "이 기기에서 푸시 알림이 켜져 있습니다.", diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index 4ebc991c0a..fca7284d65 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -616,6 +616,8 @@ "title": "ການແຈ້ງເຕືອນ", "liveActivities": "ກິດຈະກຳສົດ", "liveActivitySubtitle": "ສະແດງຕົວແທນທີ່ເຮັດວຽກຢູ່ໜ້າຈໍລັອກ", + "liveUpdates": "ການອັບເດດສົດ", + "liveUpdateSubtitle": "ສະແດງເອເຈນທີ່ເຄື່ອນໄຫວໃນການແຈ້ງເຕືອນຂອງທ່ານ", "push": "ການຜັກດັນ", "enabled": "ເປີດການແຈ້ງເຕືອນແລ້ວ", "onDescription": "ການແຈ້ງເຕືອນແບບຜັກດັນເປີດຢູ່ສຳລັບອຸປະກອນນີ້.", diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 4171d5dfb7..906fc4a493 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -630,6 +630,8 @@ "title": "Pranešimai", "liveActivities": "Tiesioginės veiklos", "liveActivitySubtitle": "Rodyti aktyvius agentus užrakinimo ekrane", + "liveUpdates": "Tiesioginiai naujinimai", + "liveUpdateSubtitle": "Rodyti aktyvius agentus pranešimuose", "push": "Push", "enabled": "Pranešimai įjungti", "onDescription": "Push pranešimai šiame įrenginyje įjungti.", diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index 0cfb086aae..c4038af28c 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -623,6 +623,8 @@ "title": "Paziņojumi", "liveActivities": "Tiešās aktivitātes", "liveActivitySubtitle": "Rādīt aktīvos aģentus bloķēšanas ekrānā", + "liveUpdates": "Tiešraides atjauninājumi", + "liveUpdateSubtitle": "Rādīt aktīvos aģentus paziņojumos", "push": "Push", "enabled": "Paziņojumi iespējoti", "onDescription": "Push paziņojumi šai ierīcei ir ieslēgti.", diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index 30509ea914..8f7b1c9b54 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -616,6 +616,8 @@ "title": "Fampandrenesana", "liveActivities": "Hetsika mivantana", "liveActivitySubtitle": "Asehoy ny agent miasa eo amin’ny efijery mihidy", + "liveUpdates": "Fanavaozana mivantana", + "liveUpdateSubtitle": "Asehoy ao amin'ny fampahafantarana ny mpandraharaha miasa", "push": "Push", "enabled": "Alefa ny fampandrenesana", "onDescription": "Miasa amin'ity fitaovana ity ny fampandrenesana push.", diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index ae0ca3bf06..931f70e795 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -616,6 +616,8 @@ "title": "Ngā Pānui", "liveActivities": "Ngā mahi ora", "liveActivitySubtitle": "Whakaatu i ngā māngai kaha ki te Mata Raka", + "liveUpdates": "Whakahoutanga mataora", + "liveUpdateSubtitle": "Whakaatu i ngā pou mahi i roto i ō pānui", "push": "Pana", "enabled": "Kua whakahohea ngā pānui", "onDescription": "Kei te kā ngā pānui pana mō tēnei pūrere.", diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 08e73b5d65..fb5a104237 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -616,6 +616,8 @@ "title": "Известувања", "liveActivities": "Активности во живо", "liveActivitySubtitle": "Прикажувај активни агенти на заклучениот екран", + "liveUpdates": "Ажурирања во живо", + "liveUpdateSubtitle": "Прикажувај активни агенти во известувањата", "push": "Притискање", "enabled": "Известувањата се овозможени", "onDescription": "Притиснатите известувања се вклучени за овој уред.", diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index e26a867df0..4fc20d8f14 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -616,6 +616,8 @@ "title": "അറിയിപ്പുകൾ", "liveActivities": "ലൈവ് ആക്റ്റിവിറ്റികൾ", "liveActivitySubtitle": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണിക്കുക", + "liveUpdates": "തത്സമയ അപ്‌ഡേറ്റുകൾ", + "liveUpdateSubtitle": "നിങ്ങളുടെ അറിയിപ്പുകളിൽ സജീവ ഏജന്റുമാരെ കാണിക്കുക", "push": "പുഷ്", "enabled": "അറിയിപ്പുകൾ പ്രവർത്തനക്ഷമമാക്കി", "onDescription": "ഈ ഉപകരണത്തിനായി പുഷ് അറിയിപ്പുകൾ ഓണാണ്.", diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index f708308912..79dd049be0 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -616,6 +616,8 @@ "title": "Мэдэгдэлүүд", "liveActivities": "Шууд үйл ажиллагаа", "liveActivitySubtitle": "Идэвхтэй агентуудыг түгжээний дэлгэцэд харуулах", + "liveUpdates": "Шууд шинэчлэлт", + "liveUpdateSubtitle": "Мэдэгдэлд идэвхтэй агентуудыг харуулах", "push": "Түлхэлт", "enabled": "Мэдэгдэл идэвхжсэн", "onDescription": "Энэ төхөөрөмжид түлхэлтийн мэдэгдэл асна.", diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index f431723b15..2103499a3a 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -616,6 +616,8 @@ "title": "सूचना", "liveActivities": "लाइव्ह क्रियाकलाप", "liveActivitySubtitle": "लॉक स्क्रीनवर सक्रिय एजंट दाखवा", + "liveUpdates": "थेट अद्यतने", + "liveUpdateSubtitle": "तुमच्या सूचनांमध्ये सक्रिय एजंट दाखवा", "push": "पुश", "enabled": "सूचना सक्षम केल्या", "onDescription": "या डिव्हाइससाठी पुश सूचना चालू आहेत.", diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 99166caa09..9229ff6288 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -616,6 +616,8 @@ "title": "Pemberitahuan", "liveActivities": "Aktiviti langsung", "liveActivitySubtitle": "Tunjukkan ejen aktif pada Skrin Kunci", + "liveUpdates": "Kemas kini langsung", + "liveUpdateSubtitle": "Tunjukkan ejen aktif dalam pemberitahuan anda", "push": "Push", "enabled": "Pemberitahuan didayakan", "onDescription": "Pemberitahuan push dihidupkan untuk peranti ini.", diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index a58cdb286c..2b2cc9cb75 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -637,6 +637,8 @@ "title": "Notifiki", "liveActivities": "Attivitajiet diretti", "liveActivitySubtitle": "Uri l-aġenti attivi fuq l-iskrin imsakkar", + "liveUpdates": "Aġġornamenti diretti", + "liveUpdateSubtitle": "Uri l-aġenti attivi fin-notifiki tiegħek", "push": "Push", "enabled": "Notifiki attivati", "onDescription": "In-notifiki push huma mixgħula għal dan l-apparat.", diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index e0005ae576..61914ac4b7 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -616,6 +616,8 @@ "title": "အသိပေးချက်များ", "liveActivities": "တိုက်ရိုက် လှုပ်ရှားမှုများ", "liveActivitySubtitle": "လော့ခ်စခရင်တွင် လုပ်ဆောင်နေသော agent များကို ပြပါ", + "liveUpdates": "တိုက်ရိုက် အပ်ဒိတ်များ", + "liveUpdateSubtitle": "အသုံးပြုနေသော အေးဂျင့်များကို အကြောင်းကြားချက်တွင် ပြပါ", "push": "Push", "enabled": "အကြောင်းကြားချက်များ ဖွင့်ထားသည်", "onDescription": "ဤစက်အတွက် push အကြောင်းကြားချက်များ ဖွင့်ထားသည်။", diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index 1861bd1110..fcbc43b59e 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -616,6 +616,8 @@ "title": "Varsler", "liveActivities": "Sanntidsaktiviteter", "liveActivitySubtitle": "Vis aktive agenter på låseskjermen", + "liveUpdates": "Sanntidsoppdateringer", + "liveUpdateSubtitle": "Vis aktive agenter i varslene dine", "push": "Push", "enabled": "Varsler aktivert", "onDescription": "Push-varsler er på for denne enheten.", diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index 6bc3290f65..116d4aa36e 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -616,6 +616,8 @@ "title": "सूचनाहरू", "liveActivities": "लाइभ गतिविधिहरू", "liveActivitySubtitle": "लक स्क्रिनमा सक्रिय एजेन्टहरू देखाउनुहोस्", + "liveUpdates": "प्रत्यक्ष अद्यावधिक", + "liveUpdateSubtitle": "आफ्ना सूचनाहरूमा सक्रिय एजेन्ट देखाउनुहोस्", "push": "पुश", "enabled": "सूचनाहरू सक्षम गरियो", "onDescription": "यो यन्त्रको लागि पुश सूचनाहरू सक्रिय छन्।", diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 36173deb9b..17b51c1a8f 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -185,6 +185,8 @@ "title": "Meldingen", "liveActivities": "Live activiteiten", "liveActivitySubtitle": "Actieve agents tonen op het toegangsscherm", + "liveUpdates": "Live-updates", + "liveUpdateSubtitle": "Actieve agents in je meldingen tonen", "push": "Push", "enabled": "Meldingen ingeschakeld", "onDescription": "Pushmeldingen staan aan voor dit apparaat.", diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index f876ade8a9..1e8c42dc38 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -616,6 +616,8 @@ "title": "Beeksisota", "liveActivities": "Sochoota kallattii", "liveActivitySubtitle": "Eejentoota hojjetan gaaffii cufaa irratti agarsiisi", + "liveUpdates": "Haaromsa kallattii", + "liveUpdateSubtitle": "Ergamtoota hojjetan beeksisa kee keessatti agarsiisi", "push": "Push", "enabled": "Beeksisni dandeesame", "onDescription": "Beeksisni push meeshaa kanaaf jira.", diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 81db0eb214..00a0c1eb62 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -616,6 +616,8 @@ "title": "ବିଜ୍ଞପ୍ତି", "liveActivities": "ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ", "liveActivitySubtitle": "ଲକ୍ ସ୍କ୍ରିନରେ ସକ୍ରିୟ ଏଜେଣ୍ଟ ଦେଖାନ୍ତୁ", + "liveUpdates": "ଲାଇଭ୍ ଅପଡେଟ୍", + "liveUpdateSubtitle": "ଆପଣଙ୍କ ବିଜ୍ଞପ୍ତିରେ ସକ୍ରିୟ ଏଜେଣ୍ଟ ଦେଖାନ୍ତୁ", "push": "ପୁସ୍", "enabled": "ବିଜ୍ଞପ୍ତି ସକ୍ଷମ ହେଲା", "onDescription": "ଏହି ଉପକରଣ ପାଇଁ ପୁସ୍ ବିଜ୍ଞପ୍ତି ଚାଲୁ ଅଛି।", diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 09fda15734..93d0367e36 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -616,6 +616,8 @@ "title": "ਸੂਚਨਾਵਾਂ", "liveActivities": "ਲਾਈਵ ਸਰਗਰਮੀਆਂ", "liveActivitySubtitle": "ਲਾਕ ਸਕ੍ਰੀਨ ਉੱਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦਿਖਾਓ", + "liveUpdates": "ਲਾਈਵ ਅੱਪਡੇਟ", + "liveUpdateSubtitle": "ਆਪਣੀਆਂ ਸੂਚਨਾਵਾਂ ਵਿੱਚ ਸਰਗਰਮ ਏਜੰਟ ਦਿਖਾਓ", "push": "ਪੁਸ਼", "enabled": "ਸੂਚਨਾਵਾਂ ਸਮਰੱਥ ਹਨ", "onDescription": "ਇਸ ਡਿਵਾਈਸ ਲਈ ਪੁਸ਼ ਸੂਚਨਾਵਾਂ ਚਾਲੂ ਹਨ।", diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index a9e3db06dd..4b7db6fef9 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -162,6 +162,8 @@ "title": "Powiadomienia", "liveActivities": "Aktywności na żywo", "liveActivitySubtitle": "Pokazuj aktywnych agentów na ekranie blokady", + "liveUpdates": "Aktualizacje na żywo", + "liveUpdateSubtitle": "Pokazuj aktywne agenty w powiadomieniach", "push": "Push", "enabled": "Powiadomienia włączone", "onDescription": "Powiadomienia push są włączone dla tego urządzenia.", diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 59a92c34f1..042682619d 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -616,6 +616,8 @@ "title": "خبرتیاوې", "liveActivities": "ژوندۍ فعالیتونه", "liveActivitySubtitle": "فعال اجنټان په لاک سکرین کې وښایه", + "liveUpdates": "ژوندي تازه معلومات", + "liveUpdateSubtitle": "فعال اجنټان په خپلو خبرتیاوو کې وښایاست", "push": "پوش", "enabled": "خبرتیاوې فعالې شوې", "onDescription": "پوش خبرتیاوې د دې وسیلې لپاره فعالې دي.", diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index df0392d0b4..50f2a55175 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -149,6 +149,8 @@ "title": "Notificações", "liveActivities": "Atividades ao vivo", "liveActivitySubtitle": "Mostrar agentes ativos na tela bloqueada", + "liveUpdates": "Atualizações ao vivo", + "liveUpdateSubtitle": "Mostrar agentes ativos nas suas notificações", "push": "Push", "enabled": "Notificações ativadas", "onDescription": "As notificações push estão ativadas para este dispositivo.", diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 6fd8e81001..73ae2dd15a 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -623,6 +623,8 @@ "title": "Notificações", "liveActivities": "Atividades em direto", "liveActivitySubtitle": "Mostrar agentes ativos no ecrã bloqueado", + "liveUpdates": "Atualizações em direto", + "liveUpdateSubtitle": "Mostrar agentes ativos nas suas notificações", "push": "Push", "enabled": "Notificações ativadas", "onDescription": "As notificações push estão ativas para este dispositivo.", diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 5f910dcecc..94c9fccbcb 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -623,6 +623,8 @@ "title": "Notificări", "liveActivities": "Activități live", "liveActivitySubtitle": "Afișează agenții activi pe ecranul blocat", + "liveUpdates": "Actualizări în timp real", + "liveUpdateSubtitle": "Afișează agenții activi în notificări", "push": "Push", "enabled": "Notificări activate", "onDescription": "Notificările push sunt activate pentru acest dispozitiv.", diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index a2c7968fa1..74d7263ccd 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -162,6 +162,8 @@ "title": "Уведомления", "liveActivities": "Живые активности", "liveActivitySubtitle": "Показывать активных агентов на экране блокировки", + "liveUpdates": "Обновления в реальном времени", + "liveUpdateSubtitle": "Показывать активных агентов в уведомлениях", "push": "Push", "enabled": "Уведомления включены", "onDescription": "Push-уведомления включены для этого устройства.", diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index 661f5aefc5..68d791c220 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -616,6 +616,8 @@ "title": "දැනුම්දීම්", "liveActivities": "සජීවී ක්‍රියාකාරකම්", "liveActivitySubtitle": "අගුළු තිරයේ සක්‍රීය නියෝජිතයන් පෙන්වන්න", + "liveUpdates": "සජීවී යාවත්කාලීන", + "liveUpdateSubtitle": "ඔබේ දැනුම්දීම්වල ක්‍රියාකාරී නියෝජිතයන් පෙන්වන්න", "push": "තෙරපුම", "enabled": "දැනුම්දීම් සක්‍රීය කර ඇත", "onDescription": "මෙම උපාංගය සඳහා තෙරපුම් දැනුම්දීම් ක්‍රියාත්මකයි.", diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index a198952d40..41ee5b8e31 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -630,6 +630,8 @@ "title": "Upozornenia", "liveActivities": "Živé aktivity", "liveActivitySubtitle": "Zobrazovať aktívnych agentov na uzamknutej obrazovke", + "liveUpdates": "Živé aktualizácie", + "liveUpdateSubtitle": "Zobrazovať aktívnych agentov v upozorneniach", "push": "Push", "enabled": "Upozornenia povolené", "onDescription": "Push upozornenia sú pre toto zariadenie zapnuté.", diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index 0afed798ad..c1cae52eff 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -630,6 +630,8 @@ "title": "Obvestila", "liveActivities": "Aktivnosti v živo", "liveActivitySubtitle": "Prikaži aktivne agente na zaklenjenem zaslonu", + "liveUpdates": "Posodobitve v živo", + "liveUpdateSubtitle": "Prikaži aktivne agente v obvestilih", "push": "Potisno", "enabled": "Obvestila omogočena", "onDescription": "Potisna obvestila so vklopljena za to napravo.", diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 0458b74992..93fddbdbf1 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -616,6 +616,8 @@ "title": "Ogaysiisyada", "liveActivities": "Hawlaha tooska ah", "liveActivitySubtitle": "Ku muuji wakiillada firfircoon Shaashadda Qufulka", + "liveUpdates": "Cusboonaysiin toos ah", + "liveUpdateSubtitle": "Muuji wakiillada firfircoon ogeysiisyadaada", "push": "Push", "enabled": "Ogaysiisyadu waa daaran", "onDescription": "Ogaysiisyada push waxay u daaran aaladdan.", diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index f9ae4c64b4..39242b7943 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -616,6 +616,8 @@ "title": "Njoftimet", "liveActivities": "Aktivitete të drejtpërdrejta", "liveActivitySubtitle": "Shfaq agjentët aktivë në ekranin e kyçur", + "liveUpdates": "Përditësime të drejtpërdrejta", + "liveUpdateSubtitle": "Shfaq agjentët aktivë në njoftimet e tua", "push": "Shtytje", "enabled": "Njoftimet të aktivizuara", "onDescription": "Njoftimet shtytëse janë ndezur për këtë pajisje.", diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index 253ae3a837..588f1f5eb8 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -623,6 +623,8 @@ "title": "Obaveštenja", "liveActivities": "Aktivnosti uživo", "liveActivitySubtitle": "Prikaži aktivne agente na zaključanom ekranu", + "liveUpdates": "Ažuriranja uživo", + "liveUpdateSubtitle": "Prikazuj aktivne agente u obaveštenjima", "push": "Push", "enabled": "Obaveštenja omogućena", "onDescription": "Push obaveštenja su uključena za ovaj uređaj.", diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index 7394579515..bd86a044b4 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -616,6 +616,8 @@ "title": "Aviseringar", "liveActivities": "Liveaktiviteter", "liveActivitySubtitle": "Visa aktiva agenter på låsskärmen", + "liveUpdates": "Liveuppdateringar", + "liveUpdateSubtitle": "Visa aktiva agenter i dina aviseringar", "push": "Push", "enabled": "Aviseringar aktiverade", "onDescription": "Pushaviseringar är på för den här enheten.", diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index 6445b1b090..0bd4ad22ab 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -616,6 +616,8 @@ "title": "Arifa", "liveActivities": "Shughuli za moja kwa moja", "liveActivitySubtitle": "Onyesha mawakala hai kwenye Skrini ya Kufunga", + "liveUpdates": "Masasisho ya moja kwa moja", + "liveUpdateSubtitle": "Onyesha mawakala wanaotumika katika arifa zako", "push": "Push", "enabled": "Arifa zimewashwa", "onDescription": "Arifa za push zimewashwa kwa kifaa hiki.", diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 66fbca7b90..ca29b6cc2b 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -616,6 +616,8 @@ "title": "அறிவிப்புகள்", "liveActivities": "நேரடி செயல்பாடுகள்", "liveActivitySubtitle": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காட்டு", + "liveUpdates": "நேரடி புதுப்பிப்புகள்", + "liveUpdateSubtitle": "உங்கள் அறிவிப்புகளில் செயலில் உள்ள முகவர்களைக் காட்டு", "push": "அழுத்து", "enabled": "அறிவிப்புகள் இயக்கப்பட்டது", "onDescription": "இந்த சாதனத்திற்கு அழுத்து அறிவிப்புகள் இயக்கத்தில் உள்ளன.", diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 606201cbf8..be3a1bd9f6 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -616,6 +616,8 @@ "title": "నోటిఫికేషన్లు", "liveActivities": "ప్రత్యక్ష కార్యకలాపాలు", "liveActivitySubtitle": "లాక్ స్క్రీన్‌లో క్రియాశీల ఏజెంట్లను చూపించు", + "liveUpdates": "ప్రత్యక్ష నవీకరణలు", + "liveUpdateSubtitle": "మీ నోటిఫికేషన్‌లలో సక్రియ ఏజెంట్‌లను చూపించు", "push": "పుష్", "enabled": "నోటిఫికేషన్లు ప్రారంభించబడ్డాయి", "onDescription": "ఈ పరికరానికి పుష్ నోటిఫికేషన్లు ఆన్లో ఉన్నాయి.", diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index 33a3f1b0ab..2b853bc2bb 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -616,6 +616,8 @@ "title": "การแจ้งเตือน", "liveActivities": "กิจกรรมสด", "liveActivitySubtitle": "แสดงเอเจนต์ที่ทำงานอยู่บนหน้าจอล็อก", + "liveUpdates": "อัปเดตสด", + "liveUpdateSubtitle": "แสดงเอเจนต์ที่ทำงานอยู่ในการแจ้งเตือนของคุณ", "push": "พุช", "enabled": "เปิดการแจ้งเตือนแล้ว", "onDescription": "เปิดการแจ้งเตือนแบบพุชสำหรับอุปกรณ์นี้", diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index 1e7c15d32b..b30a3badd5 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -162,6 +162,8 @@ "title": "Bildirimler", "liveActivities": "Canlı etkinlikler", "liveActivitySubtitle": "Etkin ajanları Kilit Ekranı’nda göster", + "liveUpdates": "Canlı güncellemeler", + "liveUpdateSubtitle": "Etkin aracıları bildirimlerinizde göster", "push": "Anlık", "enabled": "Bildirimler etkin", "onDescription": "Anlık bildirimler bu cihaz için açık.", diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 3470eaa563..02ef1d0bbf 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -162,6 +162,8 @@ "title": "Сповіщення", "liveActivities": "Живі активності", "liveActivitySubtitle": "Показувати активних агентів на екрані блокування", + "liveUpdates": "Оновлення в реальному часі", + "liveUpdateSubtitle": "Показувати активних агентів у сповіщеннях", "push": "Push", "enabled": "Сповіщення увімкнено", "onDescription": "Push-сповіщення увімкнено для цього пристрою.", diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index bd6d4e29e4..1d8eb14332 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -616,6 +616,8 @@ "title": "اطلاعیں", "liveActivities": "لائیو سرگرمیاں", "liveActivitySubtitle": "لاک اسکرین پر فعال ایجنٹس دکھائیں", + "liveUpdates": "لائیو اپ ڈیٹس", + "liveUpdateSubtitle": "اپنی اطلاعات میں فعال ایجنٹ دکھائیں", "push": "پش", "enabled": "اطلاعیں فعال ہیں", "onDescription": "پش اطلاعیں اس آلے کے لیے آن ہیں۔", diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 97ad6a4193..eeefabdbde 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -616,6 +616,8 @@ "title": "Bildirishnomalar", "liveActivities": "Jonli faoliyatlar", "liveActivitySubtitle": "Faol agentlarni qulflash ekranida ko‘rsatish", + "liveUpdates": "Jonli yangilanishlar", + "liveUpdateSubtitle": "Faol agentlarni bildirishnomalaringizda ko'rsatish", "push": "Push", "enabled": "Bildirishnomalar yoqilgan", "onDescription": "Push bildirishnomalari bu qurilmada yoqilgan.", diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index b8e1c1c2bb..615edddbc4 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -162,6 +162,8 @@ "title": "Thông báo", "liveActivities": "Hoạt động trực tiếp", "liveActivitySubtitle": "Hiển thị tác nhân đang hoạt động trên Màn hình khóa", + "liveUpdates": "Cập nhật trực tiếp", + "liveUpdateSubtitle": "Hiển thị tác nhân đang hoạt động trong thông báo", "push": "Đẩy", "enabled": "Đã bật thông báo", "onDescription": "Thông báo đẩy đang bật cho thiết bị này.", diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index d163054894..b08ea05d33 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -616,6 +616,8 @@ "title": "Awọn ifitonileti", "liveActivities": "Awọn iṣẹ laaye", "liveActivitySubtitle": "Fi awọn aṣoju to n ṣiṣẹ han lori Iboju Titiipa", + "liveUpdates": "Awọn imudojuiwọn taara", + "liveUpdateSubtitle": "Fi awọn aṣoju to n ṣiṣẹ han ninu awọn iwifunni rẹ", "push": "Titari", "enabled": "Awọn ifitonileti mu ṣiṣẹ", "onDescription": "Awọn ifitonileti titari wa ni lori ẹ̀rọ yii.", diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index b8fcaac0e6..866bfe3c98 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -162,6 +162,8 @@ "title": "通知", "liveActivities": "实时活动", "liveActivitySubtitle": "在锁定屏幕上显示活跃代理", + "liveUpdates": "实时更新", + "liveUpdateSubtitle": "在通知中显示活跃代理", "push": "推送", "enabled": "通知已启用", "onDescription": "此设备的推送通知已开启。", diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index 82b5f53030..3e5d941f70 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -162,6 +162,8 @@ "title": "通知", "liveActivities": "即時動態", "liveActivitySubtitle": "在鎖定畫面上顯示使用中的代理", + "liveUpdates": "即時更新", + "liveUpdateSubtitle": "在通知中顯示活躍代理", "push": "推播", "enabled": "已啟用通知", "onDescription": "此裝置的推播通知已開啟。", diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index aa0f1b97e0..896173b34d 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -616,6 +616,8 @@ "title": "Izaziso", "liveActivities": "Imisebenzi ebukhoma", "liveActivitySubtitle": "Bonisa ama-agent asebenzayo kuSikrini Sokukhiya", + "liveUpdates": "Izibuyekezo ezibukhoma", + "liveUpdateSubtitle": "Bonisa abameli abasebenzayo ezaziso zakho", "push": "Ukudonsa", "enabled": "Izaziso zivuliwe", "onDescription": "Izaziso zokudonsa zivuliwe kule divayisi.", From 709f0cb6c4605fc739bd9cf713a5a3c0fe89e25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 02:55:34 +0200 Subject: [PATCH 36/43] fix(mobile): let the widget description be translated The widget library writes it as translatable="false", which aapt2 reads as a promise that no values- override exists. The 86 the localization plugin writes are exactly that. --- apps/mobile/plugins/withAndroidWidgetLocalizations.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/mobile/plugins/withAndroidWidgetLocalizations.js b/apps/mobile/plugins/withAndroidWidgetLocalizations.js index 12c15a9518..083465abb5 100644 --- a/apps/mobile/plugins/withAndroidWidgetLocalizations.js +++ b/apps/mobile/plugins/withAndroidWidgetLocalizations.js @@ -80,6 +80,16 @@ module.exports = function withAndroidWidgetLocalizations(config, options) { } else { resources.string.push({ $: { name: labelName }, _: english.displayName }); } + // The library writes the description as translatable="false", which aapt2 + // reads as a promise that no `values-` override exists. The 86 written + // below are exactly that, so the flag has to go. + const description = resources.string.find(entry => entry.$?.name === descriptionName); + if (!description) { + throw new Error( + `withAndroidWidgetLocalizations: no "${descriptionName}" resource; it must run after the widget plugin.` + ); + } + delete description.$.translatable; return cfg; }); From a73ee849fc989ff342cba8d8a0e0db96d4ce5090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 03:07:22 +0200 Subject: [PATCH 37/43] fix(mobile): compile the Android Live Update module expo-modules-core's AppContext exposes only the React context, so appContext.applicationContext never resolved and the module failed to compile. Every entry point here runs from a JS call, so a lost React context means the module cannot work at all. The Android native build had never run: the workflow gate skipped it on every dispatch because an artifact for the unchanged native hash already existed. --- .../results.bin | 1 + .../classes/classes_dex/classes.dex | Bin 0 -> 47584 bytes .../activeagentsliveupdate/BuildConfig.java | 10 +++++ .../aapt/AndroidManifest.xml | 20 +++++++++ .../aapt/output-metadata.json | 18 +++++++++ .../debug/syncDebugLibJars/classes.jar | Bin 0 -> 45277 bytes .../aar-metadata.properties | 6 +++ .../annotationProcessors.json | 1 + .../extractDebugAnnotations/typedefs.txt | 0 .../bundleLibCompileToJarDebug/classes.jar | Bin 0 -> 103226 bytes .../debug/generateDebugRFile/R.jar | Bin 0 -> 336 bytes .../debug/generateDebugRFile/R.txt | 0 .../debug-mergeJavaRes/merge-state | Bin 0 -> 808 bytes .../compile-file-map.properties | 1 + .../debug/packageDebugResources/merger.xml | 2 + .../incremental/mergeDebugAssets/merger.xml | 2 + .../mergeDebugJniLibFolders/merger.xml | 2 + .../incremental/mergeDebugShaders/merger.xml | 2 + ...ive-agents-live-update_debug.kotlin_module | Bin 0 -> 24 bytes .../activeagentsliveupdate/BuildConfig.class | Bin 0 -> 657 bytes .../debug/parseDebugLocalResources/R-def.txt | 2 + .../manifest-merger-blame-debug-report.txt | 31 ++++++++++++++ .../feature-active-agents-live-update.jar | Bin 0 -> 225 bytes .../processDebugManifest/AndroidManifest.xml | 20 +++++++++ .../extractDeepLinksDebug/navigation.json | 1 + .../nestedResourcesValidationReport.txt | 1 + .../bundleLibRuntimeToJarDebug/classes.jar | Bin 0 -> 102912 bytes .../generateDebugRFile/package-aware-r.txt | 1 + .../caches-jvm/inputs/source-to-output.tab | Bin 0 -> 4096 bytes .../inputs/source-to-output.tab.keystream | Bin 0 -> 4096 bytes .../inputs/source-to-output.tab.keystream.len | Bin 0 -> 8 bytes .../inputs/source-to-output.tab.len | Bin 0 -> 8 bytes .../inputs/source-to-output.tab.values.at | Bin 0 -> 6481 bytes .../caches-jvm/inputs/source-to-output.tab_i | Bin 0 -> 32768 bytes .../inputs/source-to-output.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/class-attributes.tab.keystream | Bin 0 -> 4096 bytes .../kotlin/class-attributes.tab.keystream.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab.values.at | Bin 0 -> 61 bytes .../jvm/kotlin/class-attributes.tab_i | Bin 0 -> 32768 bytes .../jvm/kotlin/class-attributes.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-fq-name-to-source.tab | Bin 0 -> 4096 bytes .../class-fq-name-to-source.tab.keystream | Bin 0 -> 4096 bytes .../class-fq-name-to-source.tab.keystream.len | Bin 0 -> 8 bytes .../kotlin/class-fq-name-to-source.tab.len | Bin 0 -> 8 bytes .../class-fq-name-to-source.tab.values.at | Bin 0 -> 1085 bytes .../jvm/kotlin/class-fq-name-to-source.tab_i | Bin 0 -> 32768 bytes .../kotlin/class-fq-name-to-source.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/constants.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/constants.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/constants.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/constants.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/constants.tab.values.at | Bin 0 -> 283 bytes .../caches-jvm/jvm/kotlin/constants.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/jvm/kotlin/constants.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/internal-name-to-source.tab | Bin 0 -> 4096 bytes .../internal-name-to-source.tab.keystream | Bin 0 -> 8192 bytes .../internal-name-to-source.tab.keystream.len | Bin 0 -> 8 bytes .../kotlin/internal-name-to-source.tab.len | Bin 0 -> 8 bytes .../internal-name-to-source.tab.values.at | Bin 0 -> 6006 bytes .../jvm/kotlin/internal-name-to-source.tab_i | Bin 0 -> 32768 bytes .../kotlin/internal-name-to-source.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/jvm/kotlin/proto.tab | Bin 0 -> 4096 bytes .../caches-jvm/jvm/kotlin/proto.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/proto.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/proto.tab.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/proto.tab.values.at | Bin 0 -> 2733 bytes .../caches-jvm/jvm/kotlin/proto.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/jvm/kotlin/proto.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/source-to-classes.tab | Bin 0 -> 4096 bytes .../kotlin/source-to-classes.tab.keystream | Bin 0 -> 4096 bytes .../source-to-classes.tab.keystream.len | Bin 0 -> 8 bytes .../jvm/kotlin/source-to-classes.tab.len | Bin 0 -> 8 bytes .../kotlin/source-to-classes.tab.values.at | Bin 0 -> 4795 bytes .../jvm/kotlin/source-to-classes.tab_i | Bin 0 -> 32768 bytes .../jvm/kotlin/source-to-classes.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/subtypes.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/subtypes.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/subtypes.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/subtypes.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/subtypes.tab.values.at | Bin 0 -> 183 bytes .../caches-jvm/jvm/kotlin/subtypes.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/jvm/kotlin/subtypes.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/supertypes.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/supertypes.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/supertypes.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/supertypes.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/supertypes.tab.values.at | Bin 0 -> 122 bytes .../caches-jvm/jvm/kotlin/supertypes.tab_i | Bin 0 -> 32768 bytes .../jvm/kotlin/supertypes.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/lookups/counters.tab | 2 + .../caches-jvm/lookups/file-to-id.tab | Bin 0 -> 4096 bytes .../lookups/file-to-id.tab.keystream | Bin 0 -> 4096 bytes .../lookups/file-to-id.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/file-to-id.tab.len | Bin 0 -> 8 bytes .../lookups/file-to-id.tab.values.at | Bin 0 -> 61 bytes .../caches-jvm/lookups/file-to-id.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/file-to-id.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/id-to-file.tab | Bin 0 -> 4096 bytes .../lookups/id-to-file.tab.keystream | Bin 0 -> 4096 bytes .../lookups/id-to-file.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/id-to-file.tab.len | Bin 0 -> 8 bytes .../lookups/id-to-file.tab.values.at | Bin 0 -> 307 bytes .../caches-jvm/lookups/id-to-file.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/id-to-file.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/lookups/lookups.tab | Bin 0 -> 4096 bytes .../caches-jvm/lookups/lookups.tab.keystream | Bin 0 -> 20480 bytes .../lookups/lookups.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/lookups.tab.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/lookups.tab.values.at | Bin 0 -> 2173 bytes .../caches-jvm/lookups/lookups.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/lookups.tab_i.len | Bin 0 -> 8 bytes .../cacheable/last-build.bin | Bin 0 -> 18 bytes .../shrunk-classpath-snapshot.bin | Bin 0 -> 94243 bytes .../local-state/build-history.bin | Bin 0 -> 31 bytes .../aar/active-agents-live-update-debug.aar | Bin 0 -> 38613 bytes .../logs/manifest-merger-debug-report.txt | 38 ++++++++++++++++++ .../previous-compilation-data.bin | Bin 0 -> 12383 bytes ...ive-agents-live-update_debug.kotlin_module | Bin 0 -> 24 bytes ...tiveAgentsDeadlineReceiver$Companion.class | Bin 0 -> 9169 bytes .../ActiveAgentsDeadlineReceiver.class | Bin 0 -> 2680 bytes ...tiveAgentsLiveUpdateModule$Companion.class | Bin 0 -> 1125 bytes ...inition$lambda$6$$inlined$Function$1.class | Bin 0 -> 1471 bytes ...nition$lambda$6$$inlined$Function$10.class | Bin 0 -> 1481 bytes ...nition$lambda$6$$inlined$Function$11.class | Bin 0 -> 1512 bytes ...nition$lambda$6$$inlined$Function$12.class | Bin 0 -> 1511 bytes ...nition$lambda$6$$inlined$Function$13.class | Bin 0 -> 3822 bytes ...nition$lambda$6$$inlined$Function$14.class | Bin 0 -> 1473 bytes ...nition$lambda$6$$inlined$Function$15.class | Bin 0 -> 1511 bytes ...nition$lambda$6$$inlined$Function$16.class | Bin 0 -> 3615 bytes ...inition$lambda$6$$inlined$Function$2.class | Bin 0 -> 1471 bytes ...inition$lambda$6$$inlined$Function$3.class | Bin 0 -> 1471 bytes ...inition$lambda$6$$inlined$Function$4.class | Bin 0 -> 1479 bytes ...inition$lambda$6$$inlined$Function$5.class | Bin 0 -> 1510 bytes ...inition$lambda$6$$inlined$Function$6.class | Bin 0 -> 3479 bytes ...inition$lambda$6$$inlined$Function$7.class | Bin 0 -> 1471 bytes ...inition$lambda$6$$inlined$Function$8.class | Bin 0 -> 1471 bytes ...inition$lambda$6$$inlined$Function$9.class | Bin 0 -> 1471 bytes ...bda$6$$inlined$FunctionWithoutArgs$1.class | Bin 0 -> 2688 bytes ...bda$6$$inlined$FunctionWithoutArgs$2.class | Bin 0 -> 2600 bytes ...bda$6$$inlined$FunctionWithoutArgs$3.class | Bin 0 -> 2968 bytes .../ActiveAgentsLiveUpdateModule.class | Bin 0 -> 43358 bytes .../ActiveAgentsLiveUpdateModule.kt | 5 ++- 144 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/.transforms/f2cabb65ee93d20f76eac75d703af899/results.bin create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/.transforms/f2cabb65ee93d20f76eac75d703af899/transformed/classes/classes_dex/classes.dex create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/generated/source/buildConfig/debug/com/kilocode/activeagentsliveupdate/BuildConfig.java create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/annotations_typedef_file/debug/extractDebugAnnotations/typedefs.txt create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugAssets/merger.xml create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugShaders/merger.xml create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/java_res/debug/processDebugJavaRes/out/META-INF/active-agents-live-update_debug.kotlin_module create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/kilocode/activeagentsliveupdate/BuildConfig.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/merged_java_res/debug/mergeDebugJavaResource/feature-active-agents-live-update.jar create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/outputs/aar/active-agents-live-update-debug.aar create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/outputs/logs/manifest-merger-debug-report.txt create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/META-INF/active-agents-live-update_debug.kotlin_module create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsDeadlineReceiver$Companion.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsDeadlineReceiver.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$Companion.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$1.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$10.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$11.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$12.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$13.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$14.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$15.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$16.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$2.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$3.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$4.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$5.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$6.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$7.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$8.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$9.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$1.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$2.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$3.class create mode 100644 apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule.class diff --git a/apps/mobile/modules/active-agents-live-update/android/build/.transforms/f2cabb65ee93d20f76eac75d703af899/results.bin b/apps/mobile/modules/active-agents-live-update/android/build/.transforms/f2cabb65ee93d20f76eac75d703af899/results.bin new file mode 100644 index 0000000000..0d259ddcb5 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/.transforms/f2cabb65ee93d20f76eac75d703af899/results.bin @@ -0,0 +1 @@ +o/classes diff --git a/apps/mobile/modules/active-agents-live-update/android/build/.transforms/f2cabb65ee93d20f76eac75d703af899/transformed/classes/classes_dex/classes.dex b/apps/mobile/modules/active-agents-live-update/android/build/.transforms/f2cabb65ee93d20f76eac75d703af899/transformed/classes/classes_dex/classes.dex new file mode 100644 index 0000000000000000000000000000000000000000..0322b856f174a967003a0f89e3eeb830565e0355 GIT binary patch literal 47584 zcmeIb3w&HvwLiYjIdf*tJTjR~+DtP|GSf-YrfFU?lVp;nK+`s*Y11||X=#hvPLgRm zZ8}LO=}QnQB8d2?$Wu`fK|xVb9-^WasEP_+R8Z8afK}1Uz2cR7)q7F@-?h&^lS!Hs z>ZAAmKcC;E-&y;;_gZ`HwI64n=}_vBt0|tKr_;CI`^US!d-AH--u(~tJY9UwCta0a z`^59VdiL`~qeOIQQ?nmLd~67NUNOTDAmon@qJ5xGI*As6mK75H8g$%8R0{e~fXMbC zq7y-)A8iK(hLS{I0hSLEeQJp4H^BT9(ExB4@Dh;NL39|n8~7Pev=ixpJ-`FN?}6wp z6J-{e14jcu(2>cXKGvEza2kZvk z3ETrb0sJ1AGXkD~ejo$95BLi3ETBRGDuGsD127DH1o$1$f{ywu%c*MS#-k~cwDfIYzVz*m7k0`W@`2HXKW2NYgLbS|(LxEXj1 z_zCa_z;y(A1hfKMfW5$*f$M>LfaAcAffs?l07Y*`8-Wg>2iOK=fTO@?fv14~0_I<2Cet_AJ}9s?c+o&=r(o&in(&jLROUIhLdIC%xp z$UC9`?*@Gj(dE|?-S}Rjk6%Z0SWPmw-osZvsyO zKLVZu{uOu`_#;3!Le~KY-~oz&S->3NT%ZrQ5ZDPE0ImjZ2R;M*3vdEB3H%=L+(c9l zbOZgs5U>Xr13m~G2Yw5 z+y)#2z5~1n{0E?Z2yF%$fOCMQzzSeB&tZW z@Il}%;FG}TfG+}H1-=1%5BLG_9PkU^x4`d#KLfT8!~OtsfNG!#SPYy8tN}IxL%?3( z5O4)>3vfH|N#M)C)4)mKw}5^d$^*hc3|I#A0U6*Ra6RxT;0wUxz%#(V1Frz~j}ZBQ zc|bGJ39JD&09%1wKn6Gf90BeCz6Jai_-{bD9rg?e1C2l@a6YgdxCD4Va1Zbxa18i5 z@FegP;J3hEfH`+SmwmgfwBOm1Hv>wm;&eka1b~I90uM9TntK1J z8^B56KY{-S{s(vk=mf0KDmWDB>6Y#%(Yf5`stoUu?h^=KE8QpIUI(`>)3Dnv-Rq_M zMWnevx?hHSgLJ?VEDcz4D&99{UsXX@yxL=UrC-dB_=T`VCxPNVx z3HNWL`{g|MD|v3)cFe0XjR)?3lkPciTV+wYSza!6mU0#|y^V-k-Zl%8^FAevR6@ELy|CP-4+zq!C&i(rz zGWCSSK^Q91(4#;K{rY| zfK|j&_=oVv@fv2JF!0;^<@jCSjx`f^! zXc>K2&~iyDB%Moli|`eA5^btWLbi1Ho`WR?2^nJ0Q zBeYsjo%#elhh7gl3#*V`(Am)ICeQ%%`ex8lv_Bwd7o7__SIT29R-DY&T+9T26?7A6 zB7Plp3fe(~f^MKY1>H<{3ED#+7j!M%FX#pI70@!28%H@zx!f|$3|Aw(oD_N^XoU=~ zkoAUSdZwH{M9s7d=|ZpuOv5ssDaVIpd|1ZMlkrSBex8hm)q?Oudfmd7I5?mC%|Dd($``RZi8 zdKu4@=@k}|sLB==8_(oZ8qpY`4*4re*n`C&C43En2s0@$F@R+QJDc8ex z^)G^!iiLR$yCwyE#jui51+Ar4LAxj+=m4E3=t5d0=p0%r=q%bKXcb*3=x*98sDlm* z>Z3~qolVyWx`W;?s6xj;bprUFf;>@OPo7E&yGL_#|jIcAFdzYe`SSzXm<= z1ZWR6(PN-%VOPHa+DlRTCg=t5mkOGp_X!%KAA)X@_U=NI)dv4&+3wBYv`6?C)3-sl zV28R?P=!*UTV=Yf;B1!gpC_rA{vym!QG|0VR|wil9|C;?IK4wqGu?LDF8E-pyAa_6 zsAm{-klLsLbQe~hC4z3Fh@irF3FN44NLF@@+{tEbc ze`LkC!@UCWAC~bU_^tYnVV(Rm+SHHu&&c@j4DpZ4_<1wL|4PP3^5P#u`Tqj{5Xx8Y zg9D#RV7mI}$oQ%m;^Q*DIxpTTe;nnnlkxNO;`8|%lJN^>h`&t6*US+AJ{iAohWPts zeC-VJPs;d3dGS{JuR#0%O~%*d#pkzQzaJ6!)C1GWFCya`W{5ve#y950TlHUu`d=sG zo2H4s9r5EbJ~~bOeTaXDjE~9qb3pmS`ga`hwQPG2fb7!qNLlXIqYhm=$MIe4WuHdv zZU_91C5~@W(9x`9mNwGG%iUU3)3tW(7)7+L;O{$VQ6~I7e0#K@tx>p_!=3RxCBpnR zmQK{hvCEZAxCyq}W8)SsCl|`GVQzmPWqs#BJ{~O_ULtZuY-ZUuQJxJsf)2Cnak#l` z4&&5ZSJu~tTrRCm+eJamxO?`LS^ zyz|=~8u|TgswsJ_%+w-C)2obz>#!Hfgj-NQTIMK*PGNUs^Qf6H@5;DMezf=;~QDJz(!ajF$?LQisb}W1e;H6FXZE+Qv2T zSojx6_c>@@P|4S?T;DETO!uDfXp{T)KFBJln|<3aWH0-+sYCQF=4G#gg0`P3Sq|~a zzP7d5dMRQnqB6aVMw=GV?RqBMjB`>hjde9j{U_KBook9=cgXJBMEm;DTI?Kj^6XnW zk81lpdeH6>Id}Y+qL;8 zYHXQ}Dk%3I_w^>U{9Ms{tgqaA%g}o~H>!}ljkZIUYtfo=?}S|`6r=WpQi60mE`2e# z&DI@NoazzF~}sOa*QdH5M;QTE-mEJ3kYSxwV<$oYH%>jcB>HI0Z_JLJa<<^8~NN% z0adXo0~?Ts4Ls@eE_fEfU+%K3rW?CfJa31EJO_}{q%R(;(NUM%aa)bwrUz|Dlwt!s zIsFdDZH{}HCzUE(qCK}c+!d|`?uqv3sM&)uZKyxLT^j1=xm5wxu$%G2x_IEW!}le$ zi`OmX4y#=Ttog&}?dLIeg4zj%_1$rU?RM69g(@7P?tKoU!m+@ZsGG+S&mC;JOGRBQ zgAy_B*k&{M!{af6P(OO)eDru4F+9esIpPJh$D9jzwc!;l=DFZSgf9iPTB*UcQ?wG+ z5Y`@=L~STJ<9iaRTuPl10j((wDn4lOzp&*;yq{`ZL8UMk(vH^FI0n#$ec^7bl56b3 z-L#6DY-p9LQstknYdmm_?*H$OS$z*g+A*mgijF~ z!kpn5T8^)o=Dv+c#dDmQUWIvKyOQy3MF`KLrk-(G9EU-M_2?Gb=i}2U^E3(|tyD4xFaWnpdfF z9_rjK>hzqZPS!~7KQ`SgA4fQZ=VzI4O4iKhCmddYcpk?FPF!Fypc~Jlul)+nm=wQ% zB?|FZu7sldn^)S%=I>gmlkT_sFU0D_;kV<<9K5Ll8^`Ai$ZRE2Sctch^4?T$a$3J)5$i7_*P+0WF0i8T|d z!ub_WyK?KPsr{IqQKqKuoUR`$QSMIEkcLFhM0fViSmyvvG)}KORj6~1sI%}ibz1%S zsVV(9Dr58mx?-Pol%`v%vk5E z+s0bddAX=__G#+0^!txf^!qAVbD^|Te?@!?O0)DE>nrFtzKd2WUd10-sd`m^e5K~q z{3}*^y*59LzT;%bW?CZ~xm=F+kP{ZDCS>3{5OUxfwu{F=|I;N@;>NuK*dHEC12$+r z@}H{vCE)3Cq?m5*x@JaMy}It#q1>y5tOBQz)v3Dw?!TkXder$&QRkd9s`I^nN1Y9* z^F5-@($myw=|mO_QpBIEJ`!G_vlzqN$JeHsITyp`sSj(4aZ-U;-zTuLWXogK^I4SPM}Aoc))nRs z+=o;_#oU4WuwHYEdKIxgdjNT&$P+sH41 zQ4yE%eHK1mcWVfb;Jj$mcP~N~0KE6sK!1un9#AfiB@$Ge@|49q8#=Y#k0Yl`=oHJ2 z8~;4Y;62KAfY;j1z5w=1ykm9YeEEg`YPH$tMGbdy@1b?vSG?MXe)wuszbLC2CF!)7 zUO-tF!LAuf9E-J(dJg#^xhF)<{5|-0S*F;hHi?ssrJ~%YP_D=3$IiHm{Ei6DyZNLC zx?%iDq&mW>aO$(2a~gl16qZ+l;dk!ytq`Nw?~1t3g)TjSy`kft5YEjV5BUp%XnQ97 zIh5i!7NVsT^zK#sg@#YX-gybBMHthX0b^d-JWjQjSK(l|mIz#gBuQo&V3+fHQ_C?{;EgUT=Pbkk#J`5aWffqgHZfc=OD zaUS*%O83}V{;i-Pdnf!s?Ma+>?NN*}JNb9Cm7yo*o02u`WmJR5`*()4(kKEI?p9!2OV^=?9#4$+1)Uqch2t#*%^!6rtDl&b{L^XwI#b7W|Cda zEDF^?cC|I6*0?C*GG)i58E)(bAiLd|jaYU#W6$Xw#|zmZk8rc>P#c%Q{=ZFjzkHR^ z>R$}mp(my6+$^Jr3$lw`fL=3Y7h%~&+?L+CS$4bK`Lg@ygzQ{TnX;?*n6h(E*fgWY zlHEsVl3gukf?CL~u9nnVH$~i1b|}s8V0Qr7?ZMK5WyjBYtTAgKUdRr4gqvlD+PDn% zpQ-Ey?B9&=^I=tA&oi)xJhR(p*P|pOtdl>*cI|%1=&J~E>1@}EsZICi#T-YBN9TE= zkPIB*hi0$CsivXBV(aTLCzoOd`3mk1cxKmVT&AJVCUrK>#_cPJXX{O3T!tc;!!bgN zt5I`}E>;j>lxx)5smwlveqgyr#H^iy+zopzo(3R~w-)tBu!0#4zZL7|5bm=_^d&Uf zdmmMwbo7RB7tYb^w~Z@bz^OE5Ld@jzkt2jOCdglbAKPq`3>v^<-04eTDU%{ZIm84=P(O~W7c%mDA_b66E$k{J#dft8xzt*glvn|ISUPJxQaX4p`F(l{DMF&mHXHPP zhu>pb1b=~%lg;>yvYC^Ujd}*M;k06Vv0=O*&EKX6 zzZ7F*<}m_2z{()UIj=lC7YNSL3m!Q#gx&bYB)f4-p54I6*plPgX>$EZi)+^e*H|&Q z1`6DQYjxgau5X`)Yff{fTo2gH+3z)22JD(9@$K3u_~usTTZFqTi{N65@IJ(bT*6+u z!1qNHdm5tQ9KO`A^Bi8CKZhet{v3Xe;#^S+IWG1D8^q4OFGrQ5iqGCR%~!o0JaZF1>E8l0-|eAWaGZ^X$+$c8h9eymWQ z;WGqr#$c{Ybgp?~Rq~bTS0zTsz^VlHl~)uF^ecQ$zf1L$QRU;{yFl!Ch1|s&Vm4;U zfG%dq*}SHjjc1FPN#(dO*b4R=(hBM0(^y(3j0TvcxLF@MqWxo@ZDLID z>E8hEDule43m}txE7*)P5MC+gTR~1Upv|~az|A2(J)X4d$MZ4X^>a;Z1DQH-EiIx= ztP?c5qlI}}t9y8@pb1GpD_{u?(;`6cjXds!HGHdO4GrdgGg5>M!My?QH{`h2&5`mZ zi+jfe_k8PUprBA#got;-{$DDs0pAnjbF2YyF-2 zRz|DV8XnC5!Uky2cw3nc8?cGbqF6H_CvgI_N$icF_qatH1P){*1V8lxB&E(`}Ofny!{y=(`50a-*0bDSI!wg@BqYCrb4 zqAl*S0(1QT7$bu%6=JXC@sE>`aK;+{nmPU}haF8~{5P3vc$_NgY&)<`pb*mJkCs8) zris8N`0bS`G5Q^lg;C}-M?ZQf&*no4E}XCzLlQXC`ZAARce@+gPw_GN_%*^Td-jm9 zWXu7|Ddc=*+Ga7n&toh0P1ZuJki+l8{wSnjbobpyRd?i05Y0B*%+kk|2eDt_bLo8; zX%n9!tWRoI%X6QVSSQG{&YGnN%RBq5X+zwU^UxaX3&d&b)bI$xac0XWw$sMroQcnN zPnX7QC7-LW0Qlb8MW8B1^8lzH5;+7~f-{8og4)2v$3O${KLVOYkJc-Ec3&*+i_^a?&lSAcnHBmWt3dsD&hP=@Z>Bpy z4cv%+7?jiDfE4K#Q#Gg`H{Uyie<>{oHSpwQ8z`syq6l9}kAnJfzW77o@1f^F4Ycgf zpv#eN4SMbupsldaDsr9!>gPA2Kz*DqK|Ofm`U+64{~6FqdMo`I)Q)uZ^tBRtpoBhNLbsOC zO$yyqLf4kiTQqvJM(--2TWoZLjjpxPQ5(IZgubBDeLCHv)2DQL#!lb0)3@#PxSjsX zK`%MzMF;%`|6KG-7oBv`AKmn_oBqR1fA!G+c<4_a`bz=5Qb2$5(jV}zkp8of-ds$7 z_0ucGe0PioM3A+PyNCysi!zER)(_@205!q1<~@Y_YW0WH8ZO9`3&fbl62Znbw6 z!s`&S9pGnkEm+|XpcS(H;=W*J>3ZB3?`xPtZ4OhOiahC|;yO_*lP#IGA_AEb> z%lZ-s^WwyeM;YE@L_Pd0ZaZMsqaoHRGpN{=Ot@U+%v;PmMgnXV`EKBm8dR&xBq4v_SLE%7nc#q)rVY*=uCR-c1b^OLQ1)W}>U;5WyJ zd5`aY^goZIAbkhVejkI&1xr;a)0pGv^GL(GVm;TMuiUL}f#va1K&!r6eF4|z11cO| zdV|W|@~bquQr(K@iMsQUrhGt~mDcq8RqfF`l=($UbfSnC(`^-+TC6V8)!WoVb-4Vd=(_IG-P8!pS)YWQoUb& zMDu=EqvPsUv$8d7lz=C!X%DHzhj$-ZlfEaS*NVkBJljUw(DVTo$gs@zZ_>7?AJ7m~!9nG^9gjNgk828u z_k-FP$||@EeW+=kYcwYAuL%Ue`_=pAf-!ZTM$pVP6&0m4N2&JWxu+MqE-zMK-t-NaBfVf=oqw)+gHo<(K~)K8 zwwu+hP_If=Q8oKlHMkvcmutqKINVvOE~!v2F4tTERdH&bh3BdNdTF^<;88C^N%WZ3 zsd@jRZdGqaj>1xup{ts2{<(@x*R&#(4@ql&)UZY?j&KdLYR|9NW{1=@THp_Azv9vZ znhu9Hr&3L0+^StDLDK?oEO}$PB8-~V52;(A6++ip1@uC_Sf8cO)&uHuZ@Slx=e2g1 z-D5AXd+mkxV*4!nYL(xx z+Z=VPrhiIZ722WM->vnBQ=0cZTK~Kut?<2Ce?-0bO0D>Mt-n%zs&?4Xwb=$y)y3{_2c*%uitEUyh7$^kjakC-VzIF~7Ct>V=c|bxD3Bf?t<5kNI`V&UHdalRDQmmEU=i z-|9SmYfXM#T8+hTt>#7;_pMu>CwO)1HG(sjy+&|aWAbEj#LU)A=B8#cHxL7Jv#4C% zG>IFxowrFOg&HzV>)3* z!)gM6<58K!}lnNW`f#64Xp$W6~uwJ9rsl~9Q>I0V@RKJN| zxWIB4*0w^k6{v@BISVuO1DDmQKZLba^?;5&i=8KR8=KmfVQO_YwRY3gdh$%|<=Sk^ z)CMe58-%H?QV(CLg>TmSRd<~hfz{P{y{1*8JgsJqx~+ojE|g(hI>q}2&c@Mw*u z?Jdw6JWvUnR0*42i}$e5+x3lTH9Allv6v4)Q{+wnLrZ;Npfk$iNp9*M=D`CY0NuH=%b~5t@V|G;z^F6>LHknx;3%wfP3w2JzMDO%nz=$6JNw0dIEQ zX0OAyM+2vav;xUcfnLixZsRtF*&KT&>o}%=i#4I+f;W2R{{tn`Qpk-4!qp^umWLob-AQ~m}9O#7L~fPubnLCX%8p24%OsZ z<>Xcgbr-sEN~@gQDi^oPULcx;Sx~mfm21&F*`j$fwup7t!}DvMT34=aEJp@0Dl0Td z>y-AmnC%(cqZActp1c;#%C%@hMzds#W}UW0E`1iasJ>i19~&HD!eEnN#&8c*U2NG= zTh5Le!j3v&N9$onzxz8_&pyq?M$xGiz$8{^USTP1yf79EBv{d{bH!F?bhWu(;KitE zBJYKXd?L?8Vm~BIq^{lz6M0maNLYCVn=zO(yQ&_;l}}qKl*%sbyOZ1H3aOZ6M7s*O zU9U5j+3(i8+$=VCv#^eLXtRYO4~WIGT`NZcc73+yWIK+j0A}2Y6*!j4*n)~>dALk3 z$5L6BOJ$o}D#N&{$KS+DWmTV*Z@j;Yw&fdd!!+JbtdpxW7dl=muq-j8BrWkQ@P&5h zT3`+@hYf8ua^;(1Z&=fXE=)4T2J56IMvB)o!8R=@EEOAF4RZF%@g0jmoIY77Zp?`jhbwxPk$Iz2Z>=sawUoSMRz=dW-f3T`4V$IZbkMvRWtxERBK zy)J(hih~Sm70ROo&6j3zJ?kWva?qEUrHdN(xsB&^-e=vR*)3fZbNRtO&0%UIcPx~n zNWFo3)}=!GU^4h5LwA_k_eE&mSM#*b=CdZ~FGBl1i#|pV>wyXFy8-)UtrYt@Q~Mm; z5`(qRDO%#J{uX3kxPqXm~{o3bZEYt?_l&YTsk zSbc@c^?JQwy}nSrQ!Q7Yc~eJFuhJKk=@ojYO!q_auDGmR_vuCIU0S)_XD_n*?Q`rE zxanMlJ%Nh-K>n@)ImK2ew{hS^3d-ULY?tM}f%i^M6k^4OM7$k4OS?lPak4aJBC{q% zpTkWO39$#a+ht0(DS450qFst!#BI;zzz$yQ-?6i@G8RsBo5(zwF@;&4R0IwK?3&${ zS3(g4nCp5`#uV}3hi|<2u^_-v{e<>4?HcVa^@G~uI6Ar&M@i}XZ-D0Gr_A`IBJ^nJ z290i0-D}pYIYc8k70FAU*3?g^1#7N4bk&-zYqsvLe{@aoJoVz$>cQ)waGLf3)p6C< zt@p&QQf3uuw%KZ#Zu|O;&mG3*`GOnoTk|_SKH`IJK6=CnG*L>@9iVvpg1hqkXP)ij zITijm%zSr<)3aZ}?>0=h&38!H&u8~c`B@uNzL>|9&ySgMxlFSFpJV#qGk-C}e7~CG zVgOTq&&aHsYv;UUl4d0}-|68pI6a?0bGklSC;aQ-=J1T9{AM)AZ^j>ozfOkl2W7sw zG;TMi;a`$4=@yZepHs4*f3v`(MUwVOxTpV)e*qSKbJKZ zrE(loPRFOJ?6z`qyE%-1|H}L``DY&ZNe0v90GG82y~(s2zYWNAvnYjS7Z7!9LH(Q_ zN9~hJG&sDH8m?nUj7Kil<{Uc`Q=61! zPD6dr@q6HNfn6qdl1(@9`;g|7gWc~My*s1Fn;dQCd0AGCP2&o?V*o1=&jkz#d;Z}yZN zB_d^=FEAy14*XnFDaXfLka&?<)+!a{2J=%RWR7ocehBiF!C$^go#>^@Pwl1rAlXa# zK`cRZZ+?*T)F9WXL2d+jHa~L2su6uqK|e$vY@OV{LbzrScemB;g4!l}#+~0mP|XQt zka@TlxxcuR!ZoIH)Z{6LVJgR}lGTQ<7;T#ci5a3NN`y37_H(4HgCc~bS(X=K_{up% zdMK%N{~(A?%^f%wcXj6`Em??MMb4(@Fs|V0WkqmAoE#?w2}gGsjHYm=Ge&Q!L8=H?|D91&xpaXHgN;~i8zO=RD$WHvR_ zn@#OVWmB2K)K~{;H48V9yB;$~Dl1B-qJ~qtKG<+DJ%pZW$e(8}6hXAKC7Os24GpA{ zv6f^a7EKKeM%!A467B7YWIW!|lp2b)CYz(pgDvf;mbO@;Wk*ZPj-jS#OMFLjOKfPU zos1=e7^>;#C zs_HwOL6gSQgUg4LV`F5j=;~b2)4i^X^c7vpHl9y~U57?TOvcBSk7QHiI=^dO*M`pi z?)B@)y{fZsTYvZ3uJs%HN$DnAcTZ0b8Qtsp`a9Px?;>S2X{%TBSI=rvdPwc*rdd7R z%Qkdw*s`s+bNQOi^SidK>s;GKT2D8A#22Yl6rlAWosKUMxRIZW#TO^pO2J7g-Z`Ai z?#=1;%<5moeGNMsHXks_0JP-NyLxngQ&PScwm*_lm^jrENm9!_;oc;=Kc z#bnO%n{>M7F5d+eOAVhcNlrddF`H|+(ZR{K!HAif&h{{|l%XloyjwFW$ zlVju7w27g2N+MQ0VeQu?_d*z@(Ac!_G7+*otx;ho!R8*uJqto<4`tvFgY-sYU~F8`9p z+4R8vaW0}L*XT@Yym4bT-I1rkBV&z1ld3j#ZRo?0>YyC=hZ=c)#(dw{&$Cqrwe$>* z>}|Bn`}oMn@K|HE4FMSI!Um5unxlCv{l?Ug7|)wx9n>ST?MV-h42}$?8cpM4+MZ#o zarTQfe`CHSoN6j3RzV%K?C&nEgNDzzjHx!bs;cYIcq*Go4(IIGvgF{N(UI&pFG*I8 zWDh2@yz;}(Wu7_zr_`1+n;o?0?{E9W0-(yA{i|B5ur}p&=TL5;RTV|k&t7HGvsoFG z^6YUIKbw^`pUuiz&Sqt;9klzbss3543%YpL_<|~)wYs2*XRR(M;#sTfZ0cd+Eb`TM z7VB#7p#5iQEax2%RAG+!->4QN{C}fb4E8f$D@|Q_bWrp)byVIKly@yd$>9U(J$Wm` z#;(lZ@W>bzL}FRUtK-w8S)0Nc>re+(Op~Y^TaBzNWZpFKYg2m%WCC9NmQRzUFTFF9 z#5o_9v^CR3i?#QP)WH6oyvWOpVb{hBaU8&nH0~8=(_@W$M#iyT;@t^0-sZ_g$FwO@ zhXzxlyzy)7vRq?T8&X&W%ZxoZX5+Q>bj9zG2jya6J!l=L=5{i?W123T)kt&OJX7J5 zQ=M{p+d;9{Q20c)70L1BOjV7D1LCp984ikB9j4=hPsYX?K(E15jVB}|;wc2MhU%sMq++uxLr<+1p0%E{--9aPto9%Y_6`mv zhld+S(|eMQ*p+8T#zyhz0mI8oU4NR?V-x9l`^<%(CVB5fYLsp&ckRrj_fD18X^Nhl z)NFWh51-sM4kxkQUWT1@DhZo2Cod3(EZM%)zWsdu$$R>V9gbxpyD z`t!p__JeaisF}##Jv^M+iG3`#@Tr`Mnw)LjezvKT!_1*PIjj$d4w;>NwlYz?8J0V( z%5P2I;j!`5-VU0bm%M*hc0`!Z4mQh z&9o)1P3}w&c8;d=Q%+yjhSZK>b0G35*>tVzn|4~AW2MbP^ZDpY5EgwP7h@LPxF)CQ zvUofX;5r-E@POhuWF)(@ad&EbAe#hRd8>~xeh#s2|1gdyt4@~)DcCY}kZ0Zc{_d6C z%gs~f!h9c}soVGnk!G*&?OL}DC&=si`?js<>gwHwbL%xUCqH;Y*99B9`uevmU%#S@ zf-_8ie4wxPHUUu}2E>78poNqT8!v2Eo>97q33>08^`>pI=J847l- z5epSpZC}`Ot#mC4N5ij{kB|EPF=uwDE*9RA+>_$wOs5$u5w~XCRoml=Rz~Zhv6jVA zxMGW=u~t`Ymy6z3bPA_a*Qu|S)9Lb^DW_A*5d4`-EpEniQ~Bhj7Iq5BH@a*wQ*=$lPlWJY2%CIEeI7Q#9I+6ONeudW-d40 z7K1A;(l)nn9UR)+(ws}%oM?8nH&wQx2{CSaYhAPr?zkBkYiY{`##-9Vz<7KjFdnx8 zTLtSPd%QJfrfiN+l-V4YWj438Wf)SnqnFH-@feFxq>RT<6sL^0%P>g09db2O zHb>j7l+7^|)znhi&V3t2S(ckqOLx3I=0Z0`n-<5AI?gWchZrg`gX2(&TyPwEA%ol7 zCxV+Mwom_)?p*j0P#OfuCr^5FTq?JFh*41lN@64D`t1czWq zm{I5k)5$|3*2c-v+rok2P#1H>V+iB%U^)^|4p+RD!=UTJfuVpfXnk}sj|J1w)`Yyx z%^U{x77h#pPRDidI4~WFW~7t#<{WW!fvgY1z^u5XT?BH?JPgd5<04R29ktpJHUh2$0gOFxt+dX7OVX zaG9d`Xe)Q9=scvg`lmg{p&|hf0n^bkA#EN2qWUINhN4rU{$}w|lAH{4a)I{CSZF_% zB2Q~#DRP>*)mmjuU2mNx%XBM-SDB9U9@908^+(sl4W-jhSa}J>j2EA@68vY4UtZX? zHZ@^#i}U2xYTBYW>{%;BoLihuvMc{YiSbf1k$_ThL%3h>u>GIw*H+Q5;vSiK49TY) zV>5N`)O50M^Ll%xoV?x{!})CPhMUORe3sRsRcb|ZD^@zJ6LDVYuzJMLwt8UG(c`Lz z#tQ9-wzUfvv=n-BRQ+iJ>gBf(39qTJ^9}Mxv8`5|6em_w9T@S*bV-#%$#lFj0UPcF>xAw>pFY;R;}+R zy{~`$hAy&+JE_VhQa9l~=BDn|xH!3~8&@wkv4ihYZo=V}4PPA1mn(JtHiK|?6CU`G zfrF7!1mNc7YW%z@2f(3j+Di5dyH}jwg-f5flDQQnZ{-V`TX8v4xro$@x~b}-(>!y? zJy9yYDDUXmTyJ!c`uZ?=lY_W^H&!*29^0G7wbTOX$5IWK@To|8akp4L#iLRglpe!F zn!ULEI)eN1qkNo5ZW+ZF>Zwe6aL*6d1XJeSM(e_xm2~BBa_1P?@sJS3aOM1(Oucz$ zjslas_15*>F$ztN80I|Y?YeD4{4|q-W`b>ILECU`FLy^%E8Vj7>-k7@ zZEsIke;2MOPoHY-#1-8QUA;Y>%aJrVebP2Kmwu^G+>L%HkuR5qB*^goKMj zbirUYg@;colj&hRl&4bDn=ckT{NfwNC|0E8OC5MdBOWMF(cu0p-U=A+#~t&v>EU6# zIiU{WAbluChBY=wHwS=g;_4$gtg-DL5>LfU5gOL*TC(9b?Ks)_dn8MRsSJrrPnW4lhTc0MleRO*+_6fp-9r#CYz35Q7JKnh)8s_l>~1`EV%;SNJUt_souUQo!2Tthq!I*A*Qry(g)UR=S2CX1&tESmgl7NRkh(u|41Pq~jRI>LNlNnD=k zG+WPofxEuBOBiFKcg#naeW~n$G#F`IOpT+ zrEHmCsTVgkHsZ?A{-MS()5MLDZBH6w&bxY$avlgXbWi}QW;Vn83JNr9H` z7Uicj6?an5wls_(738Nv#|!UaIwyk%kzvdO@P`LdaPiAHBjF6*m=#gnMPX|ugsd7m z^WH(B6=$4%T^^Gb2VO3PO&-eL~t; zp;5fg$*)tAIvRyfzFY|(Z1rd?7Z-<*U*XoZ zu0xpfPp!y-C|;XV85})gEamGCPr>|}SAb)&n8G!rR4TJ6J(eCw5A%fPLd5#a&Jo-a z5iMJv89v-OjC)w?dEVeyX~Kya?c0UdDVAsP3OMfkh_*6seS2{Mh+lmngMB0WVH1Rl zUveVmMmD2!2QPk#5HQvFj#))?b5_eH>zKZ>nDIfz_m7chJh{`nJO|R6%V`#Lu{sT$~j~080q7p zw~EI{PPyZS_KiqAEwEgsD*MJqCKn6FvRK^)*rZHUHo>%d<;4=sEltt*-&EezGsZJ`9||=`ceJ&}n_Ckte+w(8 zRL#atyduv`wY3kl#aaeq|B#X=b_=iaq7*8QY9se$B%RqB{;BV8%Q#$cARvb4gZf$Fd#hT8hsGCK_?K_5&14FH6v*H%6xP2%ZZEkCB zKARP{a>dO}ZB2uzSlijGnCnd>hvLcB)LE=nsPsTf5^w#d&gLK%8a*_SOtd81&u0G$ zg>G*ijK$jGXVZ2FeU7&^YN&Eiw>n)lV?$% z+qm8x1F@#T*7(_+4coci_Lf-7j@ITKXOG{>?|HEW!Ebr&XiuKSp`70YVNF_NiJ@rA zj{hYo2*VO>#Z9I}{2!ut+_BBEWK$yA6o0K9E6!1Hz=)qF;p~ISmiFktPz-Bi8o_@e zLu$M7j28HzvQ7W2T8979;$F2Z6vzfB&=P0}JP-O@;N`$K11kf|1K$cz;wuWi7Ab+9 ziHDWMGWHBAiTjnrx0J-4N%}V0qN}^Lq>{J4~5{B|S)u#N9TBg}RZS*q@{~zNX z)QGBnsJ5zCsJ7Bq)WA};8DVqEtX8OKg(ol;NO3C?SLf>FG6PGeEmcj3Qs>O2pmwcl z`^$f-w(yU%K+GyqL6Ppje#zMRpi2Tz1ilRhrfZ|wwBSV3P>u#xw8^ZfEvPj3ksKox zY8$u07Wj1FOM$NkT20}eusA+pM%xknmB63y{b?)7vsRL4%`88*{69ARKe7BjG5tTa z{697QKePNlQ~G6>{>b9o2F_~(R|l>W!faInH<%r`RteN4-mC-$5}TC3qQvD&V0Yr6 zlDJR_)F+Oy$oKNsdHnS_e_g5sUY|IuB(6~cgNgSmiLWY&w=0PnCGi37ek4dJGxUws zxF1=aRDquf)mC19#CBb)^5$D@ceg4xhmZ5uiB{#>BgV_Es{f@9rAkozNqWKCP~0-*%|~nxF6&dCT*kkO*?)ETS^O@hN*htv z(G^_Roh$GopQ@|$LL`3$AxCVtbZvP!d~AhMbV5YAOD{syOI;jwbmi+G4&T?M6g}C+ zxjd!Y5Os1TM_qA#^5O8EE0v<-D@jq*f}`O_DTif-!WsKV0Sh`M_N^_FAoS##k%z-4S1UzV ztldCbS9}d5asH1B+s<`wpqHbTu zQODO^{BZb%wJ2~MN6jjI6QXWf&ruJqzwF`giFHcRA4Syc(l;xJdq*$F-QD{Z<>By? z>y@Gx*K_kqj)vdL;m=-hCBl#QDn%!Ixx7H>+c?vM8#wZr4R1%}D;I#>4IDYA^c@^| zcOOSS-uEs8#_JLOOh4Kv z!si|h-^k%NZMq5Jk8M_-;|M!` z6saD3r}7a+x#=PesumG-+{1MozfRQg!gY#K!G>|Uk~pGN{aQ=>R7p6L#0f=D*p-UJ zkdhczs(zzsi54Z4n2#S{<=Xp2o(Q=5p{irXff~=_`e23;pX3Wy$ZAk;5jg-mQ^peiOc1FV#;Zm@^2wC<^R)- zDgQq4oha{ZaOZ!HPVwJ5#ed%v|C3YvFHZ3rs3ZSVHO0SlihmS-ZaeeFpK{=J2!ov# z?G8zQSkif*yAb~oQ0B`I${#B~)93Lu3d*uN0Q?a`VR`KXo`gVHX8gb4mf=U9x$a*B z<33aW`2VAPD1h?9M0?;qf&a=`gS!^)BdZaPbRBRX!~cb+BAn~`(RP#xcO}AEUkGk4 prv>_ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json new file mode 100644 index 0000000000..211b18ae6b --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json @@ -0,0 +1,18 @@ +{ + "version": 3, + "artifactType": { + "type": "AAPT_FRIENDLY_MERGED_MANIFESTS", + "kind": "Directory" + }, + "applicationId": "com.kilocode.activeagentsliveupdate", + "variantName": "debug", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "outputFile": "AndroidManifest.xml" + } + ], + "elementType": "File" +} \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar new file mode 100644 index 0000000000000000000000000000000000000000..37a2d0368ce6123ee95532674d56c2b4ee5ff8ae GIT binary patch literal 45277 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℞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 Bo!bBa literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar new file mode 100644 index 0000000000000000000000000000000000000000..d309977949b89270116d1fb8dc6ed81139f6f93e GIT binary patch literal 336 zcmWIWW@cdk14Rx+ra6|Izs>;iKv)}ylk;=+vomw@lk-zj^%IjzGRsmE(^Kn+Ro0*|NWhQe8Em`;B>4N<7sTPh*{)>jL|TKCPCIu#|9Oqmdl=y zIPgI;vfLspze*LhLfDNK{WNpdLMxe@VW296(!H0IOTD(&s_ z{=J^Jr6C zjIj~gjh9n-fIXDOGaz#->_6_v&yS#m?%?L`s?#6#f{nnBF-9(7Q*EV4AC-=sL?Z2$ h7=eETrBFT5!EZ7j*e?dKy~g#y`ubS>K3t!Rnr}mS{IUQ5 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties new file mode 100644 index 0000000000..13e23ffb54 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties @@ -0,0 +1 @@ +#Thu Sep 03 02:57:25 CEST 2026 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml new file mode 100644 index 0000000000..b37946f45c --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugAssets/merger.xml b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugAssets/merger.xml new file mode 100644 index 0000000000..0d855e1833 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugAssets/merger.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml new file mode 100644 index 0000000000..2e4c678578 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugShaders/merger.xml b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugShaders/merger.xml new file mode 100644 index 0000000000..0195be372b --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/mergeDebugShaders/merger.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/java_res/debug/processDebugJavaRes/out/META-INF/active-agents-live-update_debug.kotlin_module b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/java_res/debug/processDebugJavaRes/out/META-INF/active-agents-live-update_debug.kotlin_module new file mode 100644 index 0000000000000000000000000000000000000000..9dbc290d21e8fd8815939343a7a5484dc57cfcde GIT binary patch literal 24 YcmZQzU|?ooU|<4bMj!?QB?c`900CkEQvd(} literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/kilocode/activeagentsliveupdate/BuildConfig.class b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/kilocode/activeagentsliveupdate/BuildConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..bec532c38036b8c6a6be9786b5dfb7a118346ab8 GIT binary patch literal 657 zcmb7B%TC)+5IvKZ9b+J*&^CqgC`E!UVzGuI#UUVsp@={v*hDwZCB5`wBj=j`s=8>^ zML&R#sygNsq;}KI%$#|hdEEZ~`3t~DtT@OaZ=v9#h!UZ4DQ<+PMKtg}^)6*-2&GLG zDYHe$uda1$*o1;fQt80Km<89xI3@^Fqe4H9wGLZ{M3jj+t*^+)phzsU)Xf4UcJ zcTdiaS~li*fWGXdJV>F*BNHYA70JW&swa~Z(bMdy25~5KMmms`iJBQbd-#o*!?Bj({ zV)>RcJl4dz_~AR~>s`q_%PgG91(xo80v55v$i};;;-$L?P_9}(Q2vI=tN~%vz+$w_ Iz8v2F0hGR!HUIzs literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt new file mode 100644 index 0000000000..78ac5b8bef --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt @@ -0,0 +1,2 @@ +R_DEF: Internal format may change without notice +local diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt new file mode 100644 index 0000000000..a3289d34e4 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt @@ -0,0 +1,31 @@ +1 +2 +4 +5 +6 +7 +7-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:2:3-79 +7-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:2:20-76 +8 +9 +9-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:3:3-12:17 +10 /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:4:5-11:16 +11 android:name="com.kilocode.activeagentsliveupdate.ActiveAgentsDeadlineReceiver" +11-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:5:7-86 +12 android:exported="false" > +12-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:6:7-31 +13 +13-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:7:7-10:23 +14 +14-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:8:9-71 +14-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:8:17-68 +15 +15-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:9:9-76 +15-->/Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:9:17-73 +16 +17 +18 +19 +20 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/merged_java_res/debug/mergeDebugJavaResource/feature-active-agents-live-update.jar b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/merged_java_res/debug/mergeDebugJavaResource/feature-active-agents-live-update.jar new file mode 100644 index 0000000000000000000000000000000000000000..34b16a12d1f24c86b2c1d88aa0f9c3c61381dc29 GIT binary patch literal 225 zcmWIWW@Zs#U|`??Vg?48RjONrfwTk=n*p(}Ylx$+r=OdCVsc4lS*mVgdTL%tv2G5C zU0RTmSdto_lA2VSu9ux(l9QPipPQdjnv + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt new file mode 100644 index 0000000000..08f4ebeab5 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt @@ -0,0 +1 @@ +0 Warning/Error \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar new file mode 100644 index 0000000000000000000000000000000000000000..23fcc30845ac4d55554edab6e2f42662ceddd288 GIT binary patch literal 102912 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℞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 KFYd!kkNpd?^ie(l literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt new file mode 100644 index 0000000000..4be1373429 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt @@ -0,0 +1 @@ +com.kilocode.activeagentsliveupdate diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab new file mode 100644 index 0000000000000000000000000000000000000000..bcbd2006d42b015f261176e3ef1970e1420f83bf GIT binary patch literal 4096 zcmbR3vzw0r2$(u)-8~MOgeL6UZ;}YI`TR_)T+>+ zpi(`tSFV>Tjl>6{J7+}ciNaSO^d!rpmvc@e>Znw4I{xjbGS&7d5wFG@F^ArwJ2iX> z_)}c0Reqh*zFXJ-o>?+ymP~n~mZ##B74ffFjx8H??A$Q*e)m7Go8FQey3CB1JKpvO cfjvgR2p9n)U<8bS5ikNqzz7%tBd~V@ANJN^Q2+n{ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..b4da1318113a3c9f5b8ac8aa84c2e7cfa999dc25 GIT binary patch literal 8 McmZQz00AZj000aC0{{R3 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..01bdaa1da7d937c7e7d98e54ba912f88ab95c7f2 GIT binary patch literal 8 LcmZQz0D}nt0GI%g literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..3e5c9baddf1f82ce14654ccc8170ac4969a256cd GIT binary patch literal 6481 zcmeHL%TB^T6g^8ngGILm>jQP!s00&H<8$GH)OLhMprj9fLDznv-{HzL18O@pAt5n= zcp+2faqng3+;f{dw}q|>B9%5SRhXnvqm!hK3zcSZma9qL4YFY`nBE4XLS5xa8VuCO z2KebC#~4$z@Pvf@5n@c>B4jT>#;;;)A>!`?3Jll}DLFP8F{KhS*6S^5gtQUknWKQR zinemvsYNhX%tbv6%1# z37^$doiA~!9*%JhpWZvc89J6f+=@rsZ=bjouk=fvZdN$ar_U3v7SvSl_p3^t(>YCU zN>uCpY}|tRZ3&qFn*NJU;20M@@|>DFI@~mU91{{_2B{7(2W>XT$jm}oao!;*3d+uc zRBPQC3qx8cXvs&)Fw7|DSvbA?bDRVbn>h})gd>$$ak;mFu(N!@ i#DksZ3nm_GQq>}-ykmZU$Jlj*RV;BYu2QafiTe`?K-KsF literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..df14d918c4c7958962c31c59b348afba7775c6ae GIT binary patch literal 32768 zcmeIuAr-(d5JbUysf73_sJYU>ScsDf38=l5^wA`8J?MROmjFH#6dEh^3|E=kQR@klJr@J%cTOUx-v4KB$qN=#2> IWMF_J0DvbF2mk;8 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..508a6e835a87fcbbfceaca8d36b3f605b31af196 GIT binary patch literal 32768 zcmeIuu?d7g5QfnOHI(bfRM1qc=fVpw5iG!BjO{_N0%NC%-NZLA@yGvCovP#r5FkK+ z009C72oNAZAiKaCh343n>>&XH1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAP@rI`1YJT7z79qAV7cs0RjXFlo$9#a{TOf`Lg001^zL* zzlQ0J+ifO5fB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly SK!5-N0t5&UAV7dX4uJ=TGYPW* literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab new file mode 100644 index 0000000000000000000000000000000000000000..dd726ca7bb1dea07549a89e873a59e2f694168aa GIT binary patch literal 4096 zcmbR3vzw0r2v|S_3N>7#!j_Ttk!DmNMeqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@? L8UmvsKsW>dowyfC literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..7f06cf4b7581c42501901a20a8398eaa05c31727 GIT binary patch literal 4096 zcmeIuK?=e!5QX7)qMpH`E3q5F1BA8|v8F|>=l5^wA`8J?MRfXkvFKh)6nm6c~^O5PV+vp zzULe#_Z%m;oKQ=s*hg9VIjx}GUgY`a8u4F(FE7FT{@?4QPZ!rtyLI{xbiqd@xB$w> B*R%it literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..508a6e835a87fcbbfceaca8d36b3f605b31af196 GIT binary patch literal 32768 zcmeIuu?d7g5QfnOHI(bfRM1qc=fVpw5iG!BjO{_N0%NC%-NZLA@yGvCovP#r5FkK+ z009C72oNAZAiKaCh343n>>&XH1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAP@rI`1YJT7z79qAV7cs0RjXFlo$9#a{TOf`Lg001^zL* zzlQ0J+ifO5fB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly SK!5-N0t5&UAV7dX4uJ=TGYPW* literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab new file mode 100644 index 0000000000000000000000000000000000000000..0e523c4f92a970cc56f0e3de5984a1628fd2fc62 GIT binary patch literal 4096 zcmbR3vzw0r2$(jE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mk0O=tB07Cl?!vFvP literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..0022f40d6f4d2e88ecced75945a4920b5481e6ee GIT binary patch literal 4096 zcmeH@O$vY@6oltSU7?_5ZGsLU8WH`{-}xK0g(qkcBPj0;^K|?GOmjFH#6dEh^3|E=kQR@klJr@J%cTOUx-v4KB$qN=#4H zV*mn1Am(TAWAJAPVen*d17c?eM=;wTC>jssyJX5YX}nxBC_~6D*(-9rS$g?E6{0|q0H8R?21f>W zFe`)sq{9Wsb_L4C1I_gW%6o4sNn+i=w#O8xO9H2^AfTQ=pdGG2GeUrRoq;NWCb9DQ KOxq-Uz#0H1Ha!pk literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..166763184ebed72097c514fcea60060beb56c1b3 GIT binary patch literal 32768 zcmeIuu?>JQ3`IdJL}I*5LCXx38HLp{0DZE7CAvuG`W4?7C5~_e2oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pkslc;}yM8SV1q28XAV7cs0RjXF z5FoI%Kws1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk v1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly@JnC<5VZus literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab new file mode 100644 index 0000000000000000000000000000000000000000..b897893c660b50cdf46ed2f79b32bde023be5cec GIT binary patch literal 4096 zcmeIu%Su8~6vpu{E4y1xYT^?FO&mGw0US5%StJ}q5D0=GL_rWpi4X*lAP7MaIC9h* zRA%>^rGMAK4mfh$U@h3^+h?zJ4(wkseG!w@O7IA4X|PF+Ng3N>6`TyuSWCeQE9kBL zy$qj+{ZZMl|7UIBx(5mS_^J#0mY2V<9g*rkusJT(I*^=@s^1Wul+M54#`5eF%2Da` z1Gc85$~$aKODAuzIwO@{p=WvY0?k?J@EJ;T(!mpC=B2$yn_rNMZCE#r!UHTXO8FMF tEOYlzHQ3%Gy_xPS||fD8O5fj$D(W!C@z literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..c215febf019e070ef08c69e2a59267f3a9971798 GIT binary patch literal 8192 zcmeI0L2tq^429p1$$hW^+it6dI3TznO`J2@jcU*$O8EUeyF2tSi4)u8x>Bmda#Fu9 zZfga=3pS{+#~r>x>v&IfcCNE3 z_E&5hwswspfVn>NFJlOJRSE~HsTf(!`){yWd{cPqkX$PNL|z4BXl~BtS`0z6;e(M1 z8OaBs6Bu7)-n?rD1WafN>Lb}9` Ba=8Ei literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..d49ddd162ba3de801b05a5eee503802e56cb8518 GIT binary patch literal 8 McmZQz00AKv003|RR{#J2 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..c14ff158528dbb77ce256d3f806f2a4ffbf356e1 GIT binary patch literal 8 McmZQz00Bk|002S&IsgCw literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..735260a5014d4b25e527529d2587f0dd2bbcd3a3 GIT binary patch literal 6006 zcmeHLK~BRk5S#-K$p^r1sDw}tR6<4NzDialG}x8x^vK(I8k4j@xN(L_wDRsIyK7I@ zqa*=qx6X63^)}_Mxl6fu@Lc**J=V3A=ZzhoY#9BqrfhqEzXFdp-0%!|MM8%W10036 zp+!ZH6YXnSdybKwLTmW)k2o^Q==V7$(#p)#a}2yUGs~3Q=!w)`sGTT*{zQMkfma6V zligjid{ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..652b27ea8517c734a27e9e06285d72fe266d98e3 GIT binary patch literal 32768 zcmeI#FH3__7{=iv3fi=3G#dP~>F`S!R>Weq`WY6SnC+g_no-IafPnwagcX009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C7el2j5iOu(;<6gRz-lf~=eY%rArnU4rt*8BTHyxyV=}X#36NdnSiU_o_ z=jpPP&qI)3x*>T~9B6zB>d65FkK+009D(66j{z g{fEQbN-b^Ke=BgE%|4z6UrTu(e5M=eD4q7g-!~XIQ2+n{ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab new file mode 100644 index 0000000000000000000000000000000000000000..e7c6069991d57a7f7a2cfd657cd1f3e896b30cb8 GIT binary patch literal 4096 zcmbR3vzw0r2v|V`3U(zb<%}(+?T3|vOXJ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..e9330464178d959dc4e0dc988ba552af0183f14f GIT binary patch literal 8 McmZQz00E{5003kFTL1t6 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..ec8f944c8acd49bcace4e4c78d4306ebd9e28078 GIT binary patch literal 8 LcmZQz0D~0(0I&e5 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..ca5b0beb00a476350a6579d46e1cf2872858b45c GIT binary patch literal 2733 zcmbtWTT|Oc6kZD>WSNW1#VCe=xuk_mi5VU`ow(B(V=J+#v0YmvOkX^UwSXE~(n#x+ zymffU58;V_($~)PC-jf>?5;1ukc6h5S)V;;&z{vDo$ou!p)$c139?Pn@d7LR$_ug$ z5`=b0iA&2kygbJK8|?H*i+TaRAb(9F_G@NUNteV>CR9n2RKzv$FDa6avg(1fAA1>7 z52tSAxFTs2N4gfzNc)oqvL=I^0;zyZl+(9z5~5C{!)nLt=HA(k*YPZp!yRhBCvdDw zLkDI(-@=sS3fx@a<`O)=W4i<{K)@r&qpd1-t-!ObT*q_SHFAf6hpi3{>Bvd`(ROY6 z6PnoBeS;F7+h2i)nBu#xD0*@)PLajCqIo3SJTTJlxm5T<9wmxi*T=5yxoBP7VozLS zkKJQWT%>K0v);u&(=4O0v+qSZHAo>(=`2VYQme4k4igNi1uX$Q2oSf&pX ziZn>{nl=N8orMI?fz&{zK(fTogIqY$w8czZk8un4I7(|vV>{B^^E+*M%&vpZB0XX2 z2P(YSl!#T9AZ{k=4HjKniCSRaD(FwkQgc=m!|AnoBf~vFXGJj?(Wl0ARxHu=#F(y* z=}FM1PyZ0{Z{;;e^E#^j?-JV{zS}%Ryi-Y&EwkPY4))Yh!l%Lo^i--@zS%WLDBp z+KSajA6kO=zz{@)9)J?b;^;>J=1VB~dmbJh#ZG?}C}HHoP{I%(EPZWYm6Z4pJIN7( zwArEmfW8>U7@hz|*&RVyJ0r?cM3t7)M~b!p^|S~zt#GWQdA&1q=zTO~xP{gGYQ&;w zPnm`9TZ7p;%wB5Q=sxh?06hb8lS@V0;+SHVZLp+4J_Gp;kMb?dbU@IepwS-=Pp{)Q zvYd%4ik1K?!$C4)oz`ZWtJ-Wv&1%mX>gV?7BC9EF{#M?F%d+PaS2XtqaV^`u5KR@W+=+E(Esx^97g7{w{Ta=TB2@7iJX{jKDtzE9ldUHF z!DXV{2Bd1)Vb>1B|I)kv6rV#uMsU!b-pxLXpm!_R_xtm(_kA4eeIs4;zLCy9lurBl zt*k=AlV8!x#_{@@UY0BuT5Yp(Y}8LoI87~<3$>b2Wo>Dx-Y{zI0`xGm)h-!EqYdrr pXQW5XP2-mnqh+>>^%84{U*h)-@Igq2F#$t@5re^ho4`!$_b2#{C9<-z62TdIg@$9)VF(7n=%&eJOTs=5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+0D-JP z7l~W-xnzR^1PBlyK!5-N0t5&UAW*bGAMGEfr7ijh~)sN84>jE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mk0O=tB0AS1xFaQ7m literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..32c5a52dc327b371041d0509e95e26646cbdd401 GIT binary patch literal 4096 zcmeH^TMmLS5Jm5Xv}+*oQ-4fcKm)`8wHOdSUAu>u)-8~MOgeL6UZ;}YI`TR_)T+>+ zpi(`tSFV>Tjl>6{J7+}ciNaSO^d!rpmvc@e>Znw4I{xjbGS&7d5wFG@F^ArwJ2iX> z_)}c0Reqh*zFXJ-o>?+ymP~n~mZ##B74ffFjx8H??A$Q*e)m7Go8FQey3CB1JKpvO cfjvgR2p9n)U<8bS5ikNqzz7%tBd~V@ANJN^Q2+n{ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..b4da1318113a3c9f5b8ac8aa84c2e7cfa999dc25 GIT binary patch literal 8 McmZQz00AZj000aC0{{R3 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..01bdaa1da7d937c7e7d98e54ba912f88ab95c7f2 GIT binary patch literal 8 LcmZQz0D}nt0GI%g literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..03c1356a2b22a9fef0c731f5685ae5402cc69732 GIT binary patch literal 4795 zcmd^@&rZWI490!n%ro?mGvn`o8x=53h)HM&IHJ&+wPGYy((;Tv6bC-1ljv>YFp#`T zX%ow7{rlp!Rz@^sTXM+eT=-CC6CbiW4zZ3cyLM`0HM7m5U3Prm`d}CQwL>^As31@8 zG@>`EX^Fm!OJZ0o`Yo&<7+G52>@be->TMl0@U*arcRujW2i{j{c9~{5^yg>=T7Qp# z7;e%t|1$NG!Bt@oQZa652|?Z39+MR=p@>781v`#-^h2n66#?f_UtDN1Q9nb z-?|t~N|@IqQk_yp1T>JzLb8{XB!#T$*>!mKu)(x9hvn6kCrMBcDUvgkV+AL z2OzCfk?LeRRJcw@3Ym@-GMy-78Y^TvRmgOvkm(#uwYq=qzDg>okMymTR1^BBq@LK0 ks$J*$N$sRBE2VnUMScsDf38JQ3`9{@fdM=OJq;T~v5SZhqa+~4N1|W`ihtd|@{OTr^`sKLbSquv>Oo^@ veYI%mvO3v)xa_WHwkDhRoXVHWdk+i&1Q0*~0R#|0009ILKmY**{w(kSmZln^ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..1ddb457a113791d5e7198fe8d8fbbeeccd3514d3 GIT binary patch literal 8 LcmZQz00UP508Ic! literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..01bdaa1da7d937c7e7d98e54ba912f88ab95c7f2 GIT binary patch literal 8 LcmZQz0D}nt0GI%g literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..233a0d17fbc2663729f2271c6e8bfd81c1e08287 GIT binary patch literal 183 zcmbV_T?)c55QKY_oFHDofJh%&5XHVP$xwqaTQ(cd?~)_$_|7H z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk v1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72owm6Z0@t)1!+kFzZLiZs;30w literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab new file mode 100644 index 0000000000000000000000000000000000000000..e416deff07d51cc5b6c13fea26025732f3a7063f GIT binary patch literal 4096 zcmbR3vzw0r2$(jE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mk0O=tB0Pn{Ri~s-t literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..1d41abf972ab13dcc52eed5f7e89f9d08553b5d6 GIT binary patch literal 4096 zcmeIuF$#b%3_s%&~#lWEe1t>rP3Q&Lo6rcbFC_n)U IP~c~Q7srM{?0RjXF5FkK+009C72oNAZfB*pk s1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PJ^Rm`T+HVE_OC literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab new file mode 100644 index 0000000000..2ceb12b8de --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab @@ -0,0 +1,2 @@ +2 +0 \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab new file mode 100644 index 0000000000000000000000000000000000000000..6d3c5e916cab89b15ab2509ddefcdff43513f4fa GIT binary patch literal 4096 zcmbR3vzw0r2$(* literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..32c5a52dc327b371041d0509e95e26646cbdd401 GIT binary patch literal 4096 zcmeH^TMmLS5Jm5Xv}+*oQ-4fcKm)`8wHOdSUAu>u)-8~MOgeL6UZ;}YI`TR_)T+>+ zpi(`tSFV>Tjl>6{J7+}ciNaSO^d!rpmvc@e>Znw4I{xjbGS&7d5wFG@F^ArwJ2iX> z_)}c0Reqh*zFXJ-o>?+ymP~n~mZ##B74ffFjx8H??A$Q*e)m7Go8FQey3CB1JKpvO cfjvgR2p9n)U<8bS5ikNqzz7%tBd~V@ANJN^Q2+n{ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len new file mode 100644 index 0000000000000000000000000000000000000000..b4da1318113a3c9f5b8ac8aa84c2e7cfa999dc25 GIT binary patch literal 8 McmZQz00AZj000aC0{{R3 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len new file mode 100644 index 0000000000000000000000000000000000000000..01bdaa1da7d937c7e7d98e54ba912f88ab95c7f2 GIT binary patch literal 8 LcmZQz0D}nt0GI%g literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at new file mode 100644 index 0000000000000000000000000000000000000000..7d30a43be197cf75a7d324961e20ad5d6f3ab66e GIT binary patch literal 61 zcmdOA@JLNeNi9+cN=?o$N>OmjFH#6dEh^3|E=kQR@klJr@J%cTOUx-v4KB$qN=#2> KVE_Rz$p`>|QW6aS literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i new file mode 100644 index 0000000000000000000000000000000000000000..df14d918c4c7958962c31c59b348afba7775c6ae GIT binary patch literal 32768 zcmeIuAr-(d5JbUysf73_sJYU>ScsDf38(?knMOy3Dd`lJdUea_8Qe8LeeL^D^@N_JzK~<_ z^^v7MljM+*5ET6GM$qvZXyHzTkwKrYsO9BK45FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk U1PBlyK!5-N0t5&UAaFt81N(dcjsO4v literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab new file mode 100644 index 0000000000000000000000000000000000000000..befbdd6c53bf0765e7321b8b2a6b823bc6e23d24 GIT binary patch literal 4096 zcmeIz|4-HR9tZGuMP4H=At@0ip^_;Xff*Tr5}B!&tXLYEGb3`&Ipa33Q8R9HT<@7V zGR3~2yD{~ek(Lp$GG>P4?3ISC zc_GYVDW`J-3;90t_#fslJtl-K-op&8V>4Z!V=fFKm-(E? za%OT9$8tYM@dsYU5pf|TaV8VEjIn%`F>GgFuloF>yqLvrxPZsGhDZ21KVm!g^Bh&b zm)YFSg>2*+zQUc{&X2j7-*FR@E>drt!nG{rYOZ1xce0X)SiwFn%pTmUXP*VOBHB2s4t@6H8dZ zDlTOM7x5HJIr5Sa=5Zd2`8M|gm5vxR5b%-IH86Sr{}e_}mv z84OW`1!#7jPHnvX^)9){*8H*Ks<( zVj(j}$&ZgPhb_!v+-UQQcQc(knaUrT%))f@i`9%{CkJuH6(Iz^z@DCHGuk92yJ|fAMg{lGGm;1#t0kvHDBS48Tyk?adZDUH}U$b^e5MF zEst|GuN$vFxq+4Z7b`gVYWD$axP;wY$Okgz$$gy5^lRkFa?avzPUi(#^5kshv5qfjEQ`Uag4oIo-AeHZua~bjSihCPyU%*+{aGFUnfuA%MNbg z5uRZi^K#_LRcvKD_b~l>_XEq>z!vUc@+9{IA7UNfWi1nKP@i1HYVKi#DY@#C4{!xr zxxD}UM)k?XT+Gi|#&LP(A6K%3?JQRv@h^OW+xQrJxQw~e%}Xxl zBEG>=p5;8|&j?`-|IRzOk24uJGlZ!u;$%L{Nv!7twsAa9a15hNW6Gb*Pi8WKg^cAK z#&9wF{wrU0^J$*pR-R@fPqCF9?BEfe(^b literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream new file mode 100644 index 0000000000000000000000000000000000000000..3a27b3b1042bd5912857023ebbfe861b1186ba41 GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len new file mode 100644 index 0000000000000000000000000000000000000000..131e265740f37d77b7c4a3676d2a7704ca3e4a29 GIT binary patch literal 8 McmZQz0D%Su009U9fdBvi literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..22056f606b09d9efa19148817f611a269e2fafb2 GIT binary patch literal 18 YcmZ4UmVvdLhk=1{LCPui#SB0I05tgon*aa+ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin new file mode 100644 index 0000000000000000000000000000000000000000..99b87616b3f1d4e81ac6cd2d9f4fce4bd817d345 GIT binary patch literal 94243 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

?HcD6P~$a3~Fbl&UG<4rPZ@L){?}bDGOs2f3W1 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 B#ODA2 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin new file mode 100644 index 0000000000000000000000000000000000000000..f55ddb8a97e8d30a12a0158bddcc7cd3ed1c7c2c GIT binary patch literal 31 dcmZ4UmVvcgk^ur385kHBq?}@34COO0003Te1gQW3 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/outputs/aar/active-agents-live-update-debug.aar b/apps/mobile/modules/active-agents-live-update/android/build/outputs/aar/active-agents-live-update-debug.aar new file mode 100644 index 0000000000000000000000000000000000000000..decb6fd058cfbd9ecc9a4b554af3ff30fc62584b GIT binary patch literal 38613 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�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 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/outputs/logs/manifest-merger-debug-report.txt b/apps/mobile/modules/active-agents-live-update/android/build/outputs/logs/manifest-merger-debug-report.txt new file mode 100644 index 0000000000..377b9afcf9 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/outputs/logs/manifest-merger-debug-report.txt @@ -0,0 +1,38 @@ +-- Merging decision tree log --- +manifest +ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:1:1-13:12 +INJECTED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:1:1-13:12 + package + INJECTED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml + xmlns:android + ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:1:11-69 +uses-permission#android.permission.RECEIVE_BOOT_COMPLETED +ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:2:3-79 + android:name + ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:2:20-76 +application +ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:3:3-12:17 +receiver#com.kilocode.activeagentsliveupdate.ActiveAgentsDeadlineReceiver +ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:4:5-11:16 + android:exported + ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:6:7-31 + android:name + ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:5:7-86 +intent-filter#action:name:android.intent.action.BOOT_COMPLETED+action:name:android.intent.action.MY_PACKAGE_REPLACED +ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:7:7-10:23 +action#android.intent.action.BOOT_COMPLETED +ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:8:9-71 + android:name + ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:8:17-68 +action#android.intent.action.MY_PACKAGE_REPLACED +ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:9:9-76 + android:name + ADDED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml:9:17-73 +uses-sdk +INJECTED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml reason: use-sdk injection requested +INJECTED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml +INJECTED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml + android:targetSdkVersion + INJECTED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml + android:minSdkVersion + INJECTED from /Users/igor/Projects/.worktrees/audit-w8b-live-activities-3cf8/apps/mobile/modules/active-agents-live-update/android/src/main/AndroidManifest.xml diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin b/apps/mobile/modules/active-agents-live-update/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin new file mode 100644 index 0000000000000000000000000000000000000000..42c1dfed33e2f04edef013380c02ba5c77e8b2bf GIT binary patch literal 12383 zcmb7K2UHX3+MchB#=5h1UDvflMbXh+uVvL;#Q_ur#3YL7T4FLlBqW#w1n;V#fK&?> zuwX|(MFD#O6%Z>TASm{Ry)Je||98-P@4xr_=RarSIm~2|=X>ApeV?~|5itT9hIztM zo@K&yX8$@&w4cFf)|vHUT_#bhvl)U!t&eE3S&eWP7@*hMLHP zX}@D4j9QbAf@7kBM2p3s7de;Y=L1LPRh?+5=+L^Ro6p|wRZSe4^3C|>%mP2N&1f*S zU}Ci=OpHZq zvKl0verD#2RxYLM2fOu`t$x2|o@sn#$g+pOsQV22hpMl#pU~fJfbc6nknc2z2{(y0 z#bD6EWD+HR7;GWGF$=UNaErxG@jDYGT5T4C&NhUJ(dztsEapIyekc0r4FiLj>0fU-ideShci)Mtr$FliG}@|18oMAXmw$t4JMmtF=>s8 zkxZm$GWeKA!Cq}zSNN+FxsOKf*G4z`t$ly-+3eJFzuj_kb9Wm(%7Y)nt9UivT*HJ< z5At^z%S8EWEq&Z3@EZJIQ9=`hc2N^9ZtQS+_g6E%MblSWKdP7$V#&?={S=v?)@$b0jhe82-@ zu+0}JX)_oV2JnwgYZRw5u@=!=w1_61XjS+z;pRY#k%=-*?LbyOwMfTEwq|wbp}tof)WSvTZ=% z0)NFu1dOow8?08)ZxgI%@;2KkHp5ek*(L=81oyGotH%CS;rCE;Z&OO^re|VOkkc0A zoQjlb$Ri!O@LLfRYqAEwx3$xZ;x>?BF@xJ|A&Tv=pi%3S0b4NHEFs+cUwbvUGiF1( z)KW9Q)O0Fm2lCvB)D1HM5*Pryv|6*^R%N6eWG0x-E4A3 zF>*hTJW9~W2viEY3-T{RkeYOUkee=mqFR5E>*-RSv-kn%PfbqA^LBEAao&0Q4tK%uYUGFx#UBmlp(kmW2~&Ei(u zhV-BGe9rG*ww~`j-}C^fAEIH8knk8eJweK+$mJPQb$AYz7yW^3FF*lPK%hU>7Lri?YSF80j0L8hS0h9WCa_vFijJUjTZriY)?~&#MQh!A5 zi_j-YniUe{3S`eEXWng9GO^CV+ncve9=O0^UJc?uOZNN%5i(u0O|!sOt=ACu^O9_w z*{b)3KT1E~C6h+i4aJ%;teJ<^^RYuXRz_eU5|3MeHBp$4#(bw3%q*B`&_fY#lkl1wjH}<;Ncy1V5m4+e}B*(>d#J) zX7VB5CA`@$p{;>N8+W0rN%g5^v3|Jg&ZRrU>$n`ijt8+Te@NP;$>%VJ&cd6^ zEUT)s^P<<&Vf3U&W7naDIat$VF1PZ#5oyO3w||+Nb^P9>Q6(31@wg*cIEtOTj)AP; z0JBxpZ@?QdcV*zmt1qKpL=O0EVBXAzmtyj;V?I{%1)v1<8j=*QoZHZ_S8V5i?V&Ye zy3fh}?KpNXK^G90Qr>FM#s=|+TWnw7E&Ijjv_kB90xLt2;v{Aw#30egRpm5$>HXWT zF+*pLey;C3rqON0*-yJLC3dRd*Jnbo@0k3X^-f{O(-;!j8Q7GlHJ(IVkkgV@ZfBY| z2r|s}pENL9br!3Nu=6>rD#p&|v9bgUrC3#loiAWzITk9ga1lFS!h9vL>N2n@z+w)9 zv}sXX0WBa0Sg%U#g0v4UuASmqt?g{9@pZ}TM&mRHMtHay-+&La2IEc1_*yGeoA6%i zBY)iX<@LUl{^^cADtFz&uD7wH*B$Wvx7Gz}(n-u2vR=3G-QBB&ebvSDvpiy=?qa@q z71-EXTM4+yjV^Fq&v;+^{rSBM?nIAK%QqH z-xLVWc@EwU2zY^+xNl7#NV5**GTfWJ?bK!dxX<{>2u(FU8mul>I9D!L|uI`=qUE$QhAC8nRZZzdw)GO@v8auwh%C}f} zhn4TK@BusIqK~ka$skeNY_d~a$BIv2D1#N+V4ZJ`WISlr&lO)kJAIJfql;&&Mvgok zy5}=?{(@Efd=yF`dTZMxu5q)6t@l3lT|3<4)VU$&%voVXGmp5;C&Su>6G-roaw3Q{ zhnVa+`!-uo7r)uxOXHq9p@lfMX(Vx8KwP4TGMWf6#H~G;C6+L8Qm1MO43HFyBTz1l z@SaQE`qT399&XXo^T#*X+A}sLo;WQePN`@SEN%+)&p=#dq4)b`HJ*RjmR&!%>1Lf9 zi;43R;*mi3rG!^5Bg`@wL8PWb)CXuSIw+u!#uUpzK<&E~psYbO>bZk8A7}JXy&9zP zw7c$gwwo=HI3y9LmBeio(X1xp)(~Yf5!MpTIsy&TdLor#vkp?%1~3Uw4?^2cv7T%s zQnLnC6NGF688B^_O`AbR5M-}J#LfP>$@+V3HV+<@<#>P5ic>$P5I3(a(qc9(%wt+E z%hE;Lt$oeFL5&{oDLcv?(^0KD}T~$KA5ap z(BRpck%1Y+k>5d>Sc6pu9WoSdJvWyw4V&fp_Rrr()r@Q1_D0c8!aHYzyl>+XS1_&M zw`*3D>C2kqG4=L@b<84~Y@%+r3skpRM6JI)*JuB4(-s8qa+V@X46`1HJGzp@8$J+M$cFJV8=mcx|Ds+C(Z@L?Kn{v z66FaZoFs0ih&maahP8an7Mm{61}r*F&cF&1lh48x8bT|V>zL?2sPWJ>vG3N;ZEQ`X zMMQm$D2s`3p17PMo1hq$fDVBcZ)qI5k(k;0UG>D)4f2Ac^rewS>4XcnKeTqo$dGHQ zin9-z9q-dM4RM)oDkE(_w?Ex5q~n=~t#`VW5}^z%e*xBkI5KKO%Hd9GeuHh?$qPNa zg0q{CKN5fW56^-_*D8qmBGFvHmq2frS|q96xmHTP(Q#MZ<>=oZBzH*Y_OX%-zfAZm zguhDoYv2iKqz-Oz9Xt!U584zfq;16wV44WsPwj-Y!}I#!d)rUNRrWj)!6#L$x=Gw` z5&kye8?{i}k*F>;joidX^D>o|rE`ps zbCz)VPkg^rwD$b*dE~n~qmMl29}&mLMEQhxJ|&}{5y$7mS^EMkZLnBvF9|bW8}b)0 z)VmrOA&P#AS8&yaSh?yt*0q}8X}G|5hxd@W8RuUU%^M=TCBi$Rd{2ZA#OWh(_(b>` zh*|WRFblQ1+FgFF?}{&kiT~dZHWNF=LlR1vNMAT)5bZgBSMNR_>^d!-(&Cquy$4l& z45ONP)OkJ~5l)p6REVU)0_q$^Mjaz7yg}EIbziwIT&(~Q<$1b9di>a`L zDif%%l#W|QHOnc#g7Tew6^XQVLe(e1Rcj0s?G!615X3*gCcfD_7HT5oislYL?o5qsX=TZuPn_{Jzhw-r;8&|KmYGGId-_hpnS- zP1jRs>8!R5l$j@Pq|h?y0;QP+Qb1W-cTDnV^cz9FQexVG+NcVgQ%k*9jT!{P}UuybgZ9 zro_JpawI5xW=PS&Zl0UfF9ebxuOC zP-nHv54wJ@1B2H({C+f~Rl9vuv!A*gpvr?(a~RpfJY~<#o7`pIqW=CJLQZ^oq-fkZ z;1GO7$EBhqaPBYHGxA$K-`>P8ZuW$xhyC8?P_z~SijJPwY)=lCp?i4bLt!o* zcZ9mP>k2*8&r{qt^`R3sWL};Sy{sNvdKB!^itC(tw0X0iwqFXH?c3l^d3wWR6bAM@ zP~X>Lo(U&Hqpf?(;}3Uy5&DlIp11q)`P8+53jA?kf!S!b9Kzg7`&~bVJ^EwUlccx3@ zu)#Zo8+v`x@l;c6?q; zRVC=Wp{^y}zO3&K9vHR9!{lDc3hX$>_4AwSBc_d^_JKIipIc zP)5h{7l3Dma>DKVXSRJws{4X5mwdNr@}7B@Q%wbRxk#0ls8C5=E`y$wzY1Y+4V3k< zXajr=I_q_~*ITqRYb$XCL~{hO=ahxx7d7lR->d7!u-RAQ>hHcmH8-hniz;tZ;SN>b zrNa|(6_~?hwGOpY+@nmq6tpm6)^;-X+?>7}tu5YlO76!!`t@3zSbm?nKah<1P%@28KYU=ums$Wz74dpXQ3q>Y+3wwbXnQNKgxv2i@L&axLUn{8ZV2XN2 zo!(QY50w8%`R1P_|3I5#RV*Sk5Mt2f8sMmF*Yg)wKX3c+Sl6Y^945TY;m_3L3sr`) zLKrK|V^s-gJ_`v34i80rI15S30(Anc%t9jhdS=P>S*gC5W>l7NkH0^CO+3=T*j;C_^C{~DOJ!4qM5)=!|L+yeQW*>=T!4uLc z>c7UZsN6f#PIfK$RX;bm-k-&#BjQ=Ng{-!y@qv8W|eDMVIAwSo>gsN zoj0=lCKe{M&9FKg2Elxg!h)l%wb|Xe1x%*3SS6FCf?N^Wi8L@B3>Y@iCxq*(`%+VQ z-tEn{S2r^L?6y8Uops*IDz~x1cGe|>_2}>l?SKU!^_pND0dCc;ul8%zH-F8%lB9`U zZums)WFay$Stc&Tq=QUh0y^nAuQ~?*yC1@bC_{1=$6j)NnZ;_dSz#Be+|6qCu!43k zC;%s+CcV~Tuh`4_bBDWq*j>5Au32&SUw1i{O>eW09k!o!KftQn?WA4LA+Dx->RH=t z_UDZSeY^E2o|tlw)f{5o4ztP})*+X5JHk45I0~DCDOWruc~eRg);u79j}PQ*i2;gy z*hcMh2(B%LoKBP8=T;ZA^h)g8Oi{oJ$0aTlO1`xD!f|sP%Y9i{|A)lJwwnE0g5oo} zMp;j=&L>%qQ>=Rec}SO&6@&}Dw0xjzx8~b_m{Vo$AA9HAX;wJHx|N`_V0|DH9LyWV zBFWWStCjPAz5U}9!z|zTI~oTa%+$H=w`i+3>$A*ijeRaktSWX}`52K_&PfPUX0d37aZI zj<}q>Zp}?B8uDVnWtP8-uYe-c^?v=iqP>o)zHVdxnflJDE&F;*;8oV?8tdtGorQ+S zx(9Kt4yc#-Gma-I zB!lcG>9E^UvYIB-G@4Cw=uvu%=F=irx0Jr1QS34{iQNh7o@TGIOeg{~Wy2XHlCAmz z`H*<|kRQ4|yOT@)|LF{LT76qgZ>Q z-QMiozU#Li8~OkI)F|ElJ*T~wFRBJH`~F)L{G7o5`v9E(Pa3uUErEt&*)g;b1Bt+d z3G$)I@*$h#LsI2KvgAV!$cN-emYc)m%17qOha8uLr4Xi2x`ra=wtQNhk}vhFe8??O z5X!-L<}PNo{Wpuh1`Pl8%=mB2y(eGNOZkv$`H)xgA)n<#!iem^jg$|Gmk(JiACf2^ zl0p#EKM(qcbTTUqT7+A;h@NJ{QGRVBl7e!uDI48J4uv>}rlDe*TH9QN);1m4bo>E) zh)Nwv6nh*Oux>q)@iDrL{XU5;B}eI;-Q+4;N_VokID1A%N-|5V$j<)JnNhpwW{HsbIg>*2@6>@09wtd6SopvHzA>uS!rjK!Y<#_%HnE2Fu@M egpBMQ|ueA&^%r&q_b0fLtBmrY#h!Nrede`;)#Nm&KfZ- zo6QxqA~`i;qq*W}DU;z|iizz^QAMM`x|v*&!ik-ln~kN3ypYv0u|!rPHPT5#K}cYE za#~N$*a*h7f;OuYP~f)KDmpCL)uBv^FN~!DEWt7X)v)-s z)D`r6Moa1f{kOz_6)Cnz6=H}gqH?ZRQRj_G z+)@u`%Nr?(+R;vAbZTgYW!0UQS$C#v12$A_k}vBUg@W4zmPrem)(U!Rte~IN3wk!G z(^~JS)|4%t#FiR~)U50aVhh>=XhplgVp~}i4R#7NUr$X94h1m^Q_^KxYJFmAg;hs> zL3E?10b8+6U=1B+SfA38XK9z|lj)>s<`d~zJy)W_*4|h#rl(Xn{rFjqmn!d9Hr%@?9@}tRQu;})lpzsMw!WD`qDEV9 z5DaR?nW&nwT~+$W-Pj}jqo0oHSQ(X7w8S_<_8ZMeQ!1u2vBQTGg9_pTfdZADo0ZS> zomN{@Wf&UGJ=h<>J|u{?m~-^L%z@0vSZiBh5CFiu?Mz^I#&C}lZF-qhL zXBE7aL19*#(XHvD>Ebj;Mp0u*6!=gzg*KKEy*Qmq?I+I}-C#`0&6M(WB6pR^T(fZE zY+kPeeyj@KF|n#C8^wT@$&9CqI>mni$pAE@7W=?(zqy)F+dAq7ffJYtvly7Bp;8Zy zb&b_oUbTA};kT=3(sv3oQZ*S?15Ea_(!dxE?hhi1oP3cN*fL<|*l1oa7-^#zEl%rE zD|kn(;2m|`N(D3K`iepmnGC~MW}Nw1kDp2Ed6}ja6lIw~Uszs;9jg#I9Y6_Z1e&hr zgjt)hOTpU&_WWOq69wNR@CMnfO0okP%`o~b+8zqvL3}S2!{{tauv}L9WEJoY)nE}% zp}zy~lt{i$#Q>uR6&ycPENCV%72(-v>Udfi&v|^m#Pc48t7@-$ufXm)1l0WwO|Cbfe|-u`xZHVu|Lc=ZER(%!YdlIW3jc3>p)J5XD`XvP;T9zJBQ1Jx$ua4?LJZXjT zrvz3ya*#Y#FH`VS(hD_~Gjzvw6@k?z>@iPHDgJ~idQ!o!Gg`7PAS1d}pF0OWR*4luapb_%Xda>Dc}bx| zSb4muPD~h*h2L-Bv$90^oOE~w-IdoISgy&}za`}<&oMNQ+i$#YnM;QtTV~R1zkuJ7 z)!6S+ex%F_ph5h;ERY_=7a0y&*vB6zX>@^`oV}d<%VhNj_>w$({h<|hB)eur4dRdS zS&8>gSmepTU`o?|^=Fupul}4Y74+HM99gOtHE*fV`7-`eCe4>CNs|&VO0)VKCe6Pl z+0_-dioX>YyyXy7eMl%rqQ7H8t`0bLfE4^ai_6x86;%EqfLHL3JY6`e939GzLN3ef zJIb@Mf`7I|I;JIO81v*@5dR{z*ov>wA4HdBMP1ciZkeu%e+x5zu(r8I!FIl@lci`i zUVI~fSMg1O4Gvv&G;51ke?D(vwz*dDpNtjMcfza?We#8s>#VErV>?Ot&5bcK+k6YJ z1@K>ZorfT4x6YP{6y17bJ?vG|>f3TG5oFXA#2{SP6+Vs`lglTChh{9iG#{HvxWidl$kN~m>`qJlyN)Fk!T!q{ zEy-4d!IV`uQcr(p&dSn{U&Kp{vLedbsKsjLEwX8I!uPrF3^6qlENfYyyLGr;XKv8y zEey!>U7;k;eyj5CFN{=ThVy6A@@Xqascs?OarxYifNP~I;0j$n7jUU=4jOs)@va8sRLEuCB#0`( z{Jite=ga5RE9c!SFQ4miZ54a{s_V+rK9A}Oxrf$;7Oiq^ZS3)d7V{qBeF^Vg-ov~v z9SVh(t@5lC=8Oq3)D&8N-HIj?vLm$Oy7`rSmY5>)eNTY$a~+FcTSLQ$qlD`wNpy%lv+FGubm3iPBm~Uc;A(C&W=|;;#(&b z(IcdKyyFuG55?)Ym!_LUyPoJtwsFX6pdO$5`0hY4&^8?C-q_vI-Lt*7 zo1>oI?w%ciws?Sb$^gHJP+`OT*fCm~J)svSWJ6i@w{uA?b3`ko<+DB6a;TJ*jlM*7 zE^VYaU+(PpS3VoCTDczVnzxJCzevuEXn7lHFp*^k%WMhAzH`9R%|4!+c$BKAD4ve& z4USk;T!TKeH|%pN{u(&qXBk!w`}h=-v50qX+eN4sIBk5|T8?cctB@sh#TMH~A+QXaO z^XLe7iA%WMjgJ$e!jcc0O#+(iEi9JG1c)I*OV(SsmE4$l!P>GL+p)vu`W#dO0h_E1 zqy9&qK*IudMYU()@$d9d0K1z!BBY81?3F@rivzrR;aaxGV@kJ?B>T{g73knron@J< zpd1FswN%umvUE0;rLzfxeCskv^SjE@2+PkkmOjNdxsU9nTwTD>JVqzOV+$DX_1RMC zRU*nI9A$H<`4Ya{gO2Br1|q&on02F9jd)+g;)ojVig?YLqY?EI9uRC|tZ?rLgpJ<7 z^C(S50`r)=h_j!19uH2ohu?k?=Q`%`uJF5?=kWvK_eFg3_`%8W56OM~?e2&_{J{nM zSZ|;`{MbA`JQ@DU1w4Hj&-6A#8YHHVG*fhsei(lF@Us^_fscpQKSe4U+|6?4m!Cz@ z8DFdjb1l5DBd*{x3brbkT>Co46+Ecm5e3gIRZSshDHA4F3a^`j?xBHgqfl?>sE60> zh+`)X;vpR3eS-faKUU157Z0Ehr?Cs~;`4dz#`|#>K8QVd8vXbv_TmCnG*3lbWb^$~ z*pJWC0>6l1Q;Bb*><`hl7Vt4TOCw&wduh%-?*9TV;W;SW`6)b)U!@71=kA~6yr0+y ziRm&|JWU#Z4Oi%5)L_{Fu9gkpDn4zy$5qo%WchH_bQy0*l^-Q+uin8uRlZxk+!suqcAXpVTxeko0oZq-ob?lDS+MV^i0GsGaBd(^g_k&bZ0e+i%WU~jXZ3iDhr zJ>9nRwsvU^;mAe®>l;or7=uxt+(@p}~&`XOFJg!;35Vl&wV#<=Od{Omc72o6(S zN2$R3=y{VA@==W7{<5#A6u%Uyi_p8u!Wbe@Vou<_|r!p$6CUf zj%2ah5q{r1{=&u;@tFSBc?o~zf(2y`D}JSh|E5G2Ek0Si+vX%s?mU8V@t>HPNk9V(^O^vEDZSURsvSL%z25 zi=tV|r;|hcpsz zk;b}AY_d^yGV)8(n6t%ZNtrOEWKrXF_~U}xL@PmkVwfrAT5-47LpT@S zF8UD>BYf`?d&Ma4|8L5^gJkwS6x|jIs_huu3DKSq9SPAXx_B3{gxD&&k3krsM{GNW z^#%flxZQx)fX@)y literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsDeadlineReceiver.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsDeadlineReceiver.class new file mode 100644 index 0000000000000000000000000000000000000000..a3595bb5a1a1678074eec180e179a35838b2b2b4 GIT binary patch literal 2680 zcmcgu-A@}w5dZBN?Bg(jIDtTt=A)qvB$x{{{R&ANV>{#$V^i#s64KJ;yfKIEJ9>L2 z^ewM_?@Rj_sY<0jwUrXoN>!AHKJ-87pHQi)I(s&O*u16c!|m+u%+CB~XLjcP_~)13 z09=E|aNYLmX4R>AwpSJ=w*zNO@QQE)S)*BFv&;iwro!!1xSbKaTytEpC~QFzehiG^ zEO*Pk=afy`a|1eT&iWoN+gt{FViAVuRmXLL4;Uh2<4bXLBWj=vF@~;S-H{BlxmQIq z!_ecoEu##V$8zn+(hA{rFw?=XLgX{!OAP07o?kIHM6l*_$CV~`T`%B)?UO?AkZfw;}hcx*vubHIMr}K_2V;Eo*T z+u&Q=tZ}zu7S=X|P4M=1ib_d^fW?u(5d(*Dl&(@4amn@&!%(}AO;=(VrrW59K!wk9 z8IF#PcdCu!I8GQC!AXXJcB3(zV$j#QTu)PagmJUtID@kWPUCfk(U#@OCYni=Kb|`| zTPT!P(}nqkT(*?W=olru;)@2aQH%!~HC5_QL zR%R|s+`bk!y#FtC++>L73nlA@l}?qcLZ0CWl_BT8V~3?Exhr>@!I;bDvx_Y$gX*;K zLm3@(O3!3dnVgj;Tly|E9Ul>#{n}Tp3RZHxrUs@4 z(19xLZ#!y5S*?|)7?z(sJY#6XTJzH*hBlxX&mM-$7SVKVSV>K5m)I3%%*LKSigxMR zK;+i&K-Y2YQulOpAWAewR43YVbLjc^(OzX>T2<%vde_ly2rYp-{c>1TLrXS0+G<&> zH7X>bO5>I|xU)@G1BMfe4L5M=!g9A9>8#a6YOfbi$ilSYZm`5_4SF(N%`(}aqXwC8 z)YpVx;&eu(jc4=P689aoZf*7z18!I6`DSZ9+HUJbk9f&w+;S;a0Na8-tHKf~knSHz)ZM?aCJgbd& zYacJy#+%>AE41+z_VE^9;*Fq)5@BxQ{1_3M(|-hw6+&(jrO>1M!c*u!hOVsBsMtV1 z92(8G-o-@!4hAO@$JF=)ujP}!Bb9uDb3YIlxI&+1e*M=u zq9-a}efNOq`DEgaCm8=0{f{w}OkAY-H|YBb=EWVn{TRJ!HnD^E6{ZL_Xr$ktzi~N+ z)4h663<_m}tjDRlTQn=;2v#r{B0fVW2vrNQnjInx%Ao-dRD!!Y3A%E!@Ql qQQWieDLz{PlUT*)D~L-N5?rENA}Y}2nI2%WbB zYasZMa01_^s(;eqfzb0I&WG3}d=TQY+v(e4wcz$nxMR6a9E>4-%m-YzxpSmfTgRdu z#Gx|89$1bQJYrDS)*C5A5z`PsoFNjtv3v&8{C{igFl?=vT{o~TM?W6)bjt~Z=Wtsu zir2hv2L;#h1Fzo>T(8W%uJCr&8yXb3&irco_psRurwp5a_cVstpMtUoINczpos`}{ zrAt?2U^0M#b&0>VWS66=v&C>a8fG+QNzs(_Xs9U(m5S-JoX|$Y#Q2@MG#YLzIrb!( zP{!|Lk%W>~_ovg5jFOAK8lTpQDRh!h>>`R^L?B**K!)%s{3=7pgqteVjGb;63vuGH5O|w*a!LW3m?;9@wuPvThQn0+=cLJ*?8kTR7BJVhEzyta@eB{Vt z5&7LIEAkhh%Uz{Tmqwr*-S~{er*M3iq8tMsaF1eYasWwaK$?+wiii|vDJztvI3>j_ z6f92QL?FkwglU?oxKEuHif-XHGFYQt!5yqqHi^&B+4LdQ5@t)7D`6g&DPy68MO--q f^Ra~GLrnS5d}Mq?eZ+iR4eM(@uKTzV)+;{%l|dh* literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$1.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$1.class new file mode 100644 index 0000000000000000000000000000000000000000..8a2013ca213148641517f5bf112dc1a57bca74fc GIT binary patch literal 1471 zcmcgsYflqF6g|^Y3Z)7K6!3uxRVgY9^6*h4)&fbjKthqk_-WWq%e32>W_R1bztWh9 z(ZnC%k22obw!A{{gNgg$&Ye4R?%X-&&i?xS{U?A|SZ3I_bryp!RWOWBr7jlhj8rYEqPTumDye+oD(+a@EuE=nq8O#@ zKFPgRVo06lJL_=IBRbpCo$ZY{Zs2ANW4OgIOf{Wv6C!jN*J23a4zapyQ}@yR%sdgt z6XUp~dhrZ{d2MrNm%EbhIbz2c zhWY#IE!tCC@0OeQy2<9_EPY2vJD?29k@YC;Ga`rye7_K(k328I6(YBMg2fhhdRy zycNVy07)1)UR zJ-ByV>j)iXzP=v_eK^9z7bH!^R6L%MjZyB_hA}*+c^51^#S8L62;*fi&Ld5uspdY# X9(+VNhle>#<&eU34v+9ShbMmk=G%oD literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$10.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$10.class new file mode 100644 index 0000000000000000000000000000000000000000..3a3eb4fd487bcd5c28246d80246b178e696c8551 GIT binary patch literal 1481 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 Y-p=A7zR6OzS#(E?m=gWU+h8{M-)cWyi2wiq literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$14.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$14.class new file mode 100644 index 0000000000000000000000000000000000000000..3f6884e6b01bf2c6afccbe06781e84136efb1992 GIT binary patch literal 1473 zcmcgsYflqF6g|@ig;IqA3iv>UsuY#wB|eJ8S|F(wNGOsRKMmVy8M>WmcDD`uD~*X5 zP5c4=DC3=N%PRywn7AKi?%bJk=iYPf?62S7e*$=gB?f8hnpKsKwslEZ-1cQt@QP5r z=aAK?m$)yiY~aoY?gF{r2kv!UYB(ZQ5@o5RFSSZJytZ57sl`-EDn}|&O06{%VQFi2 zAqK|qwy@8e+;X_8Sev{1!uB(Ts`lxywco5+<@RyU>Hue!I{U6DJ9OT9w|!U_ndJ;a z|M^%kL>R^{j~7E1!@M}CYpWI%;8|^e4)XEZd956m^`;@6l5k__X6WwZMa)f}s-@_= zQdRy^%UmX1xai`Os*8up?%a^mwYpZML?(eadg<=`MsYj4ky~Y`7XF)T#BnqWwFqOS{|(?j6IMX1&c)7uG|ftdjetZIxfZ5~Kt%`m+FSAAlbW{97>Ertau z6<0FSZAARS$`E7H}DE(x1EfvgL@B~;+M zq=mH-4}_hI%(=*1AoH8Z{8E=fSENg#ER_tTR%w@4H%mM{mrhINN+n9^XQ3i2ZB5Qv zU<}U-JG{Q%ekJnq{q;KY1u-xFn*&f726_i7@$kY(sQ z?#n_4!^ofgS?FY#7P~cVSEB@cy9v-yJbp8-<%6`IhSDtw&q5bNS1T@JZnQ0zV&F-& z^_N_xvYEn37awL_+*3~Lh9zCCX+@RDrjSGr-JM@8uIE;lRv0RU|5i5QxpUqWc!424 zn%PL97kx=2(9h5jY)hZ^rK;F+BoLsFVwOZEUrYkI*CkN|qUr{>!ZA>n?-1jNv zAq;n85EqUXRH{IDio5pfMmJDvS-3hZ* z#V~+#NyKo2xSw~ZM~MXU-=D%MobD#=wu!yjJ}*VUX<9fD7OU3dG+;3!m=R!L6{Eyy zb5|N|f?@Gri)dkrA$d4}3^SC-NiEiD^~TH|_V%&~IE5L^GMqIWE(_oBWG&F%T6w$@ zP>Gi4VPH5@&<=MuxFMwP?-b%ai4$Ts#4@jUM2aW{{97|+ATL>`mB0P~}csQ>@~ literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$16.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$16.class new file mode 100644 index 0000000000000000000000000000000000000000..e83b1bc681cf1c52da47c3435e13ebe42cab4166 GIT binary patch literal 3615 zcmeHKZC4XV6n-WFLe{8&FZHFhShgAxa7n;a38ImN8V%7J#45Gzuseh$o86r3CI^2+ z>tAU-m3sQ2$It!R|I^buVM7oimC~Nm5B%L|Utu!L%eRns;-)3H^HS62+K zrc~9mEz=f;I^VEKmbMb95E#Rg!a8qq&Ej@hyT7t7bT3_~I369=)|*vr)j#fPZNbFM zau<<3N@mjx1G`s4=wZ09zxojR7$(I=&C#kYe_YKMa9Z$OA7*yj%i9gpGDJOueun-w zObTRl2hNgLH|_FYxRy+h7P=Yk1flfY*4q~5oNCRn>0;6mC^*8aHZ#HC$Kl8K#LseuaBP^&^BCDr5O# zX)#lr%P|~nmt(m}Ud`fWm^#HRhH2GQxjNpiN8|6?6cXxH29_r)?l$_Ny%>pKR1D3! za5Ll{%MzE#;HoO@Rj00t#pgAmd!R;_yHj{OSp&5M!=)}+)-|{(&mOKX9Q5`v_yufX!k1L?lxOpw9{U) z(o_UbvCMF8%e~H~-+Qi9C`7R~K-oCA>ru&RP_(jQrBTjp5JO!`S3=&~Y}E_)dubu_ zKzX-X7_^DekX+LOb%&Lxv?N;i!j5x~`U9K7d| z=;ZKzG?cmynv1*Zi4rR)iIK!uYGPtKp(L)2B&Smo31#Z)$mE!uCOffeMO}&g+X0h+ zrU+6CM=YmMN1da`n@kKx=hhOho18ktq8u2G7pRVJ^$&j~lDBP9pR>5@3hKkhbGB|d zF1=TX>1&QbiC8krHuoBJ5?8heHE&8?C%dtW;q1eP?U_}vWV$BV%-FW$arx>a?dScy zyxrWhOVu(!PXY#d8V(G|7lX_s`h66~Xyo)ON+$yJRbug17>fOjQ$Nx#h7}qk^ran~ z#s~Brl)39my&|n)T1&@=&cDXRcrdvT?@1PesUD1tzrn~RE?@6`gVE_wFxCGOTI?qb zy~fqEp)mIfu~3XmN89p3F8jIS8)QjF^7I! zBsf{*Xid)M@i{>mBv^N7w}-$ckjGupd<^5bM>AP6(pxSJWXOer@4^k=@o+5u2E|Q0 z=m6?#`Te40{B$ciVLt{SuccBMQG*mKG?UQ-MMlurL*~E2D$N39xZL_a!x{zj|GIF{ z|9k%Xbpa+_@CBm47j)SVml4dPl*eKokFb=-V|CxDm8GVIy9W>ux5ZCw@?w|&_Zd{-#n zbI5Dd%iI@ME(qs>aDl?_gYdd8Hyn{Hi;7gzms%wqUMrP(aw(aV%8^QxlWPq{SX!Ek zfib)-?DHnK9Ike)&CROQynFQkKrL*%J#qHe2@+!lA;eV+{G)J>W-d7Ca zRC*_YKJ>>C!2m;-zb8Ffm#V3&f?+h3zF4d?Qnjdx;rdyrr1FKUxMOX#bf%t(VU)7_ zB==^8A$^+fWruqn(b<;nY;PoR12^Ls!!3qks_A^25TV1k7Dotoh}Eo3-ADH`^MM4e z;A#(flMDl$?CZkk^b9z05vn!A^m0PRFk`^Ls>TV^;(;{Y48!Yxl_!R2hWN?LVpyPy zT+}?BCT=~(ldfGc1t&0rS%x9Q;fnBVSJr*)ZdRtNKFPOCFP>qrpl$B#a98q@BesoU z=qu3c-DuQG!fi(qd8LFK@GeO7zEx#A+M}i?cJ_3cp{FQ!755u1!Q(~UaBZ_|^i@|;rI=jNZ_VV0PV5QX$_Ai0QoqOXg5>M^nlG@GQ6(HNOJ!r*6e7#7JU zT0sm2kO8A7MGRnuMgxcrj(o%IPvnKLWadG1;uJdCLigtgJ>EuJZFFSnEACzfjN z#=YZON9ZW?_5MKU!x1LFAZaS5;_;MhoN~7|jNuv0yI^4s&&dlRj2FQ;j|`2bn)?)c W@DbrW9_EqCBaNv%9^r8wPyPV!yM-J8 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$3.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$3.class new file mode 100644 index 0000000000000000000000000000000000000000..42a2930d46c38b71c6d090cbaa9008b4fa513975 GIT binary patch literal 1471 zcmcgsYflqF6g|^Y3Z)7K6!3uxRVgY9^6*h4)&fbjKthqk_-WWq%e32>W_R1bztWh9 z(ZnC%k22obw!A{{gNgg$&Ye4R?%X-&&i?xS{U?A|SZ3I_bryp!RWOWBr7jlhj8rYEqPTumDye+oD(+a@EuE=nq8O#@ zKFPgRVo06lJL_=IBRbpCo$ZY{Zs2ANW4OgIOf{Wv6C!jN*J23a4zapyQ}@yR%sdgt z6XUp~dhrZ{d2MrNm%EbhIbz2c zhWY#IE!tCC@0OeQy2<9_EPY2vJD?29k@YC;Ga`rye7_K(k328I6(YBMg2fhhdRy zycNVy07)1)UR zJ-ByV>j)iXzP=v_eK^9z7bH!^R6L%MjZyB_hA}*+c^51^#S8L62;*fi&Ld5uspdY# X9(+VNhle>#<&eU34v+9ShbMmk_?v|u literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$4.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$4.class new file mode 100644 index 0000000000000000000000000000000000000000..82a2eed2c3cbfb21b415e3849a1eb9657e9b0524 GIT binary patch literal 1479 zcmcgsX-`u@6g|_0LaDMU;DQQODJrkX;!-45mZVxBp-5u=<7`LmInTn#zc%J z{s4cJ@y=_@7J?s4d>>}++?g|T?>YCrU%$Wq1n>$A412b&Sykz1TbG5!ZC^G7-xbRD z9I}FXnft=ZhwgmnE|U9w=w8$1z!B-Ps7NJ!sa4wHwVg6g&!^K;IZ}yodNojlrLEZn z7{lA*K5uZ#;cC~~*x47hpDR|iPiL+DM$M|UPJ31xmtAP@yQ1RIdF$QQVO``Fa}2%b z?@A!XFnalY33M>bh=aPeYGM4I)dJ`+9bV!m~A!!VTO^Vg$Ds1}V1l`;#v|B+#z-sY^p-a z)2#baxPq%)WQ{ZQwQpY&KBsoz#7U&qh*Aed%)rb5239pjlr|5fv9b)W|5c_0rWleZ z9mX(AiCk1f?P6{=kERrN{6CF*>J{-UqYQ zV3?yZ)pTMYg!CCb$zuRnnhl_%f9M--e}Rb(4#H1)k4Q7zvAv? z;CSolPTV_=b%>raU-u70J{)1}3(_WH3LejBOw!%W10#4&>liFN#S5|`h~i~9&mc#$ biRM1}9(+W!fQJQ46p+DW0gv#wfG2+dDaD9a literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$5.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$5.class new file mode 100644 index 0000000000000000000000000000000000000000..9b7612b9f3a4eb0f2458658767e0b320e7258bed GIT binary patch literal 1510 zcmcgsTTc@~6#k~AER-r#P{0dbs7g^;5b;(-%EhEwz*r<9J`LL`4BgH&yW2+pm6t?} zCjJ0_l<~}VOVOY{n79vT&Yqd?%y(|{?fd7i03KnHVb9SuyDDAn=(4c66Ue6EyFvxN zOIo8|=7F&Dp*bI#i)4Non%8u>;fhRIRHTxD)GFih+D@5g?q)Jlxl)O8=1D^lmbT_B zFovhaect4@%hj&Ev9m9nAXlvFfX>?c&6-__PWyHnH$C6p_e8~|^Y*jNH+7L)$T1B3 z>dQhG!`PqwS?Fe%6|d{su7&)4I|AsCk00?`IgDzlA>Fd@Ec7t+w0SYei6gO;0#B;l zzr-?^%NCEjxD$2hjdI#IEbCfbD=I`Tg(Uju?!tO$GrzvP%CKMjUsa~i0y zkVi1ui(#Doxu8-7!c*L}U$nY`I?KX&;t)_%Y*ZMsN2}8E^&3(tjNxJu7jTJTh?sX6 zMva-m5Kbo%!#L%B-k~0)ESUdv3MX)~m$d7Kd(=K_BH%PEoH7=xHRCj2F(a4}U|?0F z#A)-jG}<)7;=dNr!fl3RXZ{%OP$0*(Si99*Birfi6+<|MJD6h_G&x)mzT?SypuLUC zR5hRyEz`TeaH^;s?rw2U@*P)f8o|(Cq#0jt)OLgySyBb1gcs&rP;2zBD#z76jRs|B zPnQ{bOLAB7py3fbS<(&95l^J?HTt5V0$CGV(wF2suapkBc{zN{P^J@ z>mr>xk>q{)8>f}gnwUJmsduC>JRq5B88H|_(x#qdF@PCb4ZwPjbDwbeEvYd)B-tIB zuN;9}E%-p5;8!BJ9l;ZmACdVJI1%05gR6&Rhv_Qw^?gBX`vBKIV8T!)_Q;V;lJC}m uQOwhS7iMvn-fF`zj)ic&hsU&z(a9{?CSM_5z>NZK7H|tw1=yG_VCDyTosO;m literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$6.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$6.class new file mode 100644 index 0000000000000000000000000000000000000000..14a928ccf7b17763536851a5c9d8b319ee9ccbe2 GIT binary patch literal 3479 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 W@Dbr09_BEWLkiP5Ji_A~p8NqRQiUl1 literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$9.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$9.class new file mode 100644 index 0000000000000000000000000000000000000000..31c89d011ec29f681a21818dc92e6f648b0b930d GIT binary patch literal 1471 zcmcgsYflqF6g|_@7D|<;0zOcoDn(^M5XDwv<&jhiBos-EpN8$UOuLTzSJt|@Y-IHCl`}RsT`?9F}dDQgr%jW z7#PFbe1$i;<#1KDw)QH*_S5;Q_GzzGY1XV#d$(tGaI=}tx+_W!?YG|T9M(m8InB_2 zK35D~4C9yQiy_J|FAnP3ss;6XRvVy$dc1aBtH-x`(~wS4xG{7yba(1vkW;7bQt(}= z%73}bVmg(-=-`uE7Y>!(IblWD>ROQy=>+2FrL%LJg`Mo?${Ir@|G!itnxh#b?=nMp zDz%$HANu2nV1S{^-!7w_Nx>&3;Qnjdx;rdyrr1FKUxMOX%bf%t(VU)7_ zB==T{A$6MX6^DBs(b<;nY;PoR12^Ls!!3qks_A^25TV1k7Dotoh}Dcu-ADH`^MM4e z;A#(flMDl$>>I-8^b9z05vn!A^m0PRFk`^Ls>TV^;(;{YEW_)6l_!Q7hWN?LVpyPy zT+}?BCT=~(ldfGg1t&0zIffy_;i~X#SJr*)Zk1-LKFPO2FP>pAuWjz^a#!*_N9-8G z(3hvzyVwb7C3uef^|IMP14 z8~2WD9igMl*ZTvZ4@a2zf~2XKipO)ZamwA=FoqX2?}CM=SRyZkFkS}ZJkm6pYVK3) W!AFF1c$mXf4k=9M@Cc7{c=87_GKDJu literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$1.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$1.class new file mode 100644 index 0000000000000000000000000000000000000000..240377631cc305247c5b953d24db28069579a0ca GIT binary patch literal 2688 zcmeHJTXP#V6#nE~oGfm0YXeOo7_zjE({!_GDKt*fIyW$NE?@}L=1EyAj^eeH@$P!U zzk(Sq86Fs(`B4l<_Bu9=2@K(p2TNK>=Q~F_-}&U$7uN{`T{lvDV@}&}G zdn;08tJT*sJl0W|uU7)wO@cAJES~TNcYLlY&hFuf@WNcNszWk!P8v1mC^ih7HiWx; z(8J^s$ZC#Z^!3>!`WdFLhEHOU;gL8y)lRK>vcQQAbThml4s+w;ylav4%c7pd5W_?} z%&wC%$V^wrrLZnl|Aaw%ANgJE#lUY)I z{6Ezv*LE3LfvUAfk{Ryx$VPWmaq5#;NZ~Eqv53i!hzF+xI^LDF3xk!P4{MblY~ zMeAWTWXQGyG>Ut{-I9*#p4bqFQDyTi6vSe!DDr`3vKSjJZRhu_zlOD5G*#TnmeTFf zmNl1l(^r`EQapXN>Rvo8_9CG#rmx}v9oD*T)mYi0<<7VdTz6r~wHBYwEH1gOYh^br z60dc7JPir7$Nha?m!^)sY!%Z;ksiXGsCFpo@l2|ql&G)!JO~6y-pHo%d>zo!Kr}hl zWrpFBtSBBvb;7h-+1Qe%FmLrr4b}1&kqTu^?8`usoBgaEZYK zL_m6wM@Nk~8yeF04cw&m6Z*=~js*2qcJ2p^XMe)&@92x+Q)*Mx(+Vc>CiP>cBx0+V zWMhatF*knq9Pjn5WWUGwIqsih{#%+(pg`?S+A;`*A!v+KZy?h&H$;#R;WXV&#NY&l zDJ)Q!d2-{%ZdPcI32rz$_bV2E#N!V5U^BKB=wxft1=KJ;#1mRi;xjU~Xk5iMH-niY&ZLL?9ieAC;n^1$1dH@G~M4vbWuye*4?se*gIw02|n62t3ua8p2nes&UKp zLeb`Koy#!rX%(H;+>l$vj=0znmr4AxBi>WB$meE_zYt1X!l(OZU zcDAr^Kn}xW5tu6sTQ~QY!UQHqk-!wgKz89Eg)vMU7{Uz0ppL^3-8jmBz#}1MSje_& zaE1rjJ}6u0>0*pkhJ*-5F#Bg%2^sR1bbZT_y4iv7f+XHzxa}QtuTfE9CG!1!x8*iD zZPd}OdhU_Lh|woChj$FTjSR!noAxz{_ZY@q&*MR0)MVY=pW7nwcRVENzDhi$GhFo!NvpB^yW z?{ipvI+KKz!kf5j5YY0}rc;|C-E*iH4V>!Mvd2T0c+MppB%193qQ-xhGHS}VD zlxIS(=rS0Wq{pzD?SEyvEOO41m$b2?L#6NpPZ?&{GUU1mYC^n#R#inU zkMHoKs9rh?Ie|<`%6zEp`VnU=UEM408-MTCc-A3VBUjCISG&ey#?D-0(vOhT^`v;g z{AQlK=QGz~fQmJ43Th))HtczO$+lP4Y-81)&oAZIY}+<+C4<;yn_f2!1Le8?f!h-9 zkx=^lJ{o-OnQ#iY7l0fuNX0DOKp|XOX<$i*=4{U(v!Q3=a_m>BXF!)aQ}ie}Q_4&I=7N?(fD>MOF*P@Y*#-+PUB2R3rw zBmElp@g0`FA$bCy(wN2|h9NNmMX^sYO>6BpL$)LITgJmq?3p+M`NJ$M%C$(bR^nJ2 zv`1$+np^x8_K$dU8DO}R;JMQ~=bJ8EjbarakUj~Aj15|8C!i8Oqj8X^s)%Q_(uI^e z^A`4~^8fLF1^=kf`OMJ?KBlw!@DKwIK63D}gEbT!tYgE$CZ5wC4XSPW?;L^!DE$GR C6#)(a literal 0 HcmV?d00001 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$3.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$3.class new file mode 100644 index 0000000000000000000000000000000000000000..7f216dd16c62c91c7f1b3e5b74b64e5055d3b500 GIT binary patch literal 2968 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 haoadGo8v=sOL5Im&6;VeLtrIG Date: Thu, 3 Sep 2026 03:39:49 +0200 Subject: [PATCH 38/43] fix(mobile): fit the Android widget to every cell size A placed widget reports its cell in dp, and the two bucket thresholds were too tight: a 2x1 cell reported about 150-190 dp and took the three-state row, which clipped the last state, and a 4x1 cell reported up to about 110 dp and took the stacked layout, which clipped the last row. Move the splits between the real buckets and let a short row keep the label only on the ranked state. Also register the widget task handler from the app entry. Android redraws a placed widget from a headless JS task with no Activity, so an expo-router route never evaluates and the handler was never registered. --- apps/mobile/index.js | 16 ++++ apps/mobile/package.json | 2 +- .../plugins/withAndroidWidgetLocalizations.js | 8 +- .../active-agents-widget.test.ts | 22 ++++- .../active-agents-widget.tsx | 95 ++++++++++++------- .../src/glanceable-android/android-sink.ts | 7 +- .../src/glanceable-android/register.test.ts | 2 +- .../mobile/src/glanceable-android/register.ts | 4 +- 8 files changed, 108 insertions(+), 48 deletions(-) create mode 100644 apps/mobile/index.js diff --git a/apps/mobile/index.js b/apps/mobile/index.js new file mode 100644 index 0000000000..9d2db72f62 --- /dev/null +++ b/apps/mobile/index.js @@ -0,0 +1,16 @@ +// The app entry. +// +// Android redraws a placed widget from a headless JS task, which loads this +// bundle with no Activity and therefore never evaluates an expo-router route. +// `registerWidgetTaskHandler` has to have run by then, so the Android glanceable +// slice is required here rather than from the root layout, and before +// `expo-router/entry` so the registration cannot depend on routing at all. +// +// `require`, not `import`: ESM hoisting would run `expo-router/entry` first. +const { Platform } = require('react-native'); + +if (Platform.OS === 'android') { + require('./src/glanceable-android/register'); +} + +require('expo-router/entry'); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 02be036c6e..1eb0b3be9a 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "kilo-app", - "main": "expo-router/entry", + "main": "index.js", "version": "1.0.0", "scripts": { "start": "expo start --dev-client", diff --git a/apps/mobile/plugins/withAndroidWidgetLocalizations.js b/apps/mobile/plugins/withAndroidWidgetLocalizations.js index 083465abb5..9e5fa92dc1 100644 --- a/apps/mobile/plugins/withAndroidWidgetLocalizations.js +++ b/apps/mobile/plugins/withAndroidWidgetLocalizations.js @@ -96,13 +96,7 @@ module.exports = function withAndroidWidgetLocalizations(config, options) { return withDangerousMod(withDefaults, [ 'android', async cfg => { - const resPath = path.join( - cfg.modRequest.platformProjectRoot, - 'app', - 'src', - 'main', - 'res' - ); + const resPath = path.join(cfg.modRequest.platformProjectRoot, 'app', 'src', 'main', 'res'); if (!fs.existsSync(resPath)) { throw new Error(`withAndroidWidgetLocalizations: no res directory at ${resPath}`); } diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts index d83d40ed2a..06e83dd79d 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts +++ b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts @@ -82,12 +82,12 @@ function collectText(node: unknown): string[] { return output; } -function render(props: ReturnType, width: number) { +function render(props: ReturnType, width: number, height = 200) { return renderActiveAgentsWidget(props, { widgetName: 'ActiveAgentsWidget', widgetId: 1, width, - height: 100, + height, screenInfo: { screenWidthDp: 400, screenHeightDp: 800, density: 2, densityDpi: 320 }, }) as unknown as { light: MockElement; dark: MockElement }; } @@ -131,6 +131,24 @@ describe('renderActiveAgentsWidget', () => { expect(text).toEqual(['1', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']); }); + // One cell tall: the counts run in a row instead of stacking. A short row + // keeps the word only on the ranked state, a wide one labels all three. + it.each([ + { width: 250, visibleText: ['1', 'Needs input', '1', '0'] }, + { width: 340, visibleText: ['1', 'Needs input', '1', 'Working', '0', 'Idle'] }, + ])( + 'runs the counts in a row at width $width and one cell of height', + ({ width, visibleText }) => { + const props = buildAndroidWidgetProps( + snapshotFor([{ status: 'question' }, { status: 'busy' }], 0), + {}, + translate + ); + + expect(collectText(render(props, width, 100).light)).toEqual(visibleText); + } + ); + it.each([ { width: 120, visibleText: ['2', 'Needs input'] }, { diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.tsx b/apps/mobile/src/glanceable-android/active-agents-widget.tsx index 6e815f841f..7e89b4b9b5 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.tsx +++ b/apps/mobile/src/glanceable-android/active-agents-widget.tsx @@ -22,11 +22,25 @@ import { type AndroidWidgetProps } from './widget-props'; export const WIDGET_NAME = 'ActiveAgentsWidget'; -/** Below this width (dp) only the primary count fits beside the mark. */ -const COMPACT_MAX_WIDTH_DP = 170; -/** Below this height (dp) the three rows cannot stack, so they run in a row. */ -const ROW_MAX_HEIGHT_DP = 90; - +/** + * Below this width (dp) only the primary count fits beside the mark. + * + * Two cells wide reports about 150–190 dp and three cells about 230–280 dp, so + * the split sits between them. A tighter bound let a two-cell cell take the + * row of three states and clip the last one. + */ +const COMPACT_MAX_WIDTH_DP = 210; +/** At or above this width (dp) every state in the row can carry its label. */ +const ROW_LABEL_MIN_WIDTH_DP = 300; +/** + * Below this height (dp) the three rows cannot stack, so they run in a row. + * + * One cell tall lands anywhere from 40 dp to about 110 dp depending on the + * device and the launcher's grid, and two cells tall starts around 150 dp, so + * the split sits between them. A tighter bound let a one-cell cell take the + * stacked layout and clip its last row. + */ +const ROW_MAX_HEIGHT_DP = 130; type Palette = { background: HexColor; @@ -63,11 +77,19 @@ const DARK: Palette = { type Size = 'compact' | 'row' | 'stack'; -function sizeOf(info: WidgetInfo): Size { +/** The size bucket, plus whether a `row` cell is wide enough for its labels. */ +type Shape = { size: Size; rowLabels: boolean }; + +function shapeOf(info: WidgetInfo): Shape { if (info.width < COMPACT_MAX_WIDTH_DP) { - return 'compact'; + return { size: 'compact', rowLabels: true }; } - return info.height < ROW_MAX_HEIGHT_DP ? 'row' : 'stack'; + if (info.height < ROW_MAX_HEIGHT_DP) { + // Three cells wide fit three counts but not three labels, so the ranked + // state keeps its word and the other two show as a marker and a number. + return { size: 'row', rowLabels: info.width >= ROW_LABEL_MIN_WIDTH_DP }; + } + return { size: 'stack', rowLabels: true }; } function dotColor(kind: GlanceableCountKind, palette: Palette): HexColor { @@ -90,9 +112,7 @@ function stateDot(kind: GlanceableCountKind, palette: Palette, size: number) { width: size, height: size, borderRadius: size / 2, - ...(kind === 'idle' - ? { borderWidth: 2, borderColor: color } - : { backgroundColor: color }), + ...(kind === 'idle' ? { borderWidth: 2, borderColor: color } : { backgroundColor: color }), }} /> ); @@ -106,18 +126,15 @@ function logo(size: number) { * One count line: marker, count, label. Only the label color ranks the rows, * because a second font size in a three-row list reads as a mistake. */ -// eslint-disable-next-line max-params -- one line, its rank, and the two style inputs +type RowStyle = { palette: Palette; fontSize: number; showLabel: boolean }; + function countRow( line: AndroidWidgetProps['countLines'][number], isPrimary: boolean, - palette: Palette, - fontSize: number + { palette, fontSize, showLabel }: RowStyle ) { return ( - + {stateDot(line.kind, palette, fontSize < 14 ? 9 : 10)} - + {showLabel ? ( + + ) : null} ); } @@ -154,8 +173,7 @@ function renderCompact(props: AndroidWidgetProps, palette: Palette) { count: props.primaryCount, }, true, - palette, - 15 + { palette, fontSize: 15, showLabel: true } ); } @@ -170,14 +188,20 @@ function statusText(props: AndroidWidgetProps, palette: Palette) { ); } -function renderCounts(props: AndroidWidgetProps, palette: Palette, size: Size) { +function renderCounts(props: AndroidWidgetProps, palette: Palette, shape: Shape) { if (props.countLines.length === 0) { return statusText(props, palette); } + const { size, rowLabels } = shape; const primaryLabel = props.primaryLabel; - const rows = props.countLines.map(line => - countRow(line, line.label === primaryLabel, palette, size === 'row' ? 13 : 15) - ); + const rows = props.countLines.map(line => { + const isPrimary = line.label === primaryLabel; + return countRow(line, isPrimary, { + palette, + fontSize: size === 'row' ? 13 : 15, + showLabel: size !== 'row' || rowLabels || isPrimary, + }); + }); const stacked = ( { diff --git a/apps/mobile/src/glanceable-android/register.ts b/apps/mobile/src/glanceable-android/register.ts index 2c74b810df..cb5e86625c 100644 --- a/apps/mobile/src/glanceable-android/register.ts +++ b/apps/mobile/src/glanceable-android/register.ts @@ -54,7 +54,9 @@ registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { // queued this task when newer work or a privacy blank replaces its deadline. const stored = getStoredWidgetSnapshot(); let props = - stored === null ? getCurrentWidgetProps() : buildCurrentWidgetProps(stored, translate, formatGlanceableCount); + stored === null + ? getCurrentWidgetProps() + : buildCurrentWidgetProps(stored, translate, formatGlanceableCount); if (props === null) { // Migrate the existing mirror when this installation has no native snapshot yet. await restorePersistedGlanceable(); From 527ed111a790e186429c0b8c910f18f51215b0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 04:02:17 +0200 Subject: [PATCH 39/43] fix(mobile): speak the user's language on the Android widget A widget redraw runs as a headless JS task with no Activity, so the app root that applies the language never mounts and the placed card rendered English whatever the user picked. Resolve the stored preference in the widget task itself, which needs an awaitable read on the preference store. Mirror the layout for a right-to-left language too. The widget library's flex engine has no reading direction, so each row reverses its own children and each column flips its alignment. --- .../active-agents-widget.test.ts | 57 ++++++--- .../active-agents-widget.tsx | 117 ++++++++++++------ .../src/glanceable-android/android-sink.ts | 10 +- .../src/glanceable-android/count-format.ts | 12 ++ .../register.test-helpers.ts | 78 ++++++++++++ .../src/glanceable-android/register.test.ts | 101 ++++++--------- .../mobile/src/glanceable-android/register.ts | 25 +++- .../src/lib/hooks/secure-store-preference.ts | 34 ++++- .../src/lib/hooks/use-language-preference.ts | 5 + 9 files changed, 311 insertions(+), 128 deletions(-) create mode 100644 apps/mobile/src/glanceable-android/register.test-helpers.ts diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts index 06e83dd79d..2801f33161 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts +++ b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts @@ -82,20 +82,27 @@ function collectText(node: unknown): string[] { return output; } -function render(props: ReturnType, width: number, height = 200) { - return renderActiveAgentsWidget(props, { - widgetName: 'ActiveAgentsWidget', - widgetId: 1, - width, - height, - screenInfo: { screenWidthDp: 400, screenHeightDp: 800, density: 2, densityDpi: 320 }, - }) as unknown as { light: MockElement; dark: MockElement }; +type Cell = { width: number; height?: number; rtl?: boolean }; + +function render(props: ReturnType, cell: Cell) { + const { width, height = 200, rtl = false } = cell; + return renderActiveAgentsWidget( + props, + { + widgetName: 'ActiveAgentsWidget', + widgetId: 1, + width, + height, + screenInfo: { screenWidthDp: 400, screenHeightDp: 800, density: 2, densityDpi: 320 }, + }, + rtl + ) as unknown as { light: MockElement; dark: MockElement }; } describe('renderActiveAgentsWidget', () => { it('returns distinct light and dark layouts through the theme callback', () => { const props = buildAndroidWidgetProps(snapshotFor([{ status: 'busy' }], 0), {}, translate); - const rep = render(props, 250); + const rep = render(props, { width: 250 }); expect(rep.light).toBeDefined(); expect(rep.dark).toBeDefined(); @@ -106,13 +113,33 @@ describe('renderActiveAgentsWidget', () => { expect(rep.dark.props.style?.backgroundColor).toBe(darkColors.background); }); + // The library's flex engine has no reading direction of its own, so every + // row reverses its own children and every column flips its alignment. + it('mirrors every row for a right-to-left language', () => { + const props = buildAndroidWidgetProps( + snapshotFor([{ status: 'question' }, { status: 'busy' }], 0), + {}, + translate + ); + + expect(collectText(render(props, { width: 250, rtl: true }).light)).toEqual([ + 'Needs input', + '1', + 'Working', + '1', + 'Idle', + '0', + 'Open agents', + ]); + }); + it('shows only the primary count at a small width', () => { const props = buildAndroidWidgetProps( snapshotFor([{ status: 'question' }, { status: 'busy' }, { status: 'busy' }], 0), {}, translate ); - const rep = render(props, 120); + const rep = render(props, { width: 120 }); const text = collectText(rep.light); expect(text).toEqual(['1', 'Needs input']); @@ -124,7 +151,7 @@ describe('renderActiveAgentsWidget', () => { {}, translate ); - const rep = render(props, 250); + const rep = render(props, { width: 250 }); const text = collectText(rep.light); // The zero row draws so the rows hold still as work moves between states. @@ -145,7 +172,7 @@ describe('renderActiveAgentsWidget', () => { translate ); - expect(collectText(render(props, width, 100).light)).toEqual(visibleText); + expect(collectText(render(props, { width, height: 100 }).light)).toEqual(visibleText); } ); @@ -177,7 +204,7 @@ describe('renderActiveAgentsWidget', () => { {}, translate ); - const rep = render(props, width); + const rep = render(props, { width }); for (const surface of [rep.light, rep.dark]) { expect(surface.props.accessibilityLabel).toBe( @@ -202,7 +229,7 @@ describe('renderActiveAgentsWidget', () => { {}, translate ); - const rep = render(props, 250); + const rep = render(props, { width: 250 }); const text = collectText(rep.light); expect(text).toEqual(['Status expired']); @@ -210,7 +237,7 @@ describe('renderActiveAgentsWidget', () => { it('labels the whole widget with the Open agents deep-link click action', () => { const props = buildAndroidWidgetProps(snapshotFor([{ status: 'busy' }], 0), {}, translate); - const rep = render(props, 250); + const rep = render(props, { width: 250 }); expect(rep.light.props.clickAction).toBe('OPEN_URI'); expect(rep.light.props.clickActionData).toEqual({ uri: 'kiloapp:///cloud/sessions' }); diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.tsx b/apps/mobile/src/glanceable-android/active-agents-widget.tsx index 7e89b4b9b5..e485b41cc2 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.tsx +++ b/apps/mobile/src/glanceable-android/active-agents-widget.tsx @@ -77,19 +77,37 @@ const DARK: Palette = { type Size = 'compact' | 'row' | 'stack'; -/** The size bucket, plus whether a `row` cell is wide enough for its labels. */ -type Shape = { size: Size; rowLabels: boolean }; +/** + * The size bucket, whether a `row` cell is wide enough for its labels, and the + * reading direction. The library's flex engine has no direction of its own, so + * every row reverses its own children and every column flips its alignment. + */ +type Shape = { size: Size; rowLabels: boolean; rtl: boolean }; -function shapeOf(info: WidgetInfo): Shape { +function shapeOf(info: WidgetInfo, rtl: boolean): Shape { if (info.width < COMPACT_MAX_WIDTH_DP) { - return { size: 'compact', rowLabels: true }; + return { size: 'compact', rowLabels: true, rtl }; } if (info.height < ROW_MAX_HEIGHT_DP) { // Three cells wide fit three counts but not three labels, so the ranked // state keeps its word and the other two show as a marker and a number. - return { size: 'row', rowLabels: info.width >= ROW_LABEL_MIN_WIDTH_DP }; + return { + size: 'row', + rowLabels: info.width >= ROW_LABEL_MIN_WIDTH_DP, + rtl, + }; } - return { size: 'stack', rowLabels: true }; + return { size: 'stack', rowLabels: true, rtl }; +} + +/** The edge a column's content starts from. */ +function startEdge(rtl: boolean): 'flex-start' | 'flex-end' { + return rtl ? 'flex-end' : 'flex-start'; +} + +/** Lay a row's children out in reading order. */ +function inReadingOrder(children: React.ReactNode[], rtl: boolean): React.ReactNode[] { + return rtl ? children.toReversed() : children; } function dotColor(kind: GlanceableCountKind, palette: Palette): HexColor { @@ -108,6 +126,7 @@ function stateDot(kind: GlanceableCountKind, palette: Palette, size: number) { const color = dotColor(kind, palette); return ( - {stateDot(line.kind, palette, fontSize < 14 ? 9 : 10)} - - {showLabel ? ( - - ) : null} + {inReadingOrder( + [ + stateDot(line.kind, palette, fontSize < 14 ? 9 : 10), + , + showLabel ? ( + + ) : null, + ], + rtl + )} ); } /** Narrow cells: the mark, the ranked marker, and the one count worth a glance. */ -function renderCompact(props: AndroidWidgetProps, palette: Palette) { +function renderCompact(props: AndroidWidgetProps, palette: Palette, rtl: boolean) { if (props.primaryKind === null) { return ( { const isPrimary = line.label === primaryLabel; @@ -200,17 +234,18 @@ function renderCounts(props: AndroidWidgetProps, palette: Palette, shape: Shape) palette, fontSize: size === 'row' ? 13 : 15, showLabel: size !== 'row' || rowLabels || isPrimary, + rtl, }); }); const stacked = ( - {rows} + {size === 'row' ? inReadingOrder(rows, rtl) : rows} ); // Stale carries counts and a warning at once. Only the tall cell has a line @@ -219,7 +254,13 @@ function renderCounts(props: AndroidWidgetProps, palette: Palette, shape: Shape) return stacked; } return ( - + {stacked} {statusText(props, palette)} @@ -227,9 +268,9 @@ function renderCounts(props: AndroidWidgetProps, palette: Palette, shape: Shape) } function renderSurface(props: AndroidWidgetProps, palette: Palette, shape: Shape) { - const { size } = shape; + const { size, rtl } = shape; const body = - size === 'compact' ? renderCompact(props, palette) : renderCounts(props, palette, shape); + size === 'compact' ? renderCompact(props, palette, rtl) : renderCounts(props, palette, shape); // Short cells put the mark beside the counts; a tall cell stacks the mark on // top and lets the counts sit at the bottom, the same composition as the iOS // small family. @@ -242,7 +283,7 @@ function renderSurface(props: AndroidWidgetProps, palette: Palette, shape: Shape style={{ backgroundColor: palette.background, flexDirection: 'column', - alignItems: 'flex-start', + alignItems: startEdge(rtl), justifyContent: 'space-between', height: 'match_parent', width: 'match_parent', @@ -273,15 +314,16 @@ function renderSurface(props: AndroidWidgetProps, palette: Palette, shape: Shape backgroundColor: palette.background, flexDirection: 'row', alignItems: 'center', - justifyContent: 'flex-start', + justifyContent: startEdge(rtl), flexGap: size === 'compact' ? 10 : 14, height: 'match_parent', width: 'match_parent', padding: 12, }} > - {logo(size === 'compact' ? 22 : 28)} - {body} + {/* No array here: a wrapper element per slot would add a layout node. */} + {rtl ? body : logo(size === 'compact' ? 22 : 28)} + {rtl ? logo(size === 'compact' ? 22 : 28) : body} ); } @@ -293,9 +335,10 @@ function renderSurface(props: AndroidWidgetProps, palette: Palette, shape: Shape */ export function renderActiveAgentsWidget( props: AndroidWidgetProps, - info: WidgetInfo + info: WidgetInfo, + rtl = false ): WidgetRepresentation { - const shape = shapeOf(info); + const shape = shapeOf(info, rtl); return { light: renderSurface(props, LIGHT, shape), dark: renderSurface(props, DARK, shape), diff --git a/apps/mobile/src/glanceable-android/android-sink.ts b/apps/mobile/src/glanceable-android/android-sink.ts index 5759c4caf3..aa0eea32bf 100644 --- a/apps/mobile/src/glanceable-android/android-sink.ts +++ b/apps/mobile/src/glanceable-android/android-sink.ts @@ -14,7 +14,7 @@ import { } from '@/lib/glanceable/sink-registry'; import { renderActiveAgentsWidget, WIDGET_NAME } from './active-agents-widget'; -import { formatGlanceableCount } from './count-format'; +import { formatGlanceableCount, isWidgetRtl } from './count-format'; import { end as endLiveUpdate, setWidgetSnapshot, @@ -45,7 +45,10 @@ function translate(key: string): string { let lastWidgetSnapshot: GlanceableAgentsSnapshot | null = null; let notificationActive = false; let revision = 0; -let pending: { snapshot: GlanceableAgentsSnapshot; ctx: GlanceableSinkContext } | null = null; +let pending: { + snapshot: GlanceableAgentsSnapshot; + ctx: GlanceableSinkContext; +} | null = null; let startEpoch = 0; let terminalExpiresAt: number | null = null; @@ -59,7 +62,8 @@ export function getCurrentWidgetProps(): AndroidWidgetProps | null { function renderWidgetNow(props: AndroidWidgetProps): void { void requestWidgetUpdate({ widgetName: WIDGET_NAME, - renderWidget: info => renderActiveAgentsWidget(getCurrentWidgetProps() ?? props, info), + renderWidget: info => + renderActiveAgentsWidget(getCurrentWidgetProps() ?? props, info, isWidgetRtl()), }); } diff --git a/apps/mobile/src/glanceable-android/count-format.ts b/apps/mobile/src/glanceable-android/count-format.ts index a17db393fa..df8b68bb17 100644 --- a/apps/mobile/src/glanceable-android/count-format.ts +++ b/apps/mobile/src/glanceable-android/count-format.ts @@ -1,4 +1,5 @@ import { i18n } from '@/i18n'; +import { RTL_LANGUAGES, type SupportedLanguage } from '@/i18n/languages'; import { numberFormat } from '@/lib/intl-cache'; /** @@ -12,3 +13,14 @@ import { numberFormat } from '@/lib/intl-cache'; export function formatGlanceableCount(value: number): string { return numberFormat(i18n.language, { useGrouping: false }).format(value); } + +/** + * Whether the active language reads right to left. + * + * `syncRtl` flips the native direction for the app's own views, but a widget + * draws through the library's own flex engine, which has no direction. The + * layout mirrors itself from this instead. + */ +export function isWidgetRtl(): boolean { + return RTL_LANGUAGES.has(i18n.language as SupportedLanguage); +} diff --git a/apps/mobile/src/glanceable-android/register.test-helpers.ts b/apps/mobile/src/glanceable-android/register.test-helpers.ts new file mode 100644 index 0000000000..ca646ff461 --- /dev/null +++ b/apps/mobile/src/glanceable-android/register.test-helpers.ts @@ -0,0 +1,78 @@ +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { isValidElement, type ReactNode } from 'react'; +import { type WidgetRepresentation, type WidgetTaskHandler } from 'react-native-android-widget'; +import { vi } from 'vitest'; + +/** Shared fixtures for the widget-task suites. Mocks stay in the test files. */ + +export const NOW = 1_750_000_000_000; + +/** The persisted-snapshot mirror the suites hand to `_setSecureStoreForTests`. */ +export const store = new Map(); +export const secureStore = { + setItemAsync: vi.fn(async (key: string, value: string) => { + store.set(key, value); + await Promise.resolve(); + }), + getItemAsync: vi.fn<(key: string) => Promise>(), +}; + +export function snapshotFor( + sessions: { status: string }[] = [ + { status: 'question' }, + { status: 'retry' }, + { status: 'busy' }, + { status: 'busy' }, + ], + status: GlanceableAgentsSnapshot['status'] = 'happy' +): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + sessions, + status, + userId: 'u1', + organizationId: null, + now: NOW, + }); +} + +export async function runWidgetTask(handler: WidgetTaskHandler, width: number) { + const renders: WidgetRepresentation[] = []; + await handler({ + widgetAction: 'WIDGET_UPDATE', + widgetInfo: { + widgetName: 'ActiveAgentsWidget', + widgetId: 1, + width, + height: 200, + screenInfo: { + screenWidthDp: 400, + screenHeightDp: 800, + density: 2, + densityDpi: 320, + }, + }, + renderWidget: widget => { + renders.push(widget); + }, + }); + const [rendered] = renders; + if (rendered === undefined || !('light' in rendered)) { + throw new Error('The widget task did not render its themed layouts'); + } + return rendered; +} + +export function collectText(node: ReactNode): string[] { + if (Array.isArray(node)) { + return node.flatMap((child: ReactNode) => collectText(child)); + } + if (!isValidElement<{ text?: string; children?: ReactNode }>(node)) { + return []; + } + const text = node.props.text === undefined ? [] : [node.props.text]; + return [...text, ...collectText(node.props.children)]; +} + diff --git a/apps/mobile/src/glanceable-android/register.test.ts b/apps/mobile/src/glanceable-android/register.test.ts index 3d121e75e0..234ac74f55 100644 --- a/apps/mobile/src/glanceable-android/register.test.ts +++ b/apps/mobile/src/glanceable-android/register.test.ts @@ -2,10 +2,18 @@ import { buildGlanceableSnapshot, type GlanceableAgentsSnapshot, } from '@kilocode/app-shared/glanceable-agents-snapshot'; -import { isValidElement, type ReactNode } from 'react'; -import { type WidgetRepresentation, type WidgetTaskHandler } from 'react-native-android-widget'; +import { type WidgetTaskHandler } from 'react-native-android-widget'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + collectText, + NOW, + runWidgetTask, + secureStore, + snapshotFor, + store, +} from './register.test-helpers'; + const mocks = vi.hoisted(() => { let snapshot: string | null = null; let deadline = 0; @@ -24,10 +32,16 @@ const mocks = vi.hoisted(() => { snapshot = null; deadline = 0; }, + language: { value: 'en' }, }; }); vi.mock('expo', () => ({ requireOptionalNativeModule: () => mocks.native })); +// The widget task resolves the language itself; the real store needs natives. +vi.mock('@/lib/hooks/use-language-preference', () => ({ + getResolvedLanguage: () => mocks.language.value, + whenLanguagePreferenceLoaded: vi.fn().mockResolvedValue(undefined), +})); vi.mock('react-native', () => ({ AppState: { addEventListener: vi.fn() }, Alert: { alert: vi.fn() }, @@ -41,34 +55,6 @@ vi.mock('react-native-android-widget', () => ({ ImageWidget: () => null, })); -const NOW = 1_750_000_000_000; -const store = new Map(); -const secureStore = { - setItemAsync: vi.fn(async (key: string, value: string) => { - store.set(key, value); - await Promise.resolve(); - }), - getItemAsync: vi.fn<(key: string) => Promise>(), -}; - -function snapshotFor( - sessions: { status: string }[] = [ - { status: 'question' }, - { status: 'retry' }, - { status: 'busy' }, - { status: 'busy' }, - ], - status: GlanceableAgentsSnapshot['status'] = 'happy' -): GlanceableAgentsSnapshot { - return buildGlanceableSnapshot({ - sessions, - status, - userId: 'u1', - organizationId: null, - now: NOW, - }); -} - async function registerAfterRestart(snapshot: GlanceableAgentsSnapshot | null) { const persist = await import('@/lib/glanceable/persist'); persist._setSecureStoreForTests(secureStore); @@ -88,39 +74,6 @@ async function registerAfterRestart(snapshot: GlanceableAgentsSnapshot | null) { return handler; } -async function runWidgetTask(handler: WidgetTaskHandler, width: number) { - const renders: WidgetRepresentation[] = []; - await handler({ - widgetAction: 'WIDGET_UPDATE', - widgetInfo: { - widgetName: 'ActiveAgentsWidget', - widgetId: 1, - width, - height: 200, - screenInfo: { screenWidthDp: 400, screenHeightDp: 800, density: 2, densityDpi: 320 }, - }, - renderWidget: widget => { - renders.push(widget); - }, - }); - const [rendered] = renders; - if (rendered === undefined || !('light' in rendered)) { - throw new Error('The widget task did not render its themed layouts'); - } - return rendered; -} - -function collectText(node: ReactNode): string[] { - if (Array.isArray(node)) { - return node.flatMap((child: ReactNode) => collectText(child)); - } - if (!isValidElement<{ text?: string; children?: ReactNode }>(node)) { - return []; - } - const text = node.props.text === undefined ? [] : [node.props.text]; - return [...text, ...collectText(node.props.children)]; -} - beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); @@ -139,6 +92,18 @@ afterEach(() => { vi.useRealTimers(); }); +// A widget redraw runs headless: nothing else applies the language. +it('applies the resolved language before it renders', async () => { + mocks.language.value = 'ar'; + const handler = await registerAfterRestart(snapshotFor()); + const { i18n } = await import('@/i18n'); + + await runWidgetTask(handler, 250); + mocks.language.value = 'en'; + + expect(i18n.language).toBe('ar'); +}); + describe.each([120, 250])('registered widget handler at %d dp', width => { it('restores unexpired persisted counts after a fresh process starts', async () => { const handler = await registerAfterRestart(snapshotFor()); @@ -209,7 +174,10 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { const stored = snapshotFor(); const handler = await registerAfterRestart(stored); const { androidSink } = await import('./android-sink'); - androidSink.publish({ ...snapshotFor([{ status: 'busy' }]), revision: stored.revision + 1 }); + androidSink.publish({ + ...snapshotFor([{ status: 'busy' }]), + revision: stored.revision + 1, + }); const rendered = await runWidgetTask(handler, width); const expected = @@ -315,7 +283,10 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { secureStore.getItemAsync.mockReturnValueOnce(read.promise); const rendering = runWidgetTask(handler, width); - androidSink.publish({ ...snapshotFor([{ status: 'busy' }]), revision: stored.revision + 1 }); + androidSink.publish({ + ...snapshotFor([{ status: 'busy' }]), + revision: stored.revision + 1, + }); read.resolve(JSON.stringify(stored)); const rendered = await rendering; const expected = diff --git a/apps/mobile/src/glanceable-android/register.ts b/apps/mobile/src/glanceable-android/register.ts index cb5e86625c..e4f4d4bd48 100644 --- a/apps/mobile/src/glanceable-android/register.ts +++ b/apps/mobile/src/glanceable-android/register.ts @@ -11,10 +11,14 @@ import { } from '@/lib/glanceable/live-activity-switch'; import { getLastGlanceableSnapshot, restorePersistedGlanceable } from '@/lib/glanceable/persist'; import { registerGlanceableSink } from '@/lib/glanceable/sink-registry'; +import { + getResolvedLanguage, + whenLanguagePreferenceLoaded, +} from '@/lib/hooks/use-language-preference'; import { renderActiveAgentsWidget } from './active-agents-widget'; import { androidSink, getCurrentWidgetProps, handleAppStateActive } from './android-sink'; -import { formatGlanceableCount } from './count-format'; +import { formatGlanceableCount, isWidgetRtl } from './count-format'; import { getStoredWidgetSnapshot, setWidgetSnapshot } from './live-update'; import { buildCurrentWidgetProps, buildGenericWidgetProps } from './widget-props'; @@ -47,9 +51,26 @@ function translate(key: string): string { return i18n.t(key); } +/** + * Switch i18n to the user's language before a widget render. + * + * A widget redraw runs as a headless JS task with no Activity, so the app's + * root never mounts and nothing else applies the language — without this the + * placed widget renders English whatever the user chose. + */ +async function applyWidgetLanguage(): Promise { + await whenLanguagePreferenceLoaded(); + const language = getResolvedLanguage(); + if (i18n.language !== language) { + await i18n.changeLanguage(language); + } +} + registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { const { widgetInfo, renderWidget } = task; + await applyWidgetLanguage(); + // Re-read native storage even in a live process. An old alarm can already have // queued this task when newer work or a privacy blank replaces its deadline. const stored = getStoredWidgetSnapshot(); @@ -71,5 +92,5 @@ registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { // A live publish during restoration owns the widget. props = getCurrentWidgetProps() ?? props; } - renderWidget(renderActiveAgentsWidget(props, widgetInfo)); + renderWidget(renderActiveAgentsWidget(props, widgetInfo, isWidgetRtl())); }); diff --git a/apps/mobile/src/lib/hooks/secure-store-preference.ts b/apps/mobile/src/lib/hooks/secure-store-preference.ts index 42618da676..245442eaa5 100644 --- a/apps/mobile/src/lib/hooks/secure-store-preference.ts +++ b/apps/mobile/src/lib/hooks/secure-store-preference.ts @@ -5,6 +5,10 @@ import { toast } from 'sonner-native'; import { i18n } from '@/i18n'; import { deleteAccountMetadata, setAccountMetadata } from '@/lib/auth/account-metadata-write'; +function noop(): void { + // Placeholder until the promise executor hands over its resolve. +} + /** * Module-level store for a SecureStore-backed preference so every hook * instance (settings sheet, message list, new-session screen) shares one @@ -33,6 +37,13 @@ export function createSecureStorePreference(options: { // value even when mergeOnLoad is set. let cleared = false; let loadStarted = false; + let markLoaded = noop; + // Resolves once the disk read settles, so a caller with no React tree (the + // Android widget task) can await the stored value instead of reading the + // default. + const loaded = new Promise(resolve => { + markLoaded = resolve; + }); const listeners = new Set<() => void>(); const emit = () => { @@ -58,10 +69,14 @@ export function createSecureStorePreference(options: { // user has done anything, so there's nothing actionable to tell them. // Just log so we can see failure rates. Sentry.captureException(error, { - tags: { 'error.subsystem': 'preferences', 'error.operation': 'load_secure_store' }, + tags: { + 'error.subsystem': 'preferences', + 'error.operation': 'load_secure_store', + }, }); } finally { hasLoaded = true; + markLoaded(); emit(); } }; @@ -85,13 +100,20 @@ export function createSecureStorePreference(options: { } }; + const preload = () => { + if (!loadStarted) { + loadStarted = true; + void load(); + } + }; + return { /** Start the disk read without registering a listener (module-scope warm-up). */ - preload: () => { - if (!loadStarted) { - loadStarted = true; - void load(); - } + preload, + /** Start the disk read and await it. For callers outside a React tree. */ + whenLoaded: async () => { + preload(); + await loaded; }, subscribe: (listener: () => void) => { if (!loadStarted) { diff --git a/apps/mobile/src/lib/hooks/use-language-preference.ts b/apps/mobile/src/lib/hooks/use-language-preference.ts index 7286468478..76d3afb14b 100644 --- a/apps/mobile/src/lib/hooks/use-language-preference.ts +++ b/apps/mobile/src/lib/hooks/use-language-preference.ts @@ -50,6 +50,11 @@ export function preloadLanguagePreference(): void { store.preload(); } +/** Await the stored preference. For callers with no React tree. */ +export async function whenLanguagePreferenceLoaded(): Promise { + await store.whenLoaded(); +} + export function useLanguagePreference() { const preference = useSyncExternalStore(store.subscribe, store.get); const hasLoaded = useSyncExternalStore(store.subscribe, store.getHasLoaded); From 7310f83fe0d95b4e6ef38128a69af1d9b2d2547e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 04:04:01 +0200 Subject: [PATCH 40/43] fix(mobile): align the androidx.work versions for the widget expo-widgets pulls androidx.glance, which depends on work-runtime-ktx 2.7.1, while react-native-android-widget depends on work-runtime 2.8.1. Version 2.8.0 folded the ktx classes into the main artifact, so the two together fail :app:checkDebugDuplicateClasses. Pin both to 2.8.1, where ktx is an empty shim. --- .../plugins/withActiveAgentsAndroidWidget.js | 29 ++++++++++++++++++- .../register.test-helpers.ts | 1 - 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/mobile/plugins/withActiveAgentsAndroidWidget.js b/apps/mobile/plugins/withActiveAgentsAndroidWidget.js index 8b33f71154..62ff179ab0 100644 --- a/apps/mobile/plugins/withActiveAgentsAndroidWidget.js +++ b/apps/mobile/plugins/withActiveAgentsAndroidWidget.js @@ -1,6 +1,8 @@ const fs = require('fs'); const path = require('path'); +const { withAppBuildGradle } = require('expo/config-plugins'); + const GALLERY_COPY = require('./widget-gallery-copy.json'); // Wraps react-native-android-widget so its config plugin only applies once the @@ -16,6 +18,31 @@ const GALLERY_COPY = require('./widget-gallery-copy.json'); // wraps it in a string resource of its own. const WIDGET_CONFIG_PATH = path.resolve(__dirname, '../src/glanceable-android/widget-config.json'); +const WORK_FORCE_MARKER = 'kilo-work-runtime-alignment'; + +// expo-widgets pulls androidx.glance, which depends on work-runtime-ktx 2.7.1, +// while react-native-android-widget depends on work-runtime 2.8.1. Version +// 2.8.0 folded the ktx classes into the main artifact, so the two together fail +// :app:checkDebugDuplicateClasses. Pin both to 2.8.1, where the ktx artifact is +// an empty shim. +function withWorkRuntimeAlignment(config) { + return withAppBuildGradle(config, cfg => { + if (cfg.modResults.contents.includes(WORK_FORCE_MARKER)) { + return cfg; + } + cfg.modResults.contents += ` +// ${WORK_FORCE_MARKER} +configurations.configureEach { + resolutionStrategy { + force 'androidx.work:work-runtime:2.8.1' + force 'androidx.work:work-runtime-ktx:2.8.1' + } +} +`; + return cfg; + }); +} + function loadAndroidWidgetsPlugin() { const resolved = require.resolve('react-native-android-widget/app.plugin.js'); const mod = require(resolved); @@ -37,5 +64,5 @@ module.exports = function withActiveAgentsAndroidWidget(config) { label: `@string/widget_${widget.name.toLowerCase()}_label`, description: GALLERY_COPY.en.description, })); - return loadAndroidWidgetsPlugin()(config, { widgets: described }); + return withWorkRuntimeAlignment(loadAndroidWidgetsPlugin()(config, { widgets: described })); }; diff --git a/apps/mobile/src/glanceable-android/register.test-helpers.ts b/apps/mobile/src/glanceable-android/register.test-helpers.ts index ca646ff461..e267cc8a8a 100644 --- a/apps/mobile/src/glanceable-android/register.test-helpers.ts +++ b/apps/mobile/src/glanceable-android/register.test-helpers.ts @@ -75,4 +75,3 @@ export function collectText(node: ReactNode): string[] { const text = node.props.text === undefined ? [] : [node.props.text]; return [...text, ...collectText(node.props.children)]; } - From def8706cce94033d3445e97863aaec4246a1c83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 04:22:56 +0200 Subject: [PATCH 41/43] refactor(mobile): load the widget slice only when a widget task fires The app entry registered the headless widget task by importing the whole Android glanceable slice, which starts i18n and SecureStore before expo-router sets the app up. Register a thin handler instead and require the slice from inside it, so the entry touches one module. --- apps/mobile/index.js | 15 ++++++++++----- .../src/glanceable-android/register.test.ts | 11 ++--------- apps/mobile/src/glanceable-android/register.ts | 14 ++++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/mobile/index.js b/apps/mobile/index.js index 9d2db72f62..af9550f48b 100644 --- a/apps/mobile/index.js +++ b/apps/mobile/index.js @@ -1,16 +1,21 @@ // The app entry. // // Android redraws a placed widget from a headless JS task, which loads this -// bundle with no Activity and therefore never evaluates an expo-router route. -// `registerWidgetTaskHandler` has to have run by then, so the Android glanceable -// slice is required here rather than from the root layout, and before -// `expo-router/entry` so the registration cannot depend on routing at all. +// bundle with no Activity: no route and no notification handler runs first, so +// the widget task has to be registered here. The widget slice itself loads only +// when a task fires — requiring it at entry would start i18n and SecureStore +// before `expo-router/entry` sets the app up. // // `require`, not `import`: ESM hoisting would run `expo-router/entry` first. const { Platform } = require('react-native'); if (Platform.OS === 'android') { - require('./src/glanceable-android/register'); + const { registerWidgetTaskHandler } = require('react-native-android-widget'); + + registerWidgetTaskHandler(async task => { + const { handleWidgetTask } = require('./src/glanceable-android/register'); + await handleWidgetTask(task); + }); } require('expo-router/entry'); diff --git a/apps/mobile/src/glanceable-android/register.test.ts b/apps/mobile/src/glanceable-android/register.test.ts index 234ac74f55..7684d02800 100644 --- a/apps/mobile/src/glanceable-android/register.test.ts +++ b/apps/mobile/src/glanceable-android/register.test.ts @@ -2,7 +2,6 @@ import { buildGlanceableSnapshot, type GlanceableAgentsSnapshot, } from '@kilocode/app-shared/glanceable-agents-snapshot'; -import { type WidgetTaskHandler } from 'react-native-android-widget'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -18,7 +17,6 @@ const mocks = vi.hoisted(() => { let snapshot: string | null = null; let deadline = 0; return { - registerWidgetTaskHandler: vi.fn<(handler: WidgetTaskHandler) => void>(), native: { setWidgetSnapshot: (next: string, expiresAt: number) => { snapshot = next; @@ -48,7 +46,6 @@ vi.mock('react-native', () => ({ Linking: { openSettings: vi.fn() }, })); vi.mock('react-native-android-widget', () => ({ - registerWidgetTaskHandler: mocks.registerWidgetTaskHandler, requestWidgetUpdate: vi.fn().mockResolvedValue(undefined), FlexWidget: () => null, TextWidget: () => null, @@ -66,12 +63,8 @@ async function registerAfterRestart(snapshot: GlanceableAgentsSnapshot | null) { vi.resetModules(); const freshPersist = await import('@/lib/glanceable/persist'); freshPersist._setSecureStoreForTests(secureStore); - await import('./register'); - const handler = mocks.registerWidgetTaskHandler.mock.lastCall?.[0]; - if (handler === undefined) { - throw new Error('The widget task handler was not registered'); - } - return handler; + const { handleWidgetTask } = await import('./register'); + return handleWidgetTask; } beforeEach(() => { diff --git a/apps/mobile/src/glanceable-android/register.ts b/apps/mobile/src/glanceable-android/register.ts index e4f4d4bd48..884fbb846a 100644 --- a/apps/mobile/src/glanceable-android/register.ts +++ b/apps/mobile/src/glanceable-android/register.ts @@ -1,8 +1,5 @@ import { AppState } from 'react-native'; -import { - registerWidgetTaskHandler, - type WidgetTaskHandlerProps, -} from 'react-native-android-widget'; +import { type WidgetTaskHandlerProps } from 'react-native-android-widget'; import { i18n } from '@/i18n'; import { @@ -66,7 +63,12 @@ async function applyWidgetLanguage(): Promise { } } -registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { +/** + * Redraw a placed widget. Registered from the app entry, which loads this + * module only when a task fires: a widget redraw runs headless, so nothing + * else has loaded the Android sink by then. + */ +export async function handleWidgetTask(task: WidgetTaskHandlerProps): Promise { const { widgetInfo, renderWidget } = task; await applyWidgetLanguage(); @@ -93,4 +95,4 @@ registerWidgetTaskHandler(async (task: WidgetTaskHandlerProps) => { props = getCurrentWidgetProps() ?? props; } renderWidget(renderActiveAgentsWidget(props, widgetInfo, isWidgetRtl())); -}); +} From a7e6b31d8c076a84340eeb2d0fef2732dcb57e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 05:19:42 +0200 Subject: [PATCH 42/43] feat(mobile): align the Android widget with the iOS card Drop the "Open agents" line: the whole card already opens the agents screen, so the words only took the space the counts needed. Move the stale warning under the mark, above the rows. A fourth line below three counts read as a fourth state. Draw every state, zeros included, so the rows hold still as work moves between them. Stack the rows whenever the cell is tall enough, however narrow it is. Beside the mark a two-cell-wide card truncated its labels. --- .../active-agents-widget.test.ts | 46 ++++-- .../active-agents-widget.tsx | 138 ++++++++---------- .../glanceable-android/android-sink.test.ts | 17 ++- .../src/glanceable-android/register.test.ts | 20 +-- .../mobile/src/glanceable-android/register.ts | 2 +- .../glanceable-android/widget-props.test.ts | 17 +-- .../src/glanceable-android/widget-props.ts | 31 +--- 7 files changed, 119 insertions(+), 152 deletions(-) diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts index 2801f33161..feca5a7859 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.test.ts +++ b/apps/mobile/src/glanceable-android/active-agents-widget.test.ts @@ -129,11 +129,29 @@ describe('renderActiveAgentsWidget', () => { '1', 'Idle', '0', - 'Open agents', ]); }); - it('shows only the primary count at a small width', () => { + // Two cells wide and one tall: too narrow to run the states across, so they + // stack beside the mark and each one keeps its word. + it('stacks every state beside the mark in a short narrow cell', () => { + const props = buildAndroidWidgetProps( + snapshotFor([{ status: 'question' }, { status: 'busy' }], 0), + {}, + translate + ); + + expect(collectText(render(props, { width: 150, height: 100 }).light)).toEqual([ + '1', + 'Needs input', + '1', + 'Working', + '0', + 'Idle', + ]); + }); + + it('draws every state at a small width too, zeros included', () => { const props = buildAndroidWidgetProps( snapshotFor([{ status: 'question' }, { status: 'busy' }, { status: 'busy' }], 0), {}, @@ -142,10 +160,10 @@ describe('renderActiveAgentsWidget', () => { const rep = render(props, { width: 120 }); const text = collectText(rep.light); - expect(text).toEqual(['1', 'Needs input']); + expect(text).toEqual(['1', 'Needs input', '2', 'Working', '0', 'Idle']); }); - it('shows every count, zeros included, and the Open agents affordance at a wide width', () => { + it('shows every count, zeros included, at a wide width', () => { const props = buildAndroidWidgetProps( snapshotFor([{ status: 'question' }, { status: 'busy' }], 0), {}, @@ -155,7 +173,7 @@ describe('renderActiveAgentsWidget', () => { const text = collectText(rep.light); // The zero row draws so the rows hold still as work moves between states. - expect(text).toEqual(['1', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']); + expect(text).toEqual(['1', 'Needs input', '1', 'Working', '0', 'Idle']); }); // One cell tall: the counts run in a row instead of stacking. A short row @@ -176,20 +194,16 @@ describe('renderActiveAgentsWidget', () => { } ); + // The stale warning sits with the mark, above the rows, so a fourth line + // under three counts cannot read as a fourth state. it.each([ - { width: 120, visibleText: ['2', 'Needs input'] }, + { + width: 120, + visibleText: ['Updates delayed', '2', 'Needs input', '4', 'Working', '3', 'Idle'], + }, { width: 250, - visibleText: [ - '2', - 'Needs input', - '4', - 'Working', - '3', - 'Idle', - 'Updates delayed', - 'Open agents', - ], + visibleText: ['Updates delayed', '2', 'Needs input', '4', 'Working', '3', 'Idle'], }, ])( 'speaks stale numeric counts and keeps the deep link at width $width', diff --git a/apps/mobile/src/glanceable-android/active-agents-widget.tsx b/apps/mobile/src/glanceable-android/active-agents-widget.tsx index e485b41cc2..181e53a40c 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.tsx +++ b/apps/mobile/src/glanceable-android/active-agents-widget.tsx @@ -23,7 +23,7 @@ import { type AndroidWidgetProps } from './widget-props'; export const WIDGET_NAME = 'ActiveAgentsWidget'; /** - * Below this width (dp) only the primary count fits beside the mark. + * Below this width (dp) a short cell stacks its rows instead of running them. * * Two cells wide reports about 150–190 dp and three cells about 230–280 dp, so * the split sits between them. A tighter bound let a two-cell cell take the @@ -33,7 +33,8 @@ const COMPACT_MAX_WIDTH_DP = 210; /** At or above this width (dp) every state in the row can carry its label. */ const ROW_LABEL_MIN_WIDTH_DP = 300; /** - * Below this height (dp) the three rows cannot stack, so they run in a row. + * At or above this height (dp) the mark sits above the rows and they own the + * full width; below it the mark sits beside them. * * One cell tall lands anywhere from 40 dp to about 110 dp depending on the * device and the launcher's grid, and two cells tall starts around 150 dp, so @@ -85,19 +86,17 @@ type Size = 'compact' | 'row' | 'stack'; type Shape = { size: Size; rowLabels: boolean; rtl: boolean }; function shapeOf(info: WidgetInfo, rtl: boolean): Shape { + // Height first: a narrow cell that is tall enough still stacks, because the + // rows then own the full width. Beside the mark they truncated their labels. + if (info.height >= ROW_MAX_HEIGHT_DP) { + return { size: 'stack', rowLabels: true, rtl }; + } if (info.width < COMPACT_MAX_WIDTH_DP) { return { size: 'compact', rowLabels: true, rtl }; } - if (info.height < ROW_MAX_HEIGHT_DP) { - // Three cells wide fit three counts but not three labels, so the ranked - // state keeps its word and the other two show as a marker and a number. - return { - size: 'row', - rowLabels: info.width >= ROW_LABEL_MIN_WIDTH_DP, - rtl, - }; - } - return { size: 'stack', rowLabels: true, rtl }; + // Three cells wide fit three counts but not three labels, so the ranked + // state keeps its word and the other two show as a marker and a number. + return { size: 'row', rowLabels: info.width >= ROW_LABEL_MIN_WIDTH_DP, rtl }; } /** The edge a column's content starts from. */ @@ -188,29 +187,7 @@ function countRow( ); } -/** Narrow cells: the mark, the ranked marker, and the one count worth a glance. */ -function renderCompact(props: AndroidWidgetProps, palette: Palette, rtl: boolean) { - if (props.primaryKind === null) { - return ( - - ); - } - return countRow( - { - label: props.primaryLabel ?? '', - kind: props.primaryKind, - count: props.primaryCount, - }, - true, - { palette, fontSize: 15, showLabel: true, rtl } - ); -} - +/** The locked copy, drawn in place of the counts. */ function statusText(props: AndroidWidgetProps, palette: Palette) { return ( ; +/** A short cell runs its counts in a row, so the gap separates states, not lines. */ +const COUNT_GAP_DP = { compact: 3, row: 14, stack: 6 } satisfies Record; + +function markGroup(props: AndroidWidgetProps, palette: Palette, shape: Shape) { + const mark = logo(MARK_SIZE_DP[shape.size]); + if (props.countLines.length === 0 || props.statusLine === null) { + return mark; + } + return ( + + {mark} + + + ); +} + function renderCounts(props: AndroidWidgetProps, palette: Palette, shape: Shape) { if (props.countLines.length === 0) { return statusText(props, palette); } const { size, rowLabels, rtl } = shape; const primaryLabel = props.primaryLabel; + // Every state draws its own row, zeros included, so the rows hold still as + // work moves between them and a narrow cell says as much as a wide one. const rows = props.countLines.map(line => { const isPrimary = line.label === primaryLabel; return countRow(line, isPrimary, { palette, - fontSize: size === 'row' ? 13 : 15, + fontSize: size === 'stack' ? 15 : 13, showLabel: size !== 'row' || rowLabels || isPrimary, rtl, }); }); - const stacked = ( + return ( {size === 'row' ? inReadingOrder(rows, rtl) : rows} ); - // Stale carries counts and a warning at once. Only the tall cell has a line - // to spare for it; the short row would have to drop a count to fit it. - if (size !== 'stack' || props.statusLine === null) { - return stacked; - } - return ( - - {stacked} - {statusText(props, palette)} - - ); } function renderSurface(props: AndroidWidgetProps, palette: Palette, shape: Shape) { const { size, rtl } = shape; - const body = - size === 'compact' ? renderCompact(props, palette, rtl) : renderCounts(props, palette, shape); + const body = renderCounts(props, palette, shape); // Short cells put the mark beside the counts; a tall cell stacks the mark on // top and lets the counts sit at the bottom, the same composition as the iOS // small family. @@ -284,24 +280,18 @@ function renderSurface(props: AndroidWidgetProps, palette: Palette, shape: Shape backgroundColor: palette.background, flexDirection: 'column', alignItems: startEdge(rtl), - justifyContent: 'space-between', + // Centred, not spread: with no affordance under the counts the block + // is the mark and the rows, and spreading those two to the edges + // leaves a hole between them. + justifyContent: 'center', + flexGap: 12, height: 'match_parent', width: 'match_parent', padding: 14, }} > - {logo(26)} + {markGroup(props, palette, shape)} {body} - {props.showOpenAgents ? ( - - ) : ( - - )} ); } @@ -322,16 +312,16 @@ function renderSurface(props: AndroidWidgetProps, palette: Palette, shape: Shape }} > {/* No array here: a wrapper element per slot would add a layout node. */} - {rtl ? body : logo(size === 'compact' ? 22 : 28)} - {rtl ? logo(size === 'compact' ? 22 : 28) : body} + {rtl ? body : markGroup(props, palette, shape)} + {rtl ? markGroup(props, palette, shape) : body} ); } /** - * Distinct light and dark layouts through the library's theme callback. Narrow - * cells show only the ranked count; short cells run the three states in a row; - * a tall cell stacks them under the mark. + * Distinct light and dark layouts through the library's theme callback. A tall + * cell stacks the three states under the mark; a short wide cell runs them + * beside it in a row; a short narrow cell stacks them beside it. */ export function renderActiveAgentsWidget( props: AndroidWidgetProps, diff --git a/apps/mobile/src/glanceable-android/android-sink.test.ts b/apps/mobile/src/glanceable-android/android-sink.test.ts index f29cfccd4c..f45b2c5763 100644 --- a/apps/mobile/src/glanceable-android/android-sink.test.ts +++ b/apps/mobile/src/glanceable-android/android-sink.test.ts @@ -391,6 +391,11 @@ describe('androidSink app-state retry', () => { }); }); +/** The working row's count — the rows always carry all three states in order. */ +function runningCount(): string | undefined { + return getCurrentWidgetProps()?.countLines.find(line => line.kind === 'running')?.count; +} + describe('androidSink widget publish and end', () => { it('publishes the widget snapshot on every publish', () => { androidSink.publish(snapshotFor([{ status: 'busy' }], 0)); @@ -400,7 +405,7 @@ describe('androidSink widget publish and end', () => { expect.objectContaining({ widgetName: 'ActiveAgentsWidget' }) ); expect(getCurrentWidgetProps()?.statusLine).toBeNull(); - expect(getCurrentWidgetProps()?.primaryCount).toBe('1'); + expect(runningCount()).toBe('1'); }); it('publishes the stale warning and retained counts through the native bridge', async () => { @@ -458,7 +463,7 @@ describe('androidSink widget publish and end', () => { androidSink.endImmediate(); expect(mocks.native.end).toHaveBeenCalledTimes(1); expect(getCurrentWidgetProps()).not.toBeNull(); - expect(getCurrentWidgetProps()?.primaryCount).toBe('1'); + expect(runningCount()).toBe('1'); }); it('ends the ongoing notification without removing widget delivery', async () => { @@ -481,12 +486,10 @@ describe('androidSink widget publish and end', () => { expect(vi.getTimerCount()).toBe(0); vi.setSystemTime(NOW + 28_799_999); - expect(getCurrentWidgetProps()?.primaryCount).toBe('1'); + expect(runningCount()).toBe('1'); vi.setSystemTime(NOW + 28_800_000); expect(getCurrentWidgetProps()?.statusLine).toBe('Status expired'); expect(getCurrentWidgetProps()?.countLines).toEqual([]); - expect(getCurrentWidgetProps()?.primaryCount).toBe('0'); - expect(getCurrentWidgetProps()?.showOpenAgents).toBe(false); } ); @@ -503,7 +506,7 @@ describe('androidSink widget publish and end', () => { androidSink.endImmediate(); expect(mocks.getWidgetDeadline()).toBe(NOW + 28_860_000); vi.setSystemTime(NOW + 28_800_000); - expect(getCurrentWidgetProps()?.primaryCount).toBe('1'); + expect(runningCount()).toBe('1'); expect(mocks.getNotification()).toBeNull(); }); @@ -512,7 +515,7 @@ describe('androidSink widget publish and end', () => { vi.setSystemTime(NOW + 60_000); androidSink.publish({ ...MIXED, status: 'stale', revision: 2 }); expect(mocks.getWidgetDeadline()).toBe(NOW + 28_800_000); - expect(getCurrentWidgetProps()?.primaryCount).toBe('2'); + expect(getCurrentWidgetProps()?.countLines.at(0)?.count).toBe('2'); vi.setSystemTime(NOW + 28_800_000); expect(getCurrentWidgetProps()?.countLines).toEqual([]); }); diff --git a/apps/mobile/src/glanceable-android/register.test.ts b/apps/mobile/src/glanceable-android/register.test.ts index 7684d02800..0d3dd24a79 100644 --- a/apps/mobile/src/glanceable-android/register.test.ts +++ b/apps/mobile/src/glanceable-android/register.test.ts @@ -101,10 +101,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { it('restores unexpired persisted counts after a fresh process starts', async () => { const handler = await registerAfterRestart(snapshotFor()); const rendered = await runWidgetTask(handler, width); - const expected = - width === 120 - ? ['2', 'Needs input'] - : ['2', 'Needs input', '2', 'Working', '0', 'Idle', 'Open agents']; + const expected = ['2', 'Needs input', '2', 'Working', '0', 'Idle']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -173,10 +170,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { }); const rendered = await runWidgetTask(handler, width); - const expected = - width === 120 - ? ['1', 'Working'] - : ['0', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']; + const expected = ['0', 'Needs input', '1', 'Working', '0', 'Idle']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -198,10 +192,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { vi.setSystemTime(Date.parse(old.expiresAt)); const rendered = await runWidgetTask(handler, width); - const expected = - width === 120 - ? ['1', 'Working'] - : ['0', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']; + const expected = ['0', 'Needs input', '1', 'Working', '0', 'Idle']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); }); @@ -282,10 +273,7 @@ describe.each([120, 250])('registered widget handler at %d dp', width => { }); read.resolve(JSON.stringify(stored)); const rendered = await rendering; - const expected = - width === 120 - ? ['1', 'Working'] - : ['0', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']; + const expected = ['0', 'Needs input', '1', 'Working', '0', 'Idle']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); diff --git a/apps/mobile/src/glanceable-android/register.ts b/apps/mobile/src/glanceable-android/register.ts index 884fbb846a..581cf4462b 100644 --- a/apps/mobile/src/glanceable-android/register.ts +++ b/apps/mobile/src/glanceable-android/register.ts @@ -89,7 +89,7 @@ export async function handleWidgetTask(task: WidgetTaskHandlerProps): Promise { it('ranks the compact primary count and keeps all expanded numeric counts', () => { const props = buildAndroidWidgetProps(MIXED, {}, translate); expect(props.primaryLabel).toBe('Needs input'); - expect(props.primaryCount).toBe('2'); - expect(props.primaryKind).toBe('needsInput'); expect(props.countLines).toEqual([ { label: 'Needs input', kind: 'needsInput', count: '2' }, { label: 'Working', kind: 'running', count: '4' }, @@ -90,11 +88,10 @@ describe('buildAndroidWidgetProps', () => { ['signed_out', [], 'Sign in to see agents', 0, false], ['privacy', [], 'Open Kilo to see agents', 0, false], ]; - for (const [status, sessions, statusLine, counts, showOpenAgents] of cases) { + for (const [status, sessions, statusLine, counts] of cases) { const props = buildAndroidWidgetProps(snapshotFor(sessions, 0, status), {}, translate); expect(props.statusLine).toBe(statusLine); expect(props.countLines).toHaveLength(counts); - expect(props.showOpenAgents).toBe(showOpenAgents); } }); @@ -112,11 +109,7 @@ describe('buildAndroidWidgetProps', () => { expect(Object.keys(props).toSorted()).toEqual([ 'accessibilityLabel', 'countLines', - 'openAgentsLabel', - 'primaryCount', - 'primaryKind', 'primaryLabel', - 'showOpenAgents', 'statusLine', ]); expect(json).not.toContain('user-9f3a-leak'); @@ -133,14 +126,13 @@ describe('current widget deadline rendering', () => { vi.useRealTimers(); }); - it.each(['happy', 'stale'] as const)('hides expired %s counts and the visible action', status => { + it.each(['happy', 'stale'] as const)('hides expired %s counts', status => { vi.useFakeTimers(); vi.setSystemTime(NOW + 28_800_000); const props = buildCurrentWidgetProps({ ...MIXED, status }, translate); expect(props.statusLine).toBe('Status expired'); expect(props.countLines).toEqual([]); expect(props.accessibilityLabel).toBe('Status expired, Open agents'); - expect(props.showOpenAgents).toBe(false); }); it.each([ @@ -155,7 +147,6 @@ describe('current widget deadline rendering', () => { expect(props.statusLine).toBe(expected); expect(props.accessibilityLabel).toBe(`${expected}, Open agents`); expect(props.countLines).toEqual([]); - expect(props.showOpenAgents).toBe(false); }); it('hides counts when the stored expiry is not a valid date', () => { @@ -219,8 +210,6 @@ describe('status precedence and count hiding', () => { expect(props.statusLine).toBe(expected); expect(props.countLines).toEqual([]); expect(props.primaryLabel).toBeNull(); - expect(props.primaryCount).toBe('0'); - expect(props.showOpenAgents).toBe(false); expect(buildOngoingNotificationText(snapshot, {}, translate)).toBe(expected); expect(buildCompactNotificationText(snapshot, {})).toBeNull(); }); @@ -234,8 +223,6 @@ describe('status precedence and count hiding', () => { expect(props.statusLine).toBe(expected); expect(props.countLines).toEqual([]); expect(props.primaryLabel).toBeNull(); - expect(props.primaryCount).toBe('0'); - expect(props.showOpenAgents).toBe(false); expect(buildOngoingNotificationText(snapshot, flags, translate)).toBe(expected); expect(buildCompactNotificationText(snapshot, flags)).toBeNull(); }); diff --git a/apps/mobile/src/glanceable-android/widget-props.ts b/apps/mobile/src/glanceable-android/widget-props.ts index 4547a9c4a5..326fa3a64c 100644 --- a/apps/mobile/src/glanceable-android/widget-props.ts +++ b/apps/mobile/src/glanceable-android/widget-props.ts @@ -11,7 +11,11 @@ import { } from '@/lib/glanceable/presentation'; /** One translated count line for an Android surface. `kind` picks dot and color. */ -type AndroidWidgetCount = { label: string; kind: GlanceableCountKind; count: string }; +type AndroidWidgetCount = { + label: string; + kind: GlanceableCountKind; + count: string; +}; /** * Format a count in the active language's own digits. @@ -31,18 +35,10 @@ export type GlanceableCountFormat = (value: number) => string; export type AndroidWidgetProps = { /** Translated locked copy; null while counts show (happy). Stale carries both. */ statusLine: string | null; - /** Non-zero count lines in rank order (needs-input, running, idle). */ + /** Every count line in rank order (needs-input, running, idle), zeros included. */ countLines: AndroidWidgetCount[]; - /** Top-ranked count label for compact widths; null when no eligible work. */ + /** Top-ranked count label; the only row that keeps the foreground color. */ primaryLabel: string | null; - /** Top-ranked count state for compact widths; null when no eligible work. */ - primaryKind: GlanceableCountKind | null; - /** Top-ranked count value for compact widths; formatted "0" when none. */ - primaryCount: string; - /** Translated "Open agents" affordance. */ - openAgentsLabel: string; - /** True for happy and stale — the only statuses that show counts. */ - showOpenAgents: boolean; /** Spoken label: status words, counts, then Open agents. Never a title or id. */ accessibilityLabel: string; }; @@ -68,10 +64,6 @@ export function buildAndroidWidgetProps( count: formatCount(line.count), })), primaryLabel: primary === null ? null : translate(primary.key), - primaryKind: primary === null ? null : primary.kind, - primaryCount: formatCount(primary === null ? 0 : primary.count), - openAgentsLabel: translate('glanceable.openAgents'), - showOpenAgents: showCounts, accessibilityLabel: glanceableSpokenLabel(snapshot, flags, translate), }; } @@ -114,19 +106,12 @@ function buildExpiredWidgetProps( } /** Gallery placeholder: empty copy and no counts, with no snapshot behind it. */ -export function buildGenericWidgetProps( - translate: (key: string) => string, - formatCount: GlanceableCountFormat = String -): AndroidWidgetProps { +export function buildGenericWidgetProps(translate: (key: string) => string): AndroidWidgetProps { const empty = translate('glanceable.empty'); return { statusLine: empty, countLines: [], primaryLabel: null, - primaryKind: null, - primaryCount: formatCount(0), - openAgentsLabel: translate('glanceable.openAgents'), - showOpenAgents: false, accessibilityLabel: empty, }; } From 97c7eadd80e77e5aa95c2d9d95f0a0bbf3e62d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 05:26:57 +0200 Subject: [PATCH 43/43] fix(web): bind the session-ingest tokens to their audience --- apps/web/src/lib/active-sessions-list.ts | 1 + apps/web/src/routers/active-sessions-router.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/lib/active-sessions-list.ts b/apps/web/src/lib/active-sessions-list.ts index 3be310699b..dff79ddc51 100644 --- a/apps/web/src/lib/active-sessions-list.ts +++ b/apps/web/src/lib/active-sessions-list.ts @@ -307,6 +307,7 @@ export async function listActiveSessions({ } else { const token = generateBoundedInternalServiceToken(userId, { audience: SESSION_INGEST_AUDIENCE, + expiresIn: 60 * 60, }); const url = `${SESSION_INGEST_WORKER_URL}/api/sessions/active`; diff --git a/apps/web/src/routers/active-sessions-router.ts b/apps/web/src/routers/active-sessions-router.ts index 1d2a3dd577..66f062ed29 100644 --- a/apps/web/src/routers/active-sessions-router.ts +++ b/apps/web/src/routers/active-sessions-router.ts @@ -95,8 +95,9 @@ async function mintWebTicket(userId: string): Promise<{ token: string; expiresAt } const token = generateBoundedInternalServiceToken(userId, { - audience: SESSION_INGEST_AUDIENCE, - }); + audience: SESSION_INGEST_AUDIENCE, + expiresIn: 60 * 60, + }); const url = `${SESSION_INGEST_WORKER_URL}/api/user/web-ticket`; let response: Response; @@ -180,6 +181,7 @@ export const activeSessionsRouter = createTRPCRouter({ const token = generateBoundedInternalServiceToken(ctx.user.id, { audience: SESSION_INGEST_AUDIENCE, + expiresIn: 60 * 60, }); const url = `${SESSION_INGEST_WORKER_URL}/api/instances/active`;