diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index fb81e2ed83..8fc2e58f44 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -323,6 +323,18 @@ 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. 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. + +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 - `KILOCODE_API_KEY` - API key used by the KiloClaw controller for internal gateway identity. `[SECRET]` diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index ba3109bc9b..00b5aa53ef 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 { @@ -194,6 +197,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', @@ -255,6 +262,14 @@ 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], 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. @@ -267,13 +282,16 @@ const config: ExpoConfig = { widgets: [ { name: 'ActiveAgentsWidget', - displayName: 'Active Agents', - description: 'Counts of running, needs-input, and reconnecting agents', + 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 + // 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', @@ -284,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/assets/images/logo-widget.png b/apps/mobile/assets/images/logo-widget.png new file mode 100644 index 0000000000..8e72fff48d Binary files /dev/null and b/apps/mobile/assets/images/logo-widget.png differ 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/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/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 0000000000..0322b856f1 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/.transforms/f2cabb65ee93d20f76eac75d703af899/transformed/classes/classes_dex/classes.dex differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/generated/source/buildConfig/debug/com/kilocode/activeagentsliveupdate/BuildConfig.java b/apps/mobile/modules/active-agents-live-update/android/build/generated/source/buildConfig/debug/com/kilocode/activeagentsliveupdate/BuildConfig.java new file mode 100644 index 0000000000..590b2e2907 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/generated/source/buildConfig/debug/com/kilocode/activeagentsliveupdate/BuildConfig.java @@ -0,0 +1,10 @@ +/** + * Automatically generated file. DO NOT MODIFY + */ +package com.kilocode.activeagentsliveupdate; + +public final class BuildConfig { + public static final boolean DEBUG = Boolean.parseBoolean("true"); + public static final String LIBRARY_PACKAGE_NAME = "com.kilocode.activeagentsliveupdate"; + public static final String BUILD_TYPE = "debug"; +} diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml new file mode 100644 index 0000000000..7a3a3a6f66 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ 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 0000000000..37a2d0368c Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties new file mode 100644 index 0000000000..1211b1ef0c --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties @@ -0,0 +1,6 @@ +aarFormatVersion=1.0 +aarMetadataVersion=1.0 +minCompileSdk=1 +minCompileSdkExtension=0 +minAndroidGradlePluginVersion=1.0.0 +coreLibraryDesugaringEnabled=false diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/annotations_typedef_file/debug/extractDebugAnnotations/typedefs.txt b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/annotations_typedef_file/debug/extractDebugAnnotations/typedefs.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar new file mode 100644 index 0000000000..5b68e5ef52 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar differ 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 0000000000..d309977949 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state new file mode 100644 index 0000000000..1b26d36b17 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state differ 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 0000000000..9dbc290d21 Binary files /dev/null and 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 differ 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 0000000000..bec532c380 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/kilocode/activeagentsliveupdate/BuildConfig.class differ 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 0000000000..34b16a12d1 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/merged_java_res/debug/mergeDebugJavaResource/feature-active-agents-live-update.jar differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml new file mode 100644 index 0000000000..7a3a3a6f66 --- /dev/null +++ b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ 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 0000000000..23fcc30845 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar differ 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 0000000000..bcbd2006d4 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream new file mode 100644 index 0000000000..32c5a52dc3 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream differ 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 0000000000..b4da131811 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len differ 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 0000000000..01bdaa1da7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len differ 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 0000000000..3e5c9baddf Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at differ 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 0000000000..df14d918c4 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len new file mode 100644 index 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab new file mode 100644 index 0000000000..436a25cb7c Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream new file mode 100644 index 0000000000..7f06cf4b75 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len new file mode 100644 index 0000000000..900128838f Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len new file mode 100644 index 0000000000..93a595bd1b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at new file mode 100644 index 0000000000..8f2832759f Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at differ 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 0000000000..508a6e835a Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i differ 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 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len differ 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 0000000000..dd726ca7bb Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab differ 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 0000000000..7f06cf4b75 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream differ 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.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len new file mode 100644 index 0000000000..900128838f Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len differ 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.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len new file mode 100644 index 0000000000..93a595bd1b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len differ 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.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at new file mode 100644 index 0000000000..15db4b4fee Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at differ 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 0000000000..508a6e835a Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i differ 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 0000000000..131e265740 Binary files /dev/null and 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 differ 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 0000000000..0e523c4f92 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab differ 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 0000000000..0022f40d6f Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len new file mode 100644 index 0000000000..385642d9c9 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len new file mode 100644 index 0000000000..01bdaa1da7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at new file mode 100644 index 0000000000..ff24b36bf1 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at differ 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 0000000000..166763184e Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i differ 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 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len differ 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 0000000000..b897893c66 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab differ 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 0000000000..c215febf01 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream differ 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 0000000000..d49ddd162b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len differ 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 0000000000..c14ff15852 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len differ 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 0000000000..735260a501 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at differ 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 0000000000..652b27ea85 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i differ 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 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len differ 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 0000000000..e7c6069991 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream new file mode 100644 index 0000000000..3507d668fa Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream differ 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 0000000000..e933046417 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len differ 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 0000000000..ec8f944c8a Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len differ 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 0000000000..ca5b0beb00 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i new file mode 100644 index 0000000000..1dcde473be Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len new file mode 100644 index 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab new file mode 100644 index 0000000000..be27e8ab50 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab differ 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 0000000000..32c5a52dc3 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream differ 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 0000000000..b4da131811 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len differ 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 0000000000..01bdaa1da7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len differ 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 0000000000..03c1356a2b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i new file mode 100644 index 0000000000..df14d918c4 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len new file mode 100644 index 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab new file mode 100644 index 0000000000..1037d121a2 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream new file mode 100644 index 0000000000..c458944f0b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream differ 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 0000000000..1ddb457a11 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len differ 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 0000000000..01bdaa1da7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len differ 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 0000000000..233a0d17fb Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i new file mode 100644 index 0000000000..956a59c4e7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i differ 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 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len differ 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 0000000000..e416deff07 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab differ 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 0000000000..1d41abf972 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len new file mode 100644 index 0000000000..76db240465 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len new file mode 100644 index 0000000000..01bdaa1da7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at new file mode 100644 index 0000000000..f1dbafb1ff Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i new file mode 100644 index 0000000000..7e09067fa3 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i differ 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 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len differ 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 0000000000..6d3c5e916c Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab differ 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 0000000000..32c5a52dc3 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream differ 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 0000000000..b4da131811 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len differ 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 0000000000..01bdaa1da7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len differ 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 0000000000..7d30a43be1 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at differ 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 0000000000..df14d918c4 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len new file mode 100644 index 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab new file mode 100644 index 0000000000..f3d6a595b7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream new file mode 100644 index 0000000000..100d20553b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len new file mode 100644 index 0000000000..ccfcbf4136 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len new file mode 100644 index 0000000000..01bdaa1da7 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at new file mode 100644 index 0000000000..8331fbfd31 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i new file mode 100644 index 0000000000..f768a77ff2 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i differ 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 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len differ 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 0000000000..befbdd6c53 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab differ 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 0000000000..3a27b3b104 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len new file mode 100644 index 0000000000..febbca6391 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len new file mode 100644 index 0000000000..a1161ad50b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at new file mode 100644 index 0000000000..6ff58c81c2 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i new file mode 100644 index 0000000000..65e1b8b118 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i differ 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 0000000000..131e265740 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len differ 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 0000000000..22056f606b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin differ 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 0000000000..99b87616b3 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin differ 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 0000000000..f55ddb8a97 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin differ 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 0000000000..decb6fd058 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/outputs/aar/active-agents-live-update-debug.aar differ 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 0000000000..42c1dfed33 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/META-INF/active-agents-live-update_debug.kotlin_module b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/META-INF/active-agents-live-update_debug.kotlin_module new file mode 100644 index 0000000000..9dbc290d21 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/META-INF/active-agents-live-update_debug.kotlin_module differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsDeadlineReceiver$Companion.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsDeadlineReceiver$Companion.class new file mode 100644 index 0000000000..a697bce57b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsDeadlineReceiver$Companion.class differ 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 0000000000..a3595bb5a1 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsDeadlineReceiver.class differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$Companion.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$Companion.class new file mode 100644 index 0000000000..1af0a0c36b Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$Companion.class differ 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 0000000000..8a2013ca21 Binary files /dev/null and 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 differ 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 0000000000..3a3eb4fd48 Binary files /dev/null and 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 differ 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$11.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$11.class new file mode 100644 index 0000000000..cc022b9d95 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$11.class differ 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$12.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$12.class new file mode 100644 index 0000000000..545f63ceac Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$12.class differ 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$13.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$13.class new file mode 100644 index 0000000000..0ad13ae717 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$13.class differ 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 0000000000..3f6884e6b0 Binary files /dev/null and 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 differ 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$15.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$15.class new file mode 100644 index 0000000000..d51071afea Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$15.class differ 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 0000000000..e83b1bc681 Binary files /dev/null and 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 differ 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$2.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$2.class new file mode 100644 index 0000000000..6d73d21dd5 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$2.class differ 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 0000000000..42a2930d46 Binary files /dev/null and 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 differ 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 0000000000..82a2eed2c3 Binary files /dev/null and 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 differ 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 0000000000..9b7612b9f3 Binary files /dev/null and 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 differ 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 0000000000..14a928ccf7 Binary files /dev/null and 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 differ 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$7.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$7.class new file mode 100644 index 0000000000..b82091784a Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$7.class differ 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$8.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$8.class new file mode 100644 index 0000000000..61d6f1c59a Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$Function$8.class differ 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 0000000000..31c89d011e Binary files /dev/null and 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 differ 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 0000000000..240377631c Binary files /dev/null and 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 differ 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$2.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$2.class new file mode 100644 index 0000000000..3d88576827 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule$definition$lambda$6$$inlined$FunctionWithoutArgs$2.class differ 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 0000000000..7f216dd16c Binary files /dev/null and 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 differ diff --git a/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule.class b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule.class new file mode 100644 index 0000000000..94071b83c8 Binary files /dev/null and b/apps/mobile/modules/active-agents-live-update/android/build/tmp/kotlin-classes/debug/com/kilocode/activeagentsliveupdate/ActiveAgentsLiveUpdateModule.class differ 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 df0132a7c9..594261e891 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 @@ -6,8 +6,10 @@ 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.exception.Exceptions import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition @@ -16,7 +18,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 { @@ -26,12 +29,12 @@ class ActiveAgentsLiveUpdateModule : Module() { isPromotionCapable() } - Function("start") { title: String, text: String, compactText: String?, promotion: Boolean -> - post(title, text, compactText, promotion, 0) + Function("start") { title: String, text: String, openAgentsLabel: String, compactText: String?, promotion: Boolean -> + post(title, text, openAgentsLabel, compactText, promotion, 0) } - Function("update") { title: String, text: String, compactText: String?, promotion: Boolean, timeoutMs: Double -> - post(title, text, compactText, promotion, timeoutMs.toLong()) + Function("update") { title: String, text: String, openAgentsLabel: String, compactText: String?, promotion: Boolean, timeoutMs: Double -> + post(title, text, openAgentsLabel, compactText, promotion, timeoutMs.toLong()) } Function("end") { @@ -47,8 +50,10 @@ class ActiveAgentsLiveUpdateModule : Module() { } } + // `AppContext` exposes only the React context. Every entry point here runs + // from a JS call, so losing it means the module cannot work at all. private val context: Context - get() = appContext.reactContext ?: appContext.applicationContext + get() = appContext.reactContext ?: throw Exceptions.ReactContextLost() private val notificationManager: NotificationManager get() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -102,16 +107,24 @@ class ActiveAgentsLiveUpdateModule : Module() { ) } - private fun post(title: String, text: String, compactText: String?, promotion: Boolean, timeoutMs: Long) { + private fun post(title: String, text: String, openAgentsLabel: String, compactText: String?, promotion: Boolean, timeoutMs: Long) { + val contentIntent = openAgentsPendingIntent() val builder = newBuilder(title) .setSmallIcon(smallIconId()) .setContentTitle(title) .setContentText(text) - .setContentIntent(openAgentsPendingIntent()) + .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. diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 6fd336f443..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", @@ -94,6 +94,7 @@ "expo-sqlite": "~57.0.1", "expo-status-bar": "57.0.1", "expo-store-review": "~57.0.2", + "expo-task-manager": "57.0.12", "expo-tracking-transparency": "~57.0.1", "expo-web-browser": "~57.0.2", "expo-widgets": "57.0.11", @@ -123,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/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/withActiveAgentsAndroidWidget.js b/apps/mobile/plugins/withActiveAgentsAndroidWidget.js index f44353d81a..62ff179ab0 100644 --- a/apps/mobile/plugins/withActiveAgentsAndroidWidget.js +++ b/apps/mobile/plugins/withActiveAgentsAndroidWidget.js @@ -1,12 +1,48 @@ 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 // 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'); +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); @@ -23,5 +59,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 withWorkRuntimeAlignment(loadAndroidWidgetsPlugin()(config, { widgets: described })); }; diff --git a/apps/mobile/plugins/withAndroidWidgetLocalizations.js b/apps/mobile/plugins/withAndroidWidgetLocalizations.js new file mode 100644 index 0000000000..9e5fa92dc1 --- /dev/null +++ b/apps/mobile/plugins/withAndroidWidgetLocalizations.js @@ -0,0 +1,122 @@ +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 }); + } + // 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; + }); + + 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/plugins/withWidgetLocalizations.js b/apps/mobile/plugins/withWidgetLocalizations.js new file mode 100644 index 0000000000..8a8345f9a4 --- /dev/null +++ b/apps/mobile/plugins/withWidgetLocalizations.js @@ -0,0 +1,107 @@ +const fs = require('fs'); +const path = require('path'); +const plist = require('@expo/plist').default; +const { withDangerousMod, withXcodeProject } = require('expo/config-plugins'); + +// 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 stays English. The main +// app declares the same list for the same reason — see `CFBundleLocalizations` +// in app.config.ts. +// +// 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'; + +/** 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'); + } + 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 (!targetUuid) { + throw new Error( + `withWidgetLocalizations: the ${TARGET_NAME} target is missing — this plugin ran before expo-widgets` + ); + } + 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/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.mounted.test.tsx index 244351348c..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 @@ -1,23 +1,94 @@ /* 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 { 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: Parameters[0]) => { + const focused = focusedRoute.focused; + useEffect(() => (focused ? effect() : undefined), [effect, focused]); + }, + }; +}); + +// `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) => { + 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', () => ({ + getActivityKitDenied: () => activityKit.denied, + clearActivityKitDeniedIfAvailable: () => { + if (!activityKit.denied || !activityKit.available) { + return false; + } + activityKit.denied = false; + return true; + }, })); vi.mock('expo-web-browser', () => ({ @@ -297,3 +368,130 @@ 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); + storage.failIdentityReadOnce = false; + }); + + function changeAppState(state: string) { + act(() => { + for (const listener of appStateListeners) { + listener(state); + } + }); + } + + it('recovers on direct Settings return without refocusing, and never alerts', async () => { + activityKit.denied = true; + const renderer = mountRoute(); + await flushMicrotasks(); + expect(surface.activity).toBeNull(); + // 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(); + + changeAppState('background'); + changeAppState('active'); + await flushMicrotasks(); + expect(surface.activity).toBeNull(); + + changeAppState('background'); + activityKit.available = true; + changeAppState('inactive'); + await flushMicrotasks(); + expect(surface.activity).toBeNull(); + changeAppState('active'); + await flushMicrotasks(); + + expect(surface.activity).toEqual(snapshot); + expect(alertMock).not.toHaveBeenCalled(); + 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; + storage.failIdentityReadOnce = true; + 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 31ce78b6d5..175f89d012 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; -import { Alert, Platform } from 'react-native'; +import { useFocusEffect } from 'expo-router'; +import { Alert, AppState, Platform } from 'react-native'; import { i18n } from '@/i18n'; import { AgentSessionListScreen } from '@/components/agents/session-list-screen'; @@ -11,6 +12,7 @@ import { type GitHubInstallReturnOutcome, subscribeToGitHubInstallReturnOutcome, } from '@/lib/github-install-return'; +import { recoverGlanceableActivityKit } from '@/lib/glanceable/activity-kit-prompt'; import { trpcClient } from '@/lib/trpc'; export type GitHubInstallOutcomeAlertButton = { @@ -135,5 +137,21 @@ export default function AgentSessionList() { return subscribeToGitHubInstallReturnOutcome(consumeReturnOutcome); }, [consumeReturnOutcome]); + // 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(() => { + void recoverGlanceableActivityKit(); + const subscription = AppState.addEventListener('change', state => { + if (state === 'active') { + void recoverGlanceableActivityKit(); + } + }); + return () => { + subscription.remove(); + }; + }, []) + ); + return ; } diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index a125f2d51f..268e018d3f 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'; @@ -151,6 +152,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/components/app-unlock-screen.test-helpers.tsx b/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx index ba916fd6b7..49daa1ad38 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', diff --git a/apps/mobile/src/components/context-control.mounted.test.tsx b/apps/mobile/src/components/context-control.mounted.test.tsx index 8f55976156..70a503a959 100644 --- a/apps/mobile/src/components/context-control.mounted.test.tsx +++ b/apps/mobile/src/components/context-control.mounted.test.tsx @@ -1,4 +1,4 @@ -/* eslint-disable typescript-eslint/no-deprecated -- react-test-renderer mounts native presentation with mocked bridges. */ +/* eslint-disable typescript-eslint/no-deprecated, max-lines -- react-test-renderer mounts native presentation with mocked bridges. */ import { createElement, type ElementType } from 'react'; import { act, type ReactTestInstance } from 'react-test-renderer'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -13,6 +13,7 @@ const storage = vi.hoisted(() => ({ read: vi.fn(), write: vi.fn(), remove: vi.fn const showPicker = vi.hoisted(() => vi.fn()); const auth = vi.hoisted(() => ({ token: 'token' as string | undefined })); vi.mock('@/lib/auth/auth-context', () => ({ useAuth: () => auth })); +vi.mock('@/lib/auth/logout-cleanup', () => ({ unregisterActivityTokensAndTombstone: vi.fn() })); vi.mock('expo-secure-store', () => ({ getItemAsync: storage.read, setItemAsync: storage.write, 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..7a53d3c998 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 { @@ -63,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; /** @@ -311,12 +326,33 @@ 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 + ); + 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(); const wasActiveRef = useRef(isActive); useEffect(() => { if (!wasActiveRef.current && isActive) { void queryClient.invalidateQueries({ queryKey: permissionQueryKey }); + setSystemAllowsLiveActivities(liveActivitiesAllowedBySystem()); } wasActiveRef.current = isActive; }, [isActive, queryClient]); @@ -541,6 +577,57 @@ export function NotificationsScreen() { contentContainerClassName="px-6 gap-6 pt-4" showsVerticalScrollIndicator={false} > + {/* 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. 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('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/components/preferences-screen.mounted.test.tsx b/apps/mobile/src/components/preferences-screen.mounted.test.tsx index 976dfad412..beb7a4d1c6 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', diff --git a/apps/mobile/src/components/profile-credits-card.mounted.test.tsx b/apps/mobile/src/components/profile-credits-card.mounted.test.tsx index fae038811d..2ea47b51cf 100644 --- a/apps/mobile/src/components/profile-credits-card.mounted.test.tsx +++ b/apps/mobile/src/components/profile-credits-card.mounted.test.tsx @@ -1,4 +1,4 @@ -/* eslint-disable typescript-eslint/no-deprecated -- react-test-renderer is the DOM-free renderer for RN trees under vitest (node env, no jsdom). */ +/* eslint-disable typescript-eslint/no-deprecated, max-lines -- react-test-renderer is the DOM-free renderer for RN trees under vitest (node env, no jsdom). */ // Balance across owners: the card keeps the raw 2-element tRPC query key // (`[path, { input, type }]`), never a userId-suffixed key. Owner switches are @@ -82,6 +82,7 @@ vi.mock('@expo/react-native-action-sheet', () => ({ useActionSheet: () => ({ showActionSheetWithOptions: showPicker }), })); vi.mock('@/lib/auth/auth-context', () => ({ useAuth: () => ({ token: 'token' }) })); +vi.mock('@/lib/auth/logout-cleanup', () => ({ unregisterActivityTokensAndTombstone: vi.fn() })); vi.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ bottom: 0 }), 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..2801f33161 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, })); @@ -31,8 +34,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', @@ -79,26 +82,55 @@ function collectText(node: unknown): string[] { return output; } -function render(props: ReturnType, width: number) { - return renderActiveAgentsWidget(props, { - widgetName: 'ActiveAgentsWidget', - widgetId: 1, - width, - height: 100, - 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(); 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); + }); + + // 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', () => { @@ -107,32 +139,54 @@ describe('renderActiveAgentsWidget', () => { {}, translate ); - const rep = render(props, 120); + const rep = render(props, { width: 120 }); const text = collectText(rep.light); - expect(text).toEqual(['1 Needs input']); + 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), {}, translate ); - const rep = render(props, 250); + const rep = render(props, { width: 250 }); const text = collectText(rep.light); - expect(text).toEqual(['1 Needs input', '1 Running', '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']); }); + // 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, height: 100 }).light)).toEqual(visibleText); + } + ); + it.each([ - { width: 120, visibleText: ['2 Needs input'] }, + { width: 120, visibleText: ['2', 'Needs input'] }, { width: 250, visibleText: [ - '2 Needs input', - '3 Reconnecting', - '4 Running', + '2', + 'Needs input', + '4', + 'Working', + '3', + 'Idle', 'Updates delayed', 'Open agents', ], @@ -144,17 +198,17 @@ describe('renderActiveAgentsWidget', () => { { ...snapshotFor([], 0, 'stale'), needsInput: 2, - reconnecting: 3, + idle: 3, running: 4, }, {}, translate ); - const rep = render(props, width); + const rep = render(props, { width }); 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,12 +224,12 @@ describe('renderActiveAgentsWidget', () => { status: 'expired', running: 0, needsInput: 0, - reconnecting: 0, + idle: 0, }, {}, translate ); - const rep = render(props, 250); + const rep = render(props, { width: 250 }); const text = collectText(rep.light); expect(text).toEqual(['Status expired']); @@ -183,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 5de7dd7766..e485b41cc2 100644 --- a/apps/mobile/src/glanceable-android/active-agents-widget.tsx +++ b/apps/mobile/src/glanceable-android/active-agents-widget.tsx @@ -2,33 +2,71 @@ '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. + * + * 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; 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 +75,236 @@ 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'; + +/** + * 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, rtl: boolean): Shape { + 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 }; } -function compactText(props: AndroidWidgetProps): string { - if (props.primaryLabel === null) { - return props.statusLine ?? ''; +/** 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 { + 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) { +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. + */ +type RowStyle = { + palette: Palette; + fontSize: number; + showLabel: boolean; + rtl: boolean; +}; + +function countRow( + line: AndroidWidgetProps['countLines'][number], + isPrimary: boolean, + { palette, fontSize, showLabel, rtl }: RowStyle +) { + return ( + + {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, 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 } + ); +} + +function statusText(props: AndroidWidgetProps, palette: Palette) { + return ( + + ); +} + +function renderCounts(props: AndroidWidgetProps, palette: Palette, shape: Shape) { if (props.countLines.length === 0) { - return null; + return statusText(props, palette); } - return countRows(props, palette.primary); + const { size, rowLabels, rtl } = shape; + const primaryLabel = props.primaryLabel; + 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, + rtl, + }); + }); + const stacked = ( + + {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, compact: boolean) { +function renderSurface(props: AndroidWidgetProps, palette: Palette, shape: Shape) { + const { size, rtl } = shape; + const body = + 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. + if (size === 'stack') { + return ( + + {logo(26)} + {body} + {props.showOpenAgents ? ( + + ) : ( + + )} + + ); + } return ( - - {renderPrimaryArea(props, palette, compact)} - {!compact && props.statusLine !== null ? ( - - ) : null} - - {!compact && props.showOpenAgents ? ( - - ) : null} + {/* 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} ); } /** * 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 + info: WidgetInfo, + rtl = false ): WidgetRepresentation { - const compact = isCompact(info); + const shape = shapeOf(info, rtl); return { - light: renderSurface(props, LIGHT, compact), - dark: renderSurface(props, DARK, compact), + light: renderSurface(props, LIGHT, shape), + dark: renderSurface(props, DARK, shape), }; } diff --git a/apps/mobile/src/glanceable-android/android-sink.test.ts b/apps/mobile/src/glanceable-android/android-sink.test.ts index cc46007c49..f29cfccd4c 100644 --- a/apps/mobile/src/glanceable-android/android-sink.test.ts +++ b/apps/mobile/src/glanceable-android/android-sink.test.ts @@ -5,6 +5,8 @@ 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'; import { @@ -29,10 +31,11 @@ const mocks = vi.hoisted(() => { let widgetSnapshot: string | null = null; let widgetDeadline = 0; - // eslint-disable-next-line max-params -- the fake models the native bridge's timeout argument + // eslint-disable-next-line max-params -- the fake models the native bridge arguments function post( title: string, text: string, + _openAgentsLabel: string, compactText: string | null, promotion: boolean, timeoutMs = 0 @@ -80,11 +83,28 @@ vi.mock('react-native', () => ({ vi.mock('react-native-android-widget', () => ({ FlexWidget: () => null, TextWidget: () => null, + ImageWidget: () => null, requestWidgetUpdate: (...args: unknown[]) => mocks.requestWidgetUpdate(...args), })); const NOW = 1_750_000_000_000; -const CTX = { organizationId: null }; +const CTX = { organizationId: null, userId: 'u1' }; + +const subscriptions = new Set(); +const delivery = { + 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( sessions: { status: string }[], @@ -104,7 +124,7 @@ function snapshotFor( const MIXED = { ...snapshotFor([], 0, 'happy'), needsInput: 2, - reconnecting: 3, + idle: 3, running: 4, }; @@ -139,6 +159,12 @@ beforeEach(() => { _resetAndroidPermissionAlertForTests(); // 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(); mocks.native.start.mockClear(); @@ -154,12 +180,31 @@ afterEach(() => { }); describe('androidSink start and update', () => { + it('keeps scope delivery available before and after work arrives in the background', async () => { + const publisher = new GlanceablePublisher({ sinks: [androidSink], now: () => NOW }); + publisher.handleSessions([], 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 Working'); + + publisher.handleSessions([], 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(); 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, }); @@ -168,21 +213,36 @@ 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, }); expect(mocks.native.start).toHaveBeenCalledTimes(1); expect(mocks.native.update).toHaveBeenCalledTimes(2); + expect(mocks.native.start).toHaveBeenCalledWith( + 'Active agents', + '2 Needs input, 4 Working, 3 Idle', + 'Open agents', + '2', + true + ); + expect(mocks.native.update).toHaveBeenLastCalledWith( + 'Active agents', + '4 Working', + 'Open agents', + '4', + true, + 0 + ); }); it('keeps the full summary when the device cannot promote', async () => { @@ -192,7 +252,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, }); @@ -209,8 +269,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); @@ -280,6 +340,55 @@ describe('androidSink start and update', () => { 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', () => { @@ -291,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 () => { @@ -301,15 +410,15 @@ 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' ); }); 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); @@ -349,7 +458,18 @@ 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 () => { + androidSink.startOrUpdate(snapshotFor([{ status: 'busy' }], 0), CTX); + await flushAsync(); + expect(mocks.getNotification()).not.toBeNull(); + + androidSink.endImmediate(); + await flushAsync(); + expect(mocks.getNotification()).toBeNull(); + expect(subscriptions).toEqual(new Set(['scope'])); }); it.each(['happy', 'stale'] as const)( @@ -361,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); } ); @@ -383,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(); }); @@ -392,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([]); }); @@ -435,7 +555,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); @@ -469,7 +589,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); @@ -529,7 +649,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/android-sink.ts b/apps/mobile/src/glanceable-android/android-sink.ts index 652ed29d64..aa0eea32bf 100644 --- a/apps/mobile/src/glanceable-android/android-sink.ts +++ b/apps/mobile/src/glanceable-android/android-sink.ts @@ -6,9 +6,15 @@ import { import { requestWidgetUpdate } from 'react-native-android-widget'; import { i18n } from '@/i18n'; -import { type GlanceableSink, type GlanceableSinkContext } from '@/lib/glanceable/sink-registry'; +import { getLiveActivityEnabled } from '@/lib/glanceable/live-activity-switch'; +import { + getGlanceableDelivery, + type GlanceableSink, + type GlanceableSinkContext, +} from '@/lib/glanceable/sink-registry'; import { renderActiveAgentsWidget, WIDGET_NAME } from './active-agents-widget'; +import { formatGlanceableCount, isWidgetRtl } from './count-format'; import { end as endLiveUpdate, setWidgetSnapshot, @@ -30,6 +36,7 @@ import { * Ending the ongoing notification never cancels a still-eligible widget expiry. */ const NOTIFICATION_TITLE_KEY = 'glanceable.channelName'; +const OPEN_AGENTS_LABEL_KEY = 'glanceable.openAgents'; function translate(key: string): string { return i18n.t(key); @@ -38,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; @@ -46,13 +56,14 @@ 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 { void requestWidgetUpdate({ widgetName: WIDGET_NAME, - renderWidget: info => renderActiveAgentsWidget(getCurrentWidgetProps() ?? props, info), + renderWidget: info => + renderActiveAgentsWidget(getCurrentWidgetProps() ?? props, info, isWidgetRtl()), }); } @@ -81,7 +92,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; } @@ -89,11 +103,12 @@ async function tryStartOrUpdate( return; } const title = translate(NOTIFICATION_TITLE_KEY); - const text = buildOngoingNotificationText(snapshot, {}, translate); - const compactText = buildCompactNotificationText(snapshot, {}); + const text = buildOngoingNotificationText(snapshot, {}, translate, formatGlanceableCount); + const openAgentsLabel = translate(OPEN_AGENTS_LABEL_KEY); + const compactText = buildCompactNotificationText(snapshot, {}, formatGlanceableCount); if (notificationActive) { - updateLiveUpdate(title, text, compactText); + updateLiveUpdate(title, text, openAgentsLabel, compactText); terminalExpiresAt = null; revision = snapshot.revision; return; @@ -108,17 +123,18 @@ 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, compactText); + updateLiveUpdate(title, text, openAgentsLabel, compactText); terminalExpiresAt = null; revision = snapshot.revision; } return; } - startLiveUpdate(title, text, compactText); + startLiveUpdate(title, text, openAgentsLabel, compactText); notificationActive = true; terminalExpiresAt = null; revision = snapshot.revision; pending = null; + getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId); return; } pending = { snapshot, ctx }; @@ -127,19 +143,26 @@ 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), - buildCompactNotificationText(p.snapshot, {}) + buildOngoingNotificationText(p.snapshot, {}, translate, formatGlanceableCount), + translate(OPEN_AGENTS_LABEL_KEY), + buildCompactNotificationText(p.snapshot, {}, formatGlanceableCount) ); notificationActive = true; terminalExpiresAt = null; revision = p.snapshot.revision; pending = null; + getGlanceableDelivery().registerTokens(p.snapshot, p.ctx.organizationId, p.ctx.userId); } /** @@ -162,7 +185,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) { @@ -189,9 +212,10 @@ export const androidSink: GlanceableSink = { updateLiveUpdate( translate(NOTIFICATION_TITLE_KEY), eligible - ? buildOngoingNotificationText(snapshot, {}, translate) + ? buildOngoingNotificationText(snapshot, {}, translate, formatGlanceableCount) : (props.statusLine ?? translate('glanceable.empty')), - eligible ? buildCompactNotificationText(snapshot, {}) : null, + translate(OPEN_AGENTS_LABEL_KEY), + eligible ? buildCompactNotificationText(snapshot, {}, formatGlanceableCount) : null, terminalExpiresAt === null ? 0 : Math.max(1, terminalExpiresAt - Date.now()) ); revision = snapshot.revision; @@ -202,7 +226,10 @@ export const androidSink: GlanceableSink = { void tryStartOrUpdate(snapshot, ctx); }, - endImmediate: endNotification, + endImmediate() { + // The scope subscription also delivers widget updates while no work is active. + endNotification(); + }, }; /** Test-only: drop JS state without touching Android-owned storage or deadlines. */ 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..df8b68bb17 --- /dev/null +++ b/apps/mobile/src/glanceable-android/count-format.ts @@ -0,0 +1,26 @@ +import { i18n } from '@/i18n'; +import { RTL_LANGUAGES, type SupportedLanguage } from '@/i18n/languages'; +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); +} + +/** + * 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/live-update.ts b/apps/mobile/src/glanceable-android/live-update.ts index 4859b65ce0..cddb8275c9 100644 --- a/apps/mobile/src/glanceable-android/live-update.ts +++ b/apps/mobile/src/glanceable-android/live-update.ts @@ -13,10 +13,17 @@ import { requireOptionalNativeModule } from 'expo'; type LiveUpdateNativeModule = { isPromotionCapable(): boolean; - start(title: string, text: string, compactText: string | null, promotion: boolean): void; + start( + title: string, + text: string, + openAgentsLabel: string, + compactText: string | null, + promotion: boolean + ): void; update( title: string, text: string, + openAgentsLabel: string, compactText: string | null, promotion: boolean, timeoutMs: number @@ -36,18 +43,25 @@ function isPromotionCapable(): boolean { return nativeModule?.isPromotionCapable() ?? false; } -export function start(title: string, text: string, compactText: string | null): void { - nativeModule?.start(title, text, compactText, isPromotionCapable()); +// eslint-disable-next-line max-params -- mirrors the native presentation fields +export function start( + title: string, + text: string, + openAgentsLabel: string, + compactText: string | null +): void { + nativeModule?.start(title, text, openAgentsLabel, compactText, isPromotionCapable()); } // eslint-disable-next-line max-params -- translated bridge fields plus the native terminal timeout export function update( title: string, text: string, + openAgentsLabel: string, compactText: string | null, timeoutMs = 0 ): void { - nativeModule?.update(title, text, compactText, isPromotionCapable(), timeoutMs); + nativeModule?.update(title, text, openAgentsLabel, compactText, isPromotionCapable(), timeoutMs); } export function end(): void { 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..e267cc8a8a --- /dev/null +++ b/apps/mobile/src/glanceable-android/register.test-helpers.ts @@ -0,0 +1,77 @@ +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 2978c7896e..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() }, @@ -38,36 +52,9 @@ vi.mock('react-native-android-widget', () => ({ requestWidgetUpdate: vi.fn().mockResolvedValue(undefined), FlexWidget: () => null, TextWidget: () => null, + 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); @@ -87,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: 100, - 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(); @@ -138,14 +92,26 @@ 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()); const rendered = await runWidgetTask(handler, width); const expected = width === 120 - ? ['1 Needs input'] - : ['1 Needs input', '1 Reconnecting', '2 Running', 'Open agents']; + ? ['2', 'Needs input'] + : ['2', 'Needs input', '2', 'Working', '0', 'Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); @@ -191,7 +157,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); @@ -208,10 +174,16 @@ 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 = width === 120 ? ['1 Running'] : ['1 Running', '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); @@ -233,13 +205,16 @@ 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'] + : ['0', 'Needs input', '1', 'Working', '0', 'Idle', 'Open agents']; expect(collectText(rendered.light)).toEqual(expected); expect(collectText(rendered.dark)).toEqual(expected); }); 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(); @@ -263,8 +238,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)).toEqual(expect.arrayContaining(['2', 'Needs input'])); + expect(collectText(current.dark)).toEqual(expect.arrayContaining(['2', 'Needs input'])); expect(mocks.getDeadline()).toBe(expiresAt); vi.setSystemTime(expiresAt); @@ -308,10 +283,16 @@ 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 = width === 120 ? ['1 Running'] : ['1 Running', '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/register.ts b/apps/mobile/src/glanceable-android/register.ts index 812fb43474..e4f4d4bd48 100644 --- a/apps/mobile/src/glanceable-android/register.ts +++ b/apps/mobile/src/glanceable-android/register.ts @@ -5,11 +5,20 @@ 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 { + getResolvedLanguage, + whenLanguagePreferenceLoaded, +} from '@/lib/hooks/use-language-preference'; import { renderActiveAgentsWidget } from './active-agents-widget'; import { androidSink, getCurrentWidgetProps, handleAppStateActive } from './android-sink'; +import { formatGlanceableCount, isWidgetRtl } from './count-format'; import { getStoredWidgetSnapshot, setWidgetSnapshot } from './live-update'; import { buildCurrentWidgetProps, buildGenericWidgetProps } from './widget-props'; @@ -26,18 +35,49 @@ 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); } +/** + * 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(); 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,10 +87,10 @@ 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; } - renderWidget(renderActiveAgentsWidget(props, widgetInfo)); + renderWidget(renderActiveAgentsWidget(props, widgetInfo, isWidgetRtl())); }); 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 2fe145a0f6..48687d05d8 100644 --- a/apps/mobile/src/glanceable-android/widget-props.test.ts +++ b/apps/mobile/src/glanceable-android/widget-props.test.ts @@ -15,14 +15,14 @@ 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', '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; @@ -45,7 +45,7 @@ function snapshotFor( const MIXED = { ...snapshotFor([], 0, 'happy'), needsInput: 2, - reconnecting: 3, + idle: 3, running: 4, }; @@ -53,17 +53,18 @@ 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: 'Reconnecting', count: 3 }, - { label: 'Running', count: 4 }, + { label: 'Needs input', kind: 'needsInput', count: '2' }, + { label: 'Working', kind: 'running', count: '4' }, + { label: 'Idle', kind: '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) => { @@ -82,10 +83,12 @@ 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], + ['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); @@ -97,11 +100,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); @@ -112,6 +114,7 @@ describe('buildAndroidWidgetProps', () => { 'countLines', 'openAgentsLabel', 'primaryCount', + 'primaryKind', 'primaryLabel', 'showOpenAgents', 'statusLine', @@ -141,7 +144,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'], @@ -165,13 +168,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 +193,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); @@ -209,14 +212,14 @@ 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); 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(); @@ -224,14 +227,14 @@ 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); 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 71434f679c..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, @@ -20,12 +31,14 @@ 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; - /** 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( { @@ -85,22 +104,27 @@ function buildExpiredWidgetProps( status: 'expired', running: 0, needsInput: 0, - reconnecting: 0, - eligibleStartedAt: null, + idle: 0, + 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,16 +135,22 @@ 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') { - 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(', '); + const counts = lines + .map(line => `${formatCount(line.count)} ${translate(line.key)}`) + .join(', '); return status === 'stale' ? `${translate('glanceable.stale')}, ${counts}` : counts; } } @@ -130,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/glanceable-ios/active-agents-live-activity.tsx b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx index 9d161c2537..568f46a729 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx @@ -1,112 +1,308 @@ -import { Text, VStack } from '@expo/ui/swift-ui'; +import { HStack, Image, Spacer, Text, VStack } from '@expo/ui/swift-ui'; import { accessibilityElement, accessibilityLabel, + allowsTightening, + cornerRadius, + environment, font, foregroundStyle, + frame, + layoutPriority, + lineLimit, + minimumScaleFactor, + monospacedDigit, + 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 GlanceableViewProps } from './view-props'; +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 */ // 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. - -export const ActiveAgentsLiveActivity = createLiveActivity>( - '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 primaryForeground = foregroundStyle(PlatformColor('label')); - const mutedForeground = foregroundStyle( - dark ? PlatformColor('secondaryLabel') : PlatformColor('tertiaryLabel') +// `PlatformColor`) or a built-in. Do not call `@/` helpers or i18n from here. +// +// 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 => { + 'widget'; + + // 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'; + + // 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]; + + // 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 = [ + { + 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 = 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. + 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 + // spoken label even though no line draws it. + const spokenParts = [ + ...(statusLine !== null ? [statusLine] : []), + ...countLines.map(line => `${line.count} ${line.label}`), + COPY.openAgents, + ]; + const accessibility = spokenParts.join(', '); + + const primaryForeground = foregroundStyle(PlatformColor('label')); + // `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 + // 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 : ( + ); - const countRows = counts.map(line => ( - - {`${line.count} ${line.label}`} + // One row per non-zero state: a colored glyph carries the state (readable + // 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) => ( + + + + {count(line.count)} - )); - - return { - banner: ( - + {line.label} + + {line.kind === 'needsInput' && needsInputSince !== null ? ( + - {hasCounts ? ( - - {countRows} - - ) : null} - {statusLine !== null ? {statusLine} : null} - {elapsedAnchor !== null ? ( - - ) : null} - - ), - compactLeading: ( - - {hasCounts ? primaryCount : statusLine} - - ), - compactTrailing: ( - - {hasCounts ? (primaryLabel ?? primaryCount) : ''} - - ), - minimal: ( - - {hasCounts ? primaryCount : ''} - - ), - expandedLeading: ( - + /> + ) : null} + + ); + + // 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)); + + // 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} - ), - expandedTrailing: ( - - {statusLine !== null ? {statusLine} : null} - {elapsedAnchor !== null ? ( - - ) : null} - - ), - expandedBottom: ( - - {statusLine !== null && !hasCounts ? ( - {statusLine} - ) : null} - - ), - }; - } -); + ) : ( + {statusLine} + )} + + + ); + + return { + banner: ( + + {markAndRows(26)} + + ), + // 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 ? primaryCount : ''} + + ), + minimal: ( + + {hasCounts ? primaryCount : ''} + + ), + // 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: ( + + {markAndRows(24)} + + ), + }; +}; + +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 094f25a204..3fb58c79bf 100644 --- a/apps/mobile/src/glanceable-ios/active-agents-widget.tsx +++ b/apps/mobile/src/glanceable-ios/active-agents-widget.tsx @@ -1,17 +1,27 @@ -import { Text, VStack } from '@expo/ui/swift-ui'; +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, } from '@expo/ui/swift-ui/modifiers'; -import { createWidget } from 'expo-widgets'; +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'; /* eslint-disable new-cap -- PlatformColor is a React Native factory function, not a constructor */ @@ -19,87 +29,275 @@ import { type GlanceableViewProps } from './view-props'; // 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. - -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') +// translated copy arrives through `props`, and the gallery placeholder (which +// has no props) falls back to the baked copy below. +// +// 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, widgetEnvironment: WidgetEnvironment) => React.JSX.Element = ( + props, + widgetEnvironment +) => { + 'widget'; + + // 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'; + + // 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; + 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 : 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. + 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')); + // `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 ?? ''), + ]; + + // 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 : ( + ); - 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 ( + + // `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]; + // 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 ( + + + {count(line.count)} + + + {line.label} + + {wide ? : null} + {wide && line.kind === 'needsInput' && needsInputSince !== null ? ( + + ) : null} + + ); + }; + + // 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 ? count(primaryCount) : '—'} - ); - } + + ); + } + if (family === 'accessoryInline') { + const label = hasCounts + ? `${count(primaryCount)}${primaryLabel !== null ? ` ${primaryLabel}` : ''}` + : (statusLine ?? ''); return ( - + {primaryKind === null ? null : ( + + )} + {label} + + ); + } + + // accessoryRectangular is the Lock Screen row: the mark plus the two + // top-ranked lines is all that fits. + if (family === 'accessoryRectangular') { + return ( + + {logo(18)} {hasCounts ? ( - - {countRows} + + {counts.map(line => countRow(line, line.kind === primaryKind, true))} - ) : null} - {statusLine !== null ? {statusLine} : null} - {showOpenAgents ? ( - - {openAgentsLabel} - - ) : null} - + ) : ( + {statusLine} + )} + + + ); + } + + 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(26)} + + + {/* The mark sits at the top and the counts at the bottom, so the card + reads as one composed block. */} + + {systemRows} + + ); +}; + +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 new file mode 100644 index 0000000000..6139cd1a88 --- /dev/null +++ b/apps/mobile/src/glanceable-ios/ios-sink.native.test.ts @@ -0,0 +1,299 @@ +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, widgetsDirectory: 'file:///app-group/ExpoWidgets/' }; +}); +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, + }); +} + +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, idle: 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: 'idle' }], 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([ + { + // No row needs input, so the content state carries no wait. + props: { running: 1, idle: 1, needsInputSince: null }, + }, + ]); + 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 }); + // Omitted, not null: UserDefaults rejects a null value. See toWidgetProps. + expect(Object.values(native.snapshots.at(-1) ?? {})).not.toContain(null); + 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 26a5361cd1..d323472836 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.test.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.test.ts @@ -5,7 +5,12 @@ import { buildGlanceableSnapshot, type GlanceableAgentsSnapshot, } 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 { @@ -14,8 +19,18 @@ import { unregisterGlanceableSink, } from '@/lib/glanceable/sink-registry'; -import { _resetIosSinkForTests, getActivityKitDenied, iosSink } from './ios-sink'; -import { buildGlanceableViewProps, type GlanceableViewProps } from './view-props'; +import { + _resetIosSinkForTests, + clearActivityKitDeniedIfAvailable, + getActivityKitDenied, + iosSink, +} from './ios-sink'; +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 @@ -39,16 +54,19 @@ vi.mock('react-native', () => ({ PlatformColor: (name: string) => name })); const mockState = vi.hoisted(() => ({ startError: null as { code: string; message: string } | null, - instances: [] as unknown[], - started: [] as { props: unknown; url?: string }[], + instancesError: null as { code: string; message: string } | null, + 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 }[], ended: [] as { policy: unknown; props?: unknown; contentDate?: unknown }[], + updatePromise: null as Promise | null, })); vi.mock('expo-widgets', () => ({ after: (date: Date) => ({ after: date }), + widgetsDirectory: 'file:///app-group/ExpoWidgets/', createLiveActivity: () => ({ start: (props: unknown, url?: string) => { if (mockState.startError !== null) { @@ -56,17 +74,49 @@ vi.mock('expo-widgets', () => ({ error.code = mockState.startError.code; throw error; } - mockState.started.push({ props, url }); - return { - update: (next: unknown) => { + 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); + if (mockState.updatePromise !== null) { + await mockState.updatePromise; + } + 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; + 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: (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 + .map((instance, index) => ({ + getInfo: () => ({ id: `adopted-${index}`, state: 'active' }), + ...instance, + })) + .filter(instance => includeEnded || instance.getInfo().state === 'active'); }, - getInstances: () => mockState.instances, }), createWidget: () => ({ updateSnapshot: (props: unknown) => { @@ -84,13 +134,28 @@ 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(), + 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( - sessions: { status: string }[], + sessions: { status: string; statusUpdatedAt?: string }[], revision = 0, status?: GlanceableAgentsSnapshot['status'] ): GlanceableAgentsSnapshot { @@ -105,14 +170,18 @@ function snapshotFor( } beforeEach(() => { + _resetLiveActivitySwitchForTests(); _resetIosSinkForTests(); + subscriptions.clear(); mockState.startError = null; + mockState.instancesError = null; mockState.instances = []; mockState.started = []; mockState.updated = []; mockState.snapshots = []; mockState.timeline = []; mockState.ended = []; + mockState.updatePromise = null; setGlanceableDelivery(delivery); registerGlanceableSink(iosSink); vi.clearAllMocks(); @@ -124,17 +193,45 @@ afterEach(() => { }); describe('iosSink start and update', () => { + 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([], 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 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); 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 updatedAt and 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), @@ -147,12 +244,12 @@ describe('iosSink start and update', () => { expect(mockState.updated.length).toBe(0); iosSink.endImmediate(); - expect(mockState.ended.length).toBe(1); - expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBe( - Date.parse(newer.updatedAt) - ); + await vi.waitFor(() => { + expect(mockState.ended.length).toBe(1); + }); + expect(mockState.ended[0]?.contentDate).toBeInstanceOf(Date); expect( - (mockState.ended[0]?.props as GlanceableViewProps | undefined)?.countLines[0]?.count + (mockState.ended[0]?.props as GlanceableLiveActivityContentState | undefined)?.running ).toBe(1); }); @@ -195,7 +292,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 +309,212 @@ 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', 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()).toBeGreaterThanOrEqual(writeTime); + }); + + 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(subscriptions).toEqual(new Set(['scope'])); + }); + + 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(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + expect((mockState.ended[0]?.contentDate as Date | undefined)?.getTime()).toBeGreaterThanOrEqual( + terminalTime + ); + expect(subscriptions.size).toBe(0); }); - 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('adopts and ends a leftover activity when the handle is null after restart', () => { + 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. + 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)); + update.resolve(undefined); + + 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('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([{ status: 'busy' }], 1)); + iosSink.publish(snapshotFor([], 2, '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([{ status: 'busy' }], 5)); + iosSink.publish(snapshotFor([], 6, '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([{ status: 'busy' }], 1)); + iosSink.publish(snapshotFor([], 2, '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', async () => { mockState.instances = [ { - update: (next: unknown) => mockState.updated.push(next), + 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 }), }, @@ -247,25 +522,195 @@ 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(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + expect(subscriptions.has('activity')).toBe(false); }); - it('ends after the 8s terminal window when work becomes empty', () => { + 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(subscriptions).toEqual(new Set(['scope'])); + }); + + 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([], CTX); + await iosSink.waitForNativeTerminal?.(); - publisher.handleSessions([{ status: 'idle' }], CTX); - expect(mockState.ended.length).toBe(0); + expect(mockState.started).toMatchObject([ + { + ended: true, + dismissAt: NOW + 8000, + props: { status: 'empty', running: 0, needsInput: 0, idle: 0 }, + }, + ]); + expect(subscriptions).toEqual(new Set(['scope'])); + publisher.dispose(); + }); - vi.advanceTimersByTime(8000); - expect(mockState.ended.length).toBe(1); - expect(mockState.ended[0]?.policy).toBe('immediate'); + 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, idle: 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, idle: 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('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: '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: 'busy' }], CTX); + await vi.advanceTimersByTimeAsync(1000); + expect(mockState.started).toMatchObject([ + { ended: false, props: { running: 1, needsInput: 0, needsInputSince: null } }, + ]); publisher.dispose(); }); }); @@ -284,7 +729,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)); @@ -293,13 +738,14 @@ 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(); } ); 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); @@ -330,10 +776,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); }); @@ -347,57 +791,72 @@ 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], + ['privacy', [], 'Open Kilo to see agents', 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); } }); }); -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('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 GlanceableViewProps | undefined; - expect(updated?.statusLine).toBe('No work in progress'); - expect(updated?.countLines).toEqual([]); + expect(mockState.started).toMatchObject([ + { + ended: true, + props: { status: 'empty', running: 0, needsInput: 0, idle: 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', () => { - mockState.instances = [{ update: (next: unknown) => mockState.updated.push(next) }]; + mockState.instances = [ + { + update: (next: unknown) => { + mockState.updated.push(next); + }, + }, + ]; iosSink.publish(snapshotFor([{ status: 'busy' }], 1)); expect(mockState.started.length).toBe(0); expect(mockState.ended.length).toBe(0); - const updated = mockState.updated.at(-1) as GlanceableViewProps | undefined; - expect(updated?.countLines).toHaveLength(1); + const updated = mockState.updated.at(-1) as GlanceableLiveActivityContentState | undefined; + expect(updated?.status).toBe('happy'); + expect(updated?.running).toBe(1); expect(delivery.registerTokens).not.toHaveBeenCalled(); }); - it('ends an adopted leftover activity when publish receives ineligible work', () => { + 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), update: (next: unknown) => mockState.updated.push(next), end: (policy: unknown, props?: unknown, contentDate?: unknown) => mockState.ended.push({ policy, props, contentDate }), @@ -405,19 +864,61 @@ describe('iosSink Live Activity copy', () => { ]; iosSink.publish(snapshotFor([], 1, 'empty')); + await iosSink.waitForNativeTerminal?.(); - 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, idle: 0 }, + contentDate: new Date(NOW), + }, + ]); expect(mockState.updated.length).toBe(0); - expect(delivery.unregisterTokens).toHaveBeenCalledTimes(1); + expect(subscriptions.has('activity')).toBe(false); + }); +}); + +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', () => { + 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 ), {}, @@ -427,21 +928,19 @@ describe('buildGlanceableViewProps', () => { expect(props.primaryCount).toBe(1); expect(props.countLines.map(line => line.label)).toEqual([ 'glanceable.needsInput', - 'glanceable.reconnecting', 'glanceable.running', + 'glanceable.idle', ]); }); 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); @@ -450,11 +949,10 @@ describe('buildGlanceableViewProps', () => { expect(Object.keys(props).toSorted()).toEqual([ 'accessibilityLabel', 'countLines', - 'elapsedAnchor', - 'openAgentsLabel', + 'needsInputSince', 'primaryCount', + 'primaryKind', 'primaryLabel', - 'showOpenAgents', 'statusLine', ]); expect(json).not.toContain('user-9f3a-leak'); @@ -464,4 +962,75 @@ describe('buildGlanceableViewProps', () => { expect(json).not.toContain('revision'); expect(json).not.toContain('title'); }); + + 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'); + + // 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('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(); + + const empty = snapshotFor([], 1, 'empty'); + expect(buildGlanceableLiveActivityContentState(empty).needsInputSince).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'); + }); +}); + +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('needsInputSince' 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 }, + { 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 9049f0ef35..85ad2c919d 100644 --- a/apps/mobile/src/glanceable-ios/ios-sink.ts +++ b/apps/mobile/src/glanceable-ios/ios-sink.ts @@ -1,26 +1,35 @@ import { + GLANCEABLE_TERMINAL_MS, type GlanceableAgentsSnapshot, isEligibleGlanceableWork, } from '@kilocode/app-shared/glanceable-agents-snapshot'; -import { type LiveActivity } from 'expo-widgets'; +import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; +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'; -import { buildGlanceableViewProps, type GlanceableViewProps } from './view-props'; +import { + buildGlanceableLiveActivityContentState, + buildGlanceableViewProps, + type GlanceableViewProps, + toWidgetProps, +} from './view-props'; /** Open-agents destination, kept in step with the inlined widget URL. */ const OPEN_AGENTS_URL = 'kiloapp:///cloud/sessions'; -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; +/** 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 { return i18n.t(key); @@ -38,51 +47,183 @@ 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 { +/** 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; } } 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, + needsInputSince: null, + }, + {}, + translate + ) ); } -function endNow(): void { - // 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) { +async function readEndingToken(instance: Activity): Promise { + try { + return await instance.getPushToken(); + } catch { + // Recorded tokens still need cleanup when the native lookup fails. + return 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; } - const contentDate = lastUpdatedAt === null ? undefined : new Date(lastUpdatedAt); - void activity.end('immediate', lastProps ?? undefined, contentDate); + 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; - getGlanceableDelivery().unregisterTokens(); + 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); + } + if (dismissAt === null && endingActivities.size === 0) { + getGlanceableDelivery().cleanupTokens('activity'); + } } /** True once ActivityKit reported the surface unavailable (see slice psh for the alert). */ @@ -90,18 +231,46 @@ 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; + 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); + 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') { @@ -110,90 +279,63 @@ 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. 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. - const adopted = activity === null; - activity ??= adoptExistingActivity(); - if (activity !== null) { - lastUpdatedAt = snapshot.updatedAt; - lastProps = props; - 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. - endNow(); - return; - } - void activity.update(props); + 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)) { + // 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; } - const props = buildGlanceableViewProps(snapshot, {}, translate); + 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. - let adopted = false; try { - const instances = ActiveAgentsLiveActivity.getInstances(); - const newest = instances.at(-1); - if (newest !== undefined) { - activity = newest; - adopted = true; - } + 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; - } - // A transient getInstances failure leaves activity null; the start - // below still runs, so a later emit can retry. - } - - if (activity === null) { - try { - activity = ActiveAgentsLiveActivity.start(props, OPEN_AGENTS_URL); - } 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; } + return; } - - lastUpdatedAt = snapshot.updatedAt; - lastProps = props; + lastProps = contentState; revision = snapshot.revision; - getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId); - if (adopted) { - void activity.update(props); - } + getGlanceableDelivery().registerTokens(snapshot, ctx.organizationId, ctx.userId, activity); 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) { return; } - lastUpdatedAt = snapshot.updatedAt; - lastProps = props; - void activity.update(props); + lastProps = contentState; + inFlightUpdate = activity.update(contentState); revision = snapshot.revision; }, diff --git a/apps/mobile/src/glanceable-ios/layout-copy.test.ts b/apps/mobile/src/glanceable-ios/layout-copy.test.ts new file mode 100644 index 0000000000..a824e1bb8e --- /dev/null +++ b/apps/mobile/src/glanceable-ios/layout-copy.test.ts @@ -0,0 +1,82 @@ +/* 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'; + +import { glanceableLayoutCopy, withGlanceableCopy } from './layout-copy'; + +const PLACEHOLDER = '__KILO_GLANCEABLE_COPY__'; +const LAYOUT_FILES = ['active-agents-live-activity.tsx', 'active-agents-widget.tsx']; + +const read = (file: string) => 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('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 + // 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([ + 'digits', + '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..93506e201b --- /dev/null +++ b/apps/mobile/src/glanceable-ios/layout-copy.ts @@ -0,0 +1,100 @@ +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. + * + * 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 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. + */ +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.replace('-', '_'), + digits: glanceableDigits(), + }; +} + +/** + * 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. + */ +/** + * 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') { + 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 029cac2370..1ab06723b3 100644 --- a/apps/mobile/src/glanceable-ios/register.ts +++ b/apps/mobile/src/glanceable-ios/register.ts @@ -1,6 +1,14 @@ +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'; 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 +16,29 @@ 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(); + +// 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(); +}); + +// 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/glanceable-ios/view-props.ts b/apps/mobile/src/glanceable-ios/view-props.ts index f1bb6c09d9..aee90d52c3 100644 --- a/apps/mobile/src/glanceable-ios/view-props.ts +++ b/apps/mobile/src/glanceable-ios/view-props.ts @@ -1,16 +1,18 @@ import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; +import { type GlanceableLiveActivityContentState } from '@kilocode/notifications'; import { + type GlanceableCountKind, glanceableCountLines, - glanceableSpokenLabelKeys, + 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, @@ -20,19 +22,22 @@ 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; only happy with eligible work. */ - 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. */ + /** + * 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; }; @@ -42,23 +47,54 @@ export function buildGlanceableViewProps( flags: GlanceableSurfaceFlags, translate: (key: string) => string ): GlanceableViewProps { - const status = resolveGlanceableStatus(snapshot, flags); 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, })), primaryLabel: primary === null ? null : translate(primary.key), + primaryKind: primary === null ? null : primary.kind, primaryCount: primary === null ? 0 : primary.count, - elapsedAnchor: status === 'happy' ? snapshot.eligibleStartedAt : null, - openAgentsLabel: translate('glanceable.openAgents'), - showOpenAgents: status === 'happy' || status === 'stale', - accessibilityLabel: glanceableSpokenLabelKeys(snapshot, flags) - .map(key => translate(key)) - .join(', '), + needsInputSince: showCounts && snapshot.needsInput > 0 ? snapshot.needsInputSince : null, + 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` + * 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, + idle: snapshot.idle, + needsInputSince: snapshot.needsInputSince, }; } 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 a7f3be02fd..de4d0caa13 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "LOOP", + "running": "Werk", "needsInput": "benodig invoer", - "reconnecting": "Verbind tans weer", - "channelName": "Aktiewe agente" + "idle": "Onaktief", + "channelName": "Aktiewe agente", + "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 44d243616a..da5179b8f7 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "ማሳወቂያዎች", + "liveActivities": "የቀጥታ እንቅስቃሴዎች", + "liveActivitySubtitle": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ አሳይ", + "liveUpdates": "የቀጥታ ዝማኔዎች", + "liveUpdateSubtitle": "ንቁ ወኪሎችን በማሳወቂያዎችዎ ውስጥ አሳይ", "push": "ግፋ", "enabled": "ማሳወቂያዎች ነቅተዋል", "onDescription": "የግፋ ማሳወቂያዎች ለዚህ መሣሪያ በርተዋል።", @@ -3219,11 +3223,12 @@ "stale": "አሁን ማዘመን አይቻልም", "expired": "የሁኔታው ጊዜ አልፏል", "signedOut": "ወኪሎችን ለማየት ይግቡ", - "privacy": "ወኪሎች ተደብቀዋል", + "privacy": "ወኪሎችን ለማየት Kilo ይክፈቱ", "openAgents": "ወኪሎችን ይክፈቱ", - "running": "በስራ ላይ", + "running": "በመስራት ላይ", "needsInput": "ግብዓት ይፈልጋል", - "reconnecting": "እንደገና በመገናኘት ላይ", - "channelName": "ንቁ ወኪሎች" + "idle": "በእረፍት", + "channelName": "ንቁ ወኪሎች", + "activityKitDisabledBody": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ ለማየት በቅንብሮች ውስጥ የቀጥታ እንቅስቃሴዎችን ያብሩ።" } } diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index 1006fa8e00..4a3343fb6f 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "الإشعارات", + "liveActivities": "الأنشطة المباشرة", + "liveActivitySubtitle": "اعرض الوكلاء النشطين على شاشة القفل", + "liveUpdates": "التحديثات المباشرة", + "liveUpdateSubtitle": "اعرض الوكلاء النشطين في إشعاراتك", "push": "فوري", "enabled": "الإشعارات ممكّنة", "onDescription": "الإشعارات الفورية قيد التشغيل لهذا الجهاز.", @@ -3307,11 +3311,12 @@ "stale": "يتعذّر التحديث الآن", "expired": "انتهت صلاحية الحالة", "signedOut": "سجّل الدخول لرؤية الوكلاء", - "privacy": "الوكلاء مخفيون", + "privacy": "افتح Kilo لرؤية الوكلاء", "openAgents": "فتح الوكلاء", - "running": "قيد التشغيل", + "running": "جارٍ العمل", "needsInput": "يتطلب إدخالًا", - "reconnecting": "جارٍ إعادة الاتصال", - "channelName": "الوكلاء النشطون" + "idle": "خامل", + "channelName": "الوكلاء النشطون", + "activityKitDisabledBody": "فعّل الأنشطة المباشرة في الإعدادات لرؤية الوكلاء النشطين على شاشة القفل." } } diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 1e0eaf50e3..dd2aabe82e 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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ƏYİR", + "running": "İşlənir", "needsInput": "GİRİŞ TƏLƏB OLUNUR", - "reconnecting": "Yenidən qoşulur", - "channelName": "Aktiv agentlər" + "idle": "Boşda", + "channelName": "Aktiv agentlər", + "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 abe15bccb6..c0cc1539b2 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -628,6 +628,10 @@ }, "notifications": { "title": "Апавяшчэнні", + "liveActivities": "Жывыя актыўнасці", + "liveActivitySubtitle": "Паказваць актыўных агентаў на экране блакіроўкі", + "liveUpdates": "Абнаўленні ў рэальным часе", + "liveUpdateSubtitle": "Паказваць актыўных агентаў ва ўведамленнях", "push": "Push", "enabled": "Апавяшчэнні ўключаны", "onDescription": "Push-апавяшчэнні ўключаны для гэтай прылады.", @@ -3263,11 +3267,12 @@ "stale": "Зараз немагчыма абнавіць", "expired": "Тэрмін дзеяння статусу скончыўся", "signedOut": "Увайдзіце, каб бачыць агентаў", - "privacy": "Агенты схаваны", + "privacy": "Адкрыйце Kilo, каб убачыць агентаў", "openAgents": "Адкрыць агентаў", - "running": "ПРАЦУЕ", + "running": "Працуе", "needsInput": "патрабуецца ўвод", - "reconnecting": "Паўторнае падключэнне", - "channelName": "Актыўныя агенты" + "idle": "Чакае", + "channelName": "Актыўныя агенты", + "activityKitDisabledBody": "Уключыце «Жывыя дзеянні» ў «Наладах», каб бачыць актыўных агентаў на экране блакіроўкі." } } diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 3cf18d83ac..396233beac 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "Известия", + "liveActivities": "Живи активности", + "liveActivitySubtitle": "Показвай активните агенти на заключения екран", + "liveUpdates": "Актуализации на живо", + "liveUpdateSubtitle": "Показвай активните агенти в известията", "push": "Push", "enabled": "Известията са активирани", "onDescription": "Push известията са включени за това устройство.", @@ -3219,11 +3223,12 @@ "stale": "Не може да се актуализира сега", "expired": "Статусът е изтекъл", "signedOut": "Влезте, за да видите агентите", - "privacy": "Агентите са скрити", + "privacy": "Отворете Kilo, за да видите агентите", "openAgents": "Отворете агентите", - "running": "Изпълнява се", + "running": "Работи", "needsInput": "изисква въвеждане", - "reconnecting": "Повторно свързване", - "channelName": "Активни агенти" + "idle": "Неактивен", + "channelName": "Активни агенти", + "activityKitDisabledBody": "Включете Дейности на живо в Настройки, за да виждате активните агенти на заключения екран." } } diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index 75eda3e6b9..36181b0ca1 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "বিজ্ঞপ্তি", + "liveActivities": "লাইভ অ্যাক্টিভিটি", + "liveActivitySubtitle": "লক স্ক্রিনে সক্রিয় এজেন্ট দেখান", + "liveUpdates": "লাইভ আপডেট", + "liveUpdateSubtitle": "আপনার বিজ্ঞপ্তিতে সক্রিয় এজেন্ট দেখান", "push": "পুশ", "enabled": "বিজ্ঞপ্তি সক্রিয়", "onDescription": "এই ডিভাইসে পুশ বিজ্ঞপ্তি চালু আছে।", @@ -3219,11 +3223,12 @@ "stale": "এখন আপডেট করা যাচ্ছে না", "expired": "অবস্থার মেয়াদ শেষ হয়েছে", "signedOut": "এজেন্টগুলি দেখতে সাইন ইন করুন", - "privacy": "এজেন্টগুলি লুকানো আছে", + "privacy": "এজেন্ট দেখতে Kilo খুলুন", "openAgents": "এজেন্টগুলি খুলুন", - "running": "চলছে", + "running": "কাজ চলছে", "needsInput": "ইনপুট প্রয়োজন", - "reconnecting": "পুনরায় সংযোগ করা হচ্ছে", - "channelName": "সক্রিয় এজেন্ট" + "idle": "নিষ্ক্রিয়", + "channelName": "সক্রিয় এজেন্ট", + "activityKitDisabledBody": "লক স্ক্রিনে সক্রিয় এজেন্টগুলি দেখতে সেটিংসে সরাসরি কার্যকলাপ চালু করুন।" } } diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index 59815ad8f4..e08c4be55d 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -621,6 +621,10 @@ }, "notifications": { "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.", @@ -3241,11 +3245,12 @@ "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": "RADI", + "running": "U toku", "needsInput": "treba unos", - "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti" + "idle": "Neaktivan", + "channelName": "Aktivni agenti", + "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 cd8514e343..df62a13c41 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -621,6 +621,10 @@ }, "notifications": { "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.", @@ -3241,11 +3245,12 @@ "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": "EN EXECUCIÓ", + "running": "Treballant", "needsInput": "requereix entrada", - "reconnecting": "Reconnectant", - "channelName": "Agents actius" + "idle": "Inactiu", + "channelName": "Agents actius", + "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 50e6e5ee58..48de0bd596 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "ئاگادارکردنەوەکان", + "liveActivities": "چالاکییە ڕاستەوخۆکان", + "liveActivitySubtitle": "ئەجێنتە چالاکەکان لەسەر شاشەی داخستن پیشان بدە", + "liveUpdates": "نوێکردنەوەی ڕاستەوخۆ", + "liveUpdateSubtitle": "ئاژانسە چالاکەکان لە ئاگادارکردنەوەکانتدا پیشان بدە", "push": "پاڵدان", "enabled": "ئاگادارکردنەوەکان چالاک کراون", "onDescription": "ئاگادارکردنەوەکانی پاڵدان بۆ ئەم ئامێرە چالاکن.", @@ -3219,11 +3223,12 @@ "stale": "ئێستا ناتوانرێت نوێ بکرێتەوە", "expired": "دۆخەکە بەسەرچووە", "signedOut": "بچۆ ژوورەوە بۆ بینینی ئەجێنتەکان", - "privacy": "ئەجێنتەکان شاراونەتەوە", + "privacy": "Kilo بکەرەوە بۆ بینینی ئەجێنتەکان", "openAgents": "کردنەوەی ئەجێنتەکان", - "running": "لە کاردایە", + "running": "کارکردن", "needsInput": "پێویستی بە داخڵکردن", - "reconnecting": "لە پەیوەستبوونەوەدایە", - "channelName": "ئەجێنتە چالاکەکان" + "idle": "بێکار", + "channelName": "ئەجێنتە چالاکەکان", + "activityKitDisabledBody": "چالاکییە ڕاستەوخۆکان لە ڕێکخستنەکان چالاک بکە بۆ بینینی ئەجێنتە چالاکەکان لە شاشەی قوفڵ." } } diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 415ad246a0..4cb02ce010 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -628,6 +628,10 @@ }, "notifications": { "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á.", @@ -3263,11 +3267,12 @@ "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": "BĚŽÍ", + "running": "Pracuji", "needsInput": "vyžaduje vstup", - "reconnecting": "Obnovování připojení", - "channelName": "Aktivní agenti" + "idle": "Nečinný", + "channelName": "Aktivní agenti", + "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 e4ebfe024b..5e0b9cb57d 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -642,6 +642,10 @@ }, "notifications": { "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.", @@ -3307,11 +3311,12 @@ "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 RHEDEG", + "running": "Yn gweithio", "needsInput": "angen mewnbwn", - "reconnecting": "Yn ailgysylltu", - "channelName": "Asiantau gweithredol" + "idle": "Segur", + "channelName": "Asiantau gweithredol", + "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 6e6fe7aad4..ac3aaf7094 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "KØRER", + "running": "Arbejder", "needsInput": "kræver input", - "reconnecting": "Genopretter forbindelsen", - "channelName": "Aktive agenter" + "idle": "Inaktiv", + "channelName": "Aktive agenter", + "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 9ae0a7d356..cd77f495c7 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -160,6 +160,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "LÄUFT", + "running": "Wird bearbeitet", "needsInput": "Eingabe erforderlich", - "reconnecting": "Verbindung wird wiederhergestellt", - "channelName": "Aktive Agenten" + "idle": "Inaktiv", + "channelName": "Aktive Agenten", + "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 2fe45ec96f..58c91f52a0 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "Ειδοποιήσεις", + "liveActivities": "Ζωντανές δραστηριότητες", + "liveActivitySubtitle": "Εμφάνιση ενεργών πρακτόρων στην οθόνη κλειδώματος", + "liveUpdates": "Ζωντανές ενημερώσεις", + "liveUpdateSubtitle": "Εμφάνιση ενεργών πρακτόρων στις ειδοποιήσεις σας", "push": "Push", "enabled": "Οι ειδοποιήσεις είναι ενεργοποιημένες", "onDescription": "Οι push ειδοποιήσεις είναι ενεργές για αυτή τη συσκευή.", @@ -3219,11 +3223,12 @@ "stale": "Δεν είναι δυνατή η ενημέρωση τώρα", "expired": "Η κατάσταση έληξε", "signedOut": "Συνδεθείτε για να δείτε τους πράκτορες", - "privacy": "Οι πράκτορες είναι κρυφοί", + "privacy": "Άνοιξε το Kilo για να δεις τους πράκτορες", "openAgents": "Ανοίξτε τους πράκτορες", - "running": "Σε εξέλιξη", + "running": "Εργασία", "needsInput": "χρειάζεται είσοδο", - "reconnecting": "Επανασύνδεση", - "channelName": "Ενεργοί πράκτορες" + "idle": "Αδρανής", + "channelName": "Ενεργοί πράκτορες", + "activityKitDisabledBody": "Ενεργοποιήστε τις Ζωντανές δραστηριότητες στις Ρυθμίσεις για να δείτε τους ενεργούς πράκτορες στην Οθόνη κλειδώματος." } } diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index b040a5da2e..29096d6c53 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Running", + "running": "Working", "needsInput": "Needs input", - "reconnecting": "Reconnecting", - "channelName": "Active agents" + "idle": "Idle", + "channelName": "Active agents", + "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 69e06a31ef..dfd5232cc1 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -183,6 +183,10 @@ "securityFindingsSubtitle": "nuevos hallazgos y recordatorios de SLA" }, "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.", @@ -3241,11 +3245,12 @@ "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": "EN EJECUCIÓN", + "running": "Trabajando", "needsInput": "requiere entrada", - "reconnecting": "Reconectando", - "channelName": "Agentes activos" + "idle": "Inactivo", + "channelName": "Agentes activos", + "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 38ecc41921..bb09705b0e 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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ÖÖTAB", + "running": "Töötamine", "needsInput": "vajab sisendit", - "reconnecting": "Ühenduse taastamine", - "channelName": "Aktiivsed agendid" + "idle": "Ooterežiimis", + "channelName": "Aktiivsed agendid", + "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 e74015504c..3faa867924 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Exekutatzen", + "running": "Lanean", "needsInput": "sarreraren zain", - "reconnecting": "Berriro konektatzen", - "channelName": "Agente aktiboak" + "idle": "Geldirik", + "channelName": "Agente aktiboak", + "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 793897193e..8bb0f9a367 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "اعلان‌ها", + "liveActivities": "فعالیت‌های زنده", + "liveActivitySubtitle": "نمایش عامل‌های فعال در صفحه قفل", + "liveUpdates": "به‌روزرسانی‌های زنده", + "liveUpdateSubtitle": "نمایش عامل‌های فعال در اعلان‌های شما", "push": "Push", "enabled": "اعلان‌ها فعال‌اند", "onDescription": "اعلان‌های push برای این دستگاه روشن‌اند.", @@ -3219,11 +3223,12 @@ "stale": "اکنون به‌روزرسانی ممکن نیست", "expired": "وضعیت منقضی شد", "signedOut": "برای دیدن عامل‌ها وارد شوید", - "privacy": "عامل‌ها پنهان هستند", + "privacy": "برای دیدن عامل‌ها Kilo را باز کنید", "openAgents": "عامل‌ها را باز کنید", - "running": "در حال اجرا", + "running": "در حال کار", "needsInput": "نیاز به ورودی", - "reconnecting": "در حال اتصال مجدد", - "channelName": "عامل‌های فعال" + "idle": "غیرفعال", + "channelName": "عامل‌های فعال", + "activityKitDisabledBody": "برای دیدن عامل‌های فعال در صفحه قفل، فعالیت‌های زنده را در تنظیمات روشن کنید." } } diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index af3988a494..ab9ab25c17 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "KÄYNNISSÄ", + "running": "Työstetään", "needsInput": "vaatii syötettä", - "reconnecting": "Yhdistetään uudelleen", - "channelName": "Aktiiviset agentit" + "idle": "Vapaalla", + "channelName": "Aktiiviset agentit", + "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 0ef6565760..f5f5f3e127 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "TUMATAKBO", + "running": "Gumagawa", "needsInput": "kailangan ng input", - "reconnecting": "Muling kumokonekta", - "channelName": "Mga aktibong agent" + "idle": "Idle", + "channelName": "Mga aktibong agent", + "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 27ae88be1e..9174a3ab0a 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -163,6 +163,10 @@ "securityFindingsSubtitle": "nouveaux résultats et rappels SLA" }, "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.", @@ -3241,11 +3245,12 @@ "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", + "running": "En cours", "needsInput": "saisie requise", - "reconnecting": "Reconnexion en cours", - "channelName": "Agents actifs" + "idle": "Inactif", + "channelName": "Agents actifs", + "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 e7917deb05..555f13a60a 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -635,6 +635,10 @@ }, "notifications": { "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.", @@ -3285,11 +3289,12 @@ "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 RITH", + "running": "Ag obair", "needsInput": "teastaíonn ionchur", - "reconnecting": "Ag athcheangal", - "channelName": "Gníomhairí gníomhacha" + "idle": "Díomhaoin", + "channelName": "Gníomhairí gníomhacha", + "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 32b0d3aa89..e873574382 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Executando", + "running": "Traballando", "needsInput": "precisa entrada", - "reconnecting": "Reconectando", - "channelName": "Axentes activos" + "idle": "Inactivo", + "channelName": "Axentes activos", + "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 49b082e70e..171d78dd55 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "સૂચનાઓ", + "liveActivities": "લાઇવ પ્રવૃત્તિઓ", + "liveActivitySubtitle": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો બતાવો", + "liveUpdates": "લાઇવ અપડેટ્સ", + "liveUpdateSubtitle": "તમારી સૂચનાઓમાં સક્રિય એજન્ટ બતાવો", "push": "પુશ", "enabled": "સૂચનાઓ સક્ષમ", "onDescription": "આ ઉપકરણ માટે પુશ સૂચનાઓ ચાલુ છે.", @@ -3219,11 +3223,12 @@ "stale": "હમણાં અપડેટ કરી શકાતું નથી", "expired": "સ્થિતિની સમયસીમા સમાપ્ત થઈ", "signedOut": "એજન્ટો જોવા માટે સાઇન ઇન કરો", - "privacy": "એજન્ટો છુપાવેલા છે", + "privacy": "એજન્ટો જોવા માટે Kilo ખોલો", "openAgents": "એજન્ટો ખોલો", - "running": "ચાલી રહ્યું છે", + "running": "કામ થઈ રહ્યું છે", "needsInput": "ઇનપુટ જરૂરી", - "reconnecting": "ફરી કનેક્ટ થઈ રહ્યું છે", - "channelName": "સક્રિય એજન્ટો" + "idle": "નિષ્ક્રિય", + "channelName": "સક્રિય એજન્ટો", + "activityKitDisabledBody": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો જોવા માટે સેટિંગ્સમાં લાઇવ પ્રવૃત્તિઓ ચાલુ કરો." } } diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index edf70bdaba..729aba29de 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Ana gudana", + "running": "Yana aiki", "needsInput": "yana buƙatar bayani", - "reconnecting": "Ana sake haɗawa", - "channelName": "Wakilai da ke aiki" + "idle": "Rashin aiki", + "channelName": "Wakilai da ke aiki", + "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 a4f120be8c..4a72ba9bed 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "התראות", + "liveActivities": "פעילויות בזמן אמת", + "liveActivitySubtitle": "הצג סוכנים פעילים במסך הנעילה", + "liveUpdates": "עדכונים חיים", + "liveUpdateSubtitle": "הצג סוכנים פעילים בהתראות שלך", "push": "דחיפה", "enabled": "התראות מופעלות", "onDescription": "התראות דחיפה מופעלות עבור מכשיר זה.", @@ -3241,11 +3245,12 @@ "stale": "לא ניתן לעדכן כעת", "expired": "תוקף המצב פג", "signedOut": "היכנס כדי לראות סוכנים", - "privacy": "הסוכנים מוסתרים", + "privacy": "פתח את Kilo כדי לראות סוכנים", "openAgents": "פתח סוכנים", - "running": "רץ", + "running": "עובד", "needsInput": "נדרש קלט", - "reconnecting": "מתחבר מחדש", - "channelName": "סוכנים פעילים" + "idle": "בטל", + "channelName": "סוכנים פעילים", + "activityKitDisabledBody": "הפעל פעילויות בזמן אמת בהגדרות כדי לראות סוכנים פעילים במסך הנעילה." } } diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index ec46e27847..6095149cd3 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "सूचनाएँ", + "liveActivities": "लाइव गतिविधियाँ", + "liveActivitySubtitle": "लॉक स्क्रीन पर सक्रिय एजेंट दिखाएँ", + "liveUpdates": "लाइव अपडेट", + "liveUpdateSubtitle": "अपनी सूचनाओं में सक्रिय एजेंट दिखाएँ", "push": "पुश", "enabled": "सूचनाएँ सक्षम", "onDescription": "इस डिवाइस के लिए पुश सूचनाएँ चालू हैं।", @@ -3219,11 +3223,12 @@ "stale": "अभी अपडेट नहीं हो सकता", "expired": "स्थिति की समय सीमा समाप्त हो गई", "signedOut": "एजेंट देखने के लिए साइन इन करें", - "privacy": "एजेंट छिपे हुए हैं", + "privacy": "एजेंट देखने के लिए Kilo खोलें", "openAgents": "एजेंट खोलें", - "running": "चालू", + "running": "काम कर रहा है", "needsInput": "इनपुट आवश्यक", - "reconnecting": "फिर से कनेक्ट हो रहा है", - "channelName": "सक्रिय एजेंट" + "idle": "निष्क्रिय", + "channelName": "सक्रिय एजेंट", + "activityKitDisabledBody": "लॉक स्क्रीन पर सक्रिय एजेंट देखने के लिए सेटिंग में लाइव ऐक्टिविटी चालू करें।" } } diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 00f41e3aee..fbe9856c63 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -621,6 +621,10 @@ }, "notifications": { "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.", @@ -3241,11 +3245,12 @@ "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": "RADI", + "running": "Radim", "needsInput": "treba unos", - "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti" + "idle": "Neaktivan", + "channelName": "Aktivni agenti", + "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 49daeb48d6..75050afa67 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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 KOURI", + "running": "Ap travay", "needsInput": "bezwen input", - "reconnecting": "Ap rekonekte", - "channelName": "Ajans aktif yo" + "idle": "Anchaj", + "channelName": "Ajans aktif yo", + "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 61463b715e..9bf89ce072 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -614,6 +614,10 @@ }, "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", + "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.", @@ -3219,11 +3223,12 @@ "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": "Folyamatban", + "running": "Feldolgozás", "needsInput": "bemenetet igényel", - "reconnecting": "Újracsatlakozás", - "channelName": "Aktív ügynökök" + "idle": "Tétlen", + "channelName": "Aktív ügynökök", + "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 a44cbfc573..a00cf5a267 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "Ծանուցումներ", + "liveActivities": "Ուղիղ գործողություններ", + "liveActivitySubtitle": "Ցուցադրել ակտիվ գործակալները կողպէկրանին", + "liveUpdates": "Ուղիղ թարմացումներ", + "liveUpdateSubtitle": "Ցուցադրել ակտիվ գործակալները ձեր ծանուցումներում", "push": "Push", "enabled": "Ծանուցումները միացված են", "onDescription": "Push ծանուցումները միացված են այս սարքի համար:", @@ -3219,11 +3223,12 @@ "stale": "Այժմ հնարավոր չէ թարմացնել", "expired": "Կարգավիճակի ժամկետը լրացել է", "signedOut": "Մուտք գործեք՝ գործակալներին տեսնելու համար", - "privacy": "Գործակալները թաքցված են", + "privacy": "Բացեք Kilo-ն՝ գործակալները տեսնելու համար", "openAgents": "Բացեք գործակալները", - "running": "Ընթացքի մեջ է", + "running": "Մշակվում է", "needsInput": "մուտքագրման կարիք ունի", - "reconnecting": "Կրկին միացում", - "channelName": "Ակտիվ գործակալներ" + "idle": "Պարապ", + "channelName": "Ակտիվ գործակալներ", + "activityKitDisabledBody": "Միացրեք «Ուղիղ ակտիվություններ»-ը Կարգավորումներում՝ ակտիվ գործակալներին կողպման էկրանին տեսնելու համար։" } } diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index ec4a33c673..ef3bce76d4 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -160,6 +160,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "BERJALAN", + "running": "Mengerjakan", "needsInput": "memerlukan input", - "reconnecting": "Menghubungkan kembali", - "channelName": "Agen aktif" + "idle": "Idle", + "channelName": "Agen aktif", + "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 434cc22d6d..89e4743826 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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-AGBA", + "running": "Na-arụ ọrụ", "needsInput": "chọrọ ntinye", - "reconnecting": "Na-ejikọ ọzọ", - "channelName": "Ndị ọrụ na-arụ ọrụ" + "idle": "Ọrụ na-agaghị", + "channelName": "Ndị ọrụ na-arụ ọrụ", + "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 e8a6d0d3a4..475d494540 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Í gangi", + "running": "Vinnur", "needsInput": "þarfnast inntaks", - "reconnecting": "Tengist aftur", - "channelName": "Virk umboð" + "idle": "Í bið", + "channelName": "Virk umboð", + "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 04fd8a341b..a9e9d9a1a7 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -163,6 +163,10 @@ "securityFindingsSubtitle": "nuovi risultati e promemoria SLA" }, "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.", @@ -3241,11 +3245,12 @@ "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 ESECUZIONE", + "running": "In corso", "needsInput": "richiede input", - "reconnecting": "Riconnessione in corso", - "channelName": "Agenti attivi" + "idle": "Inattivo", + "channelName": "Agenti attivi", + "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 b981927358..387715c7f0 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "通知", + "liveActivities": "ライブアクティビティ", + "liveActivitySubtitle": "ロック画面に稼働中のエージェントを表示", + "liveUpdates": "ライブアップデート", + "liveUpdateSubtitle": "通知にアクティブなエージェントを表示", "push": "プッシュ", "enabled": "通知が有効です", "onDescription": "このデバイスではプッシュ通知がオンです。", @@ -3219,11 +3223,12 @@ "stale": "現在更新できません", "expired": "ステータスの有効期限が切れました", "signedOut": "エージェントを表示するにはサインインしてください", - "privacy": "エージェントは非表示です", + "privacy": "エージェントを表示するには Kilo を開いてください", "openAgents": "エージェントを開く", - "running": "実行中", + "running": "作業中", "needsInput": "入力が必要", - "reconnecting": "再接続中", - "channelName": "アクティブなエージェント" + "idle": "アイドル", + "channelName": "アクティブなエージェント", + "activityKitDisabledBody": "ロック画面にアクティブなエージェントを表示するには、設定でライブアクティビティをオンにしてください。" } } diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 409cecd0fc..c23f4c862f 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "შეტყობინებები", + "liveActivities": "ცოცხალი აქტივობები", + "liveActivitySubtitle": "აქტიური აგენტების ჩვენება ჩაკეტილ ეკრანზე", + "liveUpdates": "ცოცხალი განახლებები", + "liveUpdateSubtitle": "აქტიური აგენტების ჩვენება შეტყობინებებში", "push": "Push", "enabled": "შეტყობინებები ჩართულია", "onDescription": "Push შეტყობინებები ჩართულია ამ მოწყობილობაზე.", @@ -3219,11 +3223,12 @@ "stale": "ახლა განახლება ვერ ხერხდება", "expired": "სტატუსს ვადა გაუვიდა", "signedOut": "შედით აგენტების სანახავად", - "privacy": "აგენტები დამალულია", + "privacy": "აგენტების სანახავად გახსენით Kilo", "openAgents": "აგენტების გახსნა", - "running": "მუშაობს", + "running": "მუშავდება", "needsInput": "მოითხოვს შეყვანას", - "reconnecting": "კავშირის აღდგენა", - "channelName": "აქტიური აგენტები" + "idle": "უქმე", + "channelName": "აქტიური აგენტები", + "activityKitDisabledBody": "ჩართეთ ცოცხალი აქტივობები პარამეტრებში, რათა დაბლოკვის ეკრანზე აქტიური აგენტები ნახოთ." } } diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index d631fc7c26..bc4b27cc08 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "Хабарландырулар", + "liveActivities": "Тікелей әрекеттер", + "liveActivitySubtitle": "Белсенді агенттерді құлып экранында көрсету", + "liveUpdates": "Тікелей жаңартулар", + "liveUpdateSubtitle": "Хабарландыруларда белсенді агенттерді көрсету", "push": "Push", "enabled": "Хабарландырулар қосылған", "onDescription": "Бұл құрылғы үшін push хабарландырулар қосулы.", @@ -3219,11 +3223,12 @@ "stale": "Қазір жаңарту мүмкін емес", "expired": "Күйдің мерзімі өтті", "signedOut": "Агенттерді көру үшін кіріңіз", - "privacy": "Агенттер жасырылған", + "privacy": "Агенттерді көру үшін Kilo ашыңыз", "openAgents": "Агенттерді ашу", "running": "Орындалуда", "needsInput": "енгізу қажет", - "reconnecting": "Қайта қосылуда", - "channelName": "Белсенді агенттер" + "idle": "Бос тұр", + "channelName": "Белсенді агенттер", + "activityKitDisabledBody": "Құлыптау экранында белсенді агенттерді көру үшін Параметрлерде тікелей әрекеттерді қосыңыз." } } diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index e963769207..da8a804217 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "ការជូនដំណឹង", + "liveActivities": "សកម្មភាពផ្ទាល់", + "liveActivitySubtitle": "បង្ហាញភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ", + "liveUpdates": "ការធ្វើបច្ចុប្បន្នភាពផ្ទាល់", + "liveUpdateSubtitle": "បង្ហាញភ្នាក់ងារសកម្មនៅក្នុងការជូនដំណឹងរបស់អ្នក", "push": "Push", "enabled": "ការជូនដំណឹងត្រូវបានបើក", "onDescription": "ការជូនដំណឹងរុញបានបើកសម្រាប់ឧបករណ៍នេះ។", @@ -3219,11 +3223,12 @@ "stale": "មិនអាចធ្វើបច្ចុប្បន្នភាពឥឡូវនេះបានទេ", "expired": "ស្ថានភាពបានផុតកំណត់", "signedOut": "ចូលដើម្បីមើលភ្នាក់ងារ", - "privacy": "ភ្នាក់ងារត្រូវបានលាក់", + "privacy": "បើក Kilo ដើម្បីមើលភ្នាក់ងារ", "openAgents": "បើកភ្នាក់ងារ", "running": "កំពុងដំណើរការ", "needsInput": "ត្រូវការបញ្ចូល", - "reconnecting": "កំពុងភ្ជាប់ឡើងវិញ", - "channelName": "ភ្នាក់ងារសកម្ម" + "idle": "ទំនេរ", + "channelName": "ភ្នាក់ងារសកម្ម", + "activityKitDisabledBody": "បើកសកម្មភាពបន្តផ្ទាល់នៅក្នុងការកំណត់ ដើម្បីមើលភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ។" } } diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index af70046caf..86fff381ce 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "ಅಧಿಸೂಚನೆಗಳು", + "liveActivities": "ಲೈವ್ ಚಟುವಟಿಕೆಗಳು", + "liveActivitySubtitle": "ಲಾಕ್ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ತೋರಿಸಿ", + "liveUpdates": "ನೇರ ನವೀಕರಣಗಳು", + "liveUpdateSubtitle": "ನಿಮ್ಮ ಅಧಿಸೂಚನೆಗಳಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ತೋರಿಸಿ", "push": "ಪುಶ್", "enabled": "ಅಧಿಸೂಚನೆಗಳು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ", "onDescription": "ಈ ಸಾಧನಕ್ಕೆ ಪುಶ್ ಅಧಿಸೂಚನೆಗಳು ಆನ್ ಆಗಿವೆ.", @@ -3219,11 +3223,12 @@ "stale": "ಈಗ ನವೀಕರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ", "expired": "ಸ್ಥಿತಿಯ ಅವಧಿ ಮುಗಿದಿದೆ", "signedOut": "ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೈನ್ ಇನ್ ಮಾಡಿ", - "privacy": "ಏಜೆಂಟ್‌ಗಳನ್ನು ಮರೆಮಾಡಲಾಗಿದೆ", + "privacy": "ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು Kilo ತೆರೆಯಿರಿ", "openAgents": "ಏಜೆಂಟ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ", - "running": "ಚಾಲನೆಯಲ್ಲಿದೆ", + "running": "ಕೆಲಸ ಮಾಡುತ್ತಿದೆ", "needsInput": "ಇನ್‌ಪುಟ್ ಅಗತ್ಯವಿದೆ", - "reconnecting": "ಮರುಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ", - "channelName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು" + "idle": "ನಿಷ್ಕ್ರಿಯ", + "channelName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು", + "activityKitDisabledBody": "ಲಾಕ್ ಪರದೆಯಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ನೇರ ಚಟುವಟಿಕೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ." } } diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 997c9624fb..9d3bcf8e2e 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "알림", + "liveActivities": "실시간 활동", + "liveActivitySubtitle": "잠금 화면에 활성 에이전트 표시", + "liveUpdates": "실시간 업데이트", + "liveUpdateSubtitle": "알림에 활성 에이전트 표시", "push": "푸시", "enabled": "알림 활성화됨", "onDescription": "이 기기에서 푸시 알림이 켜져 있습니다.", @@ -3219,11 +3223,12 @@ "stale": "지금 업데이트할 수 없습니다", "expired": "상태 만료됨", "signedOut": "에이전트를 보려면 로그인하세요", - "privacy": "에이전트 숨겨짐", + "privacy": "에이전트를 보려면 Kilo를 여세요", "openAgents": "에이전트 열기", - "running": "실행 중", + "running": "작업 중", "needsInput": "입력 필요", - "reconnecting": "다시 연결 중", - "channelName": "활성 에이전트" + "idle": "유휴", + "channelName": "활성 에이전트", + "activityKitDisabledBody": "잠금 화면에서 활성 에이전트를 보려면 설정에서 실시간 현황을 켜세요." } } diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index cdc064d20c..fca7284d65 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "ການແຈ້ງເຕືອນ", + "liveActivities": "ກິດຈະກຳສົດ", + "liveActivitySubtitle": "ສະແດງຕົວແທນທີ່ເຮັດວຽກຢູ່ໜ້າຈໍລັອກ", + "liveUpdates": "ການອັບເດດສົດ", + "liveUpdateSubtitle": "ສະແດງເອເຈນທີ່ເຄື່ອນໄຫວໃນການແຈ້ງເຕືອນຂອງທ່ານ", "push": "ການຜັກດັນ", "enabled": "ເປີດການແຈ້ງເຕືອນແລ້ວ", "onDescription": "ການແຈ້ງເຕືອນແບບຜັກດັນເປີດຢູ່ສຳລັບອຸປະກອນນີ້.", @@ -3219,11 +3223,12 @@ "stale": "ບໍ່ສາມາດອັບເດດໄດ້ໃນຕອນນີ້", "expired": "ສະຖານະໝົດອາຍຸແລ້ວ", "signedOut": "ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງຕົວແທນ", - "privacy": "ຕົວແທນຖືກເຊື່ອງໄວ້", + "privacy": "ເປີດ Kilo ເພື່ອເບິ່ງຕົວແທນ", "openAgents": "ເປີດຕົວແທນ", "running": "ກຳລັງດຳເນີນການ", "needsInput": "ຕ້ອງການຂໍ້ມູນເຂົ້າ", - "reconnecting": "ກຳລັງເຊື່ອມຕໍ່ຄືນ", - "channelName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ" + "idle": "ບໍ່ຫຍຸ້ງ", + "channelName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ", + "activityKitDisabledBody": "ເປີດກິດຈະກຳສົດໃນການຕັ້ງຄ່າ ເພື່ອເບິ່ງຕົວແທນທີ່ກຳລັງເຮັດວຽກໃນໜ້າຈໍລັອກ." } } diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index bd586c9e0a..906fc4a493 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -628,6 +628,10 @@ }, "notifications": { "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.", @@ -3263,11 +3267,12 @@ "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", - "reconnecting": "Jungiamasi iš naujo", - "channelName": "Aktyvūs agentai" + "idle": "Neaktyvus", + "channelName": "Aktyvūs agentai", + "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 af9bb3784f..c4038af28c 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -621,6 +621,10 @@ }, "notifications": { "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.", @@ -3241,11 +3245,12 @@ "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": "DARBOJAS", + "running": "Apstrādā", "needsInput": "nepieciešama ievade", - "reconnecting": "Atkārtoti izveido savienojumu", - "channelName": "Aktīvie aģenti" + "idle": "Dīkstāvē", + "channelName": "Aktīvie aģenti", + "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 4df03ae849..8f7b1c9b54 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "MANDEHA", + "running": "Miasa", "needsInput": "mila fampidirana", - "reconnecting": "Mampifandray indray", - "channelName": "Agent mavitrika" + "idle": "Tsy mihetsika", + "channelName": "Agent mavitrika", + "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 3fc8eecccb..931f70e795 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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 oma", + "running": "Kei te mahi", "needsInput": "e hiahia ana ki te whakaurunga", - "reconnecting": "Kei te hono anō", - "channelName": "Ngā māngai hohe" + "idle": "Kore mahi", + "channelName": "Ngā māngai hohe", + "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 8710afd205..fb5a104237 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "Известувања", + "liveActivities": "Активности во живо", + "liveActivitySubtitle": "Прикажувај активни агенти на заклучениот екран", + "liveUpdates": "Ажурирања во живо", + "liveUpdateSubtitle": "Прикажувај активни агенти во известувањата", "push": "Притискање", "enabled": "Известувањата се овозможени", "onDescription": "Притиснатите известувања се вклучени за овој уред.", @@ -3219,11 +3223,12 @@ "stale": "Не може да се ажурира сега", "expired": "Статусот истече", "signedOut": "Најавете се за да ги видите агентите", - "privacy": "Агентите се скриени", + "privacy": "Отворете Kilo за да ги видите агентите", "openAgents": "Отворете ги агентите", - "running": "Во тек", + "running": "Работи", "needsInput": "бара внес", - "reconnecting": "Повторно поврзување", - "channelName": "Активни агенти" + "idle": "Неактивен", + "channelName": "Активни агенти", + "activityKitDisabledBody": "Вклучете Активности во живо во Поставки за да ги видите активните агенти на заклучениот екран." } } diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 84be3768ec..4fc20d8f14 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "അറിയിപ്പുകൾ", + "liveActivities": "ലൈവ് ആക്റ്റിവിറ്റികൾ", + "liveActivitySubtitle": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണിക്കുക", + "liveUpdates": "തത്സമയ അപ്‌ഡേറ്റുകൾ", + "liveUpdateSubtitle": "നിങ്ങളുടെ അറിയിപ്പുകളിൽ സജീവ ഏജന്റുമാരെ കാണിക്കുക", "push": "പുഷ്", "enabled": "അറിയിപ്പുകൾ പ്രവർത്തനക്ഷമമാക്കി", "onDescription": "ഈ ഉപകരണത്തിനായി പുഷ് അറിയിപ്പുകൾ ഓണാണ്.", @@ -3219,11 +3223,12 @@ "stale": "ഇപ്പോൾ അപ്ഡേറ്റ് ചെയ്യാൻ കഴിയില്ല", "expired": "നില കാലഹരണപ്പെട്ടു", "signedOut": "ഏജന്റുകളെ കാണാൻ സൈൻ ഇൻ ചെയ്യുക", - "privacy": "ഏജന്റുകളെ മറച്ചിരിക്കുന്നു", + "privacy": "ഏജന്റുകളെ കാണാൻ Kilo തുറക്കുക", "openAgents": "ഏജന്റുകളെ തുറക്കുക", "running": "പ്രവർത്തിക്കുന്നു", "needsInput": "ഇൻപുട്ട് ആവശ്യമാണ്", - "reconnecting": "വീണ്ടും ബന്ധിപ്പിക്കുന്നു", - "channelName": "സജീവ ഏജന്റുകൾ" + "idle": "നിഷ്ക്രിയം", + "channelName": "സജീവ ഏജന്റുകൾ", + "activityKitDisabledBody": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണാൻ ക്രമീകരണങ്ങളിൽ തത്സമയ പ്രവർത്തനങ്ങൾ പ്രവർത്തനക്ഷമമാക്കുക." } } diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index e467067546..79dd049be0 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "Мэдэгдэлүүд", + "liveActivities": "Шууд үйл ажиллагаа", + "liveActivitySubtitle": "Идэвхтэй агентуудыг түгжээний дэлгэцэд харуулах", + "liveUpdates": "Шууд шинэчлэлт", + "liveUpdateSubtitle": "Мэдэгдэлд идэвхтэй агентуудыг харуулах", "push": "Түлхэлт", "enabled": "Мэдэгдэл идэвхжсэн", "onDescription": "Энэ төхөөрөмжид түлхэлтийн мэдэгдэл асна.", @@ -3219,11 +3223,12 @@ "stale": "Одоо шинэчлэх боломжгүй", "expired": "Төлөвийн хугацаа дууссан", "signedOut": "Агентуудыг харахын тулд нэвтэрнэ үү", - "privacy": "Агентуудыг нуусан", + "privacy": "Агентуудыг харахын тулд Kilo-г онгойлго", "openAgents": "Агентуудыг нээх", - "running": "АЖИЛЛАЖ БАЙНА", + "running": "Ажиллаж байна", "needsInput": "оролт шаардлагатай", - "reconnecting": "Дахин холбогдож байна", - "channelName": "Идэвхтэй агентууд" + "idle": "Сул зогсож", + "channelName": "Идэвхтэй агентууд", + "activityKitDisabledBody": "Түгжээтэй дэлгэц дээр Идэвхтэй агентуудыг харахын тулд Тохиргоо хэсэгт Шууд үйл ажиллагааг асаана уу." } } diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index 93cf33b580..2103499a3a 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "सूचना", + "liveActivities": "लाइव्ह क्रियाकलाप", + "liveActivitySubtitle": "लॉक स्क्रीनवर सक्रिय एजंट दाखवा", + "liveUpdates": "थेट अद्यतने", + "liveUpdateSubtitle": "तुमच्या सूचनांमध्ये सक्रिय एजंट दाखवा", "push": "पुश", "enabled": "सूचना सक्षम केल्या", "onDescription": "या डिव्हाइससाठी पुश सूचना चालू आहेत.", @@ -3219,11 +3223,12 @@ "stale": "आता अद्यतनित करता येत नाही", "expired": "स्थिती कालबाह्य झाली", "signedOut": "एजंट्स पाहण्यासाठी साइन इन करा", - "privacy": "एजंट्स लपवले आहेत", + "privacy": "एजंट पाहण्यासाठी Kilo उघडा", "openAgents": "एजंट्स उघडा", - "running": "चालू आहे", + "running": "कार्यरत", "needsInput": "इनपुट आवश्यक", - "reconnecting": "पुन्हा जोडत आहे", - "channelName": "सक्रिय एजंट्स" + "idle": "निष्क्रिय", + "channelName": "सक्रिय एजंट्स", + "activityKitDisabledBody": "लॉक स्क्रीनवर सक्रिय एजंट्स पाहण्यासाठी सेटिंग्जमध्ये थेट क्रियाकलाप सुरू करा." } } diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 492f84422a..9229ff6288 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Sedang berjalan", + "running": "Memproses…", "needsInput": "perlu input", - "reconnecting": "Menyambung semula", - "channelName": "Ejen aktif" + "idle": "Melahu", + "channelName": "Ejen aktif", + "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 c034a6b5e8..2b2cc9cb75 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -635,6 +635,10 @@ }, "notifications": { "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.", @@ -3285,11 +3289,12 @@ "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": "Għaddej", + "running": "Qed jaħdem", "needsInput": "jeħtieġ input", - "reconnecting": "Qed jerġa' jaqbad", - "channelName": "Aġenti attivi" + "idle": "Idle", + "channelName": "Aġenti attivi", + "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 3f5005f08f..61914ac4b7 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "အသိပေးချက်များ", + "liveActivities": "တိုက်ရိုက် လှုပ်ရှားမှုများ", + "liveActivitySubtitle": "လော့ခ်စခရင်တွင် လုပ်ဆောင်နေသော agent များကို ပြပါ", + "liveUpdates": "တိုက်ရိုက် အပ်ဒိတ်များ", + "liveUpdateSubtitle": "အသုံးပြုနေသော အေးဂျင့်များကို အကြောင်းကြားချက်တွင် ပြပါ", "push": "Push", "enabled": "အကြောင်းကြားချက်များ ဖွင့်ထားသည်", "onDescription": "ဤစက်အတွက် push အကြောင်းကြားချက်များ ဖွင့်ထားသည်။", @@ -3219,11 +3223,12 @@ "stale": "ယခု အပ်ဒိတ်လုပ်၍ မရပါ", "expired": "အခြေအနေ သက်တမ်းကုန်သွားသည်", "signedOut": "agent များကို ကြည့်ရန် ဝင်ပါ", - "privacy": "agent များကို ဝှက်ထားသည်", + "privacy": "Agent များကို ကြည့်ရန် Kilo ကို ဖွင့်ပါ", "openAgents": "agent များကို ဖွင့်ပါ", - "running": "လည်ပတ်နေသည်", + "running": "လုပ်ဆောင်နေသည်", "needsInput": "ထည့်သွင်းမှု လိုအပ်သည်", - "reconnecting": "ပြန်ချိတ်ဆက်နေသည်", - "channelName": "လုပ်ဆောင်နေသော agent များ" + "idle": "နားနေသည်", + "channelName": "လုပ်ဆောင်နေသော agent များ", + "activityKitDisabledBody": "သော့ခတ်မျက်နှာပြင်တွင် လုပ်ဆောင်နေသော agent များကို ကြည့်ရန် ဆက်တင်များတွင် တိုက်ရိုက်လှုပ်ရှားမှုများကို ဖွင့်ပါ။" } } diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index f9b78457f5..fcbc43b59e 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "KJØRER", + "running": "Jobber", "needsInput": "trenger innspill", - "reconnecting": "Kobler til på nytt", - "channelName": "Aktive agenter" + "idle": "Ledig", + "channelName": "Aktive agenter", + "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 9d3bcef2b6..116d4aa36e 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "सूचनाहरू", + "liveActivities": "लाइभ गतिविधिहरू", + "liveActivitySubtitle": "लक स्क्रिनमा सक्रिय एजेन्टहरू देखाउनुहोस्", + "liveUpdates": "प्रत्यक्ष अद्यावधिक", + "liveUpdateSubtitle": "आफ्ना सूचनाहरूमा सक्रिय एजेन्ट देखाउनुहोस्", "push": "पुश", "enabled": "सूचनाहरू सक्षम गरियो", "onDescription": "यो यन्त्रको लागि पुश सूचनाहरू सक्रिय छन्।", @@ -3219,11 +3223,12 @@ "stale": "अहिले अद्यावधिक गर्न सकिँदैन", "expired": "स्थितिको म्याद सकियो", "signedOut": "एजेन्टहरू हेर्न साइन इन गर्नुहोस्", - "privacy": "एजेन्टहरू लुकाइएका छन्", + "privacy": "एजेन्टहरू देख्न Kilo खोल्नुहोस्", "openAgents": "एजेन्टहरू खोल्नुहोस्", - "running": "चलिरहेको", + "running": "काम गर्दै", "needsInput": "इनपुट चाहिन्छ", - "reconnecting": "पुनः जडान गर्दै", - "channelName": "सक्रिय एजेन्टहरू" + "idle": "निष्क्रिय", + "channelName": "सक्रिय एजेन्टहरू", + "activityKitDisabledBody": "लक स्क्रिनमा सक्रिय एजेन्टहरू हेर्न सेटिङ्समा प्रत्यक्ष गतिविधिहरू चालू गर्नुहोस्।" } } diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index ba3bc99279..17b51c1a8f 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -183,6 +183,10 @@ "securityFindingsSubtitle": "nieuwe bevindingen en SLA-herinneringen" }, "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.", @@ -3219,11 +3223,12 @@ "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", - "reconnecting": "Opnieuw verbinden", - "channelName": "Actieve agents" + "idle": "Inactief", + "channelName": "Actieve agents", + "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 81bf037188..1e8c42dc38 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Hojii irra jira", + "running": "Hojachaa jira", "needsInput": "seensa barbaada", - "reconnecting": "Irra deebi'ee walqabachaa jira", - "channelName": "Eejentoota hojii irra jiran" + "idle": "Hojii irraa boqachaa", + "channelName": "Eejentoota hojii irra jiran", + "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 ec8a60782b..00a0c1eb62 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "ବିଜ୍ଞପ୍ତି", + "liveActivities": "ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ", + "liveActivitySubtitle": "ଲକ୍ ସ୍କ୍ରିନରେ ସକ୍ରିୟ ଏଜେଣ୍ଟ ଦେଖାନ୍ତୁ", + "liveUpdates": "ଲାଇଭ୍ ଅପଡେଟ୍", + "liveUpdateSubtitle": "ଆପଣଙ୍କ ବିଜ୍ଞପ୍ତିରେ ସକ୍ରିୟ ଏଜେଣ୍ଟ ଦେଖାନ୍ତୁ", "push": "ପୁସ୍", "enabled": "ବିଜ୍ଞପ୍ତି ସକ୍ଷମ ହେଲା", "onDescription": "ଏହି ଉପକରଣ ପାଇଁ ପୁସ୍ ବିଜ୍ଞପ୍ତି ଚାଲୁ ଅଛି।", @@ -3219,11 +3223,12 @@ "stale": "ଏବେ ଅପଡେଟ୍ କରିହେବ ନାହିଁ", "expired": "ସ୍ଥିତିର ଅବଧି ସମାପ୍ତ ହୋଇଛି", "signedOut": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସାଇନ୍ ଇନ୍ କରନ୍ତୁ", - "privacy": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଲୁଚାଯାଇଛି", + "privacy": "ଏଜେଣ୍ଟ ଦେଖିବା ପାଇଁ Kilo ଖୋଲନ୍ତୁ", "openAgents": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଖୋଲନ୍ତୁ", - "running": "ଚାଲୁଛି", + "running": "କାମ କରୁଛି", "needsInput": "ଇନପୁଟ୍ ଆବଶ୍ୟକ", - "reconnecting": "ପୁଣି ସଂଯୋଗ ହେଉଛି", - "channelName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ" + "idle": "ନିଷ୍କ୍ରିୟ", + "channelName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ", + "activityKitDisabledBody": "ଲକ୍ ସ୍କ୍ରିନ୍‌ରେ ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସେଟିଂସ୍‌ରେ ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ଚାଲୁ କରନ୍ତୁ।" } } diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index dff2e06d00..93d0367e36 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "ਸੂਚਨਾਵਾਂ", + "liveActivities": "ਲਾਈਵ ਸਰਗਰਮੀਆਂ", + "liveActivitySubtitle": "ਲਾਕ ਸਕ੍ਰੀਨ ਉੱਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦਿਖਾਓ", + "liveUpdates": "ਲਾਈਵ ਅੱਪਡੇਟ", + "liveUpdateSubtitle": "ਆਪਣੀਆਂ ਸੂਚਨਾਵਾਂ ਵਿੱਚ ਸਰਗਰਮ ਏਜੰਟ ਦਿਖਾਓ", "push": "ਪੁਸ਼", "enabled": "ਸੂਚਨਾਵਾਂ ਸਮਰੱਥ ਹਨ", "onDescription": "ਇਸ ਡਿਵਾਈਸ ਲਈ ਪੁਸ਼ ਸੂਚਨਾਵਾਂ ਚਾਲੂ ਹਨ।", @@ -3219,11 +3223,12 @@ "stale": "ਹੁਣ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ", "expired": "ਸਥਿਤੀ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ", "signedOut": "ਏਜੰਟ ਦੇਖਣ ਲਈ ਸਾਈਨ ਇਨ ਕਰੋ", - "privacy": "ਏਜੰਟ ਲੁਕਾਏ ਗਏ ਹਨ", + "privacy": "ਏਜੰਟ ਦੇਖਣ ਲਈ Kilo ਖੋਲ੍ਹੋ", "openAgents": "ਏਜੰਟ ਖੋਲ੍ਹੋ", - "running": "ਚੱਲ ਰਿਹਾ ਹੈ", + "running": "ਕੰਮ ਹੋ ਰਿਹਾ ਹੈ", "needsInput": "ਇਨਪੁੱਟ ਦੀ ਲੋੜ ਹੈ", - "reconnecting": "ਮੁੜ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", - "channelName": "ਸਰਗਰਮ ਏਜੰਟ" + "idle": "ਸੁਸਤ", + "channelName": "ਸਰਗਰਮ ਏਜੰਟ", + "activityKitDisabledBody": "ਲਾਕ ਸਕ੍ਰੀਨ 'ਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦੇਖਣ ਲਈ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਚਾਲੂ ਕਰੋ।" } } diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index 8db562fae3..4b7db6fef9 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -160,6 +160,10 @@ }, "notifications": { "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.", @@ -3263,11 +3267,12 @@ "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": "W toku", + "running": "Pracuję", "needsInput": "wymaga danych", - "reconnecting": "Ponowne łączenie", - "channelName": "Aktywni agenci" + "idle": "Bezczynny", + "channelName": "Aktywni agenci", + "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 78dee69d20..042682619d 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "خبرتیاوې", + "liveActivities": "ژوندۍ فعالیتونه", + "liveActivitySubtitle": "فعال اجنټان په لاک سکرین کې وښایه", + "liveUpdates": "ژوندي تازه معلومات", + "liveUpdateSubtitle": "فعال اجنټان په خپلو خبرتیاوو کې وښایاست", "push": "پوش", "enabled": "خبرتیاوې فعالې شوې", "onDescription": "پوش خبرتیاوې د دې وسیلې لپاره فعالې دي.", @@ -3219,11 +3223,12 @@ "stale": "اوس تازه کول ناشوني دي", "expired": "د حالت اعتبار پای ته رسېدلی", "signedOut": "د اجنټانو د لیدلو لپاره ننوزئ", - "privacy": "اجنټان پټ دي", + "privacy": "اجنټان لیدلو لپاره Kilo پرانیزئ", "openAgents": "اجنټان پرانیزئ", "running": "روان", "needsInput": "ورودی ته اړتیا لري", - "reconnecting": "بیا نښلېږي", - "channelName": "فعال اجنټان" + "idle": "بې کاره", + "channelName": "فعال اجنټان", + "activityKitDisabledBody": "په قلف شوې پرده کې د فعالو اجنټانو د لیدلو لپاره په ترتیباتو کې ژوندي فعالیتونه فعال کړئ." } } diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index b89f936702..50f2a55175 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -147,6 +147,10 @@ "security": "Descobertas de segurança" }, "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.", @@ -3241,11 +3245,12 @@ "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": "Em execução", + "running": "Trabalhando", "needsInput": "requer entrada", - "reconnecting": "Reconectando", - "channelName": "Agentes ativos" + "idle": "Ocioso", + "channelName": "Agentes ativos", + "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 605e5f9532..73ae2dd15a 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -621,6 +621,10 @@ }, "notifications": { "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.", @@ -3241,11 +3245,12 @@ "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": "EM EXECUÇÃO", + "running": "A trabalhar", "needsInput": "requer entrada", - "reconnecting": "A restabelecer ligação", - "channelName": "Agentes ativos" + "idle": "Inativo", + "channelName": "Agentes ativos", + "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 65ff7e77f6..94c9fccbcb 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -621,6 +621,10 @@ }, "notifications": { "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.", @@ -3241,11 +3245,12 @@ "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": "Rulează", + "running": "Se procesează", "needsInput": "necesită introducere", - "reconnecting": "Se reconectează", - "channelName": "Agenți activi" + "idle": "Inactiv", + "channelName": "Agenți activi", + "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 ca5c595213..74d7263ccd 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "Уведомления", + "liveActivities": "Живые активности", + "liveActivitySubtitle": "Показывать активных агентов на экране блокировки", + "liveUpdates": "Обновления в реальном времени", + "liveUpdateSubtitle": "Показывать активных агентов в уведомлениях", "push": "Push", "enabled": "Уведомления включены", "onDescription": "Push-уведомления включены для этого устройства.", @@ -3263,11 +3267,12 @@ "stale": "Сейчас не удается обновить", "expired": "Статус устарел", "signedOut": "Войдите, чтобы видеть агентов", - "privacy": "Агенты скрыты", + "privacy": "Откройте Kilo, чтобы увидеть агентов", "openAgents": "Открыть агентов", - "running": "Выполняется", + "running": "Работаю...", "needsInput": "требует ввода", - "reconnecting": "Повторное подключение", - "channelName": "Активные агенты" + "idle": "Неактивен", + "channelName": "Активные агенты", + "activityKitDisabledBody": "Включите Эфир активности в Настройках, чтобы видеть активных агентов на экране блокировки." } } diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index 1f93dfeb3a..68d791c220 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "දැනුම්දීම්", + "liveActivities": "සජීවී ක්‍රියාකාරකම්", + "liveActivitySubtitle": "අගුළු තිරයේ සක්‍රීය නියෝජිතයන් පෙන්වන්න", + "liveUpdates": "සජීවී යාවත්කාලීන", + "liveUpdateSubtitle": "ඔබේ දැනුම්දීම්වල ක්‍රියාකාරී නියෝජිතයන් පෙන්වන්න", "push": "තෙරපුම", "enabled": "දැනුම්දීම් සක්‍රීය කර ඇත", "onDescription": "මෙම උපාංගය සඳහා තෙරපුම් දැනුම්දීම් ක්‍රියාත්මකයි.", @@ -3219,11 +3223,12 @@ "stale": "දැන් යාවත්කාලීන කළ නොහැක", "expired": "තත්ත්වය කල් ඉකුත් වී ඇත", "signedOut": "නියෝජිතයන් බැලීමට පුරනය වන්න", - "privacy": "නියෝජිතයන් සඟවා ඇත", + "privacy": "නියෝජිතයන් බැලීමට Kilo විවෘත කරන්න", "openAgents": "නියෝජිතයන් විවෘත කරන්න", - "running": "ධාවනය වෙමින්", + "running": "වැඩ කරමින්", "needsInput": "ආදානය අවශ්යයි", - "reconnecting": "නැවත සම්බන්ධ වෙමින්", - "channelName": "සක්‍රිය නියෝජිතයන්" + "idle": "නිශ්චල", + "channelName": "සක්‍රිය නියෝජිතයන්", + "activityKitDisabledBody": "අගුළු තිරයේ සක්‍රිය නියෝජිතයන් බැලීමට සැකසුම් තුළ සජීවී ක්‍රියාකාරකම් සක්‍රිය කරන්න." } } diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index ac3cb081ab..41ee5b8e31 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -628,6 +628,10 @@ }, "notifications": { "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é.", @@ -3263,11 +3267,12 @@ "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": "Prebieha", + "running": "Pracuje sa", "needsInput": "vyžaduje vstup", - "reconnecting": "Opätovné pripájanie", - "channelName": "Aktívni agenti" + "idle": "Nečinný", + "channelName": "Aktívni agenti", + "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 07f5bfedd4..c1cae52eff 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -628,6 +628,10 @@ }, "notifications": { "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.", @@ -3263,11 +3267,12 @@ "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": "DELUJE", + "running": "Delam", "needsInput": "potrebuje vnos", - "reconnecting": "Ponovno povezovanje", - "channelName": "Aktivni agenti" + "idle": "Nedejavno", + "channelName": "Aktivni agenti", + "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 4fd7ab2497..93fddbdbf1 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Socodaya", + "running": "Waa shaqaynayaa", "needsInput": "u baahan wax-soo-gal", - "reconnecting": "Dib u xiriirinaya", - "channelName": "Wakiillada firfircoon" + "idle": "Firfircooni la'aan", + "channelName": "Wakiillada firfircoon", + "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 77c2b9dbc3..39242b7943 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Në ekzekutim", + "running": "Duke punuar", "needsInput": "ka nevojë për të dhëna", - "reconnecting": "Duke u rilidhur", - "channelName": "Agjentët aktivë" + "idle": "I papunë", + "channelName": "Agjentët aktivë", + "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 5b0e30739a..588f1f5eb8 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -621,6 +621,10 @@ }, "notifications": { "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.", @@ -3241,11 +3245,12 @@ "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": "U toku", + "running": "Radim…", "needsInput": "zahteva unos", - "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti" + "idle": "Neaktivan", + "channelName": "Aktivni agenti", + "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 d79376c85a..bd86a044b4 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "KÖRS", + "running": "Arbetar", "needsInput": "kräver indata", - "reconnecting": "Återansluter", - "channelName": "Aktiva agenter" + "idle": "Inaktiv", + "channelName": "Aktiva agenter", + "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 e67edf9faf..0bd4ad22ab 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "Inaendelea", + "running": "Inafanya kazi", "needsInput": "inahitaji mchango", - "reconnecting": "Inaunganisha tena", - "channelName": "Mawakala wanaofanya kazi" + "idle": "Hakikazi", + "channelName": "Mawakala wanaofanya kazi", + "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 0b97cc2200..ca29b6cc2b 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "அறிவிப்புகள்", + "liveActivities": "நேரடி செயல்பாடுகள்", + "liveActivitySubtitle": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காட்டு", + "liveUpdates": "நேரடி புதுப்பிப்புகள்", + "liveUpdateSubtitle": "உங்கள் அறிவிப்புகளில் செயலில் உள்ள முகவர்களைக் காட்டு", "push": "அழுத்து", "enabled": "அறிவிப்புகள் இயக்கப்பட்டது", "onDescription": "இந்த சாதனத்திற்கு அழுத்து அறிவிப்புகள் இயக்கத்தில் உள்ளன.", @@ -3219,11 +3223,12 @@ "stale": "இப்போது புதுப்பிக்க முடியவில்லை", "expired": "நிலை காலாவதியானது", "signedOut": "முகவர்களைக் காண உள்நுழையவும்", - "privacy": "முகவர்கள் மறைக்கப்பட்டுள்ளனர்", + "privacy": "முகவர்களைப் பார்க்க Kilo திறக்கவும்", "openAgents": "முகவர்களைத் திறக்கவும்", - "running": "இயங்குகிறது", + "running": "வேலை செய்கிறது", "needsInput": "உள்ளீடு தேவை", - "reconnecting": "மீண்டும் இணைக்கிறது", - "channelName": "செயலில் உள்ள முகவர்கள்" + "idle": "செயலற்று", + "channelName": "செயலில் உள்ள முகவர்கள்", + "activityKitDisabledBody": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காண அமைப்புகளில் நேரலைச் செயல்பாடுகளை இயக்கவும்." } } diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 0289cb52e8..be3a1bd9f6 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "నోటిఫికేషన్లు", + "liveActivities": "ప్రత్యక్ష కార్యకలాపాలు", + "liveActivitySubtitle": "లాక్ స్క్రీన్‌లో క్రియాశీల ఏజెంట్లను చూపించు", + "liveUpdates": "ప్రత్యక్ష నవీకరణలు", + "liveUpdateSubtitle": "మీ నోటిఫికేషన్‌లలో సక్రియ ఏజెంట్‌లను చూపించు", "push": "పుష్", "enabled": "నోటిఫికేషన్లు ప్రారంభించబడ్డాయి", "onDescription": "ఈ పరికరానికి పుష్ నోటిఫికేషన్లు ఆన్లో ఉన్నాయి.", @@ -3219,11 +3223,12 @@ "stale": "ఇప్పుడు నవీకరించలేము", "expired": "స్థితి గడువు ముగిసింది", "signedOut": "ఏజెంట్లను చూడటానికి సైన్ ఇన్ చేయండి", - "privacy": "ఏజెంట్లు దాచబడ్డారు", + "privacy": "ఏజెంట్లను చూడటానికి Kilo తెరవండి", "openAgents": "ఏజెంట్లను తెరవండి", - "running": "నడుస్తోంది", + "running": "పని జరుగుతోంది", "needsInput": "ఇన్పుట్ అవసరం", - "reconnecting": "మళ్లీ కనెక్ట్ అవుతోంది", - "channelName": "చురుకైన ఏజెంట్లు" + "idle": "నిష్క్రియం", + "channelName": "చురుకైన ఏజెంట్లు", + "activityKitDisabledBody": "లాక్ స్క్రీన్‌పై చురుకైన ఏజెంట్లను చూడటానికి సెట్టింగ్‌లలో ప్రత్యక్ష కార్యకలాపాలను ఆన్ చేయండి." } } diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index 245dfd5075..2b853bc2bb 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "การแจ้งเตือน", + "liveActivities": "กิจกรรมสด", + "liveActivitySubtitle": "แสดงเอเจนต์ที่ทำงานอยู่บนหน้าจอล็อก", + "liveUpdates": "อัปเดตสด", + "liveUpdateSubtitle": "แสดงเอเจนต์ที่ทำงานอยู่ในการแจ้งเตือนของคุณ", "push": "พุช", "enabled": "เปิดการแจ้งเตือนแล้ว", "onDescription": "เปิดการแจ้งเตือนแบบพุชสำหรับอุปกรณ์นี้", @@ -3219,11 +3223,12 @@ "stale": "ไม่สามารถอัปเดตได้ในขณะนี้", "expired": "สถานะหมดอายุ", "signedOut": "ลงชื่อเข้าใช้เพื่อดูเอเจนต์", - "privacy": "ซ่อนเอเจนต์อยู่", + "privacy": "เปิด Kilo เพื่อดูเอเจนต์", "openAgents": "เปิดเอเจนต์", "running": "กำลังทำงาน", "needsInput": "ต้องป้อนข้อมูล", - "reconnecting": "กำลังเชื่อมต่อใหม่", - "channelName": "เอเจนต์ที่กำลังทำงาน" + "idle": "ว่าง", + "channelName": "เอเจนต์ที่กำลังทำงาน", + "activityKitDisabledBody": "เปิดกิจกรรมสดในการตั้งค่าเพื่อดูเอเจนต์ที่กำลังทำงานบนหน้าจอล็อค" } } diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index 0e6d9b5fef..b30a3badd5 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -160,6 +160,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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", - "reconnecting": "Yeniden bağlanılıyor", - "channelName": "Etkin ajanlar" + "idle": "Boşta", + "channelName": "Etkin ajanlar", + "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 b5caf3a7b2..02ef1d0bbf 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "Сповіщення", + "liveActivities": "Живі активності", + "liveActivitySubtitle": "Показувати активних агентів на екрані блокування", + "liveUpdates": "Оновлення в реальному часі", + "liveUpdateSubtitle": "Показувати активних агентів у сповіщеннях", "push": "Push", "enabled": "Сповіщення увімкнено", "onDescription": "Push-сповіщення увімкнено для цього пристрою.", @@ -3263,11 +3267,12 @@ "stale": "Зараз не вдається оновити", "expired": "Термін дії статусу минув", "signedOut": "Увійдіть, щоб бачити агентів", - "privacy": "Агентів приховано", + "privacy": "Відкрийте Kilo, щоб побачити агентів", "openAgents": "Відкрити агентів", - "running": "Виконується", + "running": "Працює", "needsInput": "потребує вводу", - "reconnecting": "Повторне підключення", - "channelName": "Активні агенти" + "idle": "Неактивний", + "channelName": "Активні агенти", + "activityKitDisabledBody": "Увімкніть «Дії наживо» в «Параметрах», щоб бачити активних агентів на замкненому екрані." } } diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index a707e029b2..1d8eb14332 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -614,6 +614,10 @@ }, "notifications": { "title": "اطلاعیں", + "liveActivities": "لائیو سرگرمیاں", + "liveActivitySubtitle": "لاک اسکرین پر فعال ایجنٹس دکھائیں", + "liveUpdates": "لائیو اپ ڈیٹس", + "liveUpdateSubtitle": "اپنی اطلاعات میں فعال ایجنٹ دکھائیں", "push": "پش", "enabled": "اطلاعیں فعال ہیں", "onDescription": "پش اطلاعیں اس آلے کے لیے آن ہیں۔", @@ -3219,11 +3223,12 @@ "stale": "ابھی اپڈیٹ نہیں ہو سکتا", "expired": "حالت کی میعاد ختم ہو گئی", "signedOut": "ایجنٹس دیکھنے کے لیے سائن ان کریں", - "privacy": "ایجنٹس چھپے ہوئے ہیں", + "privacy": "ایجنٹس دیکھنے کے لیے Kilo کھولیں", "openAgents": "ایجنٹس کھولیں", - "running": "چل رہا ہے", + "running": "کام جاری ہے", "needsInput": "ان پٹ درکار", - "reconnecting": "دوبارہ منسلک ہو رہا ہے", - "channelName": "فعال ایجنٹس" + "idle": "غیر فعال", + "channelName": "فعال ایجنٹس", + "activityKitDisabledBody": "لاک اسکرین پر فعال ایجنٹس دیکھنے کے لیے ترتیبات میں لائیو سرگرمیاں فعال کریں۔" } } diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index d52d59d296..eeefabdbde 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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", - "reconnecting": "Qayta ulanmoqda", - "channelName": "Faol agentlar" + "idle": "Kutmoqda", + "channelName": "Faol agentlar", + "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 1faf330156..615edddbc4 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -160,6 +160,10 @@ }, "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", + "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.", @@ -3219,11 +3223,12 @@ "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 CHẠY", + "running": "Đang xử lý", "needsInput": "cần nhập", - "reconnecting": "Đang kết nối lại", - "channelName": "Tác nhân đang hoạt động" + "idle": "Không hoạt động", + "channelName": "Tác nhân đang hoạt động", + "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 78aefd4b95..b08ea05d33 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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": "ǸJẸ́ ṢÍṢIṢẸ́", + "running": "Nṣiṣẹ́", "needsInput": "nilo igbewọle", - "reconnecting": "Ti n tun sopọ", - "channelName": "Awọn aṣoju to n ṣiṣẹ" + "idle": "Ìsinmi", + "channelName": "Awọn aṣoju to n ṣiṣẹ", + "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 6799831c82..866bfe3c98 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "通知", + "liveActivities": "实时活动", + "liveActivitySubtitle": "在锁定屏幕上显示活跃代理", + "liveUpdates": "实时更新", + "liveUpdateSubtitle": "在通知中显示活跃代理", "push": "推送", "enabled": "通知已启用", "onDescription": "此设备的推送通知已开启。", @@ -3219,11 +3223,12 @@ "stale": "暂时无法更新", "expired": "状态已过期", "signedOut": "请登录以查看代理", - "privacy": "代理已隐藏", + "privacy": "打开 Kilo 以查看代理", "openAgents": "打开代理", - "running": "运行中", + "running": "工作中", "needsInput": "需要输入", - "reconnecting": "正在重新连接", - "channelName": "活动代理" + "idle": "空闲", + "channelName": "活动代理", + "activityKitDisabledBody": "请在“设置”中开启“实时活动”,以在锁定屏幕上查看活动代理。" } } diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index 63a02225dd..3e5d941f70 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -160,6 +160,10 @@ }, "notifications": { "title": "通知", + "liveActivities": "即時動態", + "liveActivitySubtitle": "在鎖定畫面上顯示使用中的代理", + "liveUpdates": "即時更新", + "liveUpdateSubtitle": "在通知中顯示活躍代理", "push": "推播", "enabled": "已啟用通知", "onDescription": "此裝置的推播通知已開啟。", @@ -3219,11 +3223,12 @@ "stale": "目前無法更新", "expired": "狀態已過期", "signedOut": "請登入以查看代理", - "privacy": "代理已隱藏", + "privacy": "開啟 Kilo 以查看代理", "openAgents": "開啟代理", - "running": "執行中", + "running": "處理中", "needsInput": "需要輸入", - "reconnecting": "正在重新連線", - "channelName": "使用中的代理" + "idle": "閒置", + "channelName": "使用中的代理", + "activityKitDisabledBody": "請在「設定」中開啟「即時動態」,以在鎖定畫面上查看使用中的代理。" } } diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 2b3ec16543..896173b34d 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -614,6 +614,10 @@ }, "notifications": { "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.", @@ -3219,11 +3223,12 @@ "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", + "running": "Iyasebenza", "needsInput": "idinga okokufaka", - "reconnecting": "Ixhuma kabusha", - "channelName": "Ama-agent asebenzayo" + "idle": "Banga", + "channelName": "Ama-agent asebenzayo", + "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 1a35676a7d..9681c95e5a 100644 --- a/apps/mobile/src/lib/auth/auth-context.test.tsx +++ b/apps/mobile/src/lib/auth/auth-context.test.tsx @@ -101,6 +101,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), })); // Hoisted so sign-out can assert the queued consent outcome is cleared during @@ -209,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 })); @@ -523,6 +527,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 f8892ece67..81965603f9 100644 --- a/apps/mobile/src/lib/auth/auth-context.tsx +++ b/apps/mobile/src/lib/auth/auth-context.tsx @@ -24,7 +24,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'; @@ -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'; @@ -221,6 +222,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()); setToken(undefined); clearActiveToken(); @@ -385,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 b6e2c291ed..f96267aa88 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-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/auth/logout-cleanup.test.ts b/apps/mobile/src/lib/auth/logout-cleanup.test.ts index 59561939e6..ff8a7ed799 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(); @@ -19,12 +20,22 @@ 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(() => ({ + 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', () => ({ emitNotificationTokenUpdated: vi.fn(), getDevicePushTokenOutcome: vi.fn(), @@ -45,7 +56,16 @@ 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 { + 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'; @@ -87,6 +107,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 () => { @@ -128,6 +149,8 @@ describe('runLogoutCleanup', () => { userId: 'u1', pushToken: 'push-1', needsPushUnregister: true, + needsActivityUnregister: false, + activityTokens: [], failedAt: expect.any(Number), }); }); @@ -182,6 +205,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')); @@ -235,12 +307,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)); @@ -248,3 +334,270 @@ describe('runLogoutCleanup', () => { await expect(readLogoutCleanupTombstone()).resolves.toEqual(expected); }); }); + +describe('unregisterActivityTokensAndTombstone', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetLogoutReconciliationForTests(); + 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('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, + 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.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')); + + 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 52056eaf6a..463d5ee4b9 100644 --- a/apps/mobile/src/lib/auth/logout-cleanup.ts +++ b/apps/mobile/src/lib/auth/logout-cleanup.ts @@ -3,6 +3,8 @@ import * as Sentry from '@sentry/react-native'; 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'; @@ -35,6 +37,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(), }); @@ -66,7 +75,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)); } @@ -79,12 +90,15 @@ 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 { 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 @@ -110,6 +124,12 @@ export async function runLogoutCleanup(): Promise { : Promise.resolve(), ]); + // Unregister activity tokens (Live Activity / push-to-start) before the + // 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]; // A fulfilled unregister for a real token is a definitive outcome; emit @@ -126,11 +146,13 @@ export async function runLogoutCleanup(): Promise { } try { - await (needsPushUnregister + await (needsPushUnregister || needsActivityUnregister ? writeLogoutCleanupTombstone({ userId, pushToken, needsPushUnregister, + needsActivityUnregister, + activityTokens: activityResult.tokens, failedAt: Date.now(), }) : deleteLogoutCleanupTombstone()); @@ -149,3 +171,89 @@ 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 + * the Expo push token (those are logout-only). Never throws by contract. + * + * 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 — + * the same ordering `runLogoutCleanup` relies on. A successful unregister + * 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( + lifetime: 'scope' | 'activity' = 'scope', + activityToken?: Promise +): Promise { + const previous = activityCleanupInFlight; + const cleanup = (async () => { + await Promise.all([previous, runActivityCleanup(previous, lifetime, activityToken)]); + })(); + activityCleanupInFlight = cleanup; + try { + await cleanup; + } finally { + if (activityCleanupInFlight === cleanup) { + activityCleanupInFlight = null; + } + } +} + +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 cleanup writes. + const result = await getGlanceableDelivery().unregisterTokens(lifetime, activityToken); + await previous; + if (result.ok) { + return; + } + // 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 + // account or org switch. + Sentry.captureException(error, { + tags: { + 'error.subsystem': 'auth', + 'error.operation': 'unregister_activity_tokens', + }, + }); + } +} diff --git a/apps/mobile/src/lib/auth/logout-reconciliation.test.ts b/apps/mobile/src/lib/auth/logout-reconciliation.test.ts index 9b0e35890e..4a1d3f489a 100644 --- a/apps/mobile/src/lib/auth/logout-reconciliation.test.ts +++ b/apps/mobile/src/lib/auth/logout-reconciliation.test.ts @@ -3,8 +3,10 @@ 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), isNotFoundTrpcError: (error: unknown) => { if (typeof error !== 'object' || error === null) { return false; @@ -16,6 +18,7 @@ const cleanupMock = vi.hoisted(() => ({ const trpcMock = vi.hoisted(() => ({ unregisterPushToken: { mutate: vi.fn() }, + unregisterActivityToken: { mutate: vi.fn() }, })); const notificationsMock = vi.hoisted(() => ({ @@ -32,6 +35,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'; @@ -44,6 +48,8 @@ function makeTombstone(overrides: Partial = {}): LogoutC userId: 'u1', pushToken: 'push-stored', needsPushUnregister: true, + needsActivityUnregister: false, + activityTokens: [], failedAt: Date.now(), ...overrides, }; @@ -148,6 +154,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 }); @@ -231,4 +306,56 @@ 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('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 () => { + cleanupMock.readLogoutCleanupTombstone.mockResolvedValue( + makeTombstone({ needsActivityUnregister: false, needsPushUnregister: true }) + ); + + 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('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 e1c80647e1..a2e065222c 100644 --- a/apps/mobile/src/lib/auth/logout-reconciliation.ts +++ b/apps/mobile/src/lib/auth/logout-reconciliation.ts @@ -1,10 +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'; /** @@ -62,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 { @@ -83,8 +93,28 @@ export async function awaitLogoutReconciliationSettled(): Promise { } } -async function runReconciliation(userId: string): Promise { - const epoch = currentAuthEpoch(); +/** + * 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. 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(userId: string | null): Promise { + await awaitActivityCleanupSettled(); + const tombstone = await readLogoutCleanupTombstone(); + return ( + tombstone?.needsActivityUnregister === true && + (userId === null || tombstone.userId === null || tombstone.userId === userId) + ); +} + +async function runReconciliation( + userId: string, + epoch: number +): Promise { const tombstone = await readLogoutCleanupTombstone(); if (!tombstone) { return { kind: 'no-tombstone' }; @@ -102,14 +132,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 +234,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/auth/push-registration-reconciliation.test.ts b/apps/mobile/src/lib/auth/push-registration-reconciliation.test.ts index e96eb4f8de..38fe10707f 100644 --- a/apps/mobile/src/lib/auth/push-registration-reconciliation.test.ts +++ b/apps/mobile/src/lib/auth/push-registration-reconciliation.test.ts @@ -50,6 +50,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 86b2f4c958..9a6d7e54f2 100644 --- a/apps/mobile/src/lib/auth/push-registration-reconciliation.ts +++ b/apps/mobile/src/lib/auth/push-registration-reconciliation.ts @@ -17,6 +17,10 @@ import { 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.test.ts b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts new file mode 100644 index 0000000000..e9f57035ae --- /dev/null +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.test.ts @@ -0,0 +1,396 @@ +/* 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, + 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(), + instancesError: null as Error | null, + nativeActivity: null as Partial | null, +})); + +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/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', () => ({ + i18n: { t: (key: string) => key }, +})); + +const NOW = 1_750_000_000_000; + +function eligibleSnapshot(organizationId: string | null = null): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }], + userId: 'u1', + organizationId, + now: NOW, + }); +} + +function emptySnapshot(organizationId: string | null = null): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId, + now: NOW, + }); +} + +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(); + _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.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('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()])( + 'does not start absent or ineligible work: %s', + async snapshot => { + _setLastGlanceableSnapshotForTests(snapshot); + + 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, + idle: 0, + }); + publisher.dispose(); + } + ); + + 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(surface.activity).toEqual(snapshot); + expect(surface.context).toEqual({ userId: 'u1', organizationId }); + }); + + 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(); + expect(getActivityKitDenied()).toBe(true); + }); + + 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(); + expect(getActivityKitDenied()).toBe(true); + }); + + 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).toEqual(latest); + expect(surface.context).toEqual({ userId: 'u1', organizationId }); + } + ); + }); + + it('does not recover on a non-iOS platform', async () => { + mocks.platform.OS = 'android'; + + await recoverGlanceableActivityKit(); + + expect(surface.activity).toBeNull(); + }); +}); + +describe.each([ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY])( + 'ActivityKit recovery while %s is pending', + delayedKey => { + it.each([ + ['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; + + invalidate(); + read.resolve(); + await recovering; + + expect(surface.activity).toBeNull(); + expect(surface.context).toBeNull(); + expect(getActivityKitDenied()).toBe(true); + }); + + it('keeps recovery available for the new scope after a delayed read', async () => { + const read = delayIdentityRead(delayedKey); + const recovering = recoverGlanceableActivityKit(); + await read.started; + + 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])( + '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); + expect(getActivityKitDenied()).toBe(true); + + await recoverGlanceableActivityKit(); + + expect(getActivityKitDenied()).toBe(false); + 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 new file mode 100644 index 0000000000..5ed2fe2ec5 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/activity-kit-prompt.ts @@ -0,0 +1,60 @@ +import * as SecureStore from 'expo-secure-store'; +import { Platform } from 'react-native'; + +import { + buildOpaqueScopeKey, + isEligibleGlanceableWork, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { clearActivityKitDeniedIfAvailable, getActivityKitDenied } from '@/glanceable-ios/ios-sink'; +import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; +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'; + +/** + * 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()) { + return; + } + const authEpoch = currentAuthEpoch(); + const blankEpoch = getTerminalBlankEpoch(); + const scopeKey = getLocalScopeKey(); + const snapshot = getLastGlanceableSnapshot(); + 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; + } + if ( + currentAuthEpoch() !== authEpoch || + getTerminalBlankEpoch() !== blankEpoch || + getLocalScopeKey() !== scopeKey || + getLastGlanceableSnapshot() !== snapshot || + userId === null || + buildOpaqueScopeKey({ userId, organizationId }) !== scopeKey || + !clearActivityKitDeniedIfAvailable() + ) { + return; + } + if (!isEligibleGlanceableWork(snapshot)) { + return; + } + 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..40bc043b2f 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,8 +173,8 @@ describe('cleanup', () => { status: 'happy', running: 2, needsInput: 1, - reconnecting: 1, - eligibleStartedAt: '2026-08-26T23:00:00.000Z', + idle: 1, + needsInputSince: '2026-08-26T23:00:00.000Z', }; _setLastGlanceableSnapshotForTests(seeded); @@ -171,8 +201,8 @@ describe('cleanup', () => { status: 'expired', running: 0, needsInput: 0, - reconnecting: 0, - eligibleStartedAt: null, + idle: 0, + needsInputSince: null, }); } } finally { @@ -194,8 +224,8 @@ describe('cleanup', () => { status, running: 0, needsInput: 0, - reconnecting: 0, - eligibleStartedAt: null, + idle: 0, + needsInputSince: null, }; _setLastGlanceableSnapshotForTests(terminal); @@ -212,8 +242,8 @@ describe('cleanup', () => { expiresAt: terminal.expiresAt, running: 0, needsInput: 0, - reconnecting: 0, - eligibleStartedAt: null, + idle: 0, + needsInputSince: null, }); } } finally { diff --git a/apps/mobile/src/lib/glanceable/cleanup.ts b/apps/mobile/src/lib/glanceable/cleanup.ts index ded58ef91b..18345ea1b7 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 { 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,8 +68,8 @@ function buildTerminalSnapshot(status: 'signed_out' | 'privacy'): GlanceableAgen status, running: 0, needsInput: 0, - reconnecting: 0, - eligibleStartedAt: null, + idle: 0, + needsInputSince: null, }; } @@ -77,16 +77,16 @@ 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 // 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. */ @@ -114,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/delivery-registration.test.ts b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts new file mode 100644 index 0000000000..0b44de2d0e --- /dev/null +++ b/apps/mobile/src/lib/glanceable/delivery-registration.test.ts @@ -0,0 +1,873 @@ +/* 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'; + +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, + listeners: new Set<(event: { activityPushToStartToken: string }) => void>(), +})); + +const trpcMock = vi.hoisted(() => ({ + registerActivityToken: { mutate: vi.fn() }, + unregisterActivityToken: { mutate: vi.fn() }, +})); + +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: { + registerActivityToken: trpcMock.registerActivityToken, + unregisterActivityToken: trpcMock.unregisterActivityToken, + }, + }, +})); +vi.mock('expo-widgets', () => ({ + addPushToStartTokenListener: ( + 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', () => ({ + ActiveAgentsLiveActivity: { + getInstances: () => [activityMock], + }, +})); +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 { + _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' }], + userId: 'u1', + organizationId: null, + now: NOW, + previousRevision: 0, + }); +} + +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(); + platformMock.OS = 'ios'; + _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' }); + logoutMock.awaitLogoutReconciliationSettled.mockResolvedValue(undefined); + logoutMock.hasPendingActivityUnregister.mockResolvedValue(false); + }); + + 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, + }); + }); + }); + + 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'] }); + }); + + 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('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.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'); + + // 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 new file mode 100644 index 0000000000..0cf652f612 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/delivery-registration.ts @@ -0,0 +1,342 @@ +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, + hasPendingActivityUnregister, +} from '@/lib/auth/logout-reconciliation'; +import { trpcClient } from '@/lib/trpc'; + +import { + type GlanceableActivity, + type GlanceableDelivery, + type GlanceableSinkContext, + setGlanceableDelivery, +} from './sink-registry'; + +/** 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; +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: an upsert and delete of a stable token must never race. */ +let mutationTail: Promise | null = null; +const NOOP = (): void => undefined; + +async function enqueueTokenMutation(op: () => Promise): Promise { + const previous = mutationTail; + let release: () => void = NOOP; + const gate = new Promise(resolve => { + release = resolve; + }); + mutationTail = gate; + // The tail is a release gate, not the mutation promise; it always resolves. + await previous; + try { + return await op(); + } finally { + release(); + } +} + +// Pure suites must not load the native notification graph. +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; +} + +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 registerToken(target: Registration, token: string): Promise { + if (!isCurrent(target) || !token) { + return; + } + target.token = token; + if (target.registeredToken === token) { + return; + } + try { + 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 { + // Retry on the next token event or scope refresh; an uncertain upsert still needs cleanup. + } +} + +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); + } + }); +} + +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 { + if (target.registeredToken !== null || !(await canRegister(target))) { + return; + } + const token = await getDevicePushTokenLazy()(); + if (token !== null) { + await registerToken(target, token); + } + } 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; + } + 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 { + // Unsupported or transient native reads must not discard the scope subscription. + } +} + +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 = { + registerScopeTokens(organizationId, userId) { + if (Platform.OS !== 'ios' && Platform.OS !== 'android') { + return; + } + 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; + } + 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 { + instance = ActiveAgentsLiveActivity.getInstances().at(-1) ?? null; + } catch { + // Recorded tokens remain available when native discovery fails. + } + } + if (includeScope) { + registerEpoch += 1; + scopeRegistration = null; + if (Platform.OS === 'ios') { + if (pushToStartToken !== null) { + scopeTokens.add(pushToStartToken); + } + observePushToStart(null); + } + } + 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; + }, +}; + +if (Platform.OS === 'ios') { + // Cache early tokens without registering an unauthenticated scope. + observePushToStart(null); +} +setGlanceableDelivery(delivery); + +export function _setGetDevicePushTokenForTests(fn: (() => Promise) | null): void { + getDevicePushTokenForTests = fn; +} + +export function _resetDeliveryRegistrationForTests(): void { + 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/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/glanceable/presentation.test.ts b/apps/mobile/src/lib/glanceable/presentation.test.ts index c3112cab74..57ae4a8be8 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(); }); @@ -107,37 +127,69 @@ 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'); + }); +}); 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', '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; 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' ); }); @@ -152,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); }); @@ -163,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/glanceable/presentation.ts b/apps/mobile/src/lib/glanceable/presentation.ts index 4d533ad2b6..ad8b60003d 100644 --- a/apps/mobile/src/lib/glanceable/presentation.ts +++ b/apps/mobile/src/lib/glanceable/presentation.ts @@ -18,40 +18,49 @@ 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). */ +/** + * 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, field } of COUNT_ORDER) { - const count = snapshot[field]; - if (count > 0) { - lines.push({ key, 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 = { @@ -96,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 { @@ -118,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 6615ea8479..0845573591 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,10 +173,10 @@ describe('GlanceablePublisher', () => { status, running: expectedCount, needsInput: expectedCount, - reconnecting: expectedCount, + 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 e74f241e72..13709d2d8d 100644 --- a/apps/mobile/src/lib/glanceable/publisher.ts +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -5,11 +5,17 @@ import { GLANCEABLE_TERMINAL_MS, type GlanceableAgentsSnapshot, type GlanceableAgentsSnapshotStatus, + type GlanceableSessionRow, isEligibleGlanceableWork, shouldDiscardGlanceableRevision, } from '@kilocode/app-shared/glanceable-agents-snapshot'; -import { type GlanceableSink, type GlanceableSinkContext } from './sink-registry'; +import { + getGlanceableDelivery, + type GlanceableSink, + type GlanceableSinkContext, + guardSink, +} from './sink-registry'; /** * Framework-agnostic publisher state machine. Derives one versioned snapshot @@ -55,7 +61,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, needsInputSince: null } : {}), }; } const updatedAt = new Date(now).toISOString(); @@ -96,10 +102,11 @@ 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; } + getGlanceableDelivery().registerScopeTokens(ctx.organizationId, ctx.userId); const now = this.now(); this.applyExpiry(now, ctx); @@ -109,7 +116,6 @@ export class GlanceablePublisher { organizationId: ctx.organizationId, now, previousRevision: this.current?.revision ?? 0, - previousEligibleStartedAt: this.current?.eligibleStartedAt ?? null, }); if (isEligibleGlanceableWork(snapshot)) { @@ -138,6 +144,7 @@ export class GlanceablePublisher { if (this.isGated()) { return; } + getGlanceableDelivery().registerScopeTokens(ctx.organizationId, ctx.userId); if (this.current !== null) { return; } @@ -183,6 +190,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(); @@ -208,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); + }); } } @@ -242,7 +260,11 @@ export class GlanceablePublisher { this.terminalTimer = null; this.activityStarted = false; for (const sink of this.sinks) { - 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 30228c3dbe..5b9c15dda6 100644 --- a/apps/mobile/src/lib/glanceable/sink-registry.ts +++ b/apps/mobile/src/lib/glanceable/sink-registry.ts @@ -1,4 +1,6 @@ +import type * as SentryReactNative from '@sentry/react-native'; 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, @@ -9,10 +11,15 @@ 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 = { 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; }; @@ -31,18 +38,83 @@ export function getGlanceableSinks(): readonly GlanceableSink[] { return [...sinks]; } -/** Activity-token registrar, set by a later token slice. No-op by default. */ +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 + * logout can tombstone the failed tokens and retry them later. + */ +export type GlanceableActivity = Pick; + export type GlanceableDelivery = { - registerTokens(snapshot: GlanceableAgentsSnapshot, organizationId: string | null): void; - unregisterTokens(): void; + registerScopeTokens(organizationId: string | null, userId: string | null): void; + registerTokens( + snapshot: GlanceableAgentsSnapshot, + organizationId: string | null, + userId: string | null, + activity?: GlanceableActivity + ): void; + /** 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. }, - unregisterTokens() { + cleanupTokens() { + // No-op until a token slice registers a delivery. + }, + 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/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); 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/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; diff --git a/apps/mobile/src/lib/notification-path.test.ts b/apps/mobile/src/lib/notification-path.test.ts index bb7a9376f2..20cc0700c2 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, + idle: 0, + updatedAt: '2026-01-01T00:00:00.000Z', + expiresAt: '2026-01-01T08:00:00.000Z', + needsInputSince: '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/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.test.ts b/apps/mobile/src/lib/notifications.test.ts index d8990ee5d8..ed2c83d4ba 100644 --- a/apps/mobile/src/lib/notifications.test.ts +++ b/apps/mobile/src/lib/notifications.test.ts @@ -1,4 +1,31 @@ -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 { + 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 { + type GlanceableSinkContext, + registerGlanceableSink, + unregisterGlanceableSink, +} from '@/lib/glanceable/sink-registry'; +import { ACTIVE_USER_ID_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; +import { + _setGlanceableSinksLoaderForTests, + applyGlanceablePushData, + setupNotificationBackgroundHandler, +} from './notifications'; import type * as Notifications from '@kilocode/notifications'; @@ -15,6 +42,15 @@ const mocks = vi.hoisted(() => ({ lastResponse: null as Response | null, listeners: new Set(), clearLastNotificationResponse: 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(), })); @@ -34,10 +70,16 @@ vi.mock('expo-notifications', () => ({ }, getLastNotificationResponse: () => mocks.lastResponse, 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, })); @@ -46,12 +88,68 @@ vi.mock('expo-constants', () => ({ default: { expoConfig: { extra: { eas: { projectId: 'proj-1' } } } }, })); +vi.mock('expo-secure-store', () => ({ + getItemAsync: mocks.getItemAsync, + setItemAsync: mocks.setItemAsync, + deleteItemAsync: mocks.deleteItemAsync, +})); + +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: { + 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', () => ({ + banner: 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', async importOriginal => ({ ...(await importOriginal()), 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' }, ], })); @@ -115,24 +213,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 () => { @@ -143,7 +257,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 () => { @@ -152,7 +266,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', @@ -298,6 +412,1041 @@ it.each([null, '/(app)/(tabs)/(3_profile)'])( } ); +const SCOPE_KEY = buildOpaqueScopeKey({ userId: 'u1', organizationId: 'org-9' }); + +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_KEY, + organizationBound: true, + status: 'happy', + running: 1, + needsInput: 0, + idle: 0, + needsInputSince: '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() { + const surface: { + widget: GlanceableAgentsSnapshot | null; + activity: GlanceableAgentsSnapshot | null; + context: GlanceableSinkContext | null; + } = { widget: null, activity: null, context: null }; + return { + 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; + }), + }; +} + +// 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((key: string) => { + if (key === ACTIVE_USER_ID_KEY) { + return 'u1'; + } + if (key === ORGANIZATION_STORAGE_KEY) { + return 'org-9'; + } + return null; + }); +} + +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 +// 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(); + mockSecureStoreKeys(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('discards a remote snapshot that is not newer than the last applied snapshot', async () => { + _setLastGlanceableSnapshotForTests( + glanceableSnapshot({ + scopeKey: SCOPE_KEY, + revision: 3, + updatedAt: '2026-01-02T00:00:00.000Z', + }) + ); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + const result = await applyGlanceablePushData( + activeGlanceablePush({ scopeKey: SCOPE_KEY, 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_KEY, + revision: 3, + updatedAt: '2026-01-01T00:00:00.000Z', + }) + ); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + const result = await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey: SCOPE_KEY, + 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 }), { + userId: 'u1', + organizationId: 'org-9', + }); + + unregisterGlanceableSink(sink); + }); + + it('ends the sinks after the terminal window for a non-eligible remote snapshot', async () => { + vi.useFakeTimers(); + _setLastGlanceableSnapshotForTests( + glanceableSnapshot({ + scopeKey: SCOPE_KEY, + revision: 3, + updatedAt: '2026-01-01T00:00:00.000Z', + }) + ); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + const result = await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey: SCOPE_KEY, + updatedAt: '2026-01-02T00:00:00.000Z', + status: 'empty', + running: 0, + needsInput: 0, + idle: 0, + needsInputSince: 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_KEY, + revision: 4, + updatedAt: '2026-01-02T00:00:00.000Z', + status: 'empty', + running: 0, + needsInput: 0, + idle: 0, + needsInputSince: 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_KEY, + revision: 3, + updatedAt: '2026-01-01T00:00:00.000Z', + }) + ); + const sink = makeFakeSink(); + registerGlanceableSink(sink); + + await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey: SCOPE_KEY, + updatedAt: '2026-01-02T00:00:00.000Z', + status: 'empty', + running: 0, + needsInput: 0, + idle: 0, + needsInputSince: null, + }) + ); + await applyGlanceablePushData( + activeGlanceablePush({ + scopeKey: SCOPE_KEY, + 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('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', + needsInputSince: 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; + 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(); + mockSecureStoreKeys(); + 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_KEY, + revision: 1, + updatedAt: '2026-01-01T00:00:00.000Z', + }); + secureStore.set('glanceable-snapshot', JSON.stringify(persisted)); + secureStore.set('glanceable-scope-key', SCOPE_KEY); + _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_KEY, + 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 }), { + userId: 'u1', + organizationId: 'org-9', + }); + + unregisterGlanceableSink(sink); + }); + + it('ignores a headless payload that is not a glanceable push', async () => { + _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); + }); +}); + +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>(), + }; + + 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, 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) { + 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, + blank, + sink, + 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); + 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.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((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 native adoption and retained end handles; the JS adapter remains real. + native.observers.add(token => { + for (const listener of listeners) { + 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 || id !== native.id) { + 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 (props: string) => { + await native.updateRead; + if (!isDismissed()) { + native.props = props; + } + }, + // 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(); + }, + }, + ]; + }); + }); + + afterEach(() => { + native.observers.clear(); + vi.clearAllTimers(); + 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, + needsInputSince: 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, + needsInputSince: 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, + needsInputSince: 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, + needsInputSince: 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); + 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(); + const applying = background.deliver({ status: 'empty', running: 0, needsInputSince: null }); + await vi.advanceTimersByTimeAsync(0); + expect(native.exists).toBe(true); + read.resolve(); + 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(); + 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('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, + needsInputSince: 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, + idle: 0, + 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, needsInputSince: 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, needsInputSince: 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' }); + mocks.unregisterActivityToken.mockRejectedValueOnce(new Error('network unavailable')); + const background = await loadColdBackground(); + expect(await background.deliver({ status: 'empty', running: 0, needsInputSince: 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/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index f79046ed75..8abc51d8b6 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -1,5 +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'; @@ -14,8 +17,24 @@ import { NOTIFICATION_PERMISSION_RESPONDED_EVENT, NOTIFICATION_TOKEN_UPDATED_EVENT, } from '@kilocode/app-shared/analytics'; +import { + buildOpaqueScopeKey, + GLANCEABLE_TERMINAL_MS, + type GlanceableAgentsSnapshot, + isEligibleGlanceableWork, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; import { captureEvent } from '@/lib/analytics/posthog'; +import { currentAuthEpoch } from '@/lib/auth/auth-epoch'; +import { getTerminalBlankEpoch } from '@/lib/glanceable/cleanup'; +import { + getLastGlanceableSnapshot, + getLocalScopeKey, + persistGlanceableSink, + restorePersistedGlanceable, +} from '@/lib/glanceable/persist'; +import { getGlanceableSinks, registerGlanceableSink } from '@/lib/glanceable/sink-registry'; +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'; @@ -51,6 +70,150 @@ export function parseNotificationData(data: unknown): PushData | null { return parsed.success ? parsed.data : null; } +// 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 { + 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()) { + if (!sink.waitForNativeTerminal) { + 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 + * must be dropped: its opaque scope key does not match the persisted local + * 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 async function applyGlanceablePushData( + data: Extract +): Promise { + 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 || + (current !== capturedSnapshot && fields.updatedAt === current.updatedAt)) + ) { + return false; + } + + const snapshot: GlanceableAgentsSnapshot = { + ...fields, + revision: current === null ? fields.revision : current.revision + 1, + accountEpoch: authEpoch, + }; + + const ctx = { userId, organizationId }; + const eligible = isEligibleGlanceableWork(snapshot); + if (eligible) { + cancelGlanceableTerminalEnd(); + for (const sink of getGlanceableSinks()) { + sink.publish(snapshot); + sink.startOrUpdate(snapshot, ctx); + } + } else { + for (const sink of getGlanceableSinks()) { + sink.publish(snapshot); + } + // 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; +} + +/** + * 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 { + return await SecureStore.getItemAsync(ORGANIZATION_STORAGE_KEY); + } catch { + return null; + } +} + +/** + * 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 { + return await SecureStore.getItemAsync(ACTIVE_USER_ID_KEY); + } catch { + return null; + } +} + const shown = { shouldPlaySound: true, shouldSetBadge: true, @@ -67,10 +230,17 @@ 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); + 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. + await applyGlanceablePushData(data); + return suppressed; + } + if ( data?.type === 'chat.message' && activeChatLocation?.sandboxId === data.sandboxId && @@ -83,6 +253,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); @@ -130,6 +413,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, { @@ -163,6 +447,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; /** @@ -184,6 +469,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, { diff --git a/apps/mobile/src/lib/organization-context.mounted.test.tsx b/apps/mobile/src/lib/organization-context.mounted.test.tsx index 5d1ca50d9a..f82582691a 100644 --- a/apps/mobile/src/lib/organization-context.mounted.test.tsx +++ b/apps/mobile/src/lib/organization-context.mounted.test.tsx @@ -10,6 +10,7 @@ import { waitFor } from '@/test/render-with-providers'; const auth = vi.hoisted(() => ({ token: 'token-a' as string | undefined })); const storage = vi.hoisted(() => ({ read: vi.fn(), write: vi.fn(), remove: vi.fn() })); vi.mock('@/lib/auth/auth-context', () => ({ useAuth: () => auth })); +vi.mock('@/lib/auth/logout-cleanup', () => ({ unregisterActivityTokensAndTombstone: vi.fn() })); vi.mock('expo-secure-store', () => ({ getItemAsync: storage.read, setItemAsync: storage.write, 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 2dfdcc2884..65273514d6 100644 --- a/apps/mobile/src/lib/organization-context.tsx +++ b/apps/mobile/src/lib/organization-context.tsx @@ -13,6 +13,7 @@ import { import { useAuth } from '@/lib/auth/auth-context'; import { deleteAccountMetadata, setAccountMetadata } from '@/lib/auth/account-metadata-write'; import { currentAuthEpoch, isCurrentAuthEpoch } from '@/lib/auth/auth-epoch'; +import { unregisterActivityTokensAndTombstone } from '@/lib/auth/logout-cleanup'; import { writePrivacySnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; @@ -117,6 +118,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(); activeId.current = id; setState(current => ({ token, diff --git a/apps/mobile/src/lib/storage-keys.ts b/apps/mobile/src/lib/storage-keys.ts index ddba8089a1..29d79ca960 100644 --- a/apps/mobile/src/lib/storage-keys.ts +++ b/apps/mobile/src/lib/storage-keys.ts @@ -31,11 +31,14 @@ 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. */ 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. */ /** SQLCipher database key for the encrypted persistence store (DEC-01). */ export const PERSIST_DB_KEY = 'persist-db-key'; /** 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 = []; 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..5e40080833 --- /dev/null +++ b/apps/web/src/lib/active-sessions-list.ts @@ -0,0 +1,495 @@ +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(), + /** + * 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 + * 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; + 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. + 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; +} + +/** + * 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 +): 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; + } + 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; + } + 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; + } + 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; + } + 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, + 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, + 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, + 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, + 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.test.ts b/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts new file mode 100644 index 0000000000..b5b986c1a6 --- /dev/null +++ b/apps/web/src/lib/glanceable-agents-snapshot-server.test.ts @@ -0,0 +1,83 @@ +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', + statusUpdatedAt: '2026-08-27T10:00:00.000Z', + }, + ]; + 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); + // 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/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 035171c8e4..29e7450930 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, @@ -1618,6 +1619,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 763039fef5..b0499651cd 100644 --- a/apps/web/src/lib/user/index.ts +++ b/apps/web/src/lib/user/index.ts @@ -83,6 +83,7 @@ import { kiloclaw_admin_audit_logs, kiloclaw_cli_runs, user_push_tokens, + user_activity_tokens, user_notification_preferences, contributor_champion_events, contributor_champion_memberships, @@ -1470,6 +1471,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 3ec2c0ad54..3a91720510 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(), @@ -92,6 +50,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. @@ -101,38 +61,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({ /** @@ -151,158 +79,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 @@ -377,208 +153,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.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; 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/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", diff --git a/packages/app-shared/src/glanceable-agents-snapshot.test.ts b/packages/app-shared/src/glanceable-agents-snapshot.test.ts index c59de79ca7..413bcddf13 100644 --- a/packages/app-shared/src/glanceable-agents-snapshot.test.ts +++ b/packages/app-shared/src/glanceable-agents-snapshot.test.ts @@ -6,13 +6,14 @@ import { countGlanceableSessions, GLANCEABLE_SNAPSHOT_EXPIRY_MS, isEligibleGlanceableWork, + oldestNeedsInputSince, shouldDiscardGlanceableRevision, } from './glanceable-agents-snapshot'; 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 +27,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 +35,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', () => { @@ -82,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 eligible work remains', () => { + it('clears needsInputSince when no session is connected', () => { const snapshot = buildGlanceableSnapshot({ - sessions: [{ status: 'idle' }], + 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: [], @@ -218,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 e7498e80f1..f115bf9463 100644 --- a/packages/app-shared/src/glanceable-agents-snapshot.ts +++ b/packages/app-shared/src/glanceable-agents-snapshot.ts @@ -46,11 +46,19 @@ 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), - /** ISO 8601 timestamp or null; binds the elapsed-time display. */ - eligibleStartedAt: z.string().nullable(), + /** Sessions connected but doing nothing. */ + idle: z.number().int().min(0), + /** + * 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; @@ -58,18 +66,35 @@ export type GlanceableAgentsSnapshot = z.infer= oldest)) { + continue; + } + oldest = at; + oldestIso = session.statusUpdatedAt; + } + return oldestIso; } // FNV-1a 32-bit over UTF-16 code units (two bytes each). Deterministic across @@ -120,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; @@ -134,18 +185,18 @@ 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 ): GlanceableAgentsSnapshot { const counts = countGlanceableSessions(input.sessions); - const eligible = counts.running + counts.needsInput + counts.reconnecting > 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; return { schemaVersion: GLANCEABLE_SNAPSHOT_SCHEMA_VERSION, @@ -158,14 +209,14 @@ export function buildGlanceableSnapshot( status: input.status ?? (eligible ? 'happy' : 'empty'), running: counts.running, needsInput: counts.needsInput, - reconnecting: counts.reconnecting, - eligibleStartedAt, + idle: counts.idle, + needsInputSince: oldestNeedsInputSince(input.sessions), }; } -/** 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/db/src/migrations/0235_bent_mercury.sql b/packages/db/src/migrations/0235_bent_mercury.sql new file mode 100644 index 0000000000..de6246d0e5 --- /dev/null +++ b/packages/db/src/migrations/0235_bent_mercury.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/0235_snapshot.json b/packages/db/src/migrations/meta/0235_snapshot.json new file mode 100644 index 0000000000..45f63c227e --- /dev/null +++ b/packages/db/src/migrations/meta/0235_snapshot.json @@ -0,0 +1,40060 @@ +{ + "id": "80422fd8-514b-4e81-ba33-ec66f1a7cdfe", + "prevId": "4ce43cc2-f500-4a55-97f1-2810d5c177f5", + "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 + }, + "cloud_agent_worktree_id": { + "name": "cloud_agent_worktree_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": {} + }, + "IDX_cli_sessions_v2_user_worktree_updated": { + "name": "IDX_cli_sessions_v2_user_worktree_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cloud_agent_worktree_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cli_sessions_v2\".\"cloud_agent_worktree_id\" is not null", + "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_agent_worktrees": { + "name": "cloud_agent_worktrees", + "schema": "", + "columns": { + "worktree_id": { + "name": "worktree_id", + "type": "text", + "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 + }, + "name": { + "name": "name", + "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()" + }, + "deletion_started_at": { + "name": "deletion_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletion_completed_at": { + "name": "deletion_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "runtime_locations": { + "name": "runtime_locations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "deletion_manifest": { + "name": "deletion_manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted_session_ids": { + "name": "deleted_session_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + } + }, + "indexes": { + "IDX_cloud_agent_worktrees_owner_scope": { + "name": "IDX_cloud_agent_worktrees_owner_scope", + "columns": [ + { + "expression": "kilo_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": { + "cloud_agent_worktrees_kilo_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_worktrees_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_worktrees", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cloud_agent_worktrees_organization_id_organizations_id_fk": { + "name": "cloud_agent_worktrees_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_worktrees", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_worktrees_deletion_check": { + "name": "cloud_agent_worktrees_deletion_check", + "value": "\"cloud_agent_worktrees\".\"deletion_completed_at\" IS NULL OR (\"cloud_agent_worktrees\".\"deletion_started_at\" IS NOT NULL AND \"cloud_agent_worktrees\".\"name\" IS NULL AND \"cloud_agent_worktrees\".\"deletion_manifest\" IS NULL AND \"cloud_agent_worktrees\".\"runtime_locations\" = '[]'::jsonb)" + } + }, + "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 df10bb33f3..fe7020e16c 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1646,6 +1646,13 @@ "when": 1788031699722, "tag": "0234_amused_sasquatch", "breakpoints": true + }, + { + "idx": 235, + "version": "7", + "when": 1788354565441, + "tag": "0235_bent_mercury", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 88461290d9..c9723c56db 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -9458,6 +9458,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/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/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/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": { diff --git a/packages/notifications/src/push-data.ts b/packages/notifications/src/push-data.ts index 29e0531f29..fe3200c18d 100644 --- a/packages/notifications/src/push-data.ts +++ b/packages/notifications/src/push-data.ts @@ -76,6 +76,38 @@ 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), + idle: z.number().int().min(0), + updatedAt: z.string(), + expiresAt: z.string(), + needsInputSince: z.string().nullable(), + }), ]); 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 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' | 'needsInputSince' +>; diff --git a/packages/notifications/src/push-presentation.test.ts b/packages/notifications/src/push-presentation.test.ts index b2f2dc17c4..0e16302ef0 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, + idle: 0, + updatedAt: '2026-01-01T00:00:00.000Z', + expiresAt: '2026-01-01T08:00:00.000Z', + needsInputSince: '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/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/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/patches/expo-widgets@57.0.11.patch b/patches/expo-widgets@57.0.11.patch new file mode 100644 index 0000000000..0e026ca8d8 --- /dev/null +++ b/patches/expo-widgets@57.0.11.patch @@ -0,0 +1,333 @@ +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..bf9d21d 100644 +--- a/build/Widgets.d.ts ++++ b/build/Widgets.d.ts +@@ -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. + * @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. + */ +- getInstances(): LiveActivity[]; ++ 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..02a6e56 100644 +--- a/build/Widgets.js ++++ b/build/Widgets.js +@@ -47,6 +47,10 @@ export class LiveActivity { + constructor(nativeLiveActivity) { + 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. + * @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. + */ +- getInstances() { ++ getInstances(includeEnded = false) { + return this.nativeLiveActivityFactory +- .getInstances() ++ .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 +@@ -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[]; ++ 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; + getPushToken(): Promise; +diff --git a/ios/LiveActivity.swift b/ios/LiveActivity.swift +index c4b5bcc..b444df0 100644 +--- a/ios/LiveActivity.swift ++++ b/ios/LiveActivity.swift +@@ -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. ++ 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) { + 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) + } + +@@ -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) + } + +@@ -58,6 +89,9 @@ 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 +@@ -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 { ++ $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..bc1aae1 100644 +--- a/ios/WidgetsModule.swift ++++ b/ios/WidgetsModule.swift +@@ -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() ++ } ++ + 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..a23a95a 100644 +--- a/src/Widgets.ts ++++ b/src/Widgets.ts +@@ -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(); ++ } ++ + /** + * Updates the Live Activity's content. The UI reflects the new properties immediately. + * @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. + */ +- getInstances() { ++ getInstances(includeEnded = false) { + return this.nativeLiveActivityFactory +- .getInstances() ++ .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 +@@ -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 d8937e11db..9501e80b4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,7 @@ packageExtensionsChecksum: sha256-1pgKZxx87NNMe1poF5N5u5kZB/qlEyILBQPxofM1shE= patchedDependencies: expo-router@57.0.15: 616f8a79932a3ebc7d31343b53b3fc425a14bfb89bb86cdafa614cefa4e603ed expo-server-sdk: 7850520582b5b394397b35d1ea195192fe78589d8a6a748fe15177b818c4ed0b + expo-widgets@57.0.11: 4d6fc2097496a5a9f303a95f326ce929fd9ef7893948b647fcd914e9ba2a120c react-native-appsflyer@6.18.0: 82df99378c830e774b0f01796d8be595da114d1d13393d85ddd47d565c5c2aab importers: @@ -545,6 +546,9 @@ importers: expo-store-review: specifier: ~57.0.2 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-task-manager: + specifier: 57.0.12 + version: 57.0.12(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-tracking-transparency: specifier: ~57.0.1 version: 57.0.1(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)) @@ -553,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(@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) @@ -630,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 @@ -12898,6 +12905,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: @@ -18086,6 +18099,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'} @@ -28198,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(@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 @@ -30513,6 +30529,12 @@ snapshots: sf-symbols-typescript: 2.2.0 optional: true + expo-task-manager@57.0.12(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)): + dependencies: + 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) + unimodules-app-loader: 57.0.1 + expo-tracking-transparency@57.0.1(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)): dependencies: 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) @@ -30527,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(@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) @@ -30541,7 +30563,7 @@ snapshots: - react-dom - react-native-worklets - 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.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) @@ -37477,6 +37499,8 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unimodules-app-loader@57.0.1: {} + unimport@6.3.0(oxc-parser@0.143.0)(rolldown@1.0.3): dependencies: acorn: 8.16.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 683e285462..c30d699a26 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -160,6 +160,7 @@ packageExtensions: patchedDependencies: expo-router@57.0.15: patches/expo-router@57.0.15.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/*' 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..4e66ef3f54 --- /dev/null +++ b/services/cloud-agent-next/src/telemetry/report-consumer.glanceable.test.ts @@ -0,0 +1,471 @@ +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, + }); + 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, + idle: 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, + needsInput: status === 'retry' ? 1 : 0, + }, + { status: 'empty', running: 0, needsInput: 0, idle: 0, needsInputSince: null }, + ]); + } + ); + + it('reports the wait only once nonterminal retry work needs input', 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(); + // 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, needsInputSince: null }, + { running: 0, needsInput: 1, needsInputSince: 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([]); + }); + + // An expired anchor is final, so its report is acknowledged. A missing anchor + // is retried: the session row can still arrive. + it.each([ + ['expired', 'ack'], + ['missing_parent', 'retry'], + ] as const)('does not publish an unapplied %s report', async (outcome, expected) => { + 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(expected); + 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) + // 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/cloud-agent-next/src/telemetry/report-consumer.ts b/services/cloud-agent-next/src/telemetry/report-consumer.ts index 64844067f2..060b5703e0 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'; @@ -53,7 +55,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); @@ -66,6 +69,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('Retrying Cloud Agent run report without a session anchor', { cloudAgentSessionId: parsed.data.session.cloudAgentSessionId, 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/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 37e90cccad..e30d8f838f 100644 --- a/services/notifications/src/index.ts +++ b/services/notifications/src/index.ts @@ -18,6 +18,8 @@ import { badgeBucketForConversation, internalDispatchRequestSchema, markBadgeReadInputSchema, + refreshGlanceableSessionsInputSchema, + type RefreshGlanceableSessionsParams, type ClearBadgeBucketForUserInput, type ClearBadgeBucketForUserOutput, type DispatchPushInput, @@ -331,6 +333,43 @@ export class NotificationsService extends WorkerEntrypoint { return dispatchCloudAgentSessionPush(params, this.cloudAgentSessionPushDeps()); } + /** 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); + } + + // 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), + }); + } + } + } + /** * Agent-callable push for the `notify_user` tool. Resolves the session * service-side, fails closed on a preference read failure, dispatches with 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..a7e0000524 --- /dev/null +++ b/services/notifications/src/lib/apns-live-activity.test.ts @@ -0,0 +1,327 @@ +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; + '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}', + }); + }); +}); + +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(); + 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('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('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 + .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..e0179dc505 --- /dev/null +++ b/services/notifications/src/lib/apns-live-activity.ts @@ -0,0 +1,184 @@ +/** + * 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. + */ + +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' | '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-----'; +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 { + 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; + 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: { + 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, + // 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, + ...(params.event === 'end' ? { 'dismissal-date': params.dismissalDateSeconds } : {}), + }, + }), + }; +} + +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; + /** Snapshot ordering time, independent of the provider token's signing time. */ + 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) { + 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 }) => { + if (params.isCurrent && !(await params.isCurrent())) return false; + if (event === 'end' && params.beforeEnd && !(await params.beforeEnd(token))) return false; + const request = buildLiveActivityApnsRequest({ + token, + ...(event === 'end' + ? { event, dismissalDateSeconds: Math.floor(Date.now() / 1000) + TERMINAL_SECONDS } + : { event }), + contentState: params.contentState, + credentials: params.credentials, + authorizationJwt, + timestampSeconds: params.timestampSeconds ?? params.nowSeconds, + }); + 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; + }) + ); + + 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..71ab042c03 --- /dev/null +++ b/services/notifications/src/lib/glanceable-delivery-deps.ts @@ -0,0 +1,199 @@ +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)); + const iosTargets = new Map< + string, + Pick + >(); + + 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, + id: user_activity_tokens.id, + updated_at: user_activity_tokens.updated_at, + }) + .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']) + ) + ); + 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, + beforeEnd, + onEndRejected + ) => { + 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, + 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', { + 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 new file mode 100644 index 0000000000..9aadc76701 --- /dev/null +++ b/services/notifications/src/lib/glanceable-delivery.test.ts @@ -0,0 +1,1809 @@ +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, + deliverGlanceableSnapshot, + toGlanceableContentState, + type ActiveAgentsGlanceable, + type GlanceableApnsContentState, + 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, + idle: 0, + updatedAt: '2026-08-27T10:00:00.000Z', + expiresAt: '2026-08-27T18:00:00.000Z', + needsInputSince: '2026-08-27T09:00:00.000Z', +}; + +function fakeDeps(overrides: Partial = {}): { + deps: GlanceableDeliveryDeps; + calls: { iosSends: unknown[][]; expoSends: ExpoPushMessage[][] }; +} { + const calls = { iosSends: [] as unknown[][], expoSends: [] as ExpoPushMessage[][] }; + + const deps: GlanceableDeliveryDeps = { + buildSnapshot: vi.fn(async () => snapshot), + listIosActivityTokens: vi.fn(async () => []), + sendIosLiveActivity: vi.fn(async (_tokens, _contentState) => { + calls.iosSends.push([_tokens, _contentState]); + }), + listIosExpoTokens: vi.fn(async () => []), + listAndroidExpoTokens: vi.fn(async () => []), + hasAndroidOngoingToken: vi.fn(async () => false), + sendExpoPush: vi.fn(async messages => { + calls.expoSends.push(messages); + }), + ...overrides, + }; + + 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 = { + token: string; + aps: { + event: string; + timestamp: number; + 'dismissal-date'?: number; + 'content-state': GlanceableApnsContentState; + }; + }; + + function setupService( + options: { + deniedOrganizationId?: string; + failedOrganizationId?: string; + response?: (scope: Scope) => Response | Promise; + beforeIosTokens?: () => Promise; + iosTokenKind?: IosActivityToken['kind']; + iosTokens?: Array>; + privateKey?: () => Promise; + beforeApnsDelivery?: (token: string) => Promise; + beforeApnsResponse?: (token: string) => Promise; + apnsStatus?: (token: string) => number; + expoAccessToken?: () => Promise; + } = {} + ) { + const messages: ExpoPushMessage[] = []; + 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' }, + { 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, + ], + ], + }; + } + // 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: [...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"')) { + 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'); + 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'); + 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, + activityRows, + activities, + liveActivityProps: () => + [...activities.values()] + .filter(activity => !activity.ended) + .map(activity => JSON.parse(activity.contentState.props) as Record), + }; + } + + function freshSnapshot(overrides: Partial = {}): ActiveAgentsGlanceable { + return { + ...snapshot, + needsInput: 0, + updatedAt: new Date(Date.now()).toISOString(), + expiresAt: new Date(Date.now() + 28_800_000).toISOString(), + needsInputSince: 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, needsInputSince: 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, + needsInputSince: null, + updatedAt: '2026-08-27T10:00:01.000Z', + }, + { + status: 'empty', + running: 0, + needsInputSince: 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, needsInputSince: null }); + await createService().refreshGlanceableSessions(personalRefresh); + release.resolve(); + await busy; + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'empty', running: 0, needsInputSince: null }, + { status: 'empty', running: 0, needsInputSince: null }, + ]); + }); + + 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), + }); + await service.refreshGlanceableSessions(personalRefresh); + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:10:00.000Z')); + 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 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[ios]') + .map(message => message.data) + ).toMatchObject([ + { 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('fences an older empty read behind the newer authoritative reads', 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, needsInputSince: 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, idle: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[ios]') + .map(message => message.data) + ).toMatchObject([ + { 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 scopes 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', 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('recovers delivery after snapshot and delivery failures', 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, idle: 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, needsInputSince: '2026-08-27T10:00:00.000Z' }, + { idle: 1, needsInputSince: '2026-08-27T10:10:00.000Z' }, + ]); + }); + + 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, 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 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect( + messages + .filter(message => message.to === 'ExponentPushToken[ios]') + .map(message => message.data) + ).toMatchObject([ + { running: 2, needsInputSince: '2026-08-27T10:00:00.000Z' }, + { idle: 1, needsInputSince: '2026-08-27T10:10:00.000Z' }, + ]); + }); + + async function generateTestPrivateKeyPem(): Promise { + const pair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [ + 'sign', + 'verify', + ])) as CryptoKeyPair; + // `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-----`; + } + + 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, needsInputSince: 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, + idle: 0, + needsInputSince: null, + }); + + vi.mocked(Date.now).mockReturnValue(Date.parse('2026-08-27T10:00:01.000Z')); + 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({ + idle: 1, + needsInputSince: '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, needsInputSince: 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, needsInputSince: 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, needsInputSince: 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, idle: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([ + { + running: 0, + needsInput: 0, + idle: 1, + // 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'; + 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, needsInputSince: 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, + idle: 0, + needsInputSince: '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, needsInputSince: 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, needsInputSince: 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, needsInputSince: 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, idle: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([ + { 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'], + ['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, 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({ + 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, needsInputSince: '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, needsInputSince: 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, needsInputSince: 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, idle: 1 }); + await createService().refreshGlanceableSessions(personalRefresh); + expect(liveActivityProps()).toMatchObject([{ running: 0, needsInput: 0, idle: 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, needsInputSince: 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, needsInputSince: 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, needsInputSince: 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, needsInputSince: 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(); + const release = Promise.withResolvers(); + let first = true; + let current = freshSnapshot(); + const { createService, messages, apns } = setupService({ + response: () => Response.json(current), + privateKey: async () => pem, + beforeApnsDelivery: async () => { + if (first) { + first = false; + started.resolve(); + await release.promise; + } + }, + }); + const busy = createService().refreshGlanceableSessions(personalRefresh); + await started.promise; + 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, needsInputSince: 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', 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(); + 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, 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 => ({ + event: request.aps.event, + props: JSON.parse(request.aps['content-state'].props), + })) + ).toEqual( + event === null + ? [] + : [ + { + event, + props: { + status: 'empty', + running: 0, + needsInput: 0, + idle: 0, + needsInputSince: null, + }, + }, + ] + ); + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'empty', running: 0, needsInputSince: null }, + { status: 'empty', running: 0, needsInputSince: 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, needsInputSince: 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, needsInputSince: 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, needsInputSince: '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, needsInputSince: '2026-08-27T09:00:00.000Z' }, + { running: 2, needsInputSince: '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( + 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' }], true)).toEqual([ + { token: 'ptt-token', event: 'start' }, + ]); + }); + + 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('does not start an activity for empty work', () => { + expect(apnsSendsForTokens([{ token: 'ptt-token', kind: 'ios_push_to_start' }], false)).toEqual( + [] + ); + expect(apnsSendsForTokens([], true)).toEqual([]); + }); +}); + +describe('toGlanceableContentState', () => { + it('wraps the renderable counts + status in the expo-widgets name/props envelope', () => { + const contentState = toGlanceableContentState(snapshot); + expect(contentState.name).toBe('ActiveAgentsLiveActivity'); + const props = JSON.parse(contentState.props) as Record; + expect(props).toEqual({ + status: 'happy', + running: 2, + needsInput: 1, + idle: 0, + needsInputSince: '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('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' }, + ], + snapshot + ); + + 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(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.listIosExpoTokens).not.toHaveBeenCalled(); + expect(deps.hasAndroidOngoingToken).not.toHaveBeenCalled(); + expect(calls.iosSends).toHaveLength(0); + expect(calls.expoSends).toHaveLength(0); + }); + + 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' }, + ]; + const { deps, calls } = fakeDeps({ + listIosActivityTokens: vi.fn(async () => + iosTokens.map((token, index) => ({ + ...token, + id: `row-${index}`, + updated_at: snapshot.updatedAt, + })) + ), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: 'org-1' }, deps); + + expect(calls.iosSends).toHaveLength(1); + const [tokens, contentState] = calls.iosSends[0] as [ + { token: string; event: string }[], + GlanceableApnsContentState, + ]; + 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'); + expect(props.running).toBe(2); + expect(props.needsInput).toBe(1); + expect(props.idle).toBe(0); + expect(props).not.toHaveProperty('type'); + expect(props).not.toHaveProperty('accountEpoch'); + expect(props).not.toHaveProperty('scopeKey'); + 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.map((token, index) => ({ + ...token, + id: `row-${index}`, + updated_at: snapshot.updatedAt, + })) + ), + }); + + 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), + listAndroidExpoTokens: vi.fn(async () => [{ token: 'ExponentPushToken[aaa]', locale: null }]), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); + + expect(deps.listAndroidExpoTokens).not.toHaveBeenCalled(); + expect(calls.expoSends).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.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 () => { + const { deps, calls } = fakeDeps({ + hasAndroidOngoingToken: vi.fn(async () => true), + listAndroidExpoTokens: vi.fn(async () => []), + }); + + await deliverGlanceableSnapshot({ userId: 'u1', organizationId: null }, deps); + + 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 new file mode 100644 index 0000000000..0a95f6a766 --- /dev/null +++ b/services/notifications/src/lib/glanceable-delivery.ts @@ -0,0 +1,175 @@ +/** + * Aggregate glanceable snapshot delivery for the Active Agents Live Activity, + * 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. + */ + +import { type GlanceableLiveActivityContentState, type PushData } from '@kilocode/notifications'; + +import type { LiveActivityEvent } from './apns-live-activity'; +import type { ExpoPushMessage } from './expo-push'; + +export type ActiveAgentsGlanceable = Extract; + +/** 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 ExpoPushToken = { token: string; locale: string | null }; + +/** + * 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[], + 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: eligible ? 'update' : 'end' })); + } + return eligible + ? tokens + .filter(token => token.kind === 'ios_push_to_start') + .map(({ token }) => ({ token, event: 'start' })) + : []; +} + +export function toGlanceableContentState( + snapshot: ActiveAgentsGlanceable +): GlanceableApnsContentState { + const contentState: GlanceableLiveActivityContentState = { + status: snapshot.status, + running: snapshot.running, + needsInput: snapshot.needsInput, + idle: snapshot.idle, + needsInputSince: snapshot.needsInputSince, + }; + return { + name: ACTIVE_AGENTS_LIVE_ACTIVITY_NAME, + props: JSON.stringify(contentState), + }; +} + +export function buildGlanceableExpoMessages( + tokens: readonly ExpoPushToken[], + snapshot: ActiveAgentsGlanceable +): 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 + ); +} + +type IosActivityRegistration = IosActivityToken & { id: string; updated_at: string }; + +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: GlanceableApnsContentState, + timestampSeconds: number, + 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, + organizationId: string | null + ) => Promise; + hasAndroidOngoingToken: (userId: string, organizationId: string | null) => Promise; + sendExpoPush: (messages: ExpoPushMessage[], isCurrent?: () => Promise) => 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 (deps.isCurrent && !(await deps.isCurrent())) return; + const eligible = snapshot.running + snapshot.needsInput + snapshot.idle > 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.beforeIosEnd, + deps.onIosEndRejected + ); + } + + // 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), 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), 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..848a90f6ad --- /dev/null +++ b/services/notifications/src/lib/glanceable-refresh.ts @@ -0,0 +1,109 @@ +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(), +}); +// `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 }) + .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( + 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])}`; + // 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(); + 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 + ), + }; + 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 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; + return { + ...snapshot, + revision: request.revision, + updatedAt: request.updatedAt, + expiresAt: new Date( + Date.parse(request.updatedAt) + + Date.parse(snapshot.expiresAt) - + Date.parse(snapshot.updatedAt) + ).toISOString(), + }; + }); + if (committed === null) return; + + const eligible = committed.running + committed.needsInput + committed.idle > 0; + 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; + }, + 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}`); + }, + }); +} 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 0cd77a2460..ba51bd0628 100644 --- a/services/notifications/wrangler.jsonc +++ b/services/notifications/wrangler.jsonc @@ -8,7 +8,18 @@ "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", + // 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": [ { @@ -60,6 +71,13 @@ ], "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/dos/UserConnectionDO.test.ts b/services/session-ingest/src/dos/UserConnectionDO.test.ts index 31dbcf3881..4e0d1475ac 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(() => ({ @@ -202,13 +221,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(); @@ -517,6 +592,179 @@ 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, 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( + 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('resends only when a reorder, rename, or child attention changes the roots', 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(); + // A reorder and a rename leave every root status unchanged: no resend. + sendHeartbeat(doInstance, cliWs, [makeSession('s2', 'retry', 'Renamed'), makeSession('s1')]); + await flushAsync(); + // A child raise hoists NEEDS INPUT onto its root, so the counts change. + sendHeartbeat(doInstance, cliWs, [ + makeSession('s1'), + makeSession('s2', 'retry'), + makeSession('child', 'question', 'Child', '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: 1, idle: 0 }, + { running: 0, needsInput: 2, idle: 0 }, + { running: 1, needsInput: 1, idle: 0 }, + ]); + }); + + 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, needsInput: 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, idle: 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(undefined); + await disconnect; + await flushAsync(); + expect(messages.map(message => message.data)).toMatchObject([ + { status: 'empty', running: 0, needsInput: 0, idle: 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 unknown 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 f75584a716..ba190c3c0d 100644 --- a/services/session-ingest/src/dos/UserConnectionDO.ts +++ b/services/session-ingest/src/dos/UserConnectionDO.ts @@ -5,6 +5,7 @@ import type { Env } from '../env'; import { getSessionIngestDO } from './SessionIngestDO'; import { hoistedAttentionChanges, hoistedChildAttention } from './child-attention'; import { resolveAccessibleKiloSession } from '../services/session-access'; +import { refreshGlanceableSessions } from '../remote-session-notifications'; import { CLIOutboundMessageSchema, type CLIInboundMessage, @@ -608,6 +609,9 @@ export class UserConnectionDO extends DurableObject { ): void { sessions = sessions.filter(session => !this.isSessionDeleted(session.id)); 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); @@ -714,6 +718,23 @@ export class UserConnectionDO extends DurableObject { ws.serializeAttachment({ ...updatedAttachment, instance: legacyInstance }); } + 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. @@ -1875,6 +1896,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 a897aaa461..d38186cc44 100644 --- a/services/session-ingest/src/ingest/metadata.test.ts +++ b/services/session-ingest/src/ingest/metadata.test.ts @@ -1,6 +1,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; +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 { @@ -22,20 +30,16 @@ vi.mock('../dos/SessionAccessCacheDO', () => ({ })); vi.mock('../session-events', () => ({ - mapSessionEventRow: vi.fn( - (row: { - session_id: string; - status: string | null; - cloud_agent_worktree_id?: string | null; - }) => ({ - source: 'v2' as const, - sessionId: row.session_id, - worktreeId: row.cloud_agent_worktree_id ?? null, - status: row.status, - statusUpdatedAt: '2026-07-25T00:00:00.000Z', - updatedAt: '2026-07-25T00:00:00.000Z', - }) - ), + mapSessionEventRow: vi.fn((row: SessionEventDbRow) => ({ + source: 'v2' as const, + sessionId: row.session_id, + worktreeId: row.cloud_agent_worktree_id ?? null, + 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', + })), notifyUserSessionEvent: vi.fn(), })); @@ -138,6 +142,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; }; /** @@ -177,21 +183,22 @@ 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, cloud_agent_worktree_id: options.cloudAgentWorktreeId ?? null, status: options.initialStatus ?? 'idle', status_updated_at: '2026-07-25T00:00:00.000Z', }; + return Object.assign(row, ...updateSets); } function sessionLimitResult() { @@ -261,11 +268,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, @@ -278,6 +290,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(); @@ -432,6 +491,144 @@ describe('applyMetadataChanges', () => { ); }); + describe('glanceable aggregate refresh', () => { + it.each([ + ['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) => { + 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 58f58fd821..4810e78bf6 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'; import { isWorktreeDeleting } from '../services/worktree-deletion'; @@ -439,6 +440,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/notifications-bindings.d.ts b/services/session-ingest/src/notifications-bindings.d.ts new file mode 100644 index 0000000000..9bc040d467 --- /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; + } +} 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;