From 3ed7b94b938506e2502a7b69f0e9de25c5bb5b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 17:28:35 +0200 Subject: [PATCH 01/75] refactor(db): rename channel_badge_counts to badge_counts (general purpose); update all consumers --- apps/web/src/routers/user-router.test.ts | 14 +++++----- apps/web/src/routers/user-router.ts | 26 +++++++++--------- packages/db/src/schema.ts | 27 ++++++++++--------- .../src/dos/NotificationChannelDO.ts | 16 +++++------ 4 files changed, 40 insertions(+), 43 deletions(-) diff --git a/apps/web/src/routers/user-router.test.ts b/apps/web/src/routers/user-router.test.ts index 954299ad87..8ecf8baade 100644 --- a/apps/web/src/routers/user-router.test.ts +++ b/apps/web/src/routers/user-router.test.ts @@ -1,6 +1,6 @@ import { createCallerForUser } from '@/routers/test-utils'; import { db } from '@/lib/drizzle'; -import { channel_badge_counts, kilocode_users } from '@kilocode/db/schema'; +import { badge_counts, kilocode_users } from '@kilocode/db/schema'; import { eq, inArray } from 'drizzle-orm'; import { insertTestUser } from '@/tests/helpers/user.helper'; import type { User } from '@kilocode/db/schema'; @@ -425,18 +425,16 @@ describe('user router - getUnreadCounts', () => { const other = await insertTestUser({ google_user_email: `unread-counts-other-${crypto.randomUUID()}@example.com`, }); - await db.insert(channel_badge_counts).values([ - { user_id: user.id, channel_id: 'sandbox-mine', badge_count: 4 }, - { user_id: other.id, channel_id: 'sandbox-theirs', badge_count: 9 }, + await db.insert(badge_counts).values([ + { user_id: user.id, badge_bucket: 'sandbox-mine', badge_count: 4 }, + { user_id: other.id, badge_bucket: 'sandbox-theirs', badge_count: 9 }, ]); const caller = await createCallerForUser(user.id); const result = await caller.user.getUnreadCounts(); - expect(result).toEqual([{ channelId: 'sandbox-mine', badgeCount: 4 }]); + expect(result).toEqual([{ badgeBucket: 'sandbox-mine', badgeCount: 4 }]); - await db - .delete(channel_badge_counts) - .where(inArray(channel_badge_counts.user_id, [user.id, other.id])); + await db.delete(badge_counts).where(inArray(badge_counts.user_id, [user.id, other.id])); }); }); diff --git a/apps/web/src/routers/user-router.ts b/apps/web/src/routers/user-router.ts index 19d3a6964a..4d41e9623a 100644 --- a/apps/web/src/routers/user-router.ts +++ b/apps/web/src/routers/user-router.ts @@ -20,7 +20,7 @@ import { kiloclaw_instances, kiloclaw_subscriptions, user_push_tokens, - channel_badge_counts, + badge_counts, } from '@kilocode/db/schema'; import { eq, and, isNull, inArray, sql, gt, gte, sum } from 'drizzle-orm'; import crypto from 'crypto'; @@ -729,22 +729,22 @@ export const userRouter = createTRPCRouter({ // count for that channel to 0 and returns the new total across all // channels, which the app applies as the OS badge count. markChatRead: baseProcedure - .input(z.object({ channelId: z.string().min(1) })) + .input(z.object({ badgeBucket: z.string().min(1) })) .mutation(async ({ ctx, input }) => { await db - .update(channel_badge_counts) + .update(badge_counts) .set({ badge_count: 0 }) .where( and( - eq(channel_badge_counts.user_id, ctx.user.id), - eq(channel_badge_counts.channel_id, input.channelId) + eq(badge_counts.user_id, ctx.user.id), + eq(badge_counts.badge_bucket, input.badgeBucket) ) ); const [totals] = await db - .select({ total: sum(channel_badge_counts.badge_count) }) - .from(channel_badge_counts) - .where(eq(channel_badge_counts.user_id, ctx.user.id)); + .select({ total: sum(badge_counts.badge_count) }) + .from(badge_counts) + .where(eq(badge_counts.user_id, ctx.user.id)); return { badgeCount: Number(totals?.total ?? 0) }; }), @@ -757,12 +757,10 @@ export const userRouter = createTRPCRouter({ getUnreadCounts: baseProcedure.query(async ({ ctx }) => { return readDb .select({ - channelId: channel_badge_counts.channel_id, - badgeCount: channel_badge_counts.badge_count, + badgeBucket: badge_counts.badge_bucket, + badgeCount: badge_counts.badge_count, }) - .from(channel_badge_counts) - .where( - and(eq(channel_badge_counts.user_id, ctx.user.id), gt(channel_badge_counts.badge_count, 0)) - ); + .from(badge_counts) + .where(and(eq(badge_counts.user_id, ctx.user.id), gt(badge_counts.badge_count, 0))); }), }); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 610f216ffa..11abc622ea 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -4598,29 +4598,30 @@ export const security_advisor_content = pgTable('security_advisor_content', { export type SecurityAdvisorContent = typeof security_advisor_content.$inferSelect; export type NewSecurityAdvisorContent = typeof security_advisor_content.$inferInsert; -// ============ CHANNEL BADGE COUNTS ============ -// Per-user per-channel unread notification counts for mobile app badge display. -// (user_id, channel_id) is the composite PK — one row per user per chat channel. -// Keyed by channel rather than instance to support multiple channels per instance -// in future. The notification service increments badge_count on each push and sums -// across all channels to get the total badge count to include in the push payload. -// The mobile client resets a channel's count (to 0) when the user views that chat. - -export const channel_badge_counts = pgTable( - 'channel_badge_counts', +// ============ BADGE COUNTS ============ +// Per-user per-bucket unread notification counts for mobile app badge display. +// (user_id, badge_bucket) is the composite PK — one row per user per bucket. +// badge_bucket is a free-form string chosen by the producer (e.g. sandbox_id +// today, conversation id later). The notification service increments badge_count +// on each push and sums across all buckets to get the total badge count to +// include in the push payload. The client resets a bucket's count (to 0) when +// the user views that item. + +export const badge_counts = pgTable( + 'badge_counts', { user_id: text() .notNull() .references(() => kilocode_users.id, { onDelete: 'cascade' }), - channel_id: text().notNull(), + badge_bucket: text().notNull(), badge_count: integer().notNull().default(0), updated_at: timestamp({ withTimezone: true, mode: 'string' }) .defaultNow() .notNull() .$onUpdateFn(() => sql`now()`), }, - table => [primaryKey({ columns: [table.user_id, table.channel_id] })] + table => [primaryKey({ columns: [table.user_id, table.badge_bucket] })] ); -export type ChannelBadgeCount = typeof channel_badge_counts.$inferSelect; +export type BadgeCount = typeof badge_counts.$inferSelect; export type NewSecurityAdvisorScan = typeof security_advisor_scans.$inferInsert; diff --git a/services/notifications/src/dos/NotificationChannelDO.ts b/services/notifications/src/dos/NotificationChannelDO.ts index a58bfda8bc..f01c2ae30a 100644 --- a/services/notifications/src/dos/NotificationChannelDO.ts +++ b/services/notifications/src/dos/NotificationChannelDO.ts @@ -1,6 +1,6 @@ import { DurableObject } from 'cloudflare:workers'; import { getWorkerDb } from '@kilocode/db/client'; -import { channel_badge_counts, kiloclaw_instances, user_push_tokens } from '@kilocode/db/schema'; +import { badge_counts, kiloclaw_instances, user_push_tokens } from '@kilocode/db/schema'; import { and, eq, inArray, isNull, sql, sum } from 'drizzle-orm'; import type { Event } from 'stream-chat'; @@ -140,17 +140,17 @@ export class NotificationChannelDO extends DurableObject { // temporarily has no registered push tokens (e.g. between reinstalls). // Uses UPSERT so the row is created on first notification for this channel. await db - .insert(channel_badge_counts) - .values({ user_id: instance.user_id, channel_id: sandboxId, badge_count: 1 }) + .insert(badge_counts) + .values({ user_id: instance.user_id, badge_bucket: sandboxId, badge_count: 1 }) .onConflictDoUpdate({ - target: [channel_badge_counts.user_id, channel_badge_counts.channel_id], - set: { badge_count: sql`${channel_badge_counts.badge_count} + 1` }, + target: [badge_counts.user_id, badge_counts.badge_bucket], + set: { badge_count: sql`${badge_counts.badge_count} + 1` }, }); const [totals] = await db - .select({ total: sum(channel_badge_counts.badge_count) }) - .from(channel_badge_counts) - .where(eq(channel_badge_counts.user_id, instance.user_id)); + .select({ total: sum(badge_counts.badge_count) }) + .from(badge_counts) + .where(eq(badge_counts.user_id, instance.user_id)); const badgeCount = Number(totals?.total ?? 0); From e8d062c6f3d82aeab6ff0fda9e57ffaaf318df68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 17:29:25 +0200 Subject: [PATCH 02/75] feat(db): migration to rename badge_counts and reset rows --- .../src/migrations/0107_dapper_power_pack.sql | 9 + .../db/src/migrations/meta/0107_snapshot.json | 18016 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + 3 files changed, 18032 insertions(+) create mode 100644 packages/db/src/migrations/0107_dapper_power_pack.sql create mode 100644 packages/db/src/migrations/meta/0107_snapshot.json diff --git a/packages/db/src/migrations/0107_dapper_power_pack.sql b/packages/db/src/migrations/0107_dapper_power_pack.sql new file mode 100644 index 0000000000..0d5d3f3185 --- /dev/null +++ b/packages/db/src/migrations/0107_dapper_power_pack.sql @@ -0,0 +1,9 @@ +ALTER TABLE "channel_badge_counts" RENAME TO "badge_counts";--> statement-breakpoint +ALTER TABLE "badge_counts" RENAME COLUMN "channel_id" TO "badge_bucket";--> statement-breakpoint +ALTER TABLE "badge_counts" DROP CONSTRAINT "channel_badge_counts_user_id_kilocode_users_id_fk"; +--> statement-breakpoint +ALTER TABLE "badge_counts" DROP CONSTRAINT "channel_badge_counts_user_id_channel_id_pk";--> statement-breakpoint +ALTER TABLE "badge_counts" ADD CONSTRAINT "badge_counts_user_id_badge_bucket_pk" PRIMARY KEY("user_id","badge_bucket");--> statement-breakpoint +ALTER TABLE "badge_counts" ADD CONSTRAINT "badge_counts_user_id_kilocode_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +DELETE FROM badge_counts; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0107_snapshot.json b/packages/db/src/migrations/meta/0107_snapshot.json new file mode 100644 index 0000000000..c9b3c910b4 --- /dev/null +++ b/packages/db/src/migrations/meta/0107_snapshot.json @@ -0,0 +1,18016 @@ +{ + "id": "5b345e9c-de70-4377-a842-4747d863b166", + "prevId": "ec49ba08-673e-479f-a8a2-490c71ad9186", + "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()" + } + }, + "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')" + } + }, + "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_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_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 + }, + "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": {} + } + }, + "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.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 + }, + "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": "'v1'" + } + }, + "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": {} + } + }, + "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.badge_counts": { + "name": "badge_counts", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "badge_bucket": { + "name": "badge_bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "badge_count": { + "name": "badge_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "badge_counts_user_id_kilocode_users_id_fk": { + "name": "badge_counts_user_id_kilocode_users_id_fk", + "tableFrom": "badge_counts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "badge_counts_user_id_badge_bucket_pk": { + "name": "badge_counts_user_id_badge_bucket_pk", + "columns": [ + "user_id", + "badge_bucket" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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": false + }, + "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 + }, + "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_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 + }, + "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 + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time 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_kilo_user_id": { + "name": "IDX_cli_sessions_v2_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_created_at": { + "name": "IDX_cli_sessions_v2_created_at", + "columns": [ + { + "expression": "created_at", + "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": {} + } + }, + "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_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 + }, + "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'" + }, + "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 + }, + "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_repo_pr_sha": { + "name": "UQ_cloud_agent_code_reviews_repo_pr_sha", + "columns": [ + { + "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, + "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 + }, + "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_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.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_file_hash": { + "name": "IDX_code_indexing_manifest_file_hash", + "columns": [ + { + "expression": "file_hash", + "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.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 + }, + "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": {}, + "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 + } + }, + "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.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 + }, + "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": {} + } + }, + "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.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 + }, + "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.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": {} + }, + "idx_free_model_usage_user_created_at": { + "name": "idx_free_model_usage_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, + "where": "\"free_model_usage\".\"kilo_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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.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', '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', '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')" + } + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time 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', 'cron')" + } + }, + "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_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 + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_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": {} + } + }, + "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_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.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 + }, + "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": { + "name": "UQ_kiloclaw_email_log_user_instance_type", + "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" + } + ], + "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 + } + }, + "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": {} + } + }, + "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": {}, + "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 + }, + "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_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_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_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 + }, + "is_admin": { + "name": "is_admin", + "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 + }, + "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 + } + }, + "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_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": { + "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" + } + }, + "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()" + } + }, + "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": {} + } + }, + "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\"" + } + }, + "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_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 + } + }, + "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": {} + } + }, + "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.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.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_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 + }, + "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": {}, + "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_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 + }, + "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": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organizations_name_not_empty_check": { + "name": "organizations_name_not_empty_check", + "value": "length(trim(\"organizations\".\"name\")) > 0" + } + }, + "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_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 + }, + "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": {} + }, + "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.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.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_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 + }, + "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 + }, + "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 + }, + "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": {} + } + }, + "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.status_change', 'security.finding.dismissed', 'security.finding.auto_dismissed', 'security.finding.analysis_started', 'security.finding.analysis_completed', 'security.finding.deleted', 'security.config.enabled', 'security.config.disabled', 'security.config.updated', 'security.sync.triggered', 'security.sync.completed', 'security.audit_log.exported')" + } + }, + "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": { + "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": { + "uq_security_findings_source": { + "name": "uq_security_findings_source", + "nullsNotDistinct": false, + "columns": [ + "repo_full_name", + "source", + "source_id" + ] + } + }, + "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.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.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.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_fingerprint_data": { + "name": "idx_fingerprint_data", + "columns": [ + { + "expression": "fingerprint_data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_reasons": { + "name": "idx_reasons", + "columns": [ + { + "expression": "reasons", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "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.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": {} + } + }, + "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_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_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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time 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.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 + } + }, + "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 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 8ed1009b42..145070107c 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -750,6 +750,13 @@ "when": 1777411448037, "tag": "0106_petite_william_stryker", "breakpoints": true + }, + { + "idx": 107, + "version": "7", + "when": 1777476540389, + "tag": "0107_dapper_power_pack", + "breakpoints": true } ] } \ No newline at end of file From 20b9b3b79153fda3bae87e15fb3182f69b6fd49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 17:39:40 +0200 Subject: [PATCH 03/75] feat(notifications): add badge-bucket key builders The badge_counts.badge_bucket column is a free-form string. To prevent namespace collisions as more surfaces start emitting badge updates (per-instance today, per-conversation later), centralize bucket-key derivation in @kilocode/notifications and route NotificationChannelDO through it. Mirrors the presence-context builders in @kilocode/event-service. Safe to introduce now without a data migration because PR 2's migration already wipes badge_counts. --- packages/notifications/src/badge-buckets.ts | 11 +++++++++++ packages/notifications/src/index.ts | 1 + pnpm-lock.yaml | 3 +++ services/notifications/package.json | 1 + .../notifications/src/dos/NotificationChannelDO.ts | 8 +++++--- 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 packages/notifications/src/badge-buckets.ts diff --git a/packages/notifications/src/badge-buckets.ts b/packages/notifications/src/badge-buckets.ts new file mode 100644 index 0000000000..bb3213307c --- /dev/null +++ b/packages/notifications/src/badge-buckets.ts @@ -0,0 +1,11 @@ +/** + * Badge-bucket key builders. The `badge_counts` table uses a free-form + * `badge_bucket` string as part of its composite PK; producers of unread + * counts MUST derive their bucket key via these helpers so namespaces + * don't collide as more surfaces start emitting badge updates. + */ + +export const badgeBucketForInstance = (sandboxId: string) => `kiloclaw:${sandboxId}` as const; + +export const badgeBucketForConversation = (sandboxId: string, conversationId: string) => + `kiloclaw:${sandboxId}:${conversationId}` as const; diff --git a/packages/notifications/src/index.ts b/packages/notifications/src/index.ts index d9b3cb2df0..1b6581a13e 100644 --- a/packages/notifications/src/index.ts +++ b/packages/notifications/src/index.ts @@ -1,2 +1,3 @@ +export * from './badge-buckets'; export * from './push-data'; export * from './rpc-schemas'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e611890cfc..00bae11778 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2008,6 +2008,9 @@ importers: '@kilocode/db': specifier: workspace:* version: link:../../packages/db + '@kilocode/notifications': + specifier: workspace:* + version: link:../../packages/notifications '@kilocode/worker-utils': specifier: workspace:* version: link:../../packages/worker-utils diff --git a/services/notifications/package.json b/services/notifications/package.json index 952f310a92..a7897326d8 100644 --- a/services/notifications/package.json +++ b/services/notifications/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@kilocode/db": "workspace:*", + "@kilocode/notifications": "workspace:*", "@kilocode/worker-utils": "workspace:*", "drizzle-orm": "catalog:", "expo-server-sdk": "^6.1.0", diff --git a/services/notifications/src/dos/NotificationChannelDO.ts b/services/notifications/src/dos/NotificationChannelDO.ts index f01c2ae30a..3568f9a8e7 100644 --- a/services/notifications/src/dos/NotificationChannelDO.ts +++ b/services/notifications/src/dos/NotificationChannelDO.ts @@ -1,6 +1,7 @@ import { DurableObject } from 'cloudflare:workers'; import { getWorkerDb } from '@kilocode/db/client'; import { badge_counts, kiloclaw_instances, user_push_tokens } from '@kilocode/db/schema'; +import { badgeBucketForInstance } from '@kilocode/notifications'; import { and, eq, inArray, isNull, sql, sum } from 'drizzle-orm'; import type { Event } from 'stream-chat'; @@ -135,13 +136,14 @@ export class NotificationChannelDO extends DurableObject { return; } - // Increment the badge count for this channel and return the new total across all channels. + // Increment the badge count for this bucket and return the new total across all buckets. // Done before the token guard so unread state is always persisted even if the user // temporarily has no registered push tokens (e.g. between reinstalls). - // Uses UPSERT so the row is created on first notification for this channel. + // Uses UPSERT so the row is created on first notification for this bucket. + const badgeBucket = badgeBucketForInstance(sandboxId); await db .insert(badge_counts) - .values({ user_id: instance.user_id, badge_bucket: sandboxId, badge_count: 1 }) + .values({ user_id: instance.user_id, badge_bucket: badgeBucket, badge_count: 1 }) .onConflictDoUpdate({ target: [badge_counts.user_id, badge_counts.badge_bucket], set: { badge_count: sql`${badge_counts.badge_count} + 1` }, From 1bb97c6cced385a38f988e606581f196c023adc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 17:55:15 +0200 Subject: [PATCH 04/75] chore(notifications): add EVENT_SERVICE binding, drop STREAM_CHAT_API_SECRET --- services/notifications/src/bindings.d.ts | 3 + .../notifications/worker-configuration.d.ts | 578 ++---------------- services/notifications/wrangler.jsonc | 10 +- 3 files changed, 61 insertions(+), 530 deletions(-) create mode 100644 services/notifications/src/bindings.d.ts diff --git a/services/notifications/src/bindings.d.ts b/services/notifications/src/bindings.d.ts new file mode 100644 index 0000000000..40e773ded4 --- /dev/null +++ b/services/notifications/src/bindings.d.ts @@ -0,0 +1,3 @@ +import type {} from './worker-configuration.d.ts'; + +export type NotificationsEnv = Env; diff --git a/services/notifications/worker-configuration.d.ts b/services/notifications/worker-configuration.d.ts index cbc9201506..6a3ebcce40 100644 --- a/services/notifications/worker-configuration.d.ts +++ b/services/notifications/worker-configuration.d.ts @@ -1,17 +1,17 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: b336c1c1e874405e99f5e26c8c9319df) -// Runtime types generated with workerd@1.20260312.1 2026-02-01 nodejs_compat +// Generated by Wrangler by running `wrangler types` (hash: 0819ae5a59d68074ad7e23557cbc10b7) +// Runtime types generated with workerd@1.20251217.0 2026-02-01 nodejs_compat declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/index"); durableNamespaces: "NotificationChannelDO"; } interface Env { - HYPERDRIVE: Hyperdrive; - RECEIPTS_QUEUE: Queue; - STREAM_CHAT_API_SECRET: SecretsStoreSecret; + NOTIFICATION_CHANNEL_DO: DurableObjectNamespace; EXPO_ACCESS_TOKEN: SecretsStoreSecret; - NOTIFICATION_CHANNEL_DO: DurableObjectNamespace /* NotificationChannelDO */; + EVENT_SERVICE: Fetcher /* event-service */; + RECEIPTS_QUEUE: Queue; + HYPERDRIVE: Hyperdrive; } } interface Env extends Cloudflare.Env {} @@ -439,22 +439,22 @@ interface ExecutionContext { readonly exports: Cloudflare.Exports; readonly props: Props; } -type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; -type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; -type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; -interface ExportedHandler { - fetch?: ExportedHandlerFetchHandler; - tail?: ExportedHandlerTailHandler; - trace?: ExportedHandlerTraceHandler; - tailStream?: ExportedHandlerTailStreamHandler; - scheduled?: ExportedHandlerScheduledHandler; - test?: ExportedHandlerTestHandler; - email?: EmailExportedHandler; - queue?: ExportedHandlerQueueHandler; +type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; +type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; +type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler; + tail?: ExportedHandlerTailHandler; + trace?: ExportedHandlerTraceHandler; + tailStream?: ExportedHandlerTailStreamHandler; + scheduled?: ExportedHandlerScheduledHandler; + test?: ExportedHandlerTestHandler; + email?: EmailExportedHandler; + queue?: ExportedHandlerQueueHandler; } interface StructuredSerializeOptions { transfer?: any[]; @@ -502,10 +502,8 @@ interface DurableObjectNamespaceNewUniqueIdOptions { jurisdiction?: DurableObjectJurisdiction; } type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me"; -type DurableObjectRoutingMode = "primary-only"; interface DurableObjectNamespaceGetDurableObjectOptions { locationHint?: DurableObjectLocationHint; - routingMode?: DurableObjectRoutingMode; } interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> { } @@ -1393,12 +1391,6 @@ declare abstract class PromiseRejectionEvent extends Event { */ declare class FormData { constructor(); - /** - * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) - */ - append(name: string, value: string | Blob): void; /** * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. * @@ -1435,12 +1427,6 @@ declare class FormData { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has) */ has(name: string): boolean; - /** - * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) - */ - set(name: string, value: string | Blob): void; /** * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. * @@ -1767,7 +1753,7 @@ interface Request> e * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal) */ signal: AbortSignal; - cf?: Cf; + cf: Cf | undefined; /** * The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request. * @@ -2102,8 +2088,6 @@ interface Transformer { expectedLength?: number; } interface StreamPipeOptions { - preventAbort?: boolean; - preventCancel?: boolean; /** * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. * @@ -2122,6 +2106,8 @@ interface StreamPipeOptions { * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set. */ preventClose?: boolean; + preventAbort?: boolean; + preventCancel?: boolean; signal?: AbortSignal; } type ReadableStreamReadResult = { @@ -2396,13 +2382,13 @@ declare abstract class TransformStreamDefaultController { terminate(): void; } interface ReadableWritablePair { - readable: ReadableStream; /** * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use. * * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. */ writable: WritableStream; + readable: ReadableStream; } /** * The **`WritableStream`** interface of the Streams API provides a standard abstraction for writing streaming data to a destination, known as a sink. @@ -3050,7 +3036,7 @@ declare var WebSocket: { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) */ interface WebSocket extends EventTarget { - accept(options?: WebSocketAcceptOptions): void; + accept(): void; /** * The **`WebSocket.send()`** method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of `bufferedAmount` by the number of bytes needed to contain the data. * @@ -3089,22 +3075,6 @@ interface WebSocket extends EventTarget { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions) */ extensions: string | null; - /** - * The **`WebSocket.binaryType`** property controls the type of binary data being received over the WebSocket connection. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) - */ - binaryType: "blob" | "arraybuffer"; -} -interface WebSocketAcceptOptions { - /** - * When set to `true`, receiving a server-initiated WebSocket Close frame will not - * automatically send a reciprocal Close frame, leaving the connection in a half-open - * state. This is useful for proxying scenarios where you need to coordinate closing - * both sides independently. Defaults to `false` when the - * `no_web_socket_half_open_by_default` compatibility flag is enabled. - */ - allowHalfOpen?: boolean; } declare const WebSocketPair: { new (): { @@ -3223,8 +3193,6 @@ interface Container { signal(signo: number): void; getTcpPort(port: number): Fetcher; setInactivityTimeout(durationMs: number | bigint): Promise; - interceptOutboundHttp(addr: string, binding: Fetcher): Promise; - interceptAllOutboundHttp(binding: Fetcher): Promise; } interface ContainerStartupOptions { entrypoint?: string[]; @@ -3350,181 +3318,6 @@ declare abstract class Performance { get timeOrigin(): number; /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ now(): number; - /** - * The **`toJSON()`** method of the Performance interface is a Serialization; it returns a JSON representation of the Performance object. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Performance/toJSON) - */ - toJSON(): object; -} -// AI Search V2 API Error Interfaces -interface AiSearchInternalError extends Error { -} -interface AiSearchNotFoundError extends Error { -} -interface AiSearchNameNotSetError extends Error { -} -// AI Search V2 Request Types -type AiSearchSearchRequest = { - messages: Array<{ - role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; - content: string | null; - }>; - ai_search_options?: { - retrieval?: { - retrieval_type?: 'vector' | 'keyword' | 'hybrid'; - /** Match threshold (0-1, default 0.4) */ - match_threshold?: number; - /** Maximum number of results (1-50, default 10) */ - max_num_results?: number; - filters?: VectorizeVectorMetadataFilter; - /** Context expansion (0-3, default 0) */ - context_expansion?: number; - [key: string]: unknown; - }; - query_rewrite?: { - enabled?: boolean; - model?: string; - rewrite_prompt?: string; - [key: string]: unknown; - }; - reranking?: { - /** Enable reranking (default false) */ - enabled?: boolean; - model?: '@cf/baai/bge-reranker-base' | ''; - /** Match threshold (0-1, default 0.4) */ - match_threshold?: number; - [key: string]: unknown; - }; - [key: string]: unknown; - }; -}; -type AiSearchChatCompletionsRequest = { - messages: Array<{ - role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; - content: string | null; - }>; - model?: string; - stream?: boolean; - ai_search_options?: { - retrieval?: { - retrieval_type?: 'vector' | 'keyword' | 'hybrid'; - match_threshold?: number; - max_num_results?: number; - filters?: VectorizeVectorMetadataFilter; - context_expansion?: number; - [key: string]: unknown; - }; - query_rewrite?: { - enabled?: boolean; - model?: string; - rewrite_prompt?: string; - [key: string]: unknown; - }; - reranking?: { - enabled?: boolean; - model?: '@cf/baai/bge-reranker-base' | ''; - match_threshold?: number; - [key: string]: unknown; - }; - [key: string]: unknown; - }; - [key: string]: unknown; -}; -// AI Search V2 Response Types -type AiSearchSearchResponse = { - search_query: string; - chunks: Array<{ - id: string; - type: string; - /** Match score (0-1) */ - score: number; - text: string; - item: { - timestamp?: number; - key: string; - metadata?: Record; - }; - scoring_details?: { - /** Keyword match score (0-1) */ - keyword_score?: number; - /** Vector similarity score (0-1) */ - vector_score?: number; - }; - }>; -}; -type AiSearchListResponse = Array<{ - id: string; - internal_id?: string; - account_id?: string; - account_tag?: string; - /** Whether the instance is enabled (default true) */ - enable?: boolean; - type?: 'r2' | 'web-crawler'; - source?: string; - [key: string]: unknown; -}>; -type AiSearchConfig = { - /** Instance ID (1-32 chars, pattern: ^[a-z0-9_]+(?:-[a-z0-9_]+)*$) */ - id: string; - type: 'r2' | 'web-crawler'; - source: string; - source_params?: object; - /** Token ID (UUID format) */ - token_id?: string; - ai_gateway_id?: string; - /** Enable query rewriting (default false) */ - rewrite_query?: boolean; - /** Enable reranking (default false) */ - reranking?: boolean; - embedding_model?: string; - ai_search_model?: string; -}; -type AiSearchInstance = { - id: string; - enable?: boolean; - type?: 'r2' | 'web-crawler'; - source?: string; - [key: string]: unknown; -}; -// AI Search Instance Service - Instance-level operations -declare abstract class AiSearchInstanceService { - /** - * Search the AI Search instance for relevant chunks. - * @param params Search request with messages and AI search options - * @returns Search response with matching chunks - */ - search(params: AiSearchSearchRequest): Promise; - /** - * Generate chat completions with AI Search context. - * @param params Chat completions request with optional streaming - * @returns Response object (if streaming) or chat completion result - */ - chatCompletions(params: AiSearchChatCompletionsRequest): Promise; - /** - * Delete this AI Search instance. - */ - delete(): Promise; -} -// AI Search Account Service - Account-level operations -declare abstract class AiSearchAccountService { - /** - * List all AI Search instances in the account. - * @returns Array of AI Search instances - */ - list(): Promise; - /** - * Get an AI Search instance by ID. - * @param name Instance ID - * @returns Instance service for performing operations - */ - get(name: string): AiSearchInstanceService; - /** - * Create a new AI Search instance. - * @param config Instance configuration - * @returns Instance service for performing operations - */ - create(config: AiSearchConfig): Promise; } type AiImageClassificationInput = { image: number[]; @@ -5714,7 +5507,7 @@ interface Ai_Cf_Qwen_Qwq_32B_Messages { }; })[]; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -5980,7 +5773,7 @@ interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages { }; })[]; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6071,7 +5864,7 @@ interface Ai_Cf_Google_Gemma_3_12B_It_Prompt { */ prompt: string; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6230,7 +6023,7 @@ interface Ai_Cf_Google_Gemma_3_12B_It_Messages { }; })[]; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6502,7 +6295,7 @@ interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages { })[]; response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -6732,7 +6525,7 @@ interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner { })[]; response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; /** - * JSON schema that should be fulfilled for the response. + * JSON schema that should be fufilled for the response. */ guided_json?: object; /** @@ -7782,7 +7575,7 @@ interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input { */ text: string | string[]; /** - * Target language to translate to + * Target langauge to translate to */ target_language: "asm_Beng" | "awa_Deva" | "ben_Beng" | "bho_Deva" | "brx_Deva" | "doi_Deva" | "eng_Latn" | "gom_Deva" | "gon_Deva" | "guj_Gujr" | "hin_Deva" | "hne_Deva" | "kan_Knda" | "kas_Arab" | "kas_Deva" | "kha_Latn" | "lus_Latn" | "mag_Deva" | "mai_Deva" | "mal_Mlym" | "mar_Deva" | "mni_Beng" | "mni_Mtei" | "npi_Deva" | "ory_Orya" | "pan_Guru" | "san_Deva" | "sat_Olck" | "snd_Arab" | "snd_Deva" | "tam_Taml" | "tel_Telu" | "urd_Arab" | "unr_Deva"; } @@ -8713,48 +8506,6 @@ type AiModelListType = Record; declare abstract class Ai { aiGatewayLogId: string | null; gateway(gatewayId: string): AiGateway; - /** - * Access the AI Search API for managing AI-powered search instances. - * - * This is the new API that replaces AutoRAG with better namespace separation: - * - Account-level operations: `list()`, `create()` - * - Instance-level operations: `get(id).search()`, `get(id).chatCompletions()`, `get(id).delete()` - * - * @example - * ```typescript - * // List all AI Search instances - * const instances = await env.AI.aiSearch.list(); - * - * // Search an instance - * const results = await env.AI.aiSearch.get('my-search').search({ - * messages: [{ role: 'user', content: 'What is the policy?' }], - * ai_search_options: { - * retrieval: { max_num_results: 10 } - * } - * }); - * - * // Generate chat completions with AI Search context - * const response = await env.AI.aiSearch.get('my-search').chatCompletions({ - * messages: [{ role: 'user', content: 'What is the policy?' }], - * model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast' - * }); - * ``` - */ - aiSearch(): AiSearchAccountService; - /** - * @deprecated AutoRAG has been replaced by AI Search. - * Use `env.AI.aiSearch` instead for better API design and new features. - * - * Migration guide: - * - `env.AI.autorag().list()` → `env.AI.aiSearch.list()` - * - `env.AI.autorag('id').search({ query: '...' })` → `env.AI.aiSearch.get('id').search({ messages: [{ role: 'user', content: '...' }] })` - * - `env.AI.autorag('id').aiSearch(...)` → `env.AI.aiSearch.get('id').chatCompletions(...)` - * - * Note: The old API continues to work for backwards compatibility, but new projects should use AI Search. - * - * @see AiSearchAccountService - * @param autoragId Optional instance ID (omit for account-level operations) - */ autorag(autoragId: string): AutoRAG; run(model: Name, inputs: InputOptions, options?: Options): Promise; getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line } -/** - * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchInternalError instead. - * @see AiSearchInternalError - */ interface AutoRAGInternalError extends Error { } -/** - * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchNotFoundError instead. - * @see AiSearchNotFoundError - */ interface AutoRAGNotFoundError extends Error { } -/** - * @deprecated This error type is no longer used in the AI Search API. - */ interface AutoRAGUnauthorizedError extends Error { } -/** - * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchNameNotSetError instead. - * @see AiSearchNameNotSetError - */ interface AutoRAGNameNotSetError extends Error { } type ComparisonFilter = { @@ -8895,11 +8631,6 @@ type CompoundFilter = { type: 'and' | 'or'; filters: ComparisonFilter[]; }; -/** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchSearchRequest with the new API instead. - * @see AiSearchSearchRequest - */ type AutoRagSearchRequest = { query: string; filters?: CompoundFilter | ComparisonFilter; @@ -8914,28 +8645,13 @@ type AutoRagSearchRequest = { }; rewrite_query?: boolean; }; -/** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchChatCompletionsRequest with the new API instead. - * @see AiSearchChatCompletionsRequest - */ type AutoRagAiSearchRequest = AutoRagSearchRequest & { stream?: boolean; system_prompt?: string; }; -/** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchChatCompletionsRequest with stream: true instead. - * @see AiSearchChatCompletionsRequest - */ type AutoRagAiSearchRequestStreaming = Omit & { stream: true; }; -/** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchSearchResponse with the new API instead. - * @see AiSearchSearchResponse - */ type AutoRagSearchResponse = { object: 'vector_store.search_results.page'; search_query: string; @@ -8952,11 +8668,6 @@ type AutoRagSearchResponse = { has_more: boolean; next_page: string | null; }; -/** - * @deprecated AutoRAG has been replaced by AI Search. - * Use AiSearchListResponse with the new API instead. - * @see AiSearchListResponse - */ type AutoRagListResponse = { id: string; enable: boolean; @@ -8966,51 +8677,14 @@ type AutoRagListResponse = { paused: boolean; status: string; }[]; -/** - * @deprecated AutoRAG has been replaced by AI Search. - * The new API returns different response formats for chat completions. - */ type AutoRagAiSearchResponse = AutoRagSearchResponse & { response: string; }; -/** - * @deprecated AutoRAG has been replaced by AI Search. - * Use the new AI Search API instead: `env.AI.aiSearch` - * - * Migration guide: - * - `env.AI.autorag().list()` → `env.AI.aiSearch.list()` - * - `env.AI.autorag('id').search(...)` → `env.AI.aiSearch.get('id').search(...)` - * - `env.AI.autorag('id').aiSearch(...)` → `env.AI.aiSearch.get('id').chatCompletions(...)` - * - * @see AiSearchAccountService - * @see AiSearchInstanceService - */ declare abstract class AutoRAG { - /** - * @deprecated Use `env.AI.aiSearch.list()` instead. - * @see AiSearchAccountService.list - */ list(): Promise; - /** - * @deprecated Use `env.AI.aiSearch.get(id).search(...)` instead. - * Note: The new API uses a messages array instead of a query string. - * @see AiSearchInstanceService.search - */ search(params: AutoRagSearchRequest): Promise; - /** - * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. - * @see AiSearchInstanceService.chatCompletions - */ aiSearch(params: AutoRagAiSearchRequestStreaming): Promise; - /** - * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. - * @see AiSearchInstanceService.chatCompletions - */ aiSearch(params: AutoRagAiSearchRequest): Promise; - /** - * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. - * @see AiSearchInstanceService.chatCompletions - */ aiSearch(params: AutoRagAiSearchRequest): Promise; } interface BasicImageTransformations { @@ -9761,10 +9435,6 @@ interface D1Meta { * The region of the database instance that executed the query. */ served_by_region?: string; - /** - * The three letters airport code of the colo that executed the query. - */ - served_by_colo?: string; /** * True if-and-only-if the database instance that executed the query was the primary. */ @@ -9853,15 +9523,6 @@ declare abstract class D1PreparedStatement { // ignored when `Disposable` is included in the standard lib. interface Disposable { } -/** - * The returned data after sending an email - */ -interface EmailSendResult { - /** - * The Email Message ID - */ - messageId: string; -} /** * An email message that can be sent from a Worker. */ @@ -9903,55 +9564,24 @@ interface ForwardableEmailMessage extends EmailMessage { * @param headers A [Headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers). * @returns A promise that resolves when the email message is forwarded. */ - forward(rcptTo: string, headers?: Headers): Promise; + forward(rcptTo: string, headers?: Headers): Promise; /** * Reply to the sender of this email message with a new EmailMessage object. * @param message The reply message. * @returns A promise that resolves when the email message is replied. */ - reply(message: EmailMessage): Promise; -} -/** A file attachment for an email message */ -type EmailAttachment = { - disposition: 'inline'; - contentId: string; - filename: string; - type: string; - content: string | ArrayBuffer | ArrayBufferView; -} | { - disposition: 'attachment'; - contentId?: undefined; - filename: string; - type: string; - content: string | ArrayBuffer | ArrayBufferView; -}; -/** An Email Address */ -interface EmailAddress { - name: string; - email: string; + reply(message: EmailMessage): Promise; } /** * A binding that allows a Worker to send email messages. */ interface SendEmail { - send(message: EmailMessage): Promise; - send(builder: { - from: string | EmailAddress; - to: string | string[]; - subject: string; - replyTo?: string | EmailAddress; - cc?: string | string[]; - bcc?: string | string[]; - headers?: Record; - text?: string; - html?: string; - attachments?: EmailAttachment[]; - }): Promise; + send(message: EmailMessage): Promise; } declare abstract class EmailEvent extends ExtendableEvent { readonly message: ForwardableEmailMessage; } -declare type EmailExportedHandler = (message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) => void | Promise; +declare type EmailExportedHandler = (message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) => void | Promise; declare module "cloudflare:email" { let _EmailMessage: { prototype: EmailMessage; @@ -9979,7 +9609,7 @@ interface Hyperdrive { /** * Connect directly to Hyperdrive as if it's your database, returning a TCP socket. * - * Calling this method returns an identical socket to if you call + * Calling this method returns an idential socket to if you call * `connect("host:port")` using the `host` and `port` fields from this object. * Pick whichever approach works better with your preferred DB client library. * @@ -10092,83 +9722,6 @@ type ImageOutputOptions = { background?: string; anim?: boolean; }; -interface ImageMetadata { - id: string; - filename?: string; - uploaded?: string; - requireSignedURLs: boolean; - meta?: Record; - variants: string[]; - draft?: boolean; - creator?: string; -} -interface ImageUploadOptions { - id?: string; - filename?: string; - requireSignedURLs?: boolean; - metadata?: Record; - creator?: string; - encoding?: 'base64'; -} -interface ImageUpdateOptions { - requireSignedURLs?: boolean; - metadata?: Record; - creator?: string; -} -interface ImageListOptions { - limit?: number; - cursor?: string; - sortOrder?: 'asc' | 'desc'; - creator?: string; -} -interface ImageList { - images: ImageMetadata[]; - cursor?: string; - listComplete: boolean; -} -interface HostedImagesBinding { - /** - * Get detailed metadata for a hosted image - * @param imageId The ID of the image (UUID or custom ID) - * @returns Image metadata, or null if not found - */ - details(imageId: string): Promise; - /** - * Get the raw image data for a hosted image - * @param imageId The ID of the image (UUID or custom ID) - * @returns ReadableStream of image bytes, or null if not found - */ - image(imageId: string): Promise | null>; - /** - * Upload a new hosted image - * @param image The image file to upload - * @param options Upload configuration - * @returns Metadata for the uploaded image - * @throws {@link ImagesError} if upload fails - */ - upload(image: ReadableStream | ArrayBuffer, options?: ImageUploadOptions): Promise; - /** - * Update hosted image metadata - * @param imageId The ID of the image - * @param options Properties to update - * @returns Updated image metadata - * @throws {@link ImagesError} if update fails - */ - update(imageId: string, options: ImageUpdateOptions): Promise; - /** - * Delete a hosted image - * @param imageId The ID of the image - * @returns True if deleted, false if not found - */ - delete(imageId: string): Promise; - /** - * List hosted images with pagination - * @param options List configuration - * @returns List of images with pagination info - * @throws {@link ImagesError} if list fails - */ - list(options?: ImageListOptions): Promise; -} interface ImagesBinding { /** * Get image metadata (type, width and height) @@ -10182,10 +9735,6 @@ interface ImagesBinding { * @returns A transform handle */ input(stream: ReadableStream, options?: ImageInputOptions): ImageTransformer; - /** - * Access hosted images CRUD operations - */ - readonly hosted: HostedImagesBinding; } interface ImageTransformer { /** @@ -10252,13 +9801,7 @@ interface MediaTransformer { * @param transform - Configuration for how the media should be transformed * @returns A generator for producing the transformed media output */ - transform(transform?: MediaTransformationInputOptions): MediaTransformationGenerator; - /** - * Generates the final media output with specified options. - * @param output - Configuration for the output format and parameters - * @returns The final transformation result containing the transformed media - */ - output(output?: MediaTransformationOutputOptions): MediaTransformationResult; + transform(transform: MediaTransformationInputOptions): MediaTransformationGenerator; } /** * Generator for producing media transformation results. @@ -10270,7 +9813,7 @@ interface MediaTransformationGenerator { * @param output - Configuration for the output format and parameters * @returns The final transformation result containing the transformed media */ - output(output?: MediaTransformationOutputOptions): MediaTransformationResult; + output(output: MediaTransformationOutputOptions): MediaTransformationResult; } /** * Result of a media transformation operation. @@ -10279,19 +9822,19 @@ interface MediaTransformationGenerator { interface MediaTransformationResult { /** * Returns the transformed media as a readable stream of bytes. - * @returns A promise containing a readable stream with the transformed media + * @returns A stream containing the transformed media data */ - media(): Promise>; + media(): ReadableStream; /** * Returns the transformed media as an HTTP response object. - * @returns The transformed media as a Promise, ready to store in cache or return to users + * @returns The transformed media as a Response, ready to store in cache or return to users */ - response(): Promise; + response(): Response; /** * Returns the MIME type of the transformed media. - * @returns A promise containing the content type string (e.g., 'image/jpeg', 'video/mp4') + * @returns The content type string (e.g., 'image/jpeg', 'video/mp4') */ - contentType(): Promise; + contentType(): string; } /** * Configuration options for transforming media input. @@ -10399,7 +9942,7 @@ declare module "cloudflare:pipelines" { protected ctx: ExecutionContext; constructor(ctx: ExecutionContext, env: Env); /** - * run receives an array of PipelineRecord which can be + * run recieves an array of PipelineRecord which can be * transformed and returned to the pipeline * @param records Incoming records from the pipeline to be transformed * @param metadata Information about the specific pipeline calling the transformation entrypoint @@ -10670,12 +10213,9 @@ declare namespace CloudflareWorkersModule { timestamp: Date; type: string; }; - export type WorkflowStepContext = { - attempt: number; - }; export abstract class WorkflowStep { - do>(name: string, callback: (ctx: WorkflowStepContext) => Promise): Promise; - do>(name: string, config: WorkflowStepConfig, callback: (ctx: WorkflowStepContext) => Promise): Promise; + do>(name: string, callback: () => Promise): Promise; + do>(name: string, config: WorkflowStepConfig, callback: () => Promise): Promise; sleep: (name: string, duration: WorkflowSleepDuration) => Promise; sleepUntil: (name: string, timestamp: Date | number) => Promise; waitForEvent>(name: string, options: { @@ -10683,7 +10223,6 @@ declare namespace CloudflareWorkersModule { timeout?: WorkflowTimeoutDuration | number; }): Promise>; } - export type WorkflowInstanceStatus = 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'waitingForPause' | 'unknown'; export abstract class WorkflowEntrypoint | unknown = unknown> implements Rpc.WorkflowEntrypointBranded { [Rpc.__WORKFLOW_ENTRYPOINT_BRAND]: never; protected ctx: ExecutionContext; @@ -10717,14 +10256,12 @@ type MarkdownDocument = { blob: Blob; }; type ConversionResponse = { - id: string; name: string; mimeType: string; format: 'markdown'; tokens: number; data: string; } | { - id: string; name: string; mimeType: string; format: 'error'; @@ -10742,8 +10279,6 @@ type ConversionOptions = { images?: EmbeddedImageConversionOptions & { convertOGImage?: boolean; }; - hostname?: string; - cssSelector?: string; }; docx?: { images?: EmbeddedImageConversionOptions; @@ -10881,15 +10416,6 @@ declare namespace TailStream { readonly level: "debug" | "error" | "info" | "log" | "warn"; readonly message: object; } - interface DroppedEventsDiagnostic { - readonly diagnosticsType: "droppedEvents"; - readonly count: number; - } - interface StreamDiagnostic { - readonly type: 'streamDiagnostic'; - // To add new diagnostic types, define a new interface and add it to this union type. - readonly diagnostic: DroppedEventsDiagnostic; - } // This marks the worker handler return information. // This is separate from Outcome because the worker invocation can live for a long time after // returning. For example - Websockets that return an http upgrade response but then continue @@ -10906,7 +10432,7 @@ declare namespace TailStream { readonly type: "attributes"; readonly info: Attribute[]; } - type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | StreamDiagnostic | Return | Attributes; + type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | Return | Attributes; // Context in which this trace event lives. interface SpanContext { // Single id for the entire top-level invocation @@ -10920,7 +10446,7 @@ declare namespace TailStream { // For Hibernate and Mark this would be the span under which they were emitted. // spanId is not set ONLY if: // 1. This is an Onset event - // 2. We are not inheriting any SpanContext. (e.g. this is a cross-account service binding or a new top-level invocation) + // 2. We are not inherting any SpanContext. (e.g. this is a cross-account service binding or a new top-level invocation) readonly spanId?: string; } interface TailEvent { diff --git a/services/notifications/wrangler.jsonc b/services/notifications/wrangler.jsonc index 943bd8176a..ab9c249bc5 100644 --- a/services/notifications/wrangler.jsonc +++ b/services/notifications/wrangler.jsonc @@ -50,12 +50,14 @@ ], }, - "secrets_store_secrets": [ + "services": [ { - "binding": "STREAM_CHAT_API_SECRET", - "store_id": "342a86d9e3a94da698e82d0c6e2a36f0", - "secret_name": "STREAM_CHAT_API_SECRET", + "binding": "EVENT_SERVICE", + "service": "event-service", }, + ], + + "secrets_store_secrets": [ { "binding": "EXPO_ACCESS_TOKEN", "store_id": "342a86d9e3a94da698e82d0c6e2a36f0", From d87c0fb99f8c12693613d3f43aa5a6a20cef2d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 17:58:35 +0200 Subject: [PATCH 05/75] chore(notifications): add vitest scaffold --- services/notifications/package.json | 2 +- services/notifications/src/__tests__/env.d.ts | 3 +++ services/notifications/src/__tests__/setup.ts | 17 ++++++++++++++ services/notifications/vitest.config.mts | 23 +++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 services/notifications/src/__tests__/env.d.ts create mode 100644 services/notifications/src/__tests__/setup.ts diff --git a/services/notifications/package.json b/services/notifications/package.json index a7897326d8..7a6f17769a 100644 --- a/services/notifications/package.json +++ b/services/notifications/package.json @@ -6,7 +6,7 @@ "deploy": "wrangler deploy", "dev": "wrangler dev", "start": "wrangler dev", - "test": "vitest", + "test": "vitest run", "cf-typegen": "wrangler types", "typecheck": "tsgo --noEmit", "lint": "pnpm -w exec oxlint --config .oxlintrc.json services/notifications/src" diff --git a/services/notifications/src/__tests__/env.d.ts b/services/notifications/src/__tests__/env.d.ts new file mode 100644 index 0000000000..1a09f443f6 --- /dev/null +++ b/services/notifications/src/__tests__/env.d.ts @@ -0,0 +1,3 @@ +declare module 'cloudflare:test' { + interface ProvidedEnv extends Env {} +} diff --git a/services/notifications/src/__tests__/setup.ts b/services/notifications/src/__tests__/setup.ts new file mode 100644 index 0000000000..ad1be4336a --- /dev/null +++ b/services/notifications/src/__tests__/setup.ts @@ -0,0 +1,17 @@ +import { vi } from 'vitest'; + +vi.mock('@kilocode/db/client', () => ({ + getWorkerDb: () => ({ + select: () => ({ + from: (table: { _: { name: string } }) => ({ + where: () => { + if (table._.name === 'user_push_tokens') return []; + if (table._.name === 'channel_badge_counts') return [{ total: 0 }]; + return []; + }, + }), + }), + insert: () => ({ values: () => ({ onConflictDoUpdate: async () => undefined }) }), + delete: () => ({ where: async () => undefined }), + }), +})); diff --git a/services/notifications/vitest.config.mts b/services/notifications/vitest.config.mts index d9430c7554..d386792f55 100644 --- a/services/notifications/vitest.config.mts +++ b/services/notifications/vitest.config.mts @@ -1,10 +1,33 @@ import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; +const kCurrentWorker = Symbol.for('miniflare.kCurrentWorker'); + export default defineWorkersConfig({ test: { + passWithNoTests: true, + setupFiles: ['./src/__tests__/setup.ts'], poolOptions: { workers: { wrangler: { configPath: './wrangler.jsonc' }, + miniflare: { + serviceBindings: { + EVENT_SERVICE: 'event-service-stub', + SELF: kCurrentWorker as unknown as string, + }, + workers: [ + { + name: 'event-service-stub', + modules: true, + script: ` + import { WorkerEntrypoint } from 'cloudflare:workers'; + export default class EventServiceStub extends WorkerEntrypoint { + async fetch() { return new Response('ok'); } + async isUserInContext() { return false; } + } + `, + }, + ], + }, }, }, }, From 2a621db8e5ca150f9138d5b5f24c76bc158b7843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:06:04 +0200 Subject: [PATCH 06/75] feat(notifications): rewrite NotificationChannelDO around dispatchPush --- .../src/__tests__/dispatch-push.test.ts | 131 +++++++++ .../src/dos/NotificationChannelDO.ts | 258 ++++++------------ 2 files changed, 211 insertions(+), 178 deletions(-) create mode 100644 services/notifications/src/__tests__/dispatch-push.test.ts diff --git a/services/notifications/src/__tests__/dispatch-push.test.ts b/services/notifications/src/__tests__/dispatch-push.test.ts new file mode 100644 index 0000000000..29160f8b08 --- /dev/null +++ b/services/notifications/src/__tests__/dispatch-push.test.ts @@ -0,0 +1,131 @@ +import { env } from 'cloudflare:test'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { getTableName } from 'drizzle-orm'; +import type { DispatchPushInput } from '@kilocode/notifications'; + +import { sendPushNotifications } from '../lib/expo-push'; +import * as dbClient from '@kilocode/db/client'; + +vi.mock('../lib/expo-push', () => ({ + sendPushNotifications: vi.fn(async () => ({ + ticketTokenPairs: [{ ticket: { status: 'ok', id: 't1' }, token: 'tok1' }], + staleTokens: [], + })), +})); + +type DbState = { + tokens: { user_id: string; token: string }[]; + badgeTotal: number; +}; + +function installDbMock(state: DbState) { + const fakeDb = { + select: (cols?: unknown) => ({ + from: (table: Parameters[0]) => ({ + where: async () => { + if (getTableName(table) === 'user_push_tokens') { + return state.tokens.map(t => ({ token: t.token })); + } + // sum(badge_count) — return single row with `total` + return [{ total: state.badgeTotal }]; + }, + }), + }), + insert: () => ({ + values: () => ({ onConflictDoUpdate: async () => undefined }), + }), + delete: () => ({ where: async () => undefined }), + }; + vi.spyOn(dbClient, 'getWorkerDb').mockReturnValue( + fakeDb as unknown as ReturnType + ); +} + +const baseInput = (over: Partial = {}): DispatchPushInput => ({ + userId: 'user-1', + presenceContext: '/presence/kiloclaw/sb1/conv1', + idempotencyKey: 'k1', + badge: { badgeBucket: 'conv1', delta: 1 }, + push: { + title: 'T', + body: 'B', + data: { type: 'chat.message', sandboxId: 'sb1', conversationId: 'conv1', messageId: 'm1' }, + sound: 'default', + priority: 'high', + }, + ...over, +}); + +function getDO(name = 'conv1') { + const id = env.NOTIFICATION_CHANNEL_DO.idFromName(name); + return env.NOTIFICATION_CHANNEL_DO.get(id); +} + +describe('NotificationChannelDO.dispatchPush', () => { + beforeEach(() => { + vi.mocked(sendPushNotifications).mockClear(); + vi.spyOn(env.EXPO_ACCESS_TOKEN, 'get').mockResolvedValue('test-token'); + }); + + it('returns suppressed_presence when EVENT_SERVICE.isUserInContext is true', async () => { + installDbMock({ tokens: [{ user_id: 'user-1', token: 'tok1' }], badgeTotal: 0 }); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValueOnce(true); + const result = await getDO().dispatchPush(baseInput()); + expect(result.kind).toBe('suppressed_presence'); + expect(sendPushNotifications).not.toHaveBeenCalled(); + }); + + it('returns no_tokens when the user has no push tokens', async () => { + installDbMock({ tokens: [], badgeTotal: 0 }); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValueOnce(false); + const result = await getDO().dispatchPush(baseInput({ userId: 'user-no-tokens' })); + expect(result.kind).toBe('no_tokens'); + expect(sendPushNotifications).not.toHaveBeenCalled(); + }); + + it('delivers, increments badge, writes idempotency key', async () => { + installDbMock({ tokens: [{ user_id: 'u', token: 'tok1' }], badgeTotal: 1 }); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValueOnce(false); + const result = await getDO('conv-deliver').dispatchPush( + baseInput({ idempotencyKey: 'k-deliver' }) + ); + expect(result.kind).toBe('delivered'); + expect(sendPushNotifications).toHaveBeenCalledOnce(); + const [[messages]] = vi.mocked(sendPushNotifications).mock.calls; + expect(messages[0].badge).toBe(1); + }); + + it('returns duplicate when the idempotency key has been seen', async () => { + installDbMock({ tokens: [{ user_id: 'u', token: 'tok1' }], badgeTotal: 1 }); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValue(false); + const stub = getDO('conv-dup'); + const input = baseInput({ idempotencyKey: 'k-dup' }); + await stub.dispatchPush(input); + const second = await stub.dispatchPush(input); + expect(second.kind).toBe('duplicate'); + expect(sendPushNotifications).toHaveBeenCalledOnce(); + }); + + it('skips badge mutation when badge is null', async () => { + installDbMock({ tokens: [{ user_id: 'u', token: 'tok1' }], badgeTotal: 0 }); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValueOnce(false); + const result = await getDO('conv-no-badge').dispatchPush( + baseInput({ badge: null, idempotencyKey: 'k-no-badge' }) + ); + expect(result.kind).toBe('delivered'); + const [[messages]] = vi.mocked(sendPushNotifications).mock.calls; + expect(messages[0].badge).toBeUndefined(); + }); + + it('does not write idempotency key on Expo failure', async () => { + installDbMock({ tokens: [{ user_id: 'u', token: 'tok1' }], badgeTotal: 0 }); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValue(false); + vi.mocked(sendPushNotifications).mockRejectedValueOnce(new Error('boom')); + const stub = getDO('conv-fail'); + const input = baseInput({ idempotencyKey: 'k-fail', badge: null }); + const first = await stub.dispatchPush(input); + expect(first.kind).toBe('failed'); + const second = await stub.dispatchPush(input); + expect(second.kind).not.toBe('duplicate'); + }); +}); diff --git a/services/notifications/src/dos/NotificationChannelDO.ts b/services/notifications/src/dos/NotificationChannelDO.ts index 3568f9a8e7..ee73d09944 100644 --- a/services/notifications/src/dos/NotificationChannelDO.ts +++ b/services/notifications/src/dos/NotificationChannelDO.ts @@ -1,210 +1,112 @@ import { DurableObject } from 'cloudflare:workers'; import { getWorkerDb } from '@kilocode/db/client'; -import { badge_counts, kiloclaw_instances, user_push_tokens } from '@kilocode/db/schema'; -import { badgeBucketForInstance } from '@kilocode/notifications'; -import { and, eq, inArray, isNull, sql, sum } from 'drizzle-orm'; -import type { Event } from 'stream-chat'; +import { badge_counts, user_push_tokens } from '@kilocode/db/schema'; +import { type DispatchPushInput, type DispatchPushOutcome } from '@kilocode/notifications'; +import { eq, inArray, sql, sum } from 'drizzle-orm'; import type { ExpoPushMessage, TicketTokenPair } from '../lib/expo-push'; import { sendPushNotifications } from '../lib/expo-push'; -type ReceiptCheckMessage = { - ticketTokenPairs: TicketTokenPair[]; -}; +type ReceiptCheckMessage = { ticketTokenPairs: TicketTokenPair[] }; -type PendingMessage = { - messageId: string; - senderId: string; - text: string; - notified: boolean; - createdAt: number; - updatedAt: string; // ISO timestamp from Stream Chat payload -}; - -const DEDUP_PREFIX = 'dedup:'; -const MSG_PREFIX = 'msg:'; -const DEDUP_TTL_MS = 60 * 60 * 1000; // 1 hour -const DEBOUNCE_MS = 10_000; // 10 seconds +const IDEM_PREFIX = 'idem:'; +const IDEM_TTL_MS = 60 * 60 * 1000; // 1 hour export class NotificationChannelDO extends DurableObject { - async processWebhook(payload: Event, webhookId: string): Promise { - // Webhook-level dedup (prevents reprocessing the same delivery) - const existing = await this.ctx.storage.get(`${DEDUP_PREFIX}${webhookId}`); - if (existing) { - return Response.json({ ok: true, deduplicated: true }); - } - await this.markWebhookSeen(webhookId); - - const messageId = payload.message?.id; - const senderId = payload.message?.user?.id; - const messageText = payload.message?.text ?? ''; - const messageUpdatedAt = payload.message?.updated_at ?? payload.created_at ?? ''; - - if (!messageId || !senderId?.startsWith('bot-')) { - return Response.json({ ok: true }); - } - - const msgKey = `${MSG_PREFIX}${messageId}`; - const pendingMessage = await this.ctx.storage.get(msgKey); - - if (pendingMessage?.notified) { - return Response.json({ ok: true }); - } - - if (pendingMessage) { - // Only accept if this event is newer than what we have - if (messageUpdatedAt <= pendingMessage.updatedAt) { - return Response.json({ ok: true }); - } - if (messageText) { - pendingMessage.text = messageText; - } - pendingMessage.updatedAt = messageUpdatedAt; - await this.ctx.storage.put(msgKey, pendingMessage); - await this.scheduleAlarm(DEBOUNCE_MS); - } else { - // First event for this message (could be message.new or a late message.updated) - const pending: PendingMessage = { - messageId, - senderId, - text: messageText, - notified: false, - createdAt: Date.now(), - updatedAt: messageUpdatedAt, - }; - await this.ctx.storage.put(msgKey, pending); - await this.scheduleAlarm(DEBOUNCE_MS); - } - - return Response.json({ ok: true }); - } - - override async alarm(): Promise { - // Prune expired dedup entries - const dedupEntries = await this.ctx.storage.list({ prefix: DEDUP_PREFIX }); - const now = Date.now(); - const expired: string[] = []; - for (const [key, timestamp] of dedupEntries) { - if (now - timestamp > DEDUP_TTL_MS) { - expired.push(key); - } - } - if (expired.length > 0) { - await this.ctx.storage.delete(expired); - } + async dispatchPush(input: DispatchPushInput): Promise { + // 1. Idempotency. DO is single-threaded, requests for a given conversation + // serialize on this instance. A `failed` outcome does NOT write the + // idempotency key, so the next attempt can retry. + const idemKey = `${IDEM_PREFIX}${input.idempotencyKey}`; + const seen = await this.ctx.storage.get(idemKey); + if (seen) return { kind: 'duplicate' }; + + // 2. Presence + const inContext = await this.env.EVENT_SERVICE.isUserInContext( + input.userId, + input.presenceContext + ); + if (inContext) return { kind: 'suppressed_presence' }; - // Process pending messages that have debounced - const pendingEntries = await this.ctx.storage.list({ prefix: MSG_PREFIX }); - for (const [key, msg] of pendingEntries) { - if (msg.notified) { - // Clean up old notified messages - if (now - msg.createdAt > DEDUP_TTL_MS) { - await this.ctx.storage.delete(key); - } - continue; - } - - if (!msg.text) { - // No text — nothing to notify about, discard - await this.ctx.storage.delete(key); - continue; - } - - await this.sendNotification(msg); - msg.notified = true; - await this.ctx.storage.put(key, msg); - } - } - - private async sendNotification(msg: PendingMessage): Promise { - const sandboxId = msg.senderId.slice(4); const db = getWorkerDb(this.env.HYPERDRIVE.connectionString); - const [instance] = await db - .select({ - id: kiloclaw_instances.id, - user_id: kiloclaw_instances.user_id, - name: kiloclaw_instances.name, - }) - .from(kiloclaw_instances) - .where( - and(eq(kiloclaw_instances.sandbox_id, sandboxId), isNull(kiloclaw_instances.destroyed_at)) - ) - .limit(1); - - if (!instance) { - return; - } - - // Increment the badge count for this bucket and return the new total across all buckets. - // Done before the token guard so unread state is always persisted even if the user - // temporarily has no registered push tokens (e.g. between reinstalls). - // Uses UPSERT so the row is created on first notification for this bucket. - const badgeBucket = badgeBucketForInstance(sandboxId); - await db - .insert(badge_counts) - .values({ user_id: instance.user_id, badge_bucket: badgeBucket, badge_count: 1 }) - .onConflictDoUpdate({ - target: [badge_counts.user_id, badge_counts.badge_bucket], - set: { badge_count: sql`${badge_counts.badge_count} + 1` }, - }); - - const [totals] = await db - .select({ total: sum(badge_counts.badge_count) }) - .from(badge_counts) - .where(eq(badge_counts.user_id, instance.user_id)); - - const badgeCount = Number(totals?.total ?? 0); - + // 3. Tokens const tokens = await db .select({ token: user_push_tokens.token }) .from(user_push_tokens) - .where(eq(user_push_tokens.user_id, instance.user_id)); - - if (tokens.length === 0) { - return; + .where(eq(user_push_tokens.user_id, input.userId)); + + if (tokens.length === 0) return { kind: 'no_tokens' }; + + // 4. Badge math (only if badge is set). + let badgeTotal: number | undefined; + if (input.badge) { + await db + .insert(badge_counts) + .values({ + user_id: input.userId, + badge_bucket: input.badge.badgeBucket, + badge_count: input.badge.delta, + }) + .onConflictDoUpdate({ + target: [badge_counts.user_id, badge_counts.badge_bucket], + set: { badge_count: sql`${badge_counts.badge_count} + ${input.badge.delta}` }, + }); + const [totals] = await db + .select({ total: sum(badge_counts.badge_count) }) + .from(badge_counts) + .where(eq(badge_counts.user_id, input.userId)); + badgeTotal = Number(totals?.total ?? 0); } - const truncatedMessage = msg.text.length > 100 ? msg.text.slice(0, 97) + '...' : msg.text; - + // 5. Send via Expo const messages: ExpoPushMessage[] = tokens.map(({ token }) => ({ to: token, - title: instance.name ?? 'KiloClaw', - body: truncatedMessage, - // Keep in sync with NotificationData in apps/mobile/src/lib/notifications.ts - data: { type: 'chat', instanceId: sandboxId }, - badge: badgeCount, - sound: 'default' as const, - priority: 'high' as const, + title: input.push.title, + body: input.push.body, + data: input.push.data, + ...(badgeTotal !== undefined && { badge: badgeTotal }), + sound: input.push.sound ?? undefined, + priority: input.push.priority ?? 'default', })); const accessToken = await this.env.EXPO_ACCESS_TOKEN.get(); - const { ticketTokenPairs, staleTokens } = await sendPushNotifications(messages, accessToken); + let result: { ticketTokenPairs: TicketTokenPair[]; staleTokens: string[] }; + try { + result = await sendPushNotifications(messages, accessToken); + } catch (err) { + // Intentionally do NOT write the idempotency key on failure — let + // upstream retry. The DO's single-threading prevents concurrent + // double-sends within the same conversation. + return { + kind: 'failed', + error: err instanceof Error ? err.message : String(err), + }; + } - if (staleTokens.length > 0) { - await db.delete(user_push_tokens).where(inArray(user_push_tokens.token, staleTokens)); + if (result.staleTokens.length > 0) { + await db.delete(user_push_tokens).where(inArray(user_push_tokens.token, result.staleTokens)); } - if (ticketTokenPairs.length > 0) { - const receiptMsg: ReceiptCheckMessage = { ticketTokenPairs }; + if (result.ticketTokenPairs.length > 0) { + const receiptMsg: ReceiptCheckMessage = { ticketTokenPairs: result.ticketTokenPairs }; await this.env.RECEIPTS_QUEUE.send(receiptMsg, { delaySeconds: 900 }); } - } - private async markWebhookSeen(webhookId: string): Promise { - await this.ctx.storage.put(`${DEDUP_PREFIX}${webhookId}`, Date.now()); - } + // 6. Idempotency write — only after a successful send. + await this.ctx.storage.put(idemKey, Date.now()); + await this.ctx.storage.setAlarm(Date.now() + IDEM_TTL_MS); - private async scheduleAlarm(delayMs: number): Promise { - // Always reset the alarm to the new debounce window - await this.ctx.storage.setAlarm(Date.now() + delayMs); + return { kind: 'delivered', tokenCount: tokens.length }; } -} -export function getNotificationChannelDO( - env: Env, - channelId: string -): DurableObjectStub { - const id = env.NOTIFICATION_CHANNEL_DO.idFromName(channelId); - return env.NOTIFICATION_CHANNEL_DO.get(id) as DurableObjectStub; + override async alarm(): Promise { + const now = Date.now(); + const entries = await this.ctx.storage.list({ prefix: IDEM_PREFIX }); + const expired: string[] = []; + for (const [key, ts] of entries) { + if (now - ts > IDEM_TTL_MS) expired.push(key); + } + if (expired.length > 0) await this.ctx.storage.delete(expired); + } } From 26fccf55c21829da2f32395433cdc0f6eb4d41f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:06:20 +0200 Subject: [PATCH 07/75] chore(notifications): drop orphan badgeBucketForInstance helper --- packages/notifications/src/badge-buckets.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/notifications/src/badge-buckets.ts b/packages/notifications/src/badge-buckets.ts index bb3213307c..56917b3d34 100644 --- a/packages/notifications/src/badge-buckets.ts +++ b/packages/notifications/src/badge-buckets.ts @@ -5,7 +5,5 @@ * don't collide as more surfaces start emitting badge updates. */ -export const badgeBucketForInstance = (sandboxId: string) => `kiloclaw:${sandboxId}` as const; - export const badgeBucketForConversation = (sandboxId: string, conversationId: string) => `kiloclaw:${sandboxId}:${conversationId}` as const; From 7fad8792511f0288dc39bc5742a1e2bc7bbe7616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:11:38 +0200 Subject: [PATCH 08/75] feat(notifications): add sendPushForConversation WorkerEntrypoint RPC --- pnpm-lock.yaml | 41 ++++++++- services/notifications/package.json | 1 + services/notifications/src/__tests__/env.d.ts | 6 +- .../send-push-for-conversation.test.ts | 66 +++++++++++++++ services/notifications/src/index.ts | 83 ++++++++++++++++++- 5 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 services/notifications/src/__tests__/send-push-for-conversation.test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00bae11778..40be24ffe2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1561,7 +1561,7 @@ importers: version: 7.0.0-dev.20260319.1 jest: specifier: ^30.3.0 - version: 30.3.0(@types/node@24.12.0)(esbuild-register@3.6.0(esbuild@0.27.4)) + version: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) typescript: specifier: 'catalog:' version: 5.9.3 @@ -2008,6 +2008,9 @@ importers: '@kilocode/db': specifier: workspace:* version: link:../../packages/db + '@kilocode/event-service': + specifier: workspace:* + version: link:../../packages/event-service '@kilocode/notifications': specifier: workspace:* version: link:../../packages/notifications @@ -16744,7 +16747,7 @@ snapshots: cjs-module-lexer: 1.4.3 esbuild: 0.27.4 miniflare: 4.20260310.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) wrangler: 4.72.0(@cloudflare/workers-types@4.20260313.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@cloudflare/workers-types' @@ -22252,7 +22255,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/utils@3.2.4': dependencies: @@ -25683,6 +25686,25 @@ snapshots: - supports-color - ts-node + jest-cli@30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)): + dependencies: + '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.4)) + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) + jest-util: 30.3.0 + jest-validate: 30.3.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jest-config@29.7.0(@types/node@24.12.0): dependencies: '@babel/core': 7.29.0 @@ -26334,6 +26356,19 @@ snapshots: - supports-color - ts-node + jest@30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)): + dependencies: + '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.4)) + '@jest/types': 30.3.0 + import-local: 3.2.0 + jest-cli: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jimp-compact@0.16.1: {} jiti@2.6.1: {} diff --git a/services/notifications/package.json b/services/notifications/package.json index 7a6f17769a..05fed40c18 100644 --- a/services/notifications/package.json +++ b/services/notifications/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@kilocode/db": "workspace:*", + "@kilocode/event-service": "workspace:*", "@kilocode/notifications": "workspace:*", "@kilocode/worker-utils": "workspace:*", "drizzle-orm": "catalog:", diff --git a/services/notifications/src/__tests__/env.d.ts b/services/notifications/src/__tests__/env.d.ts index 1a09f443f6..3257351c91 100644 --- a/services/notifications/src/__tests__/env.d.ts +++ b/services/notifications/src/__tests__/env.d.ts @@ -1,3 +1,7 @@ +import type NotificationsService from '../index'; + declare module 'cloudflare:test' { - interface ProvidedEnv extends Env {} + interface ProvidedEnv extends Env { + SELF: Service; + } } diff --git a/services/notifications/src/__tests__/send-push-for-conversation.test.ts b/services/notifications/src/__tests__/send-push-for-conversation.test.ts new file mode 100644 index 0000000000..8b993be145 --- /dev/null +++ b/services/notifications/src/__tests__/send-push-for-conversation.test.ts @@ -0,0 +1,66 @@ +import { env } from 'cloudflare:test'; +import { describe, expect, it, vi } from 'vitest'; +import type { + DispatchPushInput, + PerRecipientResult, + SendPushForConversationInput, +} from '@kilocode/notifications'; + +import * as do_module from '../dos/NotificationChannelDO'; + +const baseInput = (over: Partial = {}): SendPushForConversationInput => ({ + conversationId: 'conv1', + sandboxId: 'sb1', + senderUserId: 'sender', + recipientUserIds: ['r1', 'r2', 'r2', 'sender'], + title: 'Conv Title', + bodyPreview: 'hello', + messageId: 'm1', + ...over, +}); + +describe('NotificationsService.sendPushForConversation', () => { + it('excludes sender, dedupes, fans out to remaining recipients', async () => { + const stubSpy = vi.fn(async (_input: DispatchPushInput) => ({ + kind: 'delivered' as const, + tokenCount: 1, + })); + vi.spyOn(env.NOTIFICATION_CHANNEL_DO, 'get').mockReturnValue({ + dispatchPush: stubSpy, + } as unknown as DurableObjectStub); + + const result = await env.SELF.sendPushForConversation(baseInput()); + + expect(stubSpy).toHaveBeenCalledTimes(2); // r1, r2 + expect(result.perRecipient.map((r: PerRecipientResult) => r.userId).sort()).toEqual([ + 'r1', + 'r2', + ]); + expect(result.perRecipient.every((r: PerRecipientResult) => r.outcome === 'delivered')).toBe( + true + ); + }); + + it('passes the right presence context and badge bucket', async () => { + const stubSpy = vi.fn(async (_input: DispatchPushInput) => ({ + kind: 'delivered' as const, + tokenCount: 1, + })); + vi.spyOn(env.NOTIFICATION_CHANNEL_DO, 'get').mockReturnValue({ + dispatchPush: stubSpy, + } as unknown as DurableObjectStub); + + await env.SELF.sendPushForConversation(baseInput({ recipientUserIds: ['r1'], senderUserId: null })); + const firstCall = stubSpy.mock.calls[0]; + if (!firstCall) throw new Error('expected dispatchPush to be called'); + const call: DispatchPushInput = firstCall[0]; + expect(call.presenceContext).toBe('/presence/kiloclaw/sb1/conv1'); + expect(call.badge).toEqual({ badgeBucket: 'kiloclaw:sb1:conv1', delta: 1 }); + expect(call.push.data).toEqual({ + type: 'chat.message', + sandboxId: 'sb1', + conversationId: 'conv1', + messageId: 'm1', + }); + }); +}); diff --git a/services/notifications/src/index.ts b/services/notifications/src/index.ts index 231df770e7..37dabfd4f9 100644 --- a/services/notifications/src/index.ts +++ b/services/notifications/src/index.ts @@ -1,14 +1,89 @@ +import { WorkerEntrypoint } from 'cloudflare:workers'; import { Hono } from 'hono'; +import { presenceContextForConversation } from '@kilocode/event-service'; +import { + badgeBucketForConversation, + type DispatchPushInput, + type DispatchPushOutcome, + type PerRecipientResult, + type SendPushForConversationInput, + type SendPushForConversationOutput, +} from '@kilocode/notifications'; + import { queue } from './queue-consumer'; -import { webhooks } from './routes/webhooks'; export { NotificationChannelDO } from './dos/NotificationChannelDO'; const app = new Hono<{ Bindings: Env }>(); +app.get('/', c => c.json({ ok: true })); -app.route('/webhooks', webhooks); +type ConversationDOStub = { + dispatchPush: (input: DispatchPushInput) => Promise; +}; -app.get('/', c => c.json({ ok: true })); +/** Pure core for unit testability. */ +export async function sendPushForConversationCore( + input: SendPushForConversationInput, + deps: { + getConversationDOStub: (conversationId: string) => ConversationDOStub; + } +): Promise { + const recipients: string[] = []; + const seen = new Set(); + for (const id of input.recipientUserIds) { + if (id === input.senderUserId) continue; + if (seen.has(id)) continue; + seen.add(id); + recipients.push(id); + } + + const perRecipient: PerRecipientResult[] = []; + for (const userId of recipients) { + const stub = deps.getConversationDOStub(input.conversationId); + const outcome = await stub.dispatchPush({ + userId, + presenceContext: presenceContextForConversation(input.sandboxId, input.conversationId), + idempotencyKey: `chat:${input.messageId}:${userId}`, + badge: { + badgeBucket: badgeBucketForConversation(input.sandboxId, input.conversationId), + delta: 1, + }, + push: { + title: input.title, + body: input.bodyPreview, + data: { + type: 'chat.message', + sandboxId: input.sandboxId, + conversationId: input.conversationId, + messageId: input.messageId, + }, + sound: 'default', + priority: 'high', + }, + }); + perRecipient.push({ userId, outcome: outcome.kind }); + } + return { perRecipient }; +} + +export default class NotificationsService extends WorkerEntrypoint { + override async fetch(request: Request): Promise { + return app.fetch(request, this.env, this.ctx); + } + + override async queue(batch: MessageBatch): Promise { + return queue(batch as Parameters[0], this.env); + } -export default { fetch: app.fetch, queue }; + async sendPushForConversation( + input: SendPushForConversationInput + ): Promise { + return sendPushForConversationCore(input, { + getConversationDOStub: (conversationId: string) => + this.env.NOTIFICATION_CHANNEL_DO.get( + this.env.NOTIFICATION_CHANNEL_DO.idFromName(conversationId) + ) as unknown as ConversationDOStub, + }); + } +} From f6e1848f83de60d5d2bd911d99079ef1dd4d7f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:13:02 +0200 Subject: [PATCH 09/75] chore(notifications): delete Stream webhook route --- services/notifications/src/routes/webhooks.ts | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 services/notifications/src/routes/webhooks.ts diff --git a/services/notifications/src/routes/webhooks.ts b/services/notifications/src/routes/webhooks.ts deleted file mode 100644 index dc66a30d2d..0000000000 --- a/services/notifications/src/routes/webhooks.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createHmac, timingSafeEqual } from 'node:crypto'; -import { Hono } from 'hono'; -import type { Event } from 'stream-chat'; - -import { getNotificationChannelDO } from '../dos/NotificationChannelDO'; - -const webhooks = new Hono<{ Bindings: Env }>(); - -function verifyWebhookSignature(body: string, signature: string | null, secret: string): boolean { - if (!signature) return false; - - const expectedSignature = createHmac('sha256', secret).update(body).digest('hex'); - - if (signature.length !== expectedSignature.length) return false; - return timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature)); -} - -webhooks.post('/stream-chat', async c => { - const rawBody = await c.req.text(); - const signature = c.req.header('x-signature') ?? null; - const webhookId = c.req.header('x-webhook-id'); - - const secret = await c.env.STREAM_CHAT_API_SECRET.get(); - if (!verifyWebhookSignature(rawBody, signature, secret)) { - return c.json({ error: 'Invalid signature' }, 401); - } - - const payload = JSON.parse(rawBody) as Event; - - // Only handle new and updated messages - if (payload.type !== 'message.new' && payload.type !== 'message.updated') { - return c.json({ ok: true }); - } - - const channelId = payload.channel_id; - if (!channelId || !webhookId) { - return c.json({ ok: true }); - } - - // Forward to the channel's Durable Object for dedup + delivery - const stub = getNotificationChannelDO(c.env, channelId); - return stub.processWebhook(payload, webhookId); -}); - -export { webhooks }; From 3c7c82e483673cd34ea1f5c228b2c7f1bc459377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:13:31 +0200 Subject: [PATCH 10/75] chore(notifications): type EVENT_SERVICE RPC and enable cloudflare:test types --- services/notifications/src/bindings.d.ts | 11 +++++++++++ services/notifications/tsconfig.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/services/notifications/src/bindings.d.ts b/services/notifications/src/bindings.d.ts index 40e773ded4..5797e9eaf4 100644 --- a/services/notifications/src/bindings.d.ts +++ b/services/notifications/src/bindings.d.ts @@ -1,3 +1,14 @@ import type {} from './worker-configuration.d.ts'; +// Augment the wrangler-generated Env with RPC method signatures for service +// bindings. `worker-configuration.d.ts` types these as plain Fetcher; this +// file layers on the RPC shape so call sites don't need runtime casts. +declare global { + interface Env { + EVENT_SERVICE: Fetcher & { + isUserInContext(userId: string, context: string): Promise; + }; + } +} + export type NotificationsEnv = Env; diff --git a/services/notifications/tsconfig.json b/services/notifications/tsconfig.json index 635e98f321..71b42aebcf 100644 --- a/services/notifications/tsconfig.json +++ b/services/notifications/tsconfig.json @@ -14,7 +14,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "types": ["./worker-configuration.d.ts", "node"] + "types": ["./worker-configuration.d.ts", "node", "@cloudflare/vitest-pool-workers"] }, "exclude": ["test"], "include": ["worker-configuration.d.ts", "src/**/*.ts"] From 227b90ea5aa4ed3b290af3b9b245dce8211d6d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:15:48 +0200 Subject: [PATCH 11/75] feat(event-service): add kiloclaw event-context helpers; migrate kilo-chat producer Adds kiloclawInstanceContext and kiloclawConversationContext path builders to @kilocode/event-service, replacing hardcoded template literals in kilo-chat's event-push.ts and its test so all callers share a single source of truth. --- packages/event-service/src/index.ts | 1 + packages/event-service/src/kiloclaw-contexts.ts | 14 ++++++++++++++ services/kilo-chat/package.json | 1 + .../__tests__/conversation-status-routes.test.ts | 3 ++- services/kilo-chat/src/services/event-push.ts | 5 +++-- 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 packages/event-service/src/kiloclaw-contexts.ts diff --git a/packages/event-service/src/index.ts b/packages/event-service/src/index.ts index 3a40272cbe..c2e238f1dd 100644 --- a/packages/event-service/src/index.ts +++ b/packages/event-service/src/index.ts @@ -1,4 +1,5 @@ export { EventServiceClient, WebSocketAuthError, HandshakeTimeoutError } from './client'; export * from './presence'; +export * from './kiloclaw-contexts'; export * from './schemas'; export type * from './types'; diff --git a/packages/event-service/src/kiloclaw-contexts.ts b/packages/event-service/src/kiloclaw-contexts.ts new file mode 100644 index 0000000000..22881389ed --- /dev/null +++ b/packages/event-service/src/kiloclaw-contexts.ts @@ -0,0 +1,14 @@ +/** + * Event-context path builders for kiloclaw event subscriptions. + * + * These are the contexts on which kilo-chat publishes events (message + * created, typing, etc.) and to which clients subscribe to receive + * those events. Distinct from `/presence/*` contexts, which signal + * whether the user is actively on a surface. + */ + +export const kiloclawInstanceContext = (sandboxId: string) => + `/kiloclaw/${sandboxId}` as const; + +export const kiloclawConversationContext = (sandboxId: string, conversationId: string) => + `/kiloclaw/${sandboxId}/${conversationId}` as const; diff --git a/services/kilo-chat/package.json b/services/kilo-chat/package.json index 9eb1492829..031714384a 100644 --- a/services/kilo-chat/package.json +++ b/services/kilo-chat/package.json @@ -27,6 +27,7 @@ "dependencies": { "@kilocode/db": "workspace:*", "@kilocode/encryption": "workspace:*", + "@kilocode/event-service": "workspace:*", "@kilocode/kilo-chat": "workspace:*", "@kilocode/worker-utils": "workspace:*", "drizzle-orm": "catalog:", diff --git a/services/kilo-chat/src/__tests__/conversation-status-routes.test.ts b/services/kilo-chat/src/__tests__/conversation-status-routes.test.ts index 4b6330c0ac..022f803484 100644 --- a/services/kilo-chat/src/__tests__/conversation-status-routes.test.ts +++ b/services/kilo-chat/src/__tests__/conversation-status-routes.test.ts @@ -1,6 +1,7 @@ import { env } from 'cloudflare:test'; import { describe, it, expect, vi } from 'vitest'; import { Hono } from 'hono'; +import { kiloclawConversationContext } from '@kilocode/event-service'; import type { AuthContext } from '../auth'; import { botAuthMiddleware } from '../auth-bot'; import { registerBotRoutes } from '../routes/bot-messages'; @@ -246,7 +247,7 @@ describe('POST /bot/v1/sandboxes/:sandboxId/conversations/:cid/conversation-stat expect(pushEvent).toHaveBeenCalledTimes(1); expect(pushEvent).toHaveBeenCalledWith( userId, - `/kiloclaw/${sandboxId}/${conversationId}`, + kiloclawConversationContext(sandboxId, conversationId), 'conversation.status', { conversationId, diff --git a/services/kilo-chat/src/services/event-push.ts b/services/kilo-chat/src/services/event-push.ts index 423552ed2a..a1491c25f8 100644 --- a/services/kilo-chat/src/services/event-push.ts +++ b/services/kilo-chat/src/services/event-push.ts @@ -4,6 +4,7 @@ import type { BotStatusRequest, ConversationStatusRequest, } from '@kilocode/kilo-chat'; +import { kiloclawConversationContext, kiloclawInstanceContext } from '@kilocode/event-service'; import { formatError, withDORetry } from '@kilocode/worker-utils'; import { logger } from '../util/logger'; import { lookupSandboxOwnerUserId } from './sandbox-ownership'; @@ -26,7 +27,7 @@ export async function pushEventToHumanMembers( ): Promise> { const es = getEventService(env); if (!es) return new Map(); - const context = `/kiloclaw/${sandboxId}/${conversationId}`; + const context = kiloclawConversationContext(sandboxId, conversationId); const results = await Promise.allSettled( humanMemberIds.map(async userId => { @@ -65,7 +66,7 @@ export async function pushInstanceEvent( ): Promise { const es = getEventService(env); if (!es) return; - const context = `/kiloclaw/${sandboxId}`; + const context = kiloclawInstanceContext(sandboxId); const results = await Promise.allSettled( humanMemberIds.map(userId => es.pushEvent(userId, context, event, payload)) From 87f0fabbcfd8b2928689d686d32ae43ede39e31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:16:20 +0200 Subject: [PATCH 12/75] feat(kilo-chat): add fetchSandboxLabel helper --- .../kilo-chat/src/services/sandbox-lookup.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 services/kilo-chat/src/services/sandbox-lookup.ts diff --git a/services/kilo-chat/src/services/sandbox-lookup.ts b/services/kilo-chat/src/services/sandbox-lookup.ts new file mode 100644 index 0000000000..7dfad7055b --- /dev/null +++ b/services/kilo-chat/src/services/sandbox-lookup.ts @@ -0,0 +1,18 @@ +import { getWorkerDb } from '@kilocode/db/client'; +import { kiloclaw_instances } from '@kilocode/db/schema'; +import { and, eq, isNull } from 'drizzle-orm'; + +export async function fetchSandboxLabel( + hyperdriveConnectionString: string, + sandboxId: string +): Promise { + const db = getWorkerDb(hyperdriveConnectionString); + const [row] = await db + .select({ name: kiloclaw_instances.name }) + .from(kiloclaw_instances) + .where( + and(eq(kiloclaw_instances.sandbox_id, sandboxId), isNull(kiloclaw_instances.destroyed_at)) + ) + .limit(1); + return row?.name ?? 'KiloClaw'; +} From 822d3274bbe17d33c262bbf20b998c64d889beee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:17:33 +0200 Subject: [PATCH 13/75] chore(kilo-chat): add NOTIFICATIONS service binding --- services/kilo-chat/worker-configuration.d.ts | 3 ++- services/kilo-chat/wrangler.jsonc | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/services/kilo-chat/worker-configuration.d.ts b/services/kilo-chat/worker-configuration.d.ts index 37bd73ea62..425c447524 100644 --- a/services/kilo-chat/worker-configuration.d.ts +++ b/services/kilo-chat/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 9175d354bbd6fb2004e44ec97e77c151) +// Generated by Wrangler by running `wrangler types` (hash: 3a963525d82eadcc7359b89d1d30e039) // Runtime types generated with workerd@1.20260312.1 2026-04-25 nodejs_compat declare namespace Cloudflare { interface GlobalProps { @@ -15,6 +15,7 @@ declare namespace Cloudflare { SANDBOX_STATUS_DO: DurableObjectNamespace; KILOCLAW: Fetcher /* kiloclaw */; EVENT_SERVICE: Fetcher /* event-service */; + NOTIFICATIONS: Service /* entrypoint NotificationsService from notifications */; } } interface Env extends Cloudflare.Env {} diff --git a/services/kilo-chat/wrangler.jsonc b/services/kilo-chat/wrangler.jsonc index 167e5dce38..b07064c308 100644 --- a/services/kilo-chat/wrangler.jsonc +++ b/services/kilo-chat/wrangler.jsonc @@ -42,6 +42,7 @@ "services": [ { "binding": "KILOCLAW", "service": "kiloclaw" }, { "binding": "EVENT_SERVICE", "service": "event-service" }, + { "binding": "NOTIFICATIONS", "service": "notifications", "entrypoint": "NotificationsService" }, ], "secrets_store_secrets": [ From 372f0a07e2a86f961a68ddfbb38b9c9bc9d1c6d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:25:38 +0200 Subject: [PATCH 14/75] feat(kilo-chat): publish push on message.created via NOTIFICATIONS RPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a chat message is persisted, fire-and-forget a call to NOTIFICATIONS.sendPushForConversation so non-sender human members of the conversation receive a push. Runs after realtime/event-service delivery inside postCommitFanOut, with errors swallowed so push failures cannot fail the send. - Skip when there are no other human recipients or no sandboxId. - senderUserId = callerId for human senders, null for bot senders. - title is " · "; bodyPreview is the first 200 chars of the concatenated text blocks. - Add @kilocode/notifications workspace dep and layer the RPC method shape into Env via bindings.d.ts. - Add a notifications-stub worker to the vitest config so tests can spy on env.NOTIFICATIONS.sendPushForConversation, and globally mock sandbox-lookup in setup.ts (it imports pg via @kilocode/db). --- pnpm-lock.yaml | 6 + services/kilo-chat/package.json | 1 + .../src/__tests__/push-notifications.test.ts | 173 ++++++++++++++++++ services/kilo-chat/src/__tests__/setup.ts | 6 + services/kilo-chat/src/bindings.d.ts | 9 + services/kilo-chat/src/services/messages.ts | 28 +++ services/kilo-chat/src/util/content.ts | 12 ++ services/kilo-chat/vitest.config.mts | 16 ++ 8 files changed, 251 insertions(+) create mode 100644 services/kilo-chat/src/__tests__/push-notifications.test.ts create mode 100644 services/kilo-chat/src/util/content.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40be24ffe2..fb43aabfd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1808,9 +1808,15 @@ importers: '@kilocode/encryption': specifier: workspace:* version: link:../../packages/encryption + '@kilocode/event-service': + specifier: workspace:* + version: link:../../packages/event-service '@kilocode/kilo-chat': specifier: workspace:* version: link:../../packages/kilo-chat + '@kilocode/notifications': + specifier: workspace:* + version: link:../../packages/notifications '@kilocode/worker-utils': specifier: workspace:* version: link:../../packages/worker-utils diff --git a/services/kilo-chat/package.json b/services/kilo-chat/package.json index 031714384a..283de5871b 100644 --- a/services/kilo-chat/package.json +++ b/services/kilo-chat/package.json @@ -29,6 +29,7 @@ "@kilocode/encryption": "workspace:*", "@kilocode/event-service": "workspace:*", "@kilocode/kilo-chat": "workspace:*", + "@kilocode/notifications": "workspace:*", "@kilocode/worker-utils": "workspace:*", "drizzle-orm": "catalog:", "hono": "catalog:", diff --git a/services/kilo-chat/src/__tests__/push-notifications.test.ts b/services/kilo-chat/src/__tests__/push-notifications.test.ts new file mode 100644 index 0000000000..920bae946c --- /dev/null +++ b/services/kilo-chat/src/__tests__/push-notifications.test.ts @@ -0,0 +1,173 @@ +import { env } from 'cloudflare:test'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { ConversationDO } from '../do/conversation-do'; +import { makeApp } from './helpers'; + +// fetchSandboxLabel hits Hyperdrive/pg. Mock it so the push call site doesn't +// need a real DB. Individual tests can override per-test as needed. +vi.mock('../services/sandbox-lookup', () => ({ + fetchSandboxLabel: vi.fn(async () => 'My Sandbox'), +})); + +const sampleContent = [{ type: 'text', text: 'hello there' }]; + +async function waitForCalls(spy: { mock: { calls: unknown[][] } }, timeoutMs = 2000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (spy.mock.calls.length > 0) return; + await new Promise(r => setTimeout(r, 10)); + } +} + +describe('kilo-chat publishes push on message.created', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('does NOT call sendPushForConversation when only the sender is a human member', async () => { + // Single-human conversation: sender + bot. After excluding the sender, + // recipientUserIds is empty, so the push fanout must be skipped. + const sendSpy = vi + .spyOn(env.NOTIFICATIONS, 'sendPushForConversation') + .mockResolvedValue({ perRecipient: [] }); + + const userId = 'user-push-skip-1'; + const sandboxId = 'sandbox-push-skip-1'; + const userApp = makeApp(userId, 'user'); + + const createRes = await userApp.request( + '/v1/conversations', + { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ sandboxId, title: 'Push skip' }), + }, + env + ); + expect(createRes.status).toBe(201); + const { conversationId } = await createRes.json<{ conversationId: string }>(); + + const sendRes = await userApp.request( + '/v1/messages', + { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ conversationId, content: sampleContent }), + }, + env + ); + expect(sendRes.status).toBe(201); + + // Give any waitUntil tasks a chance to fire then assert the push wasn't + // called — there are no human recipients other than the sender. + await new Promise(r => setTimeout(r, 50)); + expect(sendSpy).not.toHaveBeenCalled(); + }); + + it('calls sendPushForConversation with non-sender humans when conversation has multiple humans', async () => { + const sendSpy = vi + .spyOn(env.NOTIFICATIONS, 'sendPushForConversation') + .mockResolvedValue({ perRecipient: [] }); + + const senderId = 'user-push-multi-sender'; + const otherId = 'user-push-multi-other'; + const sandboxId = 'sandbox-push-multi'; + const conversationId = '01KQD0T86VR3M1RPQCF4WBFX1W'; + const botId = `bot:kiloclaw:${sandboxId}`; + + // Seed a multi-human conversation directly via the ConversationDO so we + // can exercise the push fanout's non-sender recipient path. + const convStub: DurableObjectStub = env.CONVERSATION_DO.get( + env.CONVERSATION_DO.idFromName(conversationId) + ); + const initRes = await convStub.initialize({ + id: conversationId, + title: 'Multi-human', + createdBy: senderId, + createdAt: Date.now(), + members: [ + { id: senderId, kind: 'user' }, + { id: otherId, kind: 'user' }, + { id: botId, kind: 'bot' }, + ], + }); + expect(initRes.ok).toBe(true); + + const senderApp = makeApp(senderId, 'user'); + const sendRes = await senderApp.request( + '/v1/messages', + { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ conversationId, content: sampleContent }), + }, + env + ); + expect(sendRes.status).toBe(201); + const { messageId } = await sendRes.json<{ messageId: string }>(); + + await waitForCalls(sendSpy); + expect(sendSpy).toHaveBeenCalledTimes(1); + const call = sendSpy.mock.calls[0][0] as { + conversationId: string; + sandboxId: string; + senderUserId: string | null; + recipientUserIds: string[]; + title: string; + bodyPreview: string; + messageId: string; + }; + expect(call.conversationId).toBe(conversationId); + expect(call.sandboxId).toBe(sandboxId); + expect(call.senderUserId).toBe(senderId); + expect(call.recipientUserIds).toContain(otherId); + expect(call.recipientUserIds).not.toContain(senderId); + expect(call.bodyPreview).toContain('hello there'); + expect(call.title).toContain('My Sandbox'); + expect(call.messageId).toBe(messageId); + }); + + it('does not block the send when sendPushForConversation rejects', async () => { + vi.spyOn(env.NOTIFICATIONS, 'sendPushForConversation').mockRejectedValue( + new Error('downstream blew up') + ); + + const senderId = 'user-push-throw-sender'; + const otherId = 'user-push-throw-other'; + const sandboxId = 'sandbox-push-throw'; + const conversationId = '01KQD0T86WRTBR2NXX0VX3MY1M'; + const botId = `bot:kiloclaw:${sandboxId}`; + + const convStub: DurableObjectStub = env.CONVERSATION_DO.get( + env.CONVERSATION_DO.idFromName(conversationId) + ); + const initRes = await convStub.initialize({ + id: conversationId, + title: 'Throw', + createdBy: senderId, + createdAt: Date.now(), + members: [ + { id: senderId, kind: 'user' }, + { id: otherId, kind: 'user' }, + { id: botId, kind: 'bot' }, + ], + }); + expect(initRes.ok).toBe(true); + + const senderApp = makeApp(senderId, 'user'); + // Even with the push throwing inside the post-commit fan-out, the send + // must still succeed because the failure is swallowed by try/catch. + const sendRes = await senderApp.request( + '/v1/messages', + { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ conversationId, content: sampleContent }), + }, + env + ); + expect(sendRes.status).toBe(201); + const body = await sendRes.json<{ messageId: string }>(); + expect(body.messageId).toBeTruthy(); + }); +}); diff --git a/services/kilo-chat/src/__tests__/setup.ts b/services/kilo-chat/src/__tests__/setup.ts index 897c3abf77..5807193965 100644 --- a/services/kilo-chat/src/__tests__/setup.ts +++ b/services/kilo-chat/src/__tests__/setup.ts @@ -15,3 +15,9 @@ vi.mock('../services/user-lookup', () => ({ invalid: [], })), })); + +// sandbox-lookup imports @kilocode/db/client → pg which doesn't work in the +// Workers runtime. Mock globally so module resolution succeeds. +vi.mock('../services/sandbox-lookup', () => ({ + fetchSandboxLabel: vi.fn(async () => 'Sandbox'), +})); diff --git a/services/kilo-chat/src/bindings.d.ts b/services/kilo-chat/src/bindings.d.ts index 842333f2a1..d6580663b6 100644 --- a/services/kilo-chat/src/bindings.d.ts +++ b/services/kilo-chat/src/bindings.d.ts @@ -1,5 +1,9 @@ import type { z } from 'zod'; import type { chatWebhookRpcSchema, KiloChatEventName } from '@kilocode/kilo-chat'; +import type { + SendPushForConversationInput, + SendPushForConversationOutput, +} from '@kilocode/notifications'; // Augment the wrangler-generated Env with RPC method signatures for service // bindings. `worker-configuration.d.ts` types these as plain Fetcher; this @@ -23,6 +27,11 @@ declare global { payload: unknown ): Promise; }; + NOTIFICATIONS: Fetcher & { + sendPushForConversation( + input: SendPushForConversationInput + ): Promise; + }; } } diff --git a/services/kilo-chat/src/services/messages.ts b/services/kilo-chat/src/services/messages.ts index db5373ac8e..884b31de1e 100644 --- a/services/kilo-chat/src/services/messages.ts +++ b/services/kilo-chat/src/services/messages.ts @@ -10,11 +10,13 @@ import type { ContentBlock, ExecApprovalDecision } from '@kilocode/kilo-chat'; import { formatError, withDORetry } from '@kilocode/worker-utils'; import { logger } from '../util/logger'; +import { contentBlocksToText } from '../util/content'; import { extractConversationContext, pushEventToHumanMembers, pushInstanceEvent, } from './event-push'; +import { fetchSandboxLabel } from './sandbox-lookup'; import type { ConversationInfo } from '../do/conversation-do'; export type DeferCtx = { waitUntil: (p: Promise) => void }; @@ -304,6 +306,32 @@ async function postCommitFanOut( } await Promise.allSettled(instanceEvents); } + + // ── Block E: Push notification fanout ───────────────────────────────── + // Runs after realtime/event-service delivery has been attempted. Sender is + // excluded; bot members are not push recipients (kind=bot, never in + // humanMemberIds). Failures are logged but never propagate — the send has + // already succeeded and any other post-commit work must complete. + const pushRecipients = humanMemberIds.filter(id => id !== callerId); + if (sandboxId !== null && pushRecipients.length > 0) { + try { + const senderUserId = isSenderHuman ? callerId : null; + const bodyPreview = contentBlocksToText(content).slice(0, 200); + const sandboxLabel = await fetchSandboxLabel(env.HYPERDRIVE.connectionString, sandboxId); + const conversationTitle = info.title ?? autoTitle ?? 'Untitled'; + await env.NOTIFICATIONS.sendPushForConversation({ + conversationId, + sandboxId, + senderUserId, + recipientUserIds: pushRecipients, + title: `${sandboxLabel} · ${conversationTitle}`, + bodyPreview, + messageId, + }); + } catch (err) { + logger.error('sendPushForConversation failed', formatError(err)); + } + } } // ─── editMessage ──────────────────────────────────────────────────────────── diff --git a/services/kilo-chat/src/util/content.ts b/services/kilo-chat/src/util/content.ts new file mode 100644 index 0000000000..060e75726a --- /dev/null +++ b/services/kilo-chat/src/util/content.ts @@ -0,0 +1,12 @@ +import type { ContentBlock } from '@kilocode/kilo-chat'; + +/** Concatenates text content blocks into a single string. Skips non-text blocks. */ +export function contentBlocksToText(content: ContentBlock[]): string { + return content + .filter( + (b): b is { type: 'text'; text: string } => + b.type === 'text' && typeof (b as { text?: unknown }).text === 'string' + ) + .map(b => b.text) + .join(''); +} diff --git a/services/kilo-chat/vitest.config.mts b/services/kilo-chat/vitest.config.mts index 7f657f3298..f814c84e0e 100644 --- a/services/kilo-chat/vitest.config.mts +++ b/services/kilo-chat/vitest.config.mts @@ -20,6 +20,7 @@ export default defineWorkersConfig({ serviceBindings: { KILOCLAW: 'kiloclaw-stub', EVENT_SERVICE: 'event-service-stub', + NOTIFICATIONS: 'notifications-stub', KILO_CHAT_SELF: kCurrentWorker as unknown as string, }, workers: [ @@ -59,6 +60,21 @@ export default defineWorkersConfig({ } `, }, + { + name: 'notifications-stub', + modules: true, + script: ` + import { WorkerEntrypoint } from 'cloudflare:workers'; + export default class NotificationsStub extends WorkerEntrypoint { + async fetch(request) { + return new Response('ok'); + } + async sendPushForConversation(input) { + return { perRecipient: [] }; + } + } + `, + }, ], }, }, From 52fe8a691070f1106119290c23483a42a7a25b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 18:53:51 +0200 Subject: [PATCH 15/75] chore(notifications): drop orphan stream-chat dep, refresh worker types, fix test mock - Remove `stream-chat` from `services/notifications/package.json`; the Stream webhook (its only consumer) was deleted earlier in the stack. - Regenerate `worker-configuration.d.ts` so the workerd runtime types match the current toolchain (sibling services were on `1.20260312.1`; this one had drifted to `1.20251217.0` from a stale local cache). - Fix the global test mock to reference the renamed `badge_counts` table; the setup file was authored against the pre-rename name and never matched. - Tidy two pre-existing lint nits in the new test files (`import type` for type-only import, drop unused `cols` parameter). --- .../event-service/src/kiloclaw-contexts.ts | 3 +- pnpm-lock.yaml | 3 - services/notifications/package.json | 1 - .../src/__tests__/dispatch-push.test.ts | 2 +- .../send-push-for-conversation.test.ts | 10 +- services/notifications/src/__tests__/setup.ts | 2 +- .../notifications/worker-configuration.d.ts | 576 ++++++++++++++++-- 7 files changed, 535 insertions(+), 62 deletions(-) diff --git a/packages/event-service/src/kiloclaw-contexts.ts b/packages/event-service/src/kiloclaw-contexts.ts index 22881389ed..afbb6bc665 100644 --- a/packages/event-service/src/kiloclaw-contexts.ts +++ b/packages/event-service/src/kiloclaw-contexts.ts @@ -7,8 +7,7 @@ * whether the user is actively on a surface. */ -export const kiloclawInstanceContext = (sandboxId: string) => - `/kiloclaw/${sandboxId}` as const; +export const kiloclawInstanceContext = (sandboxId: string) => `/kiloclaw/${sandboxId}` as const; export const kiloclawConversationContext = (sandboxId: string, conversationId: string) => `/kiloclaw/${sandboxId}/${conversationId}` as const; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb43aabfd6..3ea3a33947 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2032,9 +2032,6 @@ importers: hono: specifier: ^4.12.7 version: 4.12.8 - stream-chat: - specifier: 'catalog:' - version: 9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: specifier: 'catalog:' version: 4.3.6 diff --git a/services/notifications/package.json b/services/notifications/package.json index 05fed40c18..a304e241c6 100644 --- a/services/notifications/package.json +++ b/services/notifications/package.json @@ -28,7 +28,6 @@ "drizzle-orm": "catalog:", "expo-server-sdk": "^6.1.0", "hono": "catalog:", - "stream-chat": "catalog:", "zod": "catalog:" } } diff --git a/services/notifications/src/__tests__/dispatch-push.test.ts b/services/notifications/src/__tests__/dispatch-push.test.ts index 29160f8b08..d00f981684 100644 --- a/services/notifications/src/__tests__/dispatch-push.test.ts +++ b/services/notifications/src/__tests__/dispatch-push.test.ts @@ -20,7 +20,7 @@ type DbState = { function installDbMock(state: DbState) { const fakeDb = { - select: (cols?: unknown) => ({ + select: () => ({ from: (table: Parameters[0]) => ({ where: async () => { if (getTableName(table) === 'user_push_tokens') { diff --git a/services/notifications/src/__tests__/send-push-for-conversation.test.ts b/services/notifications/src/__tests__/send-push-for-conversation.test.ts index 8b993be145..2eb5365773 100644 --- a/services/notifications/src/__tests__/send-push-for-conversation.test.ts +++ b/services/notifications/src/__tests__/send-push-for-conversation.test.ts @@ -6,9 +6,11 @@ import type { SendPushForConversationInput, } from '@kilocode/notifications'; -import * as do_module from '../dos/NotificationChannelDO'; +import type * as do_module from '../dos/NotificationChannelDO'; -const baseInput = (over: Partial = {}): SendPushForConversationInput => ({ +const baseInput = ( + over: Partial = {} +): SendPushForConversationInput => ({ conversationId: 'conv1', sandboxId: 'sb1', senderUserId: 'sender', @@ -50,7 +52,9 @@ describe('NotificationsService.sendPushForConversation', () => { dispatchPush: stubSpy, } as unknown as DurableObjectStub); - await env.SELF.sendPushForConversation(baseInput({ recipientUserIds: ['r1'], senderUserId: null })); + await env.SELF.sendPushForConversation( + baseInput({ recipientUserIds: ['r1'], senderUserId: null }) + ); const firstCall = stubSpy.mock.calls[0]; if (!firstCall) throw new Error('expected dispatchPush to be called'); const call: DispatchPushInput = firstCall[0]; diff --git a/services/notifications/src/__tests__/setup.ts b/services/notifications/src/__tests__/setup.ts index ad1be4336a..d54904d430 100644 --- a/services/notifications/src/__tests__/setup.ts +++ b/services/notifications/src/__tests__/setup.ts @@ -6,7 +6,7 @@ vi.mock('@kilocode/db/client', () => ({ from: (table: { _: { name: string } }) => ({ where: () => { if (table._.name === 'user_push_tokens') return []; - if (table._.name === 'channel_badge_counts') return [{ total: 0 }]; + if (table._.name === 'badge_counts') return [{ total: 0 }]; return []; }, }), diff --git a/services/notifications/worker-configuration.d.ts b/services/notifications/worker-configuration.d.ts index 6a3ebcce40..0e8a5f8948 100644 --- a/services/notifications/worker-configuration.d.ts +++ b/services/notifications/worker-configuration.d.ts @@ -1,17 +1,17 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 0819ae5a59d68074ad7e23557cbc10b7) -// Runtime types generated with workerd@1.20251217.0 2026-02-01 nodejs_compat +// Generated by Wrangler by running `wrangler types` (hash: 35f3a1e5a589a3db24bda461e9af3ff0) +// Runtime types generated with workerd@1.20260312.1 2026-02-01 nodejs_compat declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/index"); durableNamespaces: "NotificationChannelDO"; } interface Env { - NOTIFICATION_CHANNEL_DO: DurableObjectNamespace; + HYPERDRIVE: Hyperdrive; + RECEIPTS_QUEUE: Queue; EXPO_ACCESS_TOKEN: SecretsStoreSecret; + NOTIFICATION_CHANNEL_DO: DurableObjectNamespace; EVENT_SERVICE: Fetcher /* event-service */; - RECEIPTS_QUEUE: Queue; - HYPERDRIVE: Hyperdrive; } } interface Env extends Cloudflare.Env {} @@ -439,22 +439,22 @@ interface ExecutionContext { readonly exports: Cloudflare.Exports; readonly props: Props; } -type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; -type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; -type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; -interface ExportedHandler { - fetch?: ExportedHandlerFetchHandler; - tail?: ExportedHandlerTailHandler; - trace?: ExportedHandlerTraceHandler; - tailStream?: ExportedHandlerTailStreamHandler; - scheduled?: ExportedHandlerScheduledHandler; - test?: ExportedHandlerTestHandler; - email?: EmailExportedHandler; - queue?: ExportedHandlerQueueHandler; +type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; +type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; +type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler; + tail?: ExportedHandlerTailHandler; + trace?: ExportedHandlerTraceHandler; + tailStream?: ExportedHandlerTailStreamHandler; + scheduled?: ExportedHandlerScheduledHandler; + test?: ExportedHandlerTestHandler; + email?: EmailExportedHandler; + queue?: ExportedHandlerQueueHandler; } interface StructuredSerializeOptions { transfer?: any[]; @@ -502,8 +502,10 @@ interface DurableObjectNamespaceNewUniqueIdOptions { jurisdiction?: DurableObjectJurisdiction; } type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me"; +type DurableObjectRoutingMode = "primary-only"; interface DurableObjectNamespaceGetDurableObjectOptions { locationHint?: DurableObjectLocationHint; + routingMode?: DurableObjectRoutingMode; } interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> { } @@ -1391,6 +1393,12 @@ declare abstract class PromiseRejectionEvent extends Event { */ declare class FormData { constructor(); + /** + * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) + */ + append(name: string, value: string | Blob): void; /** * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. * @@ -1427,6 +1435,12 @@ declare class FormData { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has) */ has(name: string): boolean; + /** + * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) + */ + set(name: string, value: string | Blob): void; /** * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. * @@ -1753,7 +1767,7 @@ interface Request> e * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal) */ signal: AbortSignal; - cf: Cf | undefined; + cf?: Cf; /** * The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request. * @@ -2088,6 +2102,8 @@ interface Transformer { expectedLength?: number; } interface StreamPipeOptions { + preventAbort?: boolean; + preventCancel?: boolean; /** * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. * @@ -2106,8 +2122,6 @@ interface StreamPipeOptions { * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set. */ preventClose?: boolean; - preventAbort?: boolean; - preventCancel?: boolean; signal?: AbortSignal; } type ReadableStreamReadResult = { @@ -2382,13 +2396,13 @@ declare abstract class TransformStreamDefaultController { terminate(): void; } interface ReadableWritablePair { + readable: ReadableStream; /** * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use. * * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. */ writable: WritableStream; - readable: ReadableStream; } /** * The **`WritableStream`** interface of the Streams API provides a standard abstraction for writing streaming data to a destination, known as a sink. @@ -3036,7 +3050,7 @@ declare var WebSocket: { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) */ interface WebSocket extends EventTarget { - accept(): void; + accept(options?: WebSocketAcceptOptions): void; /** * The **`WebSocket.send()`** method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of `bufferedAmount` by the number of bytes needed to contain the data. * @@ -3075,6 +3089,22 @@ interface WebSocket extends EventTarget { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions) */ extensions: string | null; + /** + * The **`WebSocket.binaryType`** property controls the type of binary data being received over the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) + */ + binaryType: "blob" | "arraybuffer"; +} +interface WebSocketAcceptOptions { + /** + * When set to `true`, receiving a server-initiated WebSocket Close frame will not + * automatically send a reciprocal Close frame, leaving the connection in a half-open + * state. This is useful for proxying scenarios where you need to coordinate closing + * both sides independently. Defaults to `false` when the + * `no_web_socket_half_open_by_default` compatibility flag is enabled. + */ + allowHalfOpen?: boolean; } declare const WebSocketPair: { new (): { @@ -3193,6 +3223,8 @@ interface Container { signal(signo: number): void; getTcpPort(port: number): Fetcher; setInactivityTimeout(durationMs: number | bigint): Promise; + interceptOutboundHttp(addr: string, binding: Fetcher): Promise; + interceptAllOutboundHttp(binding: Fetcher): Promise; } interface ContainerStartupOptions { entrypoint?: string[]; @@ -3318,6 +3350,181 @@ declare abstract class Performance { get timeOrigin(): number; /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ now(): number; + /** + * The **`toJSON()`** method of the Performance interface is a Serialization; it returns a JSON representation of the Performance object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Performance/toJSON) + */ + toJSON(): object; +} +// AI Search V2 API Error Interfaces +interface AiSearchInternalError extends Error { +} +interface AiSearchNotFoundError extends Error { +} +interface AiSearchNameNotSetError extends Error { +} +// AI Search V2 Request Types +type AiSearchSearchRequest = { + messages: Array<{ + role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; + content: string | null; + }>; + ai_search_options?: { + retrieval?: { + retrieval_type?: 'vector' | 'keyword' | 'hybrid'; + /** Match threshold (0-1, default 0.4) */ + match_threshold?: number; + /** Maximum number of results (1-50, default 10) */ + max_num_results?: number; + filters?: VectorizeVectorMetadataFilter; + /** Context expansion (0-3, default 0) */ + context_expansion?: number; + [key: string]: unknown; + }; + query_rewrite?: { + enabled?: boolean; + model?: string; + rewrite_prompt?: string; + [key: string]: unknown; + }; + reranking?: { + /** Enable reranking (default false) */ + enabled?: boolean; + model?: '@cf/baai/bge-reranker-base' | ''; + /** Match threshold (0-1, default 0.4) */ + match_threshold?: number; + [key: string]: unknown; + }; + [key: string]: unknown; + }; +}; +type AiSearchChatCompletionsRequest = { + messages: Array<{ + role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; + content: string | null; + }>; + model?: string; + stream?: boolean; + ai_search_options?: { + retrieval?: { + retrieval_type?: 'vector' | 'keyword' | 'hybrid'; + match_threshold?: number; + max_num_results?: number; + filters?: VectorizeVectorMetadataFilter; + context_expansion?: number; + [key: string]: unknown; + }; + query_rewrite?: { + enabled?: boolean; + model?: string; + rewrite_prompt?: string; + [key: string]: unknown; + }; + reranking?: { + enabled?: boolean; + model?: '@cf/baai/bge-reranker-base' | ''; + match_threshold?: number; + [key: string]: unknown; + }; + [key: string]: unknown; + }; + [key: string]: unknown; +}; +// AI Search V2 Response Types +type AiSearchSearchResponse = { + search_query: string; + chunks: Array<{ + id: string; + type: string; + /** Match score (0-1) */ + score: number; + text: string; + item: { + timestamp?: number; + key: string; + metadata?: Record; + }; + scoring_details?: { + /** Keyword match score (0-1) */ + keyword_score?: number; + /** Vector similarity score (0-1) */ + vector_score?: number; + }; + }>; +}; +type AiSearchListResponse = Array<{ + id: string; + internal_id?: string; + account_id?: string; + account_tag?: string; + /** Whether the instance is enabled (default true) */ + enable?: boolean; + type?: 'r2' | 'web-crawler'; + source?: string; + [key: string]: unknown; +}>; +type AiSearchConfig = { + /** Instance ID (1-32 chars, pattern: ^[a-z0-9_]+(?:-[a-z0-9_]+)*$) */ + id: string; + type: 'r2' | 'web-crawler'; + source: string; + source_params?: object; + /** Token ID (UUID format) */ + token_id?: string; + ai_gateway_id?: string; + /** Enable query rewriting (default false) */ + rewrite_query?: boolean; + /** Enable reranking (default false) */ + reranking?: boolean; + embedding_model?: string; + ai_search_model?: string; +}; +type AiSearchInstance = { + id: string; + enable?: boolean; + type?: 'r2' | 'web-crawler'; + source?: string; + [key: string]: unknown; +}; +// AI Search Instance Service - Instance-level operations +declare abstract class AiSearchInstanceService { + /** + * Search the AI Search instance for relevant chunks. + * @param params Search request with messages and AI search options + * @returns Search response with matching chunks + */ + search(params: AiSearchSearchRequest): Promise; + /** + * Generate chat completions with AI Search context. + * @param params Chat completions request with optional streaming + * @returns Response object (if streaming) or chat completion result + */ + chatCompletions(params: AiSearchChatCompletionsRequest): Promise; + /** + * Delete this AI Search instance. + */ + delete(): Promise; +} +// AI Search Account Service - Account-level operations +declare abstract class AiSearchAccountService { + /** + * List all AI Search instances in the account. + * @returns Array of AI Search instances + */ + list(): Promise; + /** + * Get an AI Search instance by ID. + * @param name Instance ID + * @returns Instance service for performing operations + */ + get(name: string): AiSearchInstanceService; + /** + * Create a new AI Search instance. + * @param config Instance configuration + * @returns Instance service for performing operations + */ + create(config: AiSearchConfig): Promise; } type AiImageClassificationInput = { image: number[]; @@ -5507,7 +5714,7 @@ interface Ai_Cf_Qwen_Qwq_32B_Messages { }; })[]; /** - * JSON schema that should be fufilled for the response. + * JSON schema that should be fulfilled for the response. */ guided_json?: object; /** @@ -5773,7 +5980,7 @@ interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages { }; })[]; /** - * JSON schema that should be fufilled for the response. + * JSON schema that should be fulfilled for the response. */ guided_json?: object; /** @@ -5864,7 +6071,7 @@ interface Ai_Cf_Google_Gemma_3_12B_It_Prompt { */ prompt: string; /** - * JSON schema that should be fufilled for the response. + * JSON schema that should be fulfilled for the response. */ guided_json?: object; /** @@ -6023,7 +6230,7 @@ interface Ai_Cf_Google_Gemma_3_12B_It_Messages { }; })[]; /** - * JSON schema that should be fufilled for the response. + * JSON schema that should be fulfilled for the response. */ guided_json?: object; /** @@ -6295,7 +6502,7 @@ interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages { })[]; response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; /** - * JSON schema that should be fufilled for the response. + * JSON schema that should be fulfilled for the response. */ guided_json?: object; /** @@ -6525,7 +6732,7 @@ interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner { })[]; response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; /** - * JSON schema that should be fufilled for the response. + * JSON schema that should be fulfilled for the response. */ guided_json?: object; /** @@ -7575,7 +7782,7 @@ interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input { */ text: string | string[]; /** - * Target langauge to translate to + * Target language to translate to */ target_language: "asm_Beng" | "awa_Deva" | "ben_Beng" | "bho_Deva" | "brx_Deva" | "doi_Deva" | "eng_Latn" | "gom_Deva" | "gon_Deva" | "guj_Gujr" | "hin_Deva" | "hne_Deva" | "kan_Knda" | "kas_Arab" | "kas_Deva" | "kha_Latn" | "lus_Latn" | "mag_Deva" | "mai_Deva" | "mal_Mlym" | "mar_Deva" | "mni_Beng" | "mni_Mtei" | "npi_Deva" | "ory_Orya" | "pan_Guru" | "san_Deva" | "sat_Olck" | "snd_Arab" | "snd_Deva" | "tam_Taml" | "tel_Telu" | "urd_Arab" | "unr_Deva"; } @@ -8506,6 +8713,48 @@ type AiModelListType = Record; declare abstract class Ai { aiGatewayLogId: string | null; gateway(gatewayId: string): AiGateway; + /** + * Access the AI Search API for managing AI-powered search instances. + * + * This is the new API that replaces AutoRAG with better namespace separation: + * - Account-level operations: `list()`, `create()` + * - Instance-level operations: `get(id).search()`, `get(id).chatCompletions()`, `get(id).delete()` + * + * @example + * ```typescript + * // List all AI Search instances + * const instances = await env.AI.aiSearch.list(); + * + * // Search an instance + * const results = await env.AI.aiSearch.get('my-search').search({ + * messages: [{ role: 'user', content: 'What is the policy?' }], + * ai_search_options: { + * retrieval: { max_num_results: 10 } + * } + * }); + * + * // Generate chat completions with AI Search context + * const response = await env.AI.aiSearch.get('my-search').chatCompletions({ + * messages: [{ role: 'user', content: 'What is the policy?' }], + * model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast' + * }); + * ``` + */ + aiSearch(): AiSearchAccountService; + /** + * @deprecated AutoRAG has been replaced by AI Search. + * Use `env.AI.aiSearch` instead for better API design and new features. + * + * Migration guide: + * - `env.AI.autorag().list()` → `env.AI.aiSearch.list()` + * - `env.AI.autorag('id').search({ query: '...' })` → `env.AI.aiSearch.get('id').search({ messages: [{ role: 'user', content: '...' }] })` + * - `env.AI.autorag('id').aiSearch(...)` → `env.AI.aiSearch.get('id').chatCompletions(...)` + * + * Note: The old API continues to work for backwards compatibility, but new projects should use AI Search. + * + * @see AiSearchAccountService + * @param autoragId Optional instance ID (omit for account-level operations) + */ autorag(autoragId: string): AutoRAG; run(model: Name, inputs: InputOptions, options?: Options): Promise; getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line } +/** + * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchInternalError instead. + * @see AiSearchInternalError + */ interface AutoRAGInternalError extends Error { } +/** + * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchNotFoundError instead. + * @see AiSearchNotFoundError + */ interface AutoRAGNotFoundError extends Error { } +/** + * @deprecated This error type is no longer used in the AI Search API. + */ interface AutoRAGUnauthorizedError extends Error { } +/** + * @deprecated AutoRAG has been replaced by AI Search. Use AiSearchNameNotSetError instead. + * @see AiSearchNameNotSetError + */ interface AutoRAGNameNotSetError extends Error { } type ComparisonFilter = { @@ -8631,6 +8895,11 @@ type CompoundFilter = { type: 'and' | 'or'; filters: ComparisonFilter[]; }; +/** + * @deprecated AutoRAG has been replaced by AI Search. + * Use AiSearchSearchRequest with the new API instead. + * @see AiSearchSearchRequest + */ type AutoRagSearchRequest = { query: string; filters?: CompoundFilter | ComparisonFilter; @@ -8645,13 +8914,28 @@ type AutoRagSearchRequest = { }; rewrite_query?: boolean; }; +/** + * @deprecated AutoRAG has been replaced by AI Search. + * Use AiSearchChatCompletionsRequest with the new API instead. + * @see AiSearchChatCompletionsRequest + */ type AutoRagAiSearchRequest = AutoRagSearchRequest & { stream?: boolean; system_prompt?: string; }; +/** + * @deprecated AutoRAG has been replaced by AI Search. + * Use AiSearchChatCompletionsRequest with stream: true instead. + * @see AiSearchChatCompletionsRequest + */ type AutoRagAiSearchRequestStreaming = Omit & { stream: true; }; +/** + * @deprecated AutoRAG has been replaced by AI Search. + * Use AiSearchSearchResponse with the new API instead. + * @see AiSearchSearchResponse + */ type AutoRagSearchResponse = { object: 'vector_store.search_results.page'; search_query: string; @@ -8668,6 +8952,11 @@ type AutoRagSearchResponse = { has_more: boolean; next_page: string | null; }; +/** + * @deprecated AutoRAG has been replaced by AI Search. + * Use AiSearchListResponse with the new API instead. + * @see AiSearchListResponse + */ type AutoRagListResponse = { id: string; enable: boolean; @@ -8677,14 +8966,51 @@ type AutoRagListResponse = { paused: boolean; status: string; }[]; +/** + * @deprecated AutoRAG has been replaced by AI Search. + * The new API returns different response formats for chat completions. + */ type AutoRagAiSearchResponse = AutoRagSearchResponse & { response: string; }; +/** + * @deprecated AutoRAG has been replaced by AI Search. + * Use the new AI Search API instead: `env.AI.aiSearch` + * + * Migration guide: + * - `env.AI.autorag().list()` → `env.AI.aiSearch.list()` + * - `env.AI.autorag('id').search(...)` → `env.AI.aiSearch.get('id').search(...)` + * - `env.AI.autorag('id').aiSearch(...)` → `env.AI.aiSearch.get('id').chatCompletions(...)` + * + * @see AiSearchAccountService + * @see AiSearchInstanceService + */ declare abstract class AutoRAG { + /** + * @deprecated Use `env.AI.aiSearch.list()` instead. + * @see AiSearchAccountService.list + */ list(): Promise; + /** + * @deprecated Use `env.AI.aiSearch.get(id).search(...)` instead. + * Note: The new API uses a messages array instead of a query string. + * @see AiSearchInstanceService.search + */ search(params: AutoRagSearchRequest): Promise; + /** + * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. + * @see AiSearchInstanceService.chatCompletions + */ aiSearch(params: AutoRagAiSearchRequestStreaming): Promise; + /** + * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. + * @see AiSearchInstanceService.chatCompletions + */ aiSearch(params: AutoRagAiSearchRequest): Promise; + /** + * @deprecated Use `env.AI.aiSearch.get(id).chatCompletions(...)` instead. + * @see AiSearchInstanceService.chatCompletions + */ aiSearch(params: AutoRagAiSearchRequest): Promise; } interface BasicImageTransformations { @@ -9435,6 +9761,10 @@ interface D1Meta { * The region of the database instance that executed the query. */ served_by_region?: string; + /** + * The three letters airport code of the colo that executed the query. + */ + served_by_colo?: string; /** * True if-and-only-if the database instance that executed the query was the primary. */ @@ -9523,6 +9853,15 @@ declare abstract class D1PreparedStatement { // ignored when `Disposable` is included in the standard lib. interface Disposable { } +/** + * The returned data after sending an email + */ +interface EmailSendResult { + /** + * The Email Message ID + */ + messageId: string; +} /** * An email message that can be sent from a Worker. */ @@ -9564,24 +9903,55 @@ interface ForwardableEmailMessage extends EmailMessage { * @param headers A [Headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers). * @returns A promise that resolves when the email message is forwarded. */ - forward(rcptTo: string, headers?: Headers): Promise; + forward(rcptTo: string, headers?: Headers): Promise; /** * Reply to the sender of this email message with a new EmailMessage object. * @param message The reply message. * @returns A promise that resolves when the email message is replied. */ - reply(message: EmailMessage): Promise; + reply(message: EmailMessage): Promise; +} +/** A file attachment for an email message */ +type EmailAttachment = { + disposition: 'inline'; + contentId: string; + filename: string; + type: string; + content: string | ArrayBuffer | ArrayBufferView; +} | { + disposition: 'attachment'; + contentId?: undefined; + filename: string; + type: string; + content: string | ArrayBuffer | ArrayBufferView; +}; +/** An Email Address */ +interface EmailAddress { + name: string; + email: string; } /** * A binding that allows a Worker to send email messages. */ interface SendEmail { - send(message: EmailMessage): Promise; + send(message: EmailMessage): Promise; + send(builder: { + from: string | EmailAddress; + to: string | string[]; + subject: string; + replyTo?: string | EmailAddress; + cc?: string | string[]; + bcc?: string | string[]; + headers?: Record; + text?: string; + html?: string; + attachments?: EmailAttachment[]; + }): Promise; } declare abstract class EmailEvent extends ExtendableEvent { readonly message: ForwardableEmailMessage; } -declare type EmailExportedHandler = (message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) => void | Promise; +declare type EmailExportedHandler = (message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) => void | Promise; declare module "cloudflare:email" { let _EmailMessage: { prototype: EmailMessage; @@ -9609,7 +9979,7 @@ interface Hyperdrive { /** * Connect directly to Hyperdrive as if it's your database, returning a TCP socket. * - * Calling this method returns an idential socket to if you call + * Calling this method returns an identical socket to if you call * `connect("host:port")` using the `host` and `port` fields from this object. * Pick whichever approach works better with your preferred DB client library. * @@ -9722,6 +10092,83 @@ type ImageOutputOptions = { background?: string; anim?: boolean; }; +interface ImageMetadata { + id: string; + filename?: string; + uploaded?: string; + requireSignedURLs: boolean; + meta?: Record; + variants: string[]; + draft?: boolean; + creator?: string; +} +interface ImageUploadOptions { + id?: string; + filename?: string; + requireSignedURLs?: boolean; + metadata?: Record; + creator?: string; + encoding?: 'base64'; +} +interface ImageUpdateOptions { + requireSignedURLs?: boolean; + metadata?: Record; + creator?: string; +} +interface ImageListOptions { + limit?: number; + cursor?: string; + sortOrder?: 'asc' | 'desc'; + creator?: string; +} +interface ImageList { + images: ImageMetadata[]; + cursor?: string; + listComplete: boolean; +} +interface HostedImagesBinding { + /** + * Get detailed metadata for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns Image metadata, or null if not found + */ + details(imageId: string): Promise; + /** + * Get the raw image data for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns ReadableStream of image bytes, or null if not found + */ + image(imageId: string): Promise | null>; + /** + * Upload a new hosted image + * @param image The image file to upload + * @param options Upload configuration + * @returns Metadata for the uploaded image + * @throws {@link ImagesError} if upload fails + */ + upload(image: ReadableStream | ArrayBuffer, options?: ImageUploadOptions): Promise; + /** + * Update hosted image metadata + * @param imageId The ID of the image + * @param options Properties to update + * @returns Updated image metadata + * @throws {@link ImagesError} if update fails + */ + update(imageId: string, options: ImageUpdateOptions): Promise; + /** + * Delete a hosted image + * @param imageId The ID of the image + * @returns True if deleted, false if not found + */ + delete(imageId: string): Promise; + /** + * List hosted images with pagination + * @param options List configuration + * @returns List of images with pagination info + * @throws {@link ImagesError} if list fails + */ + list(options?: ImageListOptions): Promise; +} interface ImagesBinding { /** * Get image metadata (type, width and height) @@ -9735,6 +10182,10 @@ interface ImagesBinding { * @returns A transform handle */ input(stream: ReadableStream, options?: ImageInputOptions): ImageTransformer; + /** + * Access hosted images CRUD operations + */ + readonly hosted: HostedImagesBinding; } interface ImageTransformer { /** @@ -9801,7 +10252,13 @@ interface MediaTransformer { * @param transform - Configuration for how the media should be transformed * @returns A generator for producing the transformed media output */ - transform(transform: MediaTransformationInputOptions): MediaTransformationGenerator; + transform(transform?: MediaTransformationInputOptions): MediaTransformationGenerator; + /** + * Generates the final media output with specified options. + * @param output - Configuration for the output format and parameters + * @returns The final transformation result containing the transformed media + */ + output(output?: MediaTransformationOutputOptions): MediaTransformationResult; } /** * Generator for producing media transformation results. @@ -9813,7 +10270,7 @@ interface MediaTransformationGenerator { * @param output - Configuration for the output format and parameters * @returns The final transformation result containing the transformed media */ - output(output: MediaTransformationOutputOptions): MediaTransformationResult; + output(output?: MediaTransformationOutputOptions): MediaTransformationResult; } /** * Result of a media transformation operation. @@ -9822,19 +10279,19 @@ interface MediaTransformationGenerator { interface MediaTransformationResult { /** * Returns the transformed media as a readable stream of bytes. - * @returns A stream containing the transformed media data + * @returns A promise containing a readable stream with the transformed media */ - media(): ReadableStream; + media(): Promise>; /** * Returns the transformed media as an HTTP response object. - * @returns The transformed media as a Response, ready to store in cache or return to users + * @returns The transformed media as a Promise, ready to store in cache or return to users */ - response(): Response; + response(): Promise; /** * Returns the MIME type of the transformed media. - * @returns The content type string (e.g., 'image/jpeg', 'video/mp4') + * @returns A promise containing the content type string (e.g., 'image/jpeg', 'video/mp4') */ - contentType(): string; + contentType(): Promise; } /** * Configuration options for transforming media input. @@ -9942,7 +10399,7 @@ declare module "cloudflare:pipelines" { protected ctx: ExecutionContext; constructor(ctx: ExecutionContext, env: Env); /** - * run recieves an array of PipelineRecord which can be + * run receives an array of PipelineRecord which can be * transformed and returned to the pipeline * @param records Incoming records from the pipeline to be transformed * @param metadata Information about the specific pipeline calling the transformation entrypoint @@ -10213,9 +10670,12 @@ declare namespace CloudflareWorkersModule { timestamp: Date; type: string; }; + export type WorkflowStepContext = { + attempt: number; + }; export abstract class WorkflowStep { - do>(name: string, callback: () => Promise): Promise; - do>(name: string, config: WorkflowStepConfig, callback: () => Promise): Promise; + do>(name: string, callback: (ctx: WorkflowStepContext) => Promise): Promise; + do>(name: string, config: WorkflowStepConfig, callback: (ctx: WorkflowStepContext) => Promise): Promise; sleep: (name: string, duration: WorkflowSleepDuration) => Promise; sleepUntil: (name: string, timestamp: Date | number) => Promise; waitForEvent>(name: string, options: { @@ -10223,6 +10683,7 @@ declare namespace CloudflareWorkersModule { timeout?: WorkflowTimeoutDuration | number; }): Promise>; } + export type WorkflowInstanceStatus = 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'waitingForPause' | 'unknown'; export abstract class WorkflowEntrypoint | unknown = unknown> implements Rpc.WorkflowEntrypointBranded { [Rpc.__WORKFLOW_ENTRYPOINT_BRAND]: never; protected ctx: ExecutionContext; @@ -10256,12 +10717,14 @@ type MarkdownDocument = { blob: Blob; }; type ConversionResponse = { + id: string; name: string; mimeType: string; format: 'markdown'; tokens: number; data: string; } | { + id: string; name: string; mimeType: string; format: 'error'; @@ -10279,6 +10742,8 @@ type ConversionOptions = { images?: EmbeddedImageConversionOptions & { convertOGImage?: boolean; }; + hostname?: string; + cssSelector?: string; }; docx?: { images?: EmbeddedImageConversionOptions; @@ -10416,6 +10881,15 @@ declare namespace TailStream { readonly level: "debug" | "error" | "info" | "log" | "warn"; readonly message: object; } + interface DroppedEventsDiagnostic { + readonly diagnosticsType: "droppedEvents"; + readonly count: number; + } + interface StreamDiagnostic { + readonly type: 'streamDiagnostic'; + // To add new diagnostic types, define a new interface and add it to this union type. + readonly diagnostic: DroppedEventsDiagnostic; + } // This marks the worker handler return information. // This is separate from Outcome because the worker invocation can live for a long time after // returning. For example - Websockets that return an http upgrade response but then continue @@ -10432,7 +10906,7 @@ declare namespace TailStream { readonly type: "attributes"; readonly info: Attribute[]; } - type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | Return | Attributes; + type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | StreamDiagnostic | Return | Attributes; // Context in which this trace event lives. interface SpanContext { // Single id for the entire top-level invocation @@ -10446,7 +10920,7 @@ declare namespace TailStream { // For Hibernate and Mark this would be the span under which they were emitted. // spanId is not set ONLY if: // 1. This is an Onset event - // 2. We are not inherting any SpanContext. (e.g. this is a cross-account service binding or a new top-level invocation) + // 2. We are not inheriting any SpanContext. (e.g. this is a cross-account service binding or a new top-level invocation) readonly spanId?: string; } interface TailEvent { From 4e95291512e1f35275be4a38f0cc0fc2ad6ac1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 19:04:43 +0200 Subject: [PATCH 16/75] fix(notifications): named entrypoint export, retry-safe badge, alarm-leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch `NotificationsService` from default-only to a named class export with a separate default. `services/kilo-chat/wrangler.jsonc` binds via `entrypoint: "NotificationsService"`, which resolves named module exports. The default-only form (`export default class NotificationsService`) exports under the `default` key — kilo-chat's RPC binding would not have resolved at deploy. Mirrors the existing pattern in `services/kilo-chat/src/index.ts` (`KiloChatService`). - `dispatchPush` now uses a two-stage idempotency record (`pending` → `delivered`). The badge increment was previously non-idempotent: an Expo failure returned `failed` without writing the idempotency key, so upstream retries (which the design explicitly invites) re-ran the increment before the next send and inflated the badge by one per retry. The `pending` marker is written before the increment and short-circuits the increment on retry; the `delivered` marker is only written on success. - `setAlarm` is now gated on `getAlarm() === null`. Calling `setAlarm` unconditionally on each successful push — as the previous code did — replaces the pending alarm and pushes the cleanup forward indefinitely on a conversation receiving more than one push per `IDEM_TTL_MS`, leaking expired idempotency entries. Adds two test cases covering the badge-retry and alarm-reset paths. --- .../src/__tests__/dispatch-push.test.ts | 58 +++++++++++++- .../src/dos/NotificationChannelDO.ts | 76 +++++++++++++------ services/notifications/src/index.ts | 4 +- 3 files changed, 111 insertions(+), 27 deletions(-) diff --git a/services/notifications/src/__tests__/dispatch-push.test.ts b/services/notifications/src/__tests__/dispatch-push.test.ts index d00f981684..6a7de49339 100644 --- a/services/notifications/src/__tests__/dispatch-push.test.ts +++ b/services/notifications/src/__tests__/dispatch-push.test.ts @@ -1,4 +1,4 @@ -import { env } from 'cloudflare:test'; +import { env, runInDurableObject } from 'cloudflare:test'; import { describe, expect, it, vi, beforeEach } from 'vitest'; import { getTableName } from 'drizzle-orm'; import type { DispatchPushInput } from '@kilocode/notifications'; @@ -128,4 +128,60 @@ describe('NotificationChannelDO.dispatchPush', () => { const second = await stub.dispatchPush(input); expect(second.kind).not.toBe('duplicate'); }); + + it('does not re-increment the badge when retrying after Expo failure', async () => { + const insertSpy = vi.fn().mockReturnValue({ + values: () => ({ onConflictDoUpdate: async () => undefined }), + }); + vi.spyOn(dbClient, 'getWorkerDb').mockReturnValue({ + select: () => ({ + from: (table: Parameters[0]) => ({ + where: async () => { + if (getTableName(table) === 'user_push_tokens') { + return [{ token: 'tok1' }]; + } + return [{ total: 1 }]; + }, + }), + }), + insert: insertSpy, + delete: () => ({ where: async () => undefined }), + } as unknown as ReturnType); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValue(false); + vi.mocked(sendPushNotifications).mockRejectedValueOnce(new Error('boom')); + + const stub = getDO('conv-no-double'); + const input = baseInput({ idempotencyKey: 'k-no-double' }); + + const first = await stub.dispatchPush(input); + expect(first.kind).toBe('failed'); + expect(insertSpy).toHaveBeenCalledTimes(1); + + const second = await stub.dispatchPush(input); + expect(second.kind).toBe('delivered'); + // Badge must not be incremented twice across the retry — the first + // attempt's `pending` marker gates the second insert out. + expect(insertSpy).toHaveBeenCalledTimes(1); + }); + + it('does not reset the alarm on every successful send', async () => { + installDbMock({ tokens: [{ user_id: 'u', token: 'tok1' }], badgeTotal: 1 }); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValue(false); + const stub = getDO('conv-alarm'); + + await stub.dispatchPush(baseInput({ idempotencyKey: 'k-alarm-1' })); + const firstAlarm = await runInDurableObject(stub, (_inst, state) => state.storage.getAlarm()); + expect(firstAlarm).not.toBeNull(); + + // Advance Date.now so a naive setAlarm would push the alarm forward. + const realNow = Date.now; + try { + vi.spyOn(Date, 'now').mockImplementation(() => realNow.call(Date) + 60_000); + await stub.dispatchPush(baseInput({ idempotencyKey: 'k-alarm-2' })); + } finally { + vi.mocked(Date.now).mockRestore(); + } + const secondAlarm = await runInDurableObject(stub, (_inst, state) => state.storage.getAlarm()); + expect(secondAlarm).toBe(firstAlarm); + }); }); diff --git a/services/notifications/src/dos/NotificationChannelDO.ts b/services/notifications/src/dos/NotificationChannelDO.ts index ee73d09944..a0c7e3858e 100644 --- a/services/notifications/src/dos/NotificationChannelDO.ts +++ b/services/notifications/src/dos/NotificationChannelDO.ts @@ -9,17 +9,25 @@ import { sendPushNotifications } from '../lib/expo-push'; type ReceiptCheckMessage = { ticketTokenPairs: TicketTokenPair[] }; +// Two-stage idempotency record. `pending` means the badge was incremented +// for this idempotency key but the Expo send did not (yet) succeed; on +// retry we must skip the increment to avoid double-counting. `delivered` +// means the send succeeded; further attempts are duplicates. +type IdemRecord = { stage: 'pending' | 'delivered'; ts: number }; + const IDEM_PREFIX = 'idem:'; const IDEM_TTL_MS = 60 * 60 * 1000; // 1 hour export class NotificationChannelDO extends DurableObject { async dispatchPush(input: DispatchPushInput): Promise { - // 1. Idempotency. DO is single-threaded, requests for a given conversation - // serialize on this instance. A `failed` outcome does NOT write the - // idempotency key, so the next attempt can retry. + // 1. Idempotency. DO is single-threaded — requests for a given + // conversation serialize on this instance. A `failed` outcome + // leaves the record at `pending` so upstream can retry the send + // without re-incrementing the badge. const idemKey = `${IDEM_PREFIX}${input.idempotencyKey}`; - const seen = await this.ctx.storage.get(idemKey); - if (seen) return { kind: 'duplicate' }; + const existing = await this.ctx.storage.get(idemKey); + if (existing?.stage === 'delivered') return { kind: 'duplicate' }; + const isRetry = existing?.stage === 'pending'; // 2. Presence const inContext = await this.env.EVENT_SERVICE.isUserInContext( @@ -38,20 +46,31 @@ export class NotificationChannelDO extends DurableObject { if (tokens.length === 0) return { kind: 'no_tokens' }; - // 4. Badge math (only if badge is set). + // 4. Badge math. On a retry the badge was already incremented during + // the prior attempt; re-applying the delta would double-count. + // The total is recomputed in either case (other writers may have + // advanced it). let badgeTotal: number | undefined; if (input.badge) { - await db - .insert(badge_counts) - .values({ - user_id: input.userId, - badge_bucket: input.badge.badgeBucket, - badge_count: input.badge.delta, - }) - .onConflictDoUpdate({ - target: [badge_counts.user_id, badge_counts.badge_bucket], - set: { badge_count: sql`${badge_counts.badge_count} + ${input.badge.delta}` }, + if (!isRetry) { + // Mark `pending` BEFORE the increment so any later failure path + // is gated on the marker and a retry skips the increment. + await this.ctx.storage.put(idemKey, { + stage: 'pending', + ts: Date.now(), }); + await db + .insert(badge_counts) + .values({ + user_id: input.userId, + badge_bucket: input.badge.badgeBucket, + badge_count: input.badge.delta, + }) + .onConflictDoUpdate({ + target: [badge_counts.user_id, badge_counts.badge_bucket], + set: { badge_count: sql`${badge_counts.badge_count} + ${input.badge.delta}` }, + }); + } const [totals] = await db .select({ total: sum(badge_counts.badge_count) }) .from(badge_counts) @@ -75,9 +94,8 @@ export class NotificationChannelDO extends DurableObject { try { result = await sendPushNotifications(messages, accessToken); } catch (err) { - // Intentionally do NOT write the idempotency key on failure — let - // upstream retry. The DO's single-threading prevents concurrent - // double-sends within the same conversation. + // Leave any `pending` marker in place — retries will re-attempt the + // send while skipping the badge increment. return { kind: 'failed', error: err instanceof Error ? err.message : String(err), @@ -93,19 +111,27 @@ export class NotificationChannelDO extends DurableObject { await this.env.RECEIPTS_QUEUE.send(receiptMsg, { delaySeconds: 900 }); } - // 6. Idempotency write — only after a successful send. - await this.ctx.storage.put(idemKey, Date.now()); - await this.ctx.storage.setAlarm(Date.now() + IDEM_TTL_MS); + // 6. Mark `delivered` so future retries short-circuit as duplicate. + await this.ctx.storage.put(idemKey, { + stage: 'delivered', + ts: Date.now(), + }); + // 7. Schedule cleanup only when no alarm is already pending. + // `setAlarm` replaces any existing alarm; calling it on every push + // would push cleanup forward indefinitely on a busy conversation. + if ((await this.ctx.storage.getAlarm()) === null) { + await this.ctx.storage.setAlarm(Date.now() + IDEM_TTL_MS); + } return { kind: 'delivered', tokenCount: tokens.length }; } override async alarm(): Promise { const now = Date.now(); - const entries = await this.ctx.storage.list({ prefix: IDEM_PREFIX }); + const entries = await this.ctx.storage.list({ prefix: IDEM_PREFIX }); const expired: string[] = []; - for (const [key, ts] of entries) { - if (now - ts > IDEM_TTL_MS) expired.push(key); + for (const [key, rec] of entries) { + if (now - rec.ts > IDEM_TTL_MS) expired.push(key); } if (expired.length > 0) await this.ctx.storage.delete(expired); } diff --git a/services/notifications/src/index.ts b/services/notifications/src/index.ts index 37dabfd4f9..bb4f673346 100644 --- a/services/notifications/src/index.ts +++ b/services/notifications/src/index.ts @@ -67,7 +67,7 @@ export async function sendPushForConversationCore( return { perRecipient }; } -export default class NotificationsService extends WorkerEntrypoint { +export class NotificationsService extends WorkerEntrypoint { override async fetch(request: Request): Promise { return app.fetch(request, this.env, this.ctx); } @@ -87,3 +87,5 @@ export default class NotificationsService extends WorkerEntrypoint { }); } } + +export default NotificationsService; From 4faf0dd184de8b840c97bfa92f0b510bbfe59b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 19:11:26 +0200 Subject: [PATCH 17/75] fix(notifications): close two cleanup-alarm leaks - Schedule the cleanup alarm when writing the `pending` marker, not only on `delivered`. Without this, an Expo failure followed by no further push activity for the conversation leaves the `pending` record in DO storage forever (no alarm was ever set to prune it). - After the alarm fires, reschedule for the earliest remaining record's expiry instead of leaving the alarm slot empty. Otherwise a quiet conversation strands its younger entries until some unrelated future dispatch wakes the DO up. Both paths go through a small `ensureCleanupAlarm` helper that gates on `getAlarm() === null` so a busy conversation still doesn't push the alarm forward on every call. --- .../src/__tests__/dispatch-push.test.ts | 41 ++++++++++++++++++ .../src/dos/NotificationChannelDO.ts | 43 ++++++++++++------- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/services/notifications/src/__tests__/dispatch-push.test.ts b/services/notifications/src/__tests__/dispatch-push.test.ts index 6a7de49339..1e9dca7fd3 100644 --- a/services/notifications/src/__tests__/dispatch-push.test.ts +++ b/services/notifications/src/__tests__/dispatch-push.test.ts @@ -164,6 +164,47 @@ describe('NotificationChannelDO.dispatchPush', () => { expect(insertSpy).toHaveBeenCalledTimes(1); }); + it('schedules cleanup when writing the pending marker (failed send)', async () => { + installDbMock({ tokens: [{ user_id: 'u', token: 'tok1' }], badgeTotal: 0 }); + vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValue(false); + vi.mocked(sendPushNotifications).mockRejectedValueOnce(new Error('boom')); + const stub = getDO('conv-pending-alarm'); + + const result = await stub.dispatchPush(baseInput({ idempotencyKey: 'k-pending-alarm' })); + expect(result.kind).toBe('failed'); + // Even though delivery failed, an alarm must be set so the orphan + // `pending` record gets pruned after IDEM_TTL_MS. + const alarm = await runInDurableObject(stub, (_inst, state) => state.storage.getAlarm()); + expect(alarm).not.toBeNull(); + }); + + it('reschedules cleanup for younger records when alarm fires', async () => { + installDbMock({ tokens: [{ user_id: 'u', token: 'tok1' }], badgeTotal: 1 }); + const stub = getDO('conv-reschedule'); + + const now = Date.now(); + await runInDurableObject(stub, async (_inst, state) => { + await state.storage.put('idem:old', { stage: 'delivered', ts: now - 2 * 60 * 60 * 1000 }); + await state.storage.put('idem:new', { stage: 'delivered', ts: now - 30 * 60 * 1000 }); + }); + + await runInDurableObject(stub, async inst => { + await (inst as unknown as { alarm: () => Promise }).alarm(); + }); + + const remaining = await runInDurableObject(stub, async (_inst, state) => { + const entries = await state.storage.list({ prefix: 'idem:' }); + return Array.from(entries.keys()); + }); + expect(remaining).toEqual(['idem:new']); + + const alarm = await runInDurableObject(stub, (_inst, state) => state.storage.getAlarm()); + expect(alarm).not.toBeNull(); + // Should be rescheduled for the younger record's expiry, not "1h from now". + const expectedExpiry = now - 30 * 60 * 1000 + 60 * 60 * 1000; + expect(alarm).toBe(expectedExpiry); + }); + it('does not reset the alarm on every successful send', async () => { installDbMock({ tokens: [{ user_id: 'u', token: 'tok1' }], badgeTotal: 1 }); vi.spyOn(env.EVENT_SERVICE, 'isUserInContext').mockResolvedValue(false); diff --git a/services/notifications/src/dos/NotificationChannelDO.ts b/services/notifications/src/dos/NotificationChannelDO.ts index a0c7e3858e..195bdd82e9 100644 --- a/services/notifications/src/dos/NotificationChannelDO.ts +++ b/services/notifications/src/dos/NotificationChannelDO.ts @@ -55,10 +55,11 @@ export class NotificationChannelDO extends DurableObject { if (!isRetry) { // Mark `pending` BEFORE the increment so any later failure path // is gated on the marker and a retry skips the increment. - await this.ctx.storage.put(idemKey, { - stage: 'pending', - ts: Date.now(), - }); + const ts = Date.now(); + await this.ctx.storage.put(idemKey, { stage: 'pending', ts }); + // Also schedule cleanup at this point — if Expo keeps failing and + // no future push ever lands, `pending` would otherwise leak. + await this.ensureCleanupAlarm(ts); await db .insert(badge_counts) .values({ @@ -112,16 +113,9 @@ export class NotificationChannelDO extends DurableObject { } // 6. Mark `delivered` so future retries short-circuit as duplicate. - await this.ctx.storage.put(idemKey, { - stage: 'delivered', - ts: Date.now(), - }); - // 7. Schedule cleanup only when no alarm is already pending. - // `setAlarm` replaces any existing alarm; calling it on every push - // would push cleanup forward indefinitely on a busy conversation. - if ((await this.ctx.storage.getAlarm()) === null) { - await this.ctx.storage.setAlarm(Date.now() + IDEM_TTL_MS); - } + const ts = Date.now(); + await this.ctx.storage.put(idemKey, { stage: 'delivered', ts }); + await this.ensureCleanupAlarm(ts); return { kind: 'delivered', tokenCount: tokens.length }; } @@ -130,9 +124,28 @@ export class NotificationChannelDO extends DurableObject { const now = Date.now(); const entries = await this.ctx.storage.list({ prefix: IDEM_PREFIX }); const expired: string[] = []; + let earliestRemaining: number | undefined; for (const [key, rec] of entries) { - if (now - rec.ts > IDEM_TTL_MS) expired.push(key); + if (now - rec.ts > IDEM_TTL_MS) { + expired.push(key); + } else if (earliestRemaining === undefined || rec.ts < earliestRemaining) { + earliestRemaining = rec.ts; + } } if (expired.length > 0) await this.ctx.storage.delete(expired); + // Reschedule for the earliest remaining record so a quiet conversation + // still gets its leftover entries pruned exactly once their TTL elapses. + if (earliestRemaining !== undefined) { + await this.ctx.storage.setAlarm(earliestRemaining + IDEM_TTL_MS); + } + } + + // Schedule cleanup `IDEM_TTL_MS` from `refTs` only if no alarm is pending. + // `setAlarm` replaces any existing alarm; calling it unconditionally would + // push cleanup forward indefinitely on a busy conversation. + private async ensureCleanupAlarm(refTs: number): Promise { + if ((await this.ctx.storage.getAlarm()) === null) { + await this.ctx.storage.setAlarm(refTs + IDEM_TTL_MS); + } } } From 8d7b9d7fc7d3e7c725e9987284bb8259e69de132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 19:18:33 +0200 Subject: [PATCH 18/75] refactor(event-service): compose presence contexts from kiloclaw helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kiloclaw-scoped presence paths are literally `/presence` prefixed onto the kiloclaw event-context paths. Build them by composition so the `/kiloclaw/{sandboxId}[/{conversationId}]` segment shape is defined in exactly one place — `kiloclaw-contexts.ts`. Pure refactor; same string output, template-literal types still narrow to the same shape. --- packages/event-service/src/presence.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/event-service/src/presence.ts b/packages/event-service/src/presence.ts index 8459b886c3..a267aa667c 100644 --- a/packages/event-service/src/presence.ts +++ b/packages/event-service/src/presence.ts @@ -3,14 +3,19 @@ * and are subscribed by clients only when the user is *actively* on the * matching surface. The notifications pipeline queries them via * event-service.isUserInContext to skip pushes when the user is in-context. + * + * The kiloclaw-scoped variants compose `/presence` with the corresponding + * event-context paths so the segment shape is defined in exactly one place. */ +import { kiloclawConversationContext, kiloclawInstanceContext } from './kiloclaw-contexts'; + export type Platform = 'app' | 'web'; export const presenceContextForPlatform = (platform: Platform) => `/presence/${platform}` as const; export const presenceContextForInstance = (sandboxId: string) => - `/presence/kiloclaw/${sandboxId}` as const; + `/presence${kiloclawInstanceContext(sandboxId)}` as const; export const presenceContextForConversation = (sandboxId: string, conversationId: string) => - `/presence/kiloclaw/${sandboxId}/${conversationId}` as const; + `/presence${kiloclawConversationContext(sandboxId, conversationId)}` as const; From 893b7f1bc0ef85e1aafcd78143dca5a5bc09cf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 19:45:19 +0200 Subject: [PATCH 19/75] feat(web): add kiloChat.getToken tRPC procedure --- apps/web/src/routers/kilo-chat-router.test.ts | 26 +++++++++++++++++++ apps/web/src/routers/kilo-chat-router.ts | 20 ++++++++++++++ apps/web/src/routers/root-router.ts | 2 ++ 3 files changed, 48 insertions(+) create mode 100644 apps/web/src/routers/kilo-chat-router.test.ts create mode 100644 apps/web/src/routers/kilo-chat-router.ts diff --git a/apps/web/src/routers/kilo-chat-router.test.ts b/apps/web/src/routers/kilo-chat-router.test.ts new file mode 100644 index 0000000000..52569268ea --- /dev/null +++ b/apps/web/src/routers/kilo-chat-router.test.ts @@ -0,0 +1,26 @@ +import { createCallerForUser } from '@/routers/test-utils'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import type { User } from '@kilocode/db/schema'; + +let testUser: User; + +describe('kiloChat router - getToken', () => { + beforeAll(async () => { + testUser = await insertTestUser({ + google_user_email: `kilo-chat-token-${crypto.randomUUID()}@example.com`, + google_user_name: 'Kilo Chat Token Test User', + }); + }); + + it('returns a JWT-shaped token and a future expiresAt', async () => { + const caller = await createCallerForUser(testUser.id); + const before = Date.now(); + const result = await caller.kiloChat.getToken(); + + expect(result.token).toMatch(/\..+\..+/); + + const expiresAtMs = Date.parse(result.expiresAt); + expect(Number.isNaN(expiresAtMs)).toBe(false); + expect(expiresAtMs).toBeGreaterThan(before); + }); +}); diff --git a/apps/web/src/routers/kilo-chat-router.ts b/apps/web/src/routers/kilo-chat-router.ts new file mode 100644 index 0000000000..f2d3c4b65b --- /dev/null +++ b/apps/web/src/routers/kilo-chat-router.ts @@ -0,0 +1,20 @@ +import 'server-only'; +import * as z from 'zod'; +import { generateApiToken } from '@/lib/tokens'; +import { baseProcedure, createTRPCRouter } from '@/lib/trpc/init'; + +const KILO_CHAT_TOKEN_TTL_S = 60 * 60; + +export const kiloChatRouter = createTRPCRouter({ + getToken: baseProcedure + .output(z.object({ token: z.string(), expiresAt: z.string() })) + .query(({ ctx }) => { + const token = generateApiToken( + ctx.user, + { tokenSource: 'kilo-chat' }, + { expiresIn: KILO_CHAT_TOKEN_TTL_S } + ); + const expiresAt = new Date(Date.now() + KILO_CHAT_TOKEN_TTL_S * 1000).toISOString(); + return { token, expiresAt }; + }), +}); diff --git a/apps/web/src/routers/root-router.ts b/apps/web/src/routers/root-router.ts index 3380f4766f..90825e3d05 100644 --- a/apps/web/src/routers/root-router.ts +++ b/apps/web/src/routers/root-router.ts @@ -32,6 +32,7 @@ import { webhookTriggersRouter } from '@/routers/webhook-triggers-router'; import { userFeedbackRouter } from '@/routers/user-feedback-router'; import { appBuilderFeedbackRouter } from '@/routers/app-builder-feedback-router'; import { cloudAgentNextFeedbackRouter } from '@/routers/cloud-agent-next-feedback-router'; +import { kiloChatRouter } from '@/routers/kilo-chat-router'; import { kiloclawRouter } from '@/routers/kiloclaw-router'; import { modelsRouter } from '@/routers/models-router'; import { unifiedSessionsRouter } from '@/routers/unified-sessions-router'; @@ -69,6 +70,7 @@ export const rootRouter = createTRPCRouter({ userFeedback: userFeedbackRouter, appBuilderFeedback: appBuilderFeedbackRouter, cloudAgentNextFeedback: cloudAgentNextFeedbackRouter, + kiloChat: kiloChatRouter, kiloclaw: kiloclawRouter, models: modelsRouter, unifiedSessions: unifiedSessionsRouter, From a35c98ce81e22bba52d14c6f5ca8f09d0175ef05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 19:50:58 +0200 Subject: [PATCH 20/75] refactor(web): use kiloclaw-context helpers for event subscriptions --- .../(app)/claw/kilo-chat/components/MessageArea.tsx | 3 ++- .../app/(app)/claw/kilo-chat/hooks/useEventService.ts | 10 +++++++--- .../src/app/(app)/claw/kilo-chat/hooks/useMessages.ts | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx index 66beb8b8c7..58cc8d4ed1 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx @@ -15,6 +15,7 @@ import { useExecuteAction, } from '../hooks/useMessages'; import { useConversationContext } from '../hooks/useEventService'; +import { kiloclawConversationContext } from '@kilocode/event-service'; import { useTypingSender, useTypingState } from '../hooks/useTyping'; import { useConversationDetail, @@ -81,7 +82,7 @@ export function MessageArea({ conversationId }: MessageAreaProps) { // Event Service delivers subscribed contexts to every handler, so each // handler must validate the incoming `ctx` against this string before // applying changes to the active conversation's state. - const expectedContext = sandboxId ? `/kiloclaw/${sandboxId}/${conversationId}` : null; + const expectedContext = sandboxId ? kiloclawConversationContext(sandboxId, conversationId) : null; const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useMessages( kiloChatClient, diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts index 627dc60a33..ac9ec9b7fd 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts @@ -1,5 +1,9 @@ import { useEffect, useMemo } from 'react'; -import { EventServiceClient } from '@kilocode/event-service'; +import { + EventServiceClient, + kiloclawInstanceContext, + kiloclawConversationContext, +} from '@kilocode/event-service'; import { KiloChatClient } from '@kilocode/kilo-chat'; import { KILO_CHAT_URL, EVENT_SERVICE_URL } from '@/lib/constants'; import { clearKiloChatToken } from '../token'; @@ -46,7 +50,7 @@ export function useEventService(getToken: () => Promise) { export function useInstanceContext(eventService: EventServiceClient, sandboxId: string | null) { useEffect(() => { if (!sandboxId) return; - const context = `/kiloclaw/${sandboxId}`; + const context = kiloclawInstanceContext(sandboxId); eventService.subscribe([context]); return () => eventService.unsubscribe([context]); }, [eventService, sandboxId]); @@ -63,7 +67,7 @@ export function useConversationContext( ) { useEffect(() => { if (!sandboxId || !conversationId) return; - const context = `/kiloclaw/${sandboxId}/${conversationId}`; + const context = kiloclawConversationContext(sandboxId, conversationId); eventService.subscribe([context]); return () => eventService.unsubscribe([context]); }, [eventService, sandboxId, conversationId]); diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts index f21e165e74..de70a427c5 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts @@ -16,6 +16,7 @@ import type { ExecApprovalDecision, } from '@kilocode/kilo-chat'; import { useEffect } from 'react'; +import { kiloclawConversationContext } from '@kilocode/event-service'; import { toast } from 'sonner'; const PAGE_SIZE = 50; @@ -401,7 +402,7 @@ export function useMessageCacheUpdater( useEffect(() => { if (!conversationId || !sandboxId) return; const queryKey = ['kilo-chat', 'messages', conversationId]; - const expectedContext = `/kiloclaw/${sandboxId}/${conversationId}`; + const expectedContext = kiloclawConversationContext(sandboxId, conversationId); const onCreated = (ctx: string, e: MessageCreatedEvent) => { if (ctx !== expectedContext) return; From a43585d86ee4b219b5b96b7a831cfaa6ebb32557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:01:13 +0200 Subject: [PATCH 21/75] feat(web): lift EventServiceClient to global provider Introduces a single app-shell EventServiceProvider that owns the EventServiceClient and KiloChatClient for all authenticated routes. Mounted in (app)/layout.tsx so platform/instance/conversation presence subscriptions and the kilo-chat UI share one WebSocket. KiloChatLayout now consumes the global clients via useEventServiceClient() instead of spinning up its own pair, and the getToken prop is removed from KiloChatLayoutProps (along with both call sites). The local useEventService(getToken) factory is dead code and has been deleted; useInstanceContext / useConversationContext stay since they take EventServiceClient as a parameter. --- .../kilo-chat/components/KiloChatLayout.tsx | 11 ++- .../claw/kilo-chat/hooks/useEventService.ts | 42 +---------- .../src/app/(app)/claw/kilo-chat/layout.tsx | 5 -- apps/web/src/app/(app)/layout.tsx | 27 ++++--- .../[id]/claw/kilo-chat/layout.tsx | 5 -- apps/web/src/contexts/EventServiceContext.tsx | 75 +++++++++++++++++++ 6 files changed, 97 insertions(+), 68 deletions(-) create mode 100644 apps/web/src/contexts/EventServiceContext.tsx diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx index 8ab3d6b2eb..5810f23b4c 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx @@ -7,7 +7,9 @@ import { useQueryClient } from '@tanstack/react-query'; import { formatKiloChatError } from '@kilocode/kilo-chat'; import { ConversationList } from './ConversationList'; import { KiloChatContext, type KiloChatContextValue } from './kiloChatContext'; -import { useEventService, useInstanceContext } from '../hooks/useEventService'; +import { useInstanceContext } from '../hooks/useEventService'; +import { useEventServiceClient } from '@/contexts/EventServiceContext'; +import { getKiloChatToken } from '../token'; import { useConversations, useCreateConversation, @@ -20,7 +22,6 @@ import { // ── Layout component ──────────────────────────────────────────────── type KiloChatLayoutProps = { - getToken: () => Promise; currentUserId: string; sandboxId: string | null; basePath: string; @@ -32,7 +33,6 @@ type KiloChatLayoutProps = { }; export function KiloChatLayout({ - getToken, currentUserId, sandboxId, basePath, @@ -44,7 +44,7 @@ export function KiloChatLayout({ }: KiloChatLayoutProps) { const router = useRouter(); - const { eventService, kiloChatClient } = useEventService(getToken); + const { eventService, kiloChatClient } = useEventServiceClient(); useInstanceContext(eventService, sandboxId); const queryClient = useQueryClient(); @@ -189,7 +189,7 @@ export function KiloChatLayout({ const contextValue = useMemo( () => ({ - getToken, + getToken: getKiloChatToken, currentUserId, instanceStatus, leavingConversationId, @@ -202,7 +202,6 @@ export function KiloChatLayout({ kiloChatClient, }), [ - getToken, currentUserId, instanceStatus, leavingConversationId, diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts index ac9ec9b7fd..6925e041ce 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts @@ -1,47 +1,9 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { - EventServiceClient, + type EventServiceClient, kiloclawInstanceContext, kiloclawConversationContext, } from '@kilocode/event-service'; -import { KiloChatClient } from '@kilocode/kilo-chat'; -import { KILO_CHAT_URL, EVENT_SERVICE_URL } from '@/lib/constants'; -import { clearKiloChatToken } from '../token'; - -/** - * Creates and manages the EventServiceClient + KiloChatClient singleton. - * Connects the WebSocket on mount, disconnects on unmount. - * Returns the clients for use by child hooks. - */ -export function useEventService(getToken: () => Promise) { - const eventService = useMemo( - () => - new EventServiceClient({ - url: EVENT_SERVICE_URL, - getToken, - // Event Service rejected our token as 401/403. Drop the cached - // token so the next request refetches; the socket is permanently - // stopped by the client to avoid a reconnect storm. - onUnauthorized: () => { - clearKiloChatToken(); - }, - }), - [getToken] - ); - - const kiloChatClient = useMemo( - () => new KiloChatClient({ eventService, baseUrl: KILO_CHAT_URL, getToken }), - [eventService, getToken] - ); - - // Connect on mount, disconnect on unmount - useEffect(() => { - void eventService.connect(); - return () => eventService.disconnect(); - }, [eventService]); - - return { eventService, kiloChatClient }; -} /** * Subscribes to the instance-level context (`/kiloclaw/{sandboxId}`). diff --git a/apps/web/src/app/(app)/claw/kilo-chat/layout.tsx b/apps/web/src/app/(app)/claw/kilo-chat/layout.tsx index 3b207995d8..242a397ed6 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/layout.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/layout.tsx @@ -1,20 +1,15 @@ 'use client'; -import { useCallback } from 'react'; import { useUser } from '@/hooks/useUser'; import { useKiloClawStatus } from '@/hooks/useKiloClaw'; -import { getKiloChatToken } from './token'; import { KiloChatLayout } from './components/KiloChatLayout'; export default function KiloChatRootLayout({ children }: { children: React.ReactNode }) { const { data: user } = useUser(); const { data: status, isLoading } = useKiloClawStatus(); - const getToken = useCallback(() => getKiloChatToken(), []); - return ( - - - -
- - - -
{children}
-
-
-
-
+ + + + +
+ + + +
{children}
+
+
+
+
+
diff --git a/apps/web/src/app/(app)/organizations/[id]/claw/kilo-chat/layout.tsx b/apps/web/src/app/(app)/organizations/[id]/claw/kilo-chat/layout.tsx index 27733df73e..8fd6f6cedf 100644 --- a/apps/web/src/app/(app)/organizations/[id]/claw/kilo-chat/layout.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/claw/kilo-chat/layout.tsx @@ -1,10 +1,8 @@ 'use client'; -import { useCallback } from 'react'; import { useParams } from 'next/navigation'; import { useUser } from '@/hooks/useUser'; import { useOrgKiloClawStatus } from '@/hooks/useOrgKiloClaw'; -import { getKiloChatToken } from '@/app/(app)/claw/kilo-chat/token'; import { KiloChatLayout } from '@/app/(app)/claw/kilo-chat/components/KiloChatLayout'; export default function OrgKiloChatRootLayout({ children }: { children: React.ReactNode }) { @@ -13,14 +11,11 @@ export default function OrgKiloChatRootLayout({ children }: { children: React.Re const { data: user } = useUser(); const { data: status, isLoading } = useOrgKiloClawStatus(organizationId); - const getToken = useCallback(() => getKiloChatToken(), []); - const basePath = `/organizations/${organizationId}/claw/kilo-chat`; const noInstanceRedirect = `/organizations/${organizationId}/claw/new`; return ( (null); + +type EventServiceProviderProps = { + children: ReactNode; +}; + +/** + * Global EventService provider — owns the single `EventServiceClient` and + * `KiloChatClient` for the authenticated app. Mounted in `(app)/layout.tsx` + * so platform-, instance-, and conversation-level presence subscriptions + * (and the kilo-chat UI) all share one WebSocket. + */ +export function EventServiceProvider({ children }: EventServiceProviderProps) { + const eventService = useMemo( + () => + new EventServiceClient({ + url: EVENT_SERVICE_URL, + getToken: getKiloChatToken, + // Event Service rejected our token as 401/403. Drop the cached + // token so the next request refetches; the socket is permanently + // stopped by the client to avoid a reconnect storm. + onUnauthorized: () => { + clearKiloChatToken(); + }, + }), + [] + ); + + const kiloChatClient = useMemo( + () => + new KiloChatClient({ + eventService, + baseUrl: KILO_CHAT_URL, + getToken: getKiloChatToken, + }), + [eventService] + ); + + // Connect on mount, disconnect on unmount. + useEffect(() => { + void eventService.connect(); + return () => eventService.disconnect(); + }, [eventService]); + + const value = useMemo( + () => ({ eventService, kiloChatClient }), + [eventService, kiloChatClient] + ); + + return {children}; +} + +export function useEventServiceClient(): EventServiceContextValue { + const ctx = useContext(EventServiceContext); + if (!ctx) { + throw new Error('useEventServiceClient must be used within an EventServiceProvider'); + } + return ctx; +} From e98f3707191cf61dca44695eb4c22d42b8bb0bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:01:18 +0200 Subject: [PATCH 22/75] feat(web): add usePresenceSubscription primitive Thin hook that subscribes the global EventServiceClient to a single context for the lifetime of the calling component, gated by an `active` flag. Will back upcoming platform- and instance-level presence indicators. --- apps/web/src/hooks/usePresenceSubscription.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 apps/web/src/hooks/usePresenceSubscription.ts diff --git a/apps/web/src/hooks/usePresenceSubscription.ts b/apps/web/src/hooks/usePresenceSubscription.ts new file mode 100644 index 0000000000..0087fc6b7f --- /dev/null +++ b/apps/web/src/hooks/usePresenceSubscription.ts @@ -0,0 +1,23 @@ +'use client'; + +import { useEffect } from 'react'; +import { useEventServiceClient } from '@/contexts/EventServiceContext'; + +/** + * Subscribes to a single presence/event-service context for the lifetime of + * the calling component. Bails out when `active` is false so callers can + * gate the subscription on, e.g., feature flags or page visibility. + * + * Reads the global `EventServiceClient` from `EventServiceProvider`, mounted + * in `(app)/layout.tsx` for every authenticated route. + */ +export function usePresenceSubscription(context: string, active: boolean) { + const { eventService } = useEventServiceClient(); + useEffect(() => { + if (!active) return; + eventService.subscribe([context]); + return () => { + eventService.unsubscribe([context]); + }; + }, [eventService, context, active]); +} From 6bfbf9562b5861ed2f229ecad283bea7345a5ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:07:59 +0200 Subject: [PATCH 23/75] refactor(web): collapse kilo-chat event subscriptions into usePresenceSubscription - Drop dead getToken field from KiloChatContextValue (no consumers). - Remove useInstanceContext / useConversationContext hooks; both call sites now use the shared usePresenceSubscription primitive directly. - Harden usePresenceSubscription against empty-string contexts. --- .../kilo-chat/components/KiloChatLayout.tsx | 7 ++-- .../claw/kilo-chat/components/MessageArea.tsx | 7 ++-- .../kilo-chat/components/kiloChatContext.ts | 1 - .../claw/kilo-chat/hooks/useEventService.ts | 36 ------------------- apps/web/src/contexts/EventServiceContext.tsx | 5 +-- apps/web/src/hooks/usePresenceSubscription.ts | 2 +- 6 files changed, 10 insertions(+), 48 deletions(-) delete mode 100644 apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx index 5810f23b4c..f85b101a45 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx @@ -7,9 +7,9 @@ import { useQueryClient } from '@tanstack/react-query'; import { formatKiloChatError } from '@kilocode/kilo-chat'; import { ConversationList } from './ConversationList'; import { KiloChatContext, type KiloChatContextValue } from './kiloChatContext'; -import { useInstanceContext } from '../hooks/useEventService'; +import { kiloclawInstanceContext } from '@kilocode/event-service'; +import { usePresenceSubscription } from '@/hooks/usePresenceSubscription'; import { useEventServiceClient } from '@/contexts/EventServiceContext'; -import { getKiloChatToken } from '../token'; import { useConversations, useCreateConversation, @@ -45,7 +45,7 @@ export function KiloChatLayout({ const router = useRouter(); const { eventService, kiloChatClient } = useEventServiceClient(); - useInstanceContext(eventService, sandboxId); + usePresenceSubscription(sandboxId ? kiloclawInstanceContext(sandboxId) : '', Boolean(sandboxId)); const queryClient = useQueryClient(); const params = useParams<{ conversationId?: string }>(); @@ -189,7 +189,6 @@ export function KiloChatLayout({ const contextValue = useMemo( () => ({ - getToken: getKiloChatToken, currentUserId, instanceStatus, leavingConversationId, diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx index 58cc8d4ed1..3aa827ae7e 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx @@ -14,8 +14,8 @@ import { useRemoveReaction, useExecuteAction, } from '../hooks/useMessages'; -import { useConversationContext } from '../hooks/useEventService'; import { kiloclawConversationContext } from '@kilocode/event-service'; +import { usePresenceSubscription } from '@/hooks/usePresenceSubscription'; import { useTypingSender, useTypingState } from '../hooks/useTyping'; import { useConversationDetail, @@ -77,7 +77,10 @@ export function MessageArea({ conversationId }: MessageAreaProps) { const [renameText, setRenameText] = useState(''); // Subscribe to this conversation's events via the event-service WebSocket - useConversationContext(eventService, sandboxId, conversationId); + usePresenceSubscription( + sandboxId && conversationId ? kiloclawConversationContext(sandboxId, conversationId) : '', + Boolean(sandboxId && conversationId) + ); // Event Service delivers subscribed contexts to every handler, so each // handler must validate the incoming `ctx` against this string before diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/kiloChatContext.ts b/apps/web/src/app/(app)/claw/kilo-chat/components/kiloChatContext.ts index eb7154a26b..823296599f 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/kiloChatContext.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/kiloChatContext.ts @@ -5,7 +5,6 @@ import type { EventServiceClient } from '@kilocode/event-service'; import type { KiloChatClient } from '@kilocode/kilo-chat'; export type KiloChatContextValue = { - getToken: () => Promise; currentUserId: string; instanceStatus: string | null; leavingConversationId: string | null; diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts deleted file mode 100644 index 6925e041ce..0000000000 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect } from 'react'; -import { - type EventServiceClient, - kiloclawInstanceContext, - kiloclawConversationContext, -} from '@kilocode/event-service'; - -/** - * Subscribes to the instance-level context (`/kiloclaw/{sandboxId}`). - * Used at the layout level for cross-conversation events (future: unread counts). - */ -export function useInstanceContext(eventService: EventServiceClient, sandboxId: string | null) { - useEffect(() => { - if (!sandboxId) return; - const context = kiloclawInstanceContext(sandboxId); - eventService.subscribe([context]); - return () => eventService.unsubscribe([context]); - }, [eventService, sandboxId]); -} - -/** - * Subscribes to the conversation-level context (`/kiloclaw/{sandboxId}/{conversationId}`). - * Used in MessageArea for message/typing/reaction events. - */ -export function useConversationContext( - eventService: EventServiceClient, - sandboxId: string | null, - conversationId: string | null -) { - useEffect(() => { - if (!sandboxId || !conversationId) return; - const context = kiloclawConversationContext(sandboxId, conversationId); - eventService.subscribe([context]); - return () => eventService.unsubscribe([context]); - }, [eventService, sandboxId, conversationId]); -} diff --git a/apps/web/src/contexts/EventServiceContext.tsx b/apps/web/src/contexts/EventServiceContext.tsx index 2bc5b78c41..03bc458faa 100644 --- a/apps/web/src/contexts/EventServiceContext.tsx +++ b/apps/web/src/contexts/EventServiceContext.tsx @@ -4,10 +4,7 @@ import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'r import { EventServiceClient } from '@kilocode/event-service'; import { KiloChatClient } from '@kilocode/kilo-chat'; import { EVENT_SERVICE_URL, KILO_CHAT_URL } from '@/lib/constants'; -import { - getKiloChatToken, - clearKiloChatToken, -} from '@/app/(app)/claw/kilo-chat/token'; +import { getKiloChatToken, clearKiloChatToken } from '@/app/(app)/claw/kilo-chat/token'; export type EventServiceContextValue = { eventService: EventServiceClient; diff --git a/apps/web/src/hooks/usePresenceSubscription.ts b/apps/web/src/hooks/usePresenceSubscription.ts index 0087fc6b7f..485a3bf957 100644 --- a/apps/web/src/hooks/usePresenceSubscription.ts +++ b/apps/web/src/hooks/usePresenceSubscription.ts @@ -14,7 +14,7 @@ import { useEventServiceClient } from '@/contexts/EventServiceContext'; export function usePresenceSubscription(context: string, active: boolean) { const { eventService } = useEventServiceClient(); useEffect(() => { - if (!active) return; + if (!active || !context) return; eventService.subscribe([context]); return () => { eventService.unsubscribe([context]); From 832e2b77f1263c8daf8337767af102875b2080af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:11:19 +0200 Subject: [PATCH 24/75] feat(web): subscribe to /presence/web while tab is visible --- .../components/PlatformPresenceMount.tsx | 8 ++++++++ apps/web/src/app/(app)/layout.tsx | 2 ++ apps/web/src/hooks/usePlatformPresence.ts | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 apps/web/src/app/(app)/components/PlatformPresenceMount.tsx create mode 100644 apps/web/src/hooks/usePlatformPresence.ts diff --git a/apps/web/src/app/(app)/components/PlatformPresenceMount.tsx b/apps/web/src/app/(app)/components/PlatformPresenceMount.tsx new file mode 100644 index 0000000000..c3719aa9a6 --- /dev/null +++ b/apps/web/src/app/(app)/components/PlatformPresenceMount.tsx @@ -0,0 +1,8 @@ +'use client'; + +import { usePlatformPresence } from '@/hooks/usePlatformPresence'; + +export function PlatformPresenceMount() { + usePlatformPresence(); + return null; +} diff --git a/apps/web/src/app/(app)/layout.tsx b/apps/web/src/app/(app)/layout.tsx index abddb55aa0..afb80e8546 100644 --- a/apps/web/src/app/(app)/layout.tsx +++ b/apps/web/src/app/(app)/layout.tsx @@ -6,6 +6,7 @@ import { PageTitleProvider } from '@/contexts/PageTitleContext'; import { EventServiceProvider } from '@/contexts/EventServiceContext'; import { AdminOmnibox } from '@/components/admin-omnibox'; import { PrefetchedOrganizations } from './components/PrefetchedOrganizations'; +import { PlatformPresenceMount } from './components/PlatformPresenceMount'; import { ImpactIdentify } from '@/components/ImpactIdentify'; export default function AppLayout({ children }: { children: React.ReactNode }) { @@ -13,6 +14,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { + diff --git a/apps/web/src/hooks/usePlatformPresence.ts b/apps/web/src/hooks/usePlatformPresence.ts new file mode 100644 index 0000000000..072158efc1 --- /dev/null +++ b/apps/web/src/hooks/usePlatformPresence.ts @@ -0,0 +1,19 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { presenceContextForPlatform } from '@kilocode/event-service'; +import { usePresenceSubscription } from './usePresenceSubscription'; + +export function usePlatformPresence() { + const [visible, setVisible] = useState( + typeof document === 'undefined' ? true : !document.hidden, + ); + + useEffect(() => { + const onChange = () => setVisible(!document.hidden); + document.addEventListener('visibilitychange', onChange); + return () => document.removeEventListener('visibilitychange', onChange); + }, []); + + usePresenceSubscription(presenceContextForPlatform('web'), visible); +} From 99b52d592fb438f9fbdfe5ed2367f43dccf66fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:18:28 +0200 Subject: [PATCH 25/75] feat(web): subscribe to /presence/kiloclaw/{sandboxId} on instance views --- .../PersonalInstancePresenceMount.tsx | 10 +++++++++ apps/web/src/app/(app)/claw/layout.tsx | 2 ++ .../components/OrgInstancePresenceMount.tsx | 13 +++++++++++ .../(app)/organizations/[id]/claw/layout.tsx | 2 ++ apps/web/src/hooks/useInstancePresence.ts | 22 +++++++++++++++++++ apps/web/src/hooks/usePlatformPresence.ts | 4 +--- 6 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/app/(app)/claw/components/PersonalInstancePresenceMount.tsx create mode 100644 apps/web/src/app/(app)/organizations/[id]/claw/components/OrgInstancePresenceMount.tsx create mode 100644 apps/web/src/hooks/useInstancePresence.ts diff --git a/apps/web/src/app/(app)/claw/components/PersonalInstancePresenceMount.tsx b/apps/web/src/app/(app)/claw/components/PersonalInstancePresenceMount.tsx new file mode 100644 index 0000000000..d1236318ae --- /dev/null +++ b/apps/web/src/app/(app)/claw/components/PersonalInstancePresenceMount.tsx @@ -0,0 +1,10 @@ +'use client'; + +import { useInstancePresence } from '@/hooks/useInstancePresence'; +import { useKiloClawStatus } from '@/hooks/useKiloClaw'; + +export function PersonalInstancePresenceMount() { + const { data: status } = useKiloClawStatus(); + useInstancePresence(status?.sandboxId ?? undefined); + return null; +} diff --git a/apps/web/src/app/(app)/claw/layout.tsx b/apps/web/src/app/(app)/claw/layout.tsx index 97b5299011..6aa97f7666 100644 --- a/apps/web/src/app/(app)/claw/layout.tsx +++ b/apps/web/src/app/(app)/claw/layout.tsx @@ -1,12 +1,14 @@ import { getUserFromAuthOrRedirect } from '@/lib/user.server'; import { PylonWidget } from '@/components/pylon-widget'; import { PylonSupportButton } from '@/components/pylon-support-button'; +import { PersonalInstancePresenceMount } from './components/PersonalInstancePresenceMount'; import './claw-chat.css'; export default async function ClawLayout({ children }: { children: React.ReactNode }) { await getUserFromAuthOrRedirect(); return ( <> + {children} diff --git a/apps/web/src/app/(app)/organizations/[id]/claw/components/OrgInstancePresenceMount.tsx b/apps/web/src/app/(app)/organizations/[id]/claw/components/OrgInstancePresenceMount.tsx new file mode 100644 index 0000000000..23c300b371 --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/claw/components/OrgInstancePresenceMount.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { useInstancePresence } from '@/hooks/useInstancePresence'; +import { useOrgKiloClawStatus } from '@/hooks/useOrgKiloClaw'; + +export function OrgInstancePresenceMount() { + const params = useParams<{ id: string }>(); + const organizationId = params?.id; + const { data: status } = useOrgKiloClawStatus(organizationId); + useInstancePresence(status?.sandboxId ?? undefined); + return null; +} diff --git a/apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx b/apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx index a53196be12..cabace358d 100644 --- a/apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx @@ -1,10 +1,12 @@ import { PylonSupportButton } from '@/components/pylon-support-button'; import { PylonWidget } from '@/components/pylon-widget'; +import { OrgInstancePresenceMount } from './components/OrgInstancePresenceMount'; import '@/app/(app)/claw/claw-chat.css'; export default function OrgClawLayout({ children }: { children: React.ReactNode }) { return ( <> + {children} diff --git a/apps/web/src/hooks/useInstancePresence.ts b/apps/web/src/hooks/useInstancePresence.ts new file mode 100644 index 0000000000..58c7c3731a --- /dev/null +++ b/apps/web/src/hooks/useInstancePresence.ts @@ -0,0 +1,22 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { presenceContextForInstance } from '@kilocode/event-service'; +import { usePresenceSubscription } from './usePresenceSubscription'; + +export function useInstancePresence(sandboxId: string | undefined, enabled = true) { + const [visible, setVisible] = useState( + typeof document === 'undefined' ? true : !document.hidden + ); + + useEffect(() => { + const onChange = () => setVisible(!document.hidden); + document.addEventListener('visibilitychange', onChange); + return () => document.removeEventListener('visibilitychange', onChange); + }, []); + + usePresenceSubscription( + sandboxId ? presenceContextForInstance(sandboxId) : '', + Boolean(sandboxId) && enabled && visible + ); +} diff --git a/apps/web/src/hooks/usePlatformPresence.ts b/apps/web/src/hooks/usePlatformPresence.ts index 072158efc1..15b37ce4a1 100644 --- a/apps/web/src/hooks/usePlatformPresence.ts +++ b/apps/web/src/hooks/usePlatformPresence.ts @@ -5,9 +5,7 @@ import { presenceContextForPlatform } from '@kilocode/event-service'; import { usePresenceSubscription } from './usePresenceSubscription'; export function usePlatformPresence() { - const [visible, setVisible] = useState( - typeof document === 'undefined' ? true : !document.hidden, - ); + const [visible, setVisible] = useState(typeof document === 'undefined' ? true : !document.hidden); useEffect(() => { const onChange = () => setVisible(!document.hidden); From bdb99c633865a71bbd87c0e048513f73aa96dd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:22:46 +0200 Subject: [PATCH 26/75] refactor(web): extract useDocumentVisible primitive --- apps/web/src/hooks/useDocumentVisible.ts | 21 +++++++++++++++++++++ apps/web/src/hooks/useInstancePresence.ts | 14 +++----------- apps/web/src/hooks/usePlatformPresence.ts | 12 +++--------- 3 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/hooks/useDocumentVisible.ts diff --git a/apps/web/src/hooks/useDocumentVisible.ts b/apps/web/src/hooks/useDocumentVisible.ts new file mode 100644 index 0000000000..6a34452232 --- /dev/null +++ b/apps/web/src/hooks/useDocumentVisible.ts @@ -0,0 +1,21 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +/** + * Returns whether the current document is visible (not hidden). + * SSR-safe: returns `true` when `document` is undefined. + */ +export function useDocumentVisible(): boolean { + const [visible, setVisible] = useState( + typeof document === 'undefined' ? true : !document.hidden, + ); + + useEffect(() => { + const onChange = () => setVisible(!document.hidden); + document.addEventListener('visibilitychange', onChange); + return () => document.removeEventListener('visibilitychange', onChange); + }, []); + + return visible; +} diff --git a/apps/web/src/hooks/useInstancePresence.ts b/apps/web/src/hooks/useInstancePresence.ts index 58c7c3731a..b8a76e72d3 100644 --- a/apps/web/src/hooks/useInstancePresence.ts +++ b/apps/web/src/hooks/useInstancePresence.ts @@ -1,20 +1,12 @@ 'use client'; -import { useEffect, useState } from 'react'; import { presenceContextForInstance } from '@kilocode/event-service'; + +import { useDocumentVisible } from './useDocumentVisible'; import { usePresenceSubscription } from './usePresenceSubscription'; export function useInstancePresence(sandboxId: string | undefined, enabled = true) { - const [visible, setVisible] = useState( - typeof document === 'undefined' ? true : !document.hidden - ); - - useEffect(() => { - const onChange = () => setVisible(!document.hidden); - document.addEventListener('visibilitychange', onChange); - return () => document.removeEventListener('visibilitychange', onChange); - }, []); - + const visible = useDocumentVisible(); usePresenceSubscription( sandboxId ? presenceContextForInstance(sandboxId) : '', Boolean(sandboxId) && enabled && visible diff --git a/apps/web/src/hooks/usePlatformPresence.ts b/apps/web/src/hooks/usePlatformPresence.ts index 15b37ce4a1..86cb6fa2e8 100644 --- a/apps/web/src/hooks/usePlatformPresence.ts +++ b/apps/web/src/hooks/usePlatformPresence.ts @@ -1,17 +1,11 @@ 'use client'; -import { useEffect, useState } from 'react'; import { presenceContextForPlatform } from '@kilocode/event-service'; + +import { useDocumentVisible } from './useDocumentVisible'; import { usePresenceSubscription } from './usePresenceSubscription'; export function usePlatformPresence() { - const [visible, setVisible] = useState(typeof document === 'undefined' ? true : !document.hidden); - - useEffect(() => { - const onChange = () => setVisible(!document.hidden); - document.addEventListener('visibilitychange', onChange); - return () => document.removeEventListener('visibilitychange', onChange); - }, []); - + const visible = useDocumentVisible(); usePresenceSubscription(presenceContextForPlatform('web'), visible); } From 405b185fad91dd6b29459a6d345ec74d9c4bdf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:25:18 +0200 Subject: [PATCH 27/75] feat(web): subscribe to conversation presence while tab visible --- .../claw/kilo-chat/components/MessageArea.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx index 3aa827ae7e..da809c9f63 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx @@ -14,8 +14,12 @@ import { useRemoveReaction, useExecuteAction, } from '../hooks/useMessages'; -import { kiloclawConversationContext } from '@kilocode/event-service'; +import { + kiloclawConversationContext, + presenceContextForConversation, +} from '@kilocode/event-service'; import { usePresenceSubscription } from '@/hooks/usePresenceSubscription'; +import { useDocumentVisible } from '@/hooks/useDocumentVisible'; import { useTypingSender, useTypingState } from '../hooks/useTyping'; import { useConversationDetail, @@ -76,12 +80,20 @@ export function MessageArea({ conversationId }: MessageAreaProps) { const [isRenamingTitle, setIsRenamingTitle] = useState(false); const [renameText, setRenameText] = useState(''); + const visible = useDocumentVisible(); + // Subscribe to this conversation's events via the event-service WebSocket usePresenceSubscription( sandboxId && conversationId ? kiloclawConversationContext(sandboxId, conversationId) : '', Boolean(sandboxId && conversationId) ); + // Subscribe to presence for this conversation while the tab is visible. + usePresenceSubscription( + sandboxId && conversationId ? presenceContextForConversation(sandboxId, conversationId) : '', + Boolean(sandboxId && conversationId) && visible + ); + // Event Service delivers subscribed contexts to every handler, so each // handler must validate the incoming `ctx` against this string before // applying changes to the active conversation's state. From 4429bdf060a4080e2c0cc24289bba4c1aa7d5b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:32:42 +0200 Subject: [PATCH 28/75] style(web): reflow useDocumentVisible useState init to one line --- apps/web/src/hooks/useDocumentVisible.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/hooks/useDocumentVisible.ts b/apps/web/src/hooks/useDocumentVisible.ts index 6a34452232..df55c211cc 100644 --- a/apps/web/src/hooks/useDocumentVisible.ts +++ b/apps/web/src/hooks/useDocumentVisible.ts @@ -7,9 +7,7 @@ import { useEffect, useState } from 'react'; * SSR-safe: returns `true` when `document` is undefined. */ export function useDocumentVisible(): boolean { - const [visible, setVisible] = useState( - typeof document === 'undefined' ? true : !document.hidden, - ); + const [visible, setVisible] = useState(typeof document === 'undefined' ? true : !document.hidden); useEffect(() => { const onChange = () => setVisible(!document.hidden); From eca983e2e93849009ffbeb788bc76c6f1a51498a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:39:52 +0200 Subject: [PATCH 29/75] refactor(web): tighten presence hook + kilo-chat router contract - usePresenceSubscription: accept 'string | null' instead of empty-string sentinel; update call sites (KiloChatLayout, MessageArea, useInstancePresence) - kilo-chat router: validate expiresAt with z.iso.datetime() - kilo-chat-router test: verify the JWT payload (kiloUserId, tokenSource, version) and that expiresAt lands in the expected ~1h window - MessageArea: comment distinguishing the always-on chat-event subscription from the visibility-gated presence subscription --- .../kilo-chat/components/KiloChatLayout.tsx | 5 ++++- .../claw/kilo-chat/components/MessageArea.tsx | 11 +++++++---- apps/web/src/hooks/useInstancePresence.ts | 2 +- apps/web/src/hooks/usePresenceSubscription.ts | 4 ++-- apps/web/src/routers/kilo-chat-router.test.ts | 19 ++++++++++++++++--- apps/web/src/routers/kilo-chat-router.ts | 2 +- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx index f85b101a45..ab8933d548 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/KiloChatLayout.tsx @@ -45,7 +45,10 @@ export function KiloChatLayout({ const router = useRouter(); const { eventService, kiloChatClient } = useEventServiceClient(); - usePresenceSubscription(sandboxId ? kiloclawInstanceContext(sandboxId) : '', Boolean(sandboxId)); + usePresenceSubscription( + sandboxId ? kiloclawInstanceContext(sandboxId) : null, + Boolean(sandboxId) + ); const queryClient = useQueryClient(); const params = useParams<{ conversationId?: string }>(); diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx index da809c9f63..e8d373a230 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx @@ -82,15 +82,18 @@ export function MessageArea({ conversationId }: MessageAreaProps) { const visible = useDocumentVisible(); - // Subscribe to this conversation's events via the event-service WebSocket + // Subscribe to this conversation's chat-event stream while the conversation + // is open. Not gated on visibility — we want incoming messages to land in + // the cache even when the tab is hidden. usePresenceSubscription( - sandboxId && conversationId ? kiloclawConversationContext(sandboxId, conversationId) : '', + sandboxId && conversationId ? kiloclawConversationContext(sandboxId, conversationId) : null, Boolean(sandboxId && conversationId) ); - // Subscribe to presence for this conversation while the tab is visible. + // Signal our own presence on this conversation. Gated on visibility so we + // only appear "viewing" while the tab is actually in the foreground. usePresenceSubscription( - sandboxId && conversationId ? presenceContextForConversation(sandboxId, conversationId) : '', + sandboxId && conversationId ? presenceContextForConversation(sandboxId, conversationId) : null, Boolean(sandboxId && conversationId) && visible ); diff --git a/apps/web/src/hooks/useInstancePresence.ts b/apps/web/src/hooks/useInstancePresence.ts index b8a76e72d3..085e28f2e2 100644 --- a/apps/web/src/hooks/useInstancePresence.ts +++ b/apps/web/src/hooks/useInstancePresence.ts @@ -8,7 +8,7 @@ import { usePresenceSubscription } from './usePresenceSubscription'; export function useInstancePresence(sandboxId: string | undefined, enabled = true) { const visible = useDocumentVisible(); usePresenceSubscription( - sandboxId ? presenceContextForInstance(sandboxId) : '', + sandboxId ? presenceContextForInstance(sandboxId) : null, Boolean(sandboxId) && enabled && visible ); } diff --git a/apps/web/src/hooks/usePresenceSubscription.ts b/apps/web/src/hooks/usePresenceSubscription.ts index 485a3bf957..56bb55a381 100644 --- a/apps/web/src/hooks/usePresenceSubscription.ts +++ b/apps/web/src/hooks/usePresenceSubscription.ts @@ -11,10 +11,10 @@ import { useEventServiceClient } from '@/contexts/EventServiceContext'; * Reads the global `EventServiceClient` from `EventServiceProvider`, mounted * in `(app)/layout.tsx` for every authenticated route. */ -export function usePresenceSubscription(context: string, active: boolean) { +export function usePresenceSubscription(context: string | null, active: boolean) { const { eventService } = useEventServiceClient(); useEffect(() => { - if (!active || !context) return; + if (!active || context === null) return; eventService.subscribe([context]); return () => { eventService.unsubscribe([context]); diff --git a/apps/web/src/routers/kilo-chat-router.test.ts b/apps/web/src/routers/kilo-chat-router.test.ts index 52569268ea..9d64775140 100644 --- a/apps/web/src/routers/kilo-chat-router.test.ts +++ b/apps/web/src/routers/kilo-chat-router.test.ts @@ -1,5 +1,8 @@ +import jwt from 'jsonwebtoken'; import { createCallerForUser } from '@/routers/test-utils'; import { insertTestUser } from '@/tests/helpers/user.helper'; +import { NEXTAUTH_SECRET } from '@/lib/config.server'; +import { JWT_TOKEN_VERSION } from '@/lib/tokens'; import type { User } from '@kilocode/db/schema'; let testUser: User; @@ -12,15 +15,25 @@ describe('kiloChat router - getToken', () => { }); }); - it('returns a JWT-shaped token and a future expiresAt', async () => { + it('returns a verifiable kilo-chat JWT for the caller, expiring in ~1h', async () => { const caller = await createCallerForUser(testUser.id); const before = Date.now(); const result = await caller.kiloChat.getToken(); + const after = Date.now(); - expect(result.token).toMatch(/\..+\..+/); + const payload = jwt.verify(result.token, NEXTAUTH_SECRET, { + algorithms: ['HS256'], + }) as jwt.JwtPayload & { kiloUserId: string; tokenSource: string; version: number }; + + expect(payload.kiloUserId).toBe(testUser.id); + expect(payload.tokenSource).toBe('kilo-chat'); + expect(payload.version).toBe(JWT_TOKEN_VERSION); const expiresAtMs = Date.parse(result.expiresAt); expect(Number.isNaN(expiresAtMs)).toBe(false); - expect(expiresAtMs).toBeGreaterThan(before); + // Router uses a 1h TTL; allow ±5s of clock slop around the call window. + const oneHourMs = 60 * 60 * 1000; + expect(expiresAtMs).toBeGreaterThanOrEqual(before + oneHourMs - 5_000); + expect(expiresAtMs).toBeLessThanOrEqual(after + oneHourMs + 5_000); }); }); diff --git a/apps/web/src/routers/kilo-chat-router.ts b/apps/web/src/routers/kilo-chat-router.ts index f2d3c4b65b..4b9b4e4cd5 100644 --- a/apps/web/src/routers/kilo-chat-router.ts +++ b/apps/web/src/routers/kilo-chat-router.ts @@ -7,7 +7,7 @@ const KILO_CHAT_TOKEN_TTL_S = 60 * 60; export const kiloChatRouter = createTRPCRouter({ getToken: baseProcedure - .output(z.object({ token: z.string(), expiresAt: z.string() })) + .output(z.object({ token: z.string(), expiresAt: z.iso.datetime() })) .query(({ ctx }) => { const token = generateApiToken( ctx.user, From 7edca1a13ee5a2950cdf25a30ad8200844483923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 20:48:31 +0200 Subject: [PATCH 30/75] fix(event-service): refcount subscribe/unsubscribe by context Multiple consumers can now independently hold the same context without trampling each other. The wire context.subscribe/context.unsubscribe messages are only sent on the 0->1 and 1->0 refcount transitions; the intermediate churn stays client-side. Resubscribe-on-reconnect dedupes by context key. Tests cover: double-subscribe collapses to a single wire send, partial unsubscribe keeps the context alive, last-consumer-out releases it, mixed batches only send newly-active contexts, unknown-context unsubscribes are no-ops, and reconnect resubscribes each context once. --- .../src/__tests__/client.test.ts | 98 +++++++++++++++++++ packages/event-service/src/client.ts | 31 ++++-- 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/packages/event-service/src/__tests__/client.test.ts b/packages/event-service/src/__tests__/client.test.ts index 9527dec426..01fe89f415 100644 --- a/packages/event-service/src/__tests__/client.test.ts +++ b/packages/event-service/src/__tests__/client.test.ts @@ -387,4 +387,102 @@ describe('EventServiceClient', () => { expect(err).toBeInstanceOf(Error); expect(err.name).toBe('HandshakeTimeoutError'); }); + + describe('subscribe/unsubscribe refcounting', () => { + function sentMessages() { + return lastMockWs.sent.map(s => JSON.parse(s) as unknown); + } + + it('only sends one wire context.subscribe when two consumers subscribe to the same context', async () => { + const client = makeClient(); + await client.connect(); + lastMockWs.sent = []; + + client.subscribe(['room:1']); + client.subscribe(['room:1']); + + expect(sentMessages()).toEqual([{ type: 'context.subscribe', contexts: ['room:1'] }]); + }); + + it('keeps the subscription alive when one of two consumers unsubscribes', async () => { + const client = makeClient(); + await client.connect(); + lastMockWs.sent = []; + + client.subscribe(['room:1']); + client.subscribe(['room:1']); + client.unsubscribe(['room:1']); + + // Only the initial 0→1 subscribe should have been sent. No unsubscribe yet + // because the second consumer is still holding a ref. + expect(sentMessages()).toEqual([{ type: 'context.subscribe', contexts: ['room:1'] }]); + }); + + it('sends context.unsubscribe only when the last consumer drops the ref (1→0)', async () => { + const client = makeClient(); + await client.connect(); + lastMockWs.sent = []; + + client.subscribe(['room:1']); + client.subscribe(['room:1']); + client.unsubscribe(['room:1']); + client.unsubscribe(['room:1']); + + expect(sentMessages()).toEqual([ + { type: 'context.subscribe', contexts: ['room:1'] }, + { type: 'context.unsubscribe', contexts: ['room:1'] }, + ]); + }); + + it('handles a mixed batch: only newly-active contexts get sent', async () => { + const client = makeClient(); + await client.connect(); + client.subscribe(['room:1']); + lastMockWs.sent = []; + + // room:1 already at refcount 1, room:2 is new. Only room:2 should hit the wire. + client.subscribe(['room:1', 'room:2']); + + expect(sentMessages()).toEqual([{ type: 'context.subscribe', contexts: ['room:2'] }]); + }); + + it('extra unsubscribes for an unknown context are no-ops', async () => { + const client = makeClient(); + await client.connect(); + lastMockWs.sent = []; + + // Never subscribed — must not crash and must not emit a wire message. + client.unsubscribe(['ghost']); + + expect(sentMessages()).toEqual([]); + }); + + it('resubscribe-on-reconnect deduplicates by context (one entry per active context)', async () => { + vi.useFakeTimers(); + const client = makeClient(); + await client.connect(); + + // Two consumers hold the same context. + client.subscribe(['room:1']); + client.subscribe(['room:1']); + + // Drop the connection — auto-reconnect kicks in. + lastMockWs.triggerClose(); + await vi.advanceTimersByTimeAsync(2000); + expect(allMockWs.length).toBe(2); + // The second mock socket also auto-triggers open via the global stub. + await vi.advanceTimersByTimeAsync(0); + + const resubMessages = allMockWs[1].sent + .map(s => JSON.parse(s) as { type: string; contexts?: string[] }) + .filter(m => m.type === 'context.subscribe'); + + // Exactly one resubscribe message containing the context exactly once, + // regardless of how many consumers hold the ref. + expect(resubMessages).toHaveLength(1); + expect(resubMessages[0]?.contexts).toEqual(['room:1']); + + vi.useRealTimers(); + }); + }); }); diff --git a/packages/event-service/src/client.ts b/packages/event-service/src/client.ts index 9466d50b73..3c68d43db5 100644 --- a/packages/event-service/src/client.ts +++ b/packages/event-service/src/client.ts @@ -48,7 +48,11 @@ export class EventServiceClient { private ws: WebSocket | null = null; private connected = false; private eventHandlers = new Map void>>(); - private activeContexts = new Set(); + // Refcounted so multiple consumers can independently subscribe to and + // unsubscribe from the same context without trampling each other. The wire + // `context.subscribe`/`context.unsubscribe` messages are only sent on the + // 0↔1 transitions; intermediate refcount churn stays client-side. + private activeContexts = new Map(); private reconnectTimer: ReturnType | null = null; private destroyed = false; private reconnectAttempts = 0; @@ -215,20 +219,31 @@ export class EventServiceClient { } subscribe(contexts: string[]): void { + const newlyActive: string[] = []; for (const ctx of contexts) { - this.activeContexts.add(ctx); + const next = (this.activeContexts.get(ctx) ?? 0) + 1; + this.activeContexts.set(ctx, next); + if (next === 1) newlyActive.push(ctx); } - if (this.isConnected()) { - this.send({ type: 'context.subscribe', contexts }); + if (newlyActive.length > 0 && this.isConnected()) { + this.send({ type: 'context.subscribe', contexts: newlyActive }); } } unsubscribe(contexts: string[]): void { + const released: string[] = []; for (const ctx of contexts) { - this.activeContexts.delete(ctx); + const current = this.activeContexts.get(ctx); + if (current === undefined) continue; + if (current <= 1) { + this.activeContexts.delete(ctx); + released.push(ctx); + } else { + this.activeContexts.set(ctx, current - 1); + } } - if (this.isConnected()) { - this.send({ type: 'context.unsubscribe', contexts }); + if (released.length > 0 && this.isConnected()) { + this.send({ type: 'context.unsubscribe', contexts: released }); } } @@ -311,7 +326,7 @@ export class EventServiceClient { if (this.activeContexts.size > 0) { this.send({ type: 'context.subscribe', - contexts: Array.from(this.activeContexts), + contexts: Array.from(this.activeContexts.keys()), }); } } From 67e0fe368c74bfc7d04199d6e3af196c1d51ab25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:00:37 +0200 Subject: [PATCH 31/75] chore(mobile): add EXPO_PUBLIC_KILO_CHAT_URL and EXPO_PUBLIC_EVENT_SERVICE_URL --- apps/mobile/src/lib/config.ts | 3 +++ apps/mobile/src/lib/env-keys.js | 2 ++ 2 files changed, 5 insertions(+) diff --git a/apps/mobile/src/lib/config.ts b/apps/mobile/src/lib/config.ts index b90c113990..be543127f8 100644 --- a/apps/mobile/src/lib/config.ts +++ b/apps/mobile/src/lib/config.ts @@ -18,3 +18,6 @@ export const APPSFLYER_APP_ID: string = required('appsFlyerAppId'); export const CLOUD_AGENT_WS_URL: string = required('cloudAgentWsUrl'); export const SESSION_INGEST_WS_URL: string = required('sessionIngestWsUrl'); + +export const KILO_CHAT_URL: string = required('kiloChatUrl'); +export const EVENT_SERVICE_URL: string = required('eventServiceUrl'); diff --git a/apps/mobile/src/lib/env-keys.js b/apps/mobile/src/lib/env-keys.js index 3ec4d72001..3200f2ecda 100644 --- a/apps/mobile/src/lib/env-keys.js +++ b/apps/mobile/src/lib/env-keys.js @@ -7,4 +7,6 @@ export const ENV_KEYS = { sessionIngestWsUrl: 'SESSION_INGEST_WS_URL', appsFlyerDevKey: 'APPSFLYER_DEV_KEY', appsFlyerAppId: 'APPSFLYER_APP_ID', + kiloChatUrl: 'EXPO_PUBLIC_KILO_CHAT_URL', + eventServiceUrl: 'EXPO_PUBLIC_EVENT_SERVICE_URL', }; From 7b2d7faa5f9c27776eea142e7b4a4fa40c9521be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:01:11 +0200 Subject: [PATCH 32/75] chore(mobile): add kilo-chat workspace deps --- apps/mobile/package.json | 3 +++ pnpm-lock.yaml | 47 ++++++++++------------------------------ 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 3899cef255..75cea06b0a 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -23,6 +23,9 @@ "dependencies": { "@expo-google-fonts/jetbrains-mono": "^0.4.1", "@expo/react-native-action-sheet": "^4.1.1", + "@kilocode/event-service": "workspace:*", + "@kilocode/kilo-chat": "workspace:*", + "@kilocode/notifications": "workspace:*", "@kilocode/trpc": "workspace:*", "@react-native-community/netinfo": "11.5.2", "@rn-primitives/portal": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ea3a33947..85dd420104 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,6 +146,15 @@ importers: '@expo/react-native-action-sheet': specifier: ^4.1.1 version: 4.1.1(@types/react@19.2.14)(react@19.2.0) + '@kilocode/event-service': + specifier: workspace:* + version: link:../../packages/event-service + '@kilocode/kilo-chat': + specifier: workspace:* + version: link:../../packages/kilo-chat + '@kilocode/notifications': + specifier: workspace:* + version: link:../../packages/notifications '@kilocode/trpc': specifier: workspace:* version: link:../../packages/trpc @@ -1561,7 +1570,7 @@ importers: version: 7.0.0-dev.20260319.1 jest: specifier: ^30.3.0 - version: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) + version: 30.3.0(@types/node@24.12.0)(esbuild-register@3.6.0(esbuild@0.27.4)) typescript: specifier: 'catalog:' version: 5.9.3 @@ -16750,7 +16759,7 @@ snapshots: cjs-module-lexer: 1.4.3 esbuild: 0.27.4 miniflare: 4.20260310.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) wrangler: 4.72.0(@cloudflare/workers-types@4.20260313.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@cloudflare/workers-types' @@ -22258,7 +22267,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/utils@3.2.4': dependencies: @@ -25689,25 +25698,6 @@ snapshots: - supports-color - ts-node - jest-cli@30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)): - dependencies: - '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.4)) - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) - jest-util: 30.3.0 - jest-validate: 30.3.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jest-config@29.7.0(@types/node@24.12.0): dependencies: '@babel/core': 7.29.0 @@ -26359,19 +26349,6 @@ snapshots: - supports-color - ts-node - jest@30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)): - dependencies: - '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.4)) - '@jest/types': 30.3.0 - import-local: 3.2.0 - jest-cli: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jimp-compact@0.16.1: {} jiti@2.6.1: {} From 0735765109d20cef76d0c46729b627bdedb37f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:01:32 +0200 Subject: [PATCH 33/75] feat(mobile): add kilo-chat token getter with caching --- .../kilo-chat/hooks/use-kilo-chat-token.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts new file mode 100644 index 0000000000..b484c5e150 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts @@ -0,0 +1,39 @@ +import { useCallback, useRef } from 'react'; + +import { trpcClient } from '@/lib/trpc'; + +type TokenCache = { + token: string; + expiresAtMs: number; +}; + +/** + * Returns a stable getter function that fetches a kilo-chat JWT, caching it + * until 60 seconds before expiry. Concurrent callers share a single in-flight + * fetch via a dedup ref. + */ +export function useKiloChatTokenGetter(): () => Promise { + const cacheRef = useRef(null); + const inFlightRef = useRef | null>(null); + + return useCallback(async () => { + const cached = cacheRef.current; + if (cached && cached.expiresAtMs - Date.now() > 60_000) { + return cached.token; + } + + if (inFlightRef.current) { + return inFlightRef.current; + } + + const promise = trpcClient.kiloChat.getToken.query().then(({ token, expiresAt }) => { + const expiresAtMs = new Date(expiresAt).getTime(); + cacheRef.current = { token, expiresAtMs }; + inFlightRef.current = null; + return token; + }); + + inFlightRef.current = promise; + return promise; + }, []); +} From 4294250d957387f8a345325b22c132fd7fb88cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:01:53 +0200 Subject: [PATCH 34/75] feat(mobile): add useCurrentUserId from JWT sub --- .../kilo-chat/hooks/use-current-user-id.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts b/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts new file mode 100644 index 0000000000..d499643486 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; + +import { useKiloChatTokenGetter } from './use-kilo-chat-token'; + +/** + * Decodes the `sub` claim from the kilo-chat JWT and returns it as the current + * user's ID. Returns `null` while loading or if the token cannot be decoded. + */ +export function useCurrentUserId(): string | null { + const getToken = useKiloChatTokenGetter(); + const [userId, setUserId] = useState(null); + + useEffect(() => { + let cancelled = false; + + getToken() + .then(token => { + if (cancelled) return; + const parts = token.split('.'); + if (parts.length < 2 || !parts[1]) return; + const payload = parts[1]; + const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/')); + const parsed = JSON.parse(decoded) as Record; + const sub = typeof parsed.sub === 'string' ? parsed.sub : null; + setUserId(sub); + }) + .catch(() => { + // Leave userId as null on failure + }); + + return () => { + cancelled = true; + }; + }, [getToken]); + + return userId; +} From 4081c29db9f0dbf6dcc9e5cc38cce24fe076269e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:02:21 +0200 Subject: [PATCH 35/75] feat(mobile): add KiloChatProvider --- .../kilo-chat/kilo-chat-provider.tsx | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx diff --git a/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx b/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx new file mode 100644 index 0000000000..f255f96a2b --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx @@ -0,0 +1,53 @@ +import { createContext, useContext, useEffect, useState } from 'react'; + +import { EventServiceClient } from '@kilocode/event-service'; +import { KiloChatClient } from '@kilocode/kilo-chat'; + +import { EVENT_SERVICE_URL, KILO_CHAT_URL } from '@/lib/config'; + +import { useKiloChatTokenGetter } from './hooks/use-kilo-chat-token'; + +type KiloChatContextValue = { + eventService: EventServiceClient; + kiloChatClient: KiloChatClient; +}; + +export const KiloChatContext = createContext(null); + +export function useKiloChatContext(): KiloChatContextValue { + const ctx = useContext(KiloChatContext); + if (!ctx) { + throw new Error('useKiloChatContext must be used within a KiloChatProvider'); + } + return ctx; +} + +type KiloChatProviderProps = { + children: React.ReactNode; +}; + +export function KiloChatProvider({ children }: KiloChatProviderProps) { + const getToken = useKiloChatTokenGetter(); + + const [value] = useState(() => { + const eventService = new EventServiceClient({ + url: EVENT_SERVICE_URL, + getToken, + }); + const kiloChatClient = new KiloChatClient({ + eventService, + baseUrl: KILO_CHAT_URL, + getToken, + }); + return { eventService, kiloChatClient }; + }); + + useEffect(() => { + void value.eventService.connect(); + return () => { + value.eventService.disconnect(); + }; + }, [value]); + + return {children}; +} From 57448bd77b65345fc2563e70a8d527585934a510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:02:33 +0200 Subject: [PATCH 36/75] feat(mobile): add useKiloChatClient and useEventServiceClient hooks --- .../kilo-chat/hooks/use-kilo-chat-client.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts new file mode 100644 index 0000000000..6d57e243e7 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts @@ -0,0 +1,20 @@ +import type { EventServiceClient } from '@kilocode/event-service'; +import type { KiloChatClient } from '@kilocode/kilo-chat'; + +import { useKiloChatContext } from '../kilo-chat-provider'; + +/** + * Returns the {@link KiloChatClient} instance from the nearest + * {@link KiloChatProvider}. Throws if called outside a provider. + */ +export function useKiloChatClient(): KiloChatClient { + return useKiloChatContext().kiloChatClient; +} + +/** + * Returns the {@link EventServiceClient} instance from the nearest + * {@link KiloChatProvider}. Throws if called outside a provider. + */ +export function useEventServiceClient(): EventServiceClient { + return useKiloChatContext().eventService; +} From 257f381c7ddabd9427dfaef38c8d1cff418313ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:05:55 +0200 Subject: [PATCH 37/75] fix(mobile): fix lint errors in kilo-chat token getter --- .../kilo-chat/hooks/use-kilo-chat-token.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts index b484c5e150..4e05d42755 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts @@ -22,18 +22,24 @@ export function useKiloChatTokenGetter(): () => Promise { return cached.token; } - if (inFlightRef.current) { - return inFlightRef.current; + const existing = inFlightRef.current; + if (existing) { + return existing; } - const promise = trpcClient.kiloChat.getToken.query().then(({ token, expiresAt }) => { - const expiresAtMs = new Date(expiresAt).getTime(); - cacheRef.current = { token, expiresAtMs }; - inFlightRef.current = null; - return token; + // Create a shared promise and set inFlightRef before awaiting so concurrent + // callers share this fetch rather than starting duplicate requests. + let resolveShared: (token: string) => void = () => undefined; + const sharedPromise = new Promise(resolve => { + resolveShared = resolve; }); + inFlightRef.current = sharedPromise; - inFlightRef.current = promise; - return promise; + const { token, expiresAt } = await trpcClient.kiloChat.getToken.query(); + const expiresAtMs = new Date(expiresAt).getTime(); + cacheRef.current = { token, expiresAtMs }; + inFlightRef.current = null; + resolveShared(token); + return token; }, []); } From 5844aaf77c6002e8f3f70fa20d64b8ac027270ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:05:58 +0200 Subject: [PATCH 38/75] fix(mobile): fix lint errors in useCurrentUserId hook --- .../kilo-chat/hooks/use-current-user-id.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts b/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts index d499643486..edcf453549 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts @@ -13,20 +13,27 @@ export function useCurrentUserId(): string | null { useEffect(() => { let cancelled = false; - getToken() - .then(token => { - if (cancelled) return; + async function fetchUserId() { + try { + const token = await getToken(); + if (cancelled) { + return; + } const parts = token.split('.'); - if (parts.length < 2 || !parts[1]) return; + if (parts.length < 2 || !parts[1]) { + return; + } const payload = parts[1]; - const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/')); + const decoded = atob(payload.replaceAll('-', '+').replaceAll('_', '/')); const parsed = JSON.parse(decoded) as Record; const sub = typeof parsed.sub === 'string' ? parsed.sub : null; setUserId(sub); - }) - .catch(() => { + } catch { // Leave userId as null on failure - }); + } + } + + void fetchUserId(); return () => { cancelled = true; From 825d1ac2e0681f7401ac2a1fed586a7ccb1a9038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:06:01 +0200 Subject: [PATCH 39/75] fix(mobile): fix lint errors in useKiloChatClient hook --- .../src/components/kilo-chat/hooks/use-kilo-chat-client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts index 6d57e243e7..32e7402aa3 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts @@ -1,5 +1,5 @@ -import type { EventServiceClient } from '@kilocode/event-service'; -import type { KiloChatClient } from '@kilocode/kilo-chat'; +import { type EventServiceClient } from '@kilocode/event-service'; +import { type KiloChatClient } from '@kilocode/kilo-chat'; import { useKiloChatContext } from '../kilo-chat-provider'; From fe060dd62337d251b9e1222ebd0a4372ce3a742e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:06:06 +0200 Subject: [PATCH 40/75] feat(mobile): mount KiloChatProvider in (app) layout --- apps/mobile/src/app/(app)/_layout.tsx | 129 +++++++++++++------------- 1 file changed, 66 insertions(+), 63 deletions(-) diff --git a/apps/mobile/src/app/(app)/_layout.tsx b/apps/mobile/src/app/(app)/_layout.tsx index 60998ebc61..d47c26095a 100644 --- a/apps/mobile/src/app/(app)/_layout.tsx +++ b/apps/mobile/src/app/(app)/_layout.tsx @@ -1,75 +1,78 @@ import { Stack } from 'expo-router'; +import { KiloChatProvider } from '@/components/kilo-chat/kilo-chat-provider'; import { useThemeColors } from '@/lib/hooks/use-theme-colors'; export default function AppLayout() { const colors = useThemeColors(); return ( - - - - + - - - - - - - - + > + + + + + + + + + + + + ); } From 029c69ac301abaac643c1a0b514e0505bd781062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:07:33 +0200 Subject: [PATCH 41/75] fix(kilo-chat): assert non-null in base64urlEncode loop --- packages/kilo-chat/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-chat/src/utils.ts b/packages/kilo-chat/src/utils.ts index 741140875d..f3b280f53f 100644 --- a/packages/kilo-chat/src/utils.ts +++ b/packages/kilo-chat/src/utils.ts @@ -18,7 +18,7 @@ export type ConversationCursor = { t: number; c: string }; function base64urlEncode(bytes: Uint8Array): string { let binary = ''; - for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); + for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } From f1eb38cd9bb05fb32da888a19322d4b11a7d9e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 29 Apr 2026 21:10:40 +0200 Subject: [PATCH 42/75] fix(mobile): share kilo-chat token cache + handle fetch errors Hoist cache and in-flight promise refs to module scope so all useKiloChatTokenGetter() instances (provider + useCurrentUserId) share one cache instead of each maintaining an independent one. Wrap the fetch in try/catch/finally: on error rejectShared() is called so concurrent waiters fail fast instead of hanging forever, and inFlight is always cleared in finally regardless of outcome. --- .../kilo-chat/hooks/use-kilo-chat-token.ts | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts index 4e05d42755..1ec529d8e4 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useCallback } from 'react'; import { trpcClient } from '@/lib/trpc'; @@ -7,39 +7,48 @@ type TokenCache = { expiresAtMs: number; }; +// Module-level singletons so all useKiloChatTokenGetter() instances share the +// same cache and in-flight dedup. Auth token is a per-app singleton (one +// logged-in user), so process-wide state is correct. +let cache: TokenCache | null = null; +let inFlight: Promise | null = null; + /** * Returns a stable getter function that fetches a kilo-chat JWT, caching it * until 60 seconds before expiry. Concurrent callers share a single in-flight - * fetch via a dedup ref. + * fetch via a module-level dedup ref. */ export function useKiloChatTokenGetter(): () => Promise { - const cacheRef = useRef(null); - const inFlightRef = useRef | null>(null); - return useCallback(async () => { - const cached = cacheRef.current; - if (cached && cached.expiresAtMs - Date.now() > 60_000) { - return cached.token; + if (cache && cache.expiresAtMs - Date.now() > 60_000) { + return cache.token; } - const existing = inFlightRef.current; - if (existing) { - return existing; + if (inFlight) { + return inFlight; } - // Create a shared promise and set inFlightRef before awaiting so concurrent + // Create a shared promise and set inFlight before awaiting so concurrent // callers share this fetch rather than starting duplicate requests. let resolveShared: (token: string) => void = () => undefined; - const sharedPromise = new Promise(resolve => { + let rejectShared: (err: unknown) => void = () => undefined; + const sharedPromise = new Promise((resolve, reject) => { resolveShared = resolve; + rejectShared = reject; }); - inFlightRef.current = sharedPromise; + inFlight = sharedPromise; - const { token, expiresAt } = await trpcClient.kiloChat.getToken.query(); - const expiresAtMs = new Date(expiresAt).getTime(); - cacheRef.current = { token, expiresAtMs }; - inFlightRef.current = null; - resolveShared(token); - return token; + try { + const { token, expiresAt } = await trpcClient.kiloChat.getToken.query(); + const expiresAtMs = new Date(expiresAt).getTime(); + cache = { token, expiresAtMs }; + resolveShared(token); + return token; + } catch (error) { + rejectShared(error); + throw error; + } finally { + inFlight = null; + } }, []); } From ecf29a102a01b390cea10f998afe3af710aa7629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 11:47:24 +0200 Subject: [PATCH 43/75] fix(mobile): tie kilo-chat token cache to auth token, decode kiloUserId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Key the module-level kilo-chat JWT cache and in-flight ref on the current auth token, so signing out and back in as a different user within the 1h token window no longer returns the previous user's cached JWT. - Restructure dedup so the first caller awaits the same shared promise via a slot reference, eliminating the unhandled rejection that the prior resolve/reject-pair pattern produced when the only caller's fetch failed. - Decode kiloUserId from the JWT payload instead of the standard `sub` claim — generateApiToken writes the user id as kiloUserId, so the sub-based version always returned null. --- .../kilo-chat/hooks/use-current-user-id.ts | 10 ++-- .../kilo-chat/hooks/use-kilo-chat-token.ts | 55 ++++++++++--------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts b/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts index edcf453549..6c1e850f4c 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-current-user-id.ts @@ -3,8 +3,10 @@ import { useEffect, useState } from 'react'; import { useKiloChatTokenGetter } from './use-kilo-chat-token'; /** - * Decodes the `sub` claim from the kilo-chat JWT and returns it as the current - * user's ID. Returns `null` while loading or if the token cannot be decoded. + * Decodes the `kiloUserId` claim from the kilo-chat JWT and returns it as the + * current user's ID. Returns `null` while loading or if the token cannot be + * decoded. The token is minted by `generateApiToken`, which writes the user id + * as `kiloUserId` (not the standard JWT `sub` claim). */ export function useCurrentUserId(): string | null { const getToken = useKiloChatTokenGetter(); @@ -26,8 +28,8 @@ export function useCurrentUserId(): string | null { const payload = parts[1]; const decoded = atob(payload.replaceAll('-', '+').replaceAll('_', '/')); const parsed = JSON.parse(decoded) as Record; - const sub = typeof parsed.sub === 'string' ? parsed.sub : null; - setUserId(sub); + const kiloUserId = typeof parsed.kiloUserId === 'string' ? parsed.kiloUserId : null; + setUserId(kiloUserId); } catch { // Leave userId as null on failure } diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts index 1ec529d8e4..41e710cfb7 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts @@ -1,17 +1,19 @@ import { useCallback } from 'react'; +import { useAuth } from '@/lib/auth/auth-context'; import { trpcClient } from '@/lib/trpc'; type TokenCache = { + authToken: string; token: string; expiresAtMs: number; }; -// Module-level singletons so all useKiloChatTokenGetter() instances share the -// same cache and in-flight dedup. Auth token is a per-app singleton (one -// logged-in user), so process-wide state is correct. +// Module-level cache keyed on the user's auth token, so a sign-out followed by +// a different sign-in within the JWT window doesn't return the previous user's +// token. The in-flight ref is keyed the same way for the same reason. let cache: TokenCache | null = null; -let inFlight: Promise | null = null; +let inFlight: { authToken: string; promise: Promise } | null = null; /** * Returns a stable getter function that fetches a kilo-chat JWT, caching it @@ -19,36 +21,35 @@ let inFlight: Promise | null = null; * fetch via a module-level dedup ref. */ export function useKiloChatTokenGetter(): () => Promise { + const { token: authToken } = useAuth(); return useCallback(async () => { - if (cache && cache.expiresAtMs - Date.now() > 60_000) { - return cache.token; + if (!authToken) { + throw new Error('Cannot fetch kilo-chat token: not authenticated'); } - if (inFlight) { - return inFlight; + if (cache && cache.authToken === authToken && cache.expiresAtMs - Date.now() > 60_000) { + return cache.token; } - // Create a shared promise and set inFlight before awaiting so concurrent - // callers share this fetch rather than starting duplicate requests. - let resolveShared: (token: string) => void = () => undefined; - let rejectShared: (err: unknown) => void = () => undefined; - const sharedPromise = new Promise((resolve, reject) => { - resolveShared = resolve; - rejectShared = reject; - }); - inFlight = sharedPromise; + if (inFlight && inFlight.authToken === authToken) { + return inFlight.promise; + } + const slot = { authToken, promise: fetchAndCacheToken(authToken) }; + inFlight = slot; try { - const { token, expiresAt } = await trpcClient.kiloChat.getToken.query(); - const expiresAtMs = new Date(expiresAt).getTime(); - cache = { token, expiresAtMs }; - resolveShared(token); - return token; - } catch (error) { - rejectShared(error); - throw error; + return await slot.promise; } finally { - inFlight = null; + // Only clear the slot if a concurrent caller hasn't replaced it. + if (inFlight === slot) { + inFlight = null; + } } - }, []); + }, [authToken]); +} + +async function fetchAndCacheToken(authToken: string): Promise { + const { token, expiresAt } = await trpcClient.kiloChat.getToken.query(); + cache = { authToken, token, expiresAtMs: new Date(expiresAt).getTime() }; + return token; } From d73befb4a10d1b2eb8f3bd45d858e43a2ebf28f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 11:53:26 +0200 Subject: [PATCH 44/75] fix(mobile): read auth token at call time, not at hook render KiloChatProvider builds its EventService and KiloChat clients exactly once via useState initializer, so it captures whatever getter exists at first mount. Closing the previous getter over a render-time `authToken` meant a cold start where the (app) layout mounted before SecureStore finished loading would freeze the clients with an undefined token, trapping them in a permanent reconnect loop. Read the auth token from SecureStore inside the getter, the same pattern trpcClient uses. The hook returns a stable callback with no React deps, and the cache stays keyed on the auth token so user-switch safety is preserved. --- .../kilo-chat/hooks/use-kilo-chat-token.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts index 41e710cfb7..bec3cbaef0 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-token.ts @@ -1,6 +1,7 @@ +import * as SecureStore from 'expo-secure-store'; import { useCallback } from 'react'; -import { useAuth } from '@/lib/auth/auth-context'; +import { AUTH_TOKEN_KEY } from '@/lib/storage-keys'; import { trpcClient } from '@/lib/trpc'; type TokenCache = { @@ -19,10 +20,15 @@ let inFlight: { authToken: string; promise: Promise } | null = null; * Returns a stable getter function that fetches a kilo-chat JWT, caching it * until 60 seconds before expiry. Concurrent callers share a single in-flight * fetch via a module-level dedup ref. + * + * The auth token is read from SecureStore at call time (matching `trpcClient`) + * rather than captured from `useAuth()`, so a getter constructed before auth + * has loaded — or before the user signs in — picks up the correct token on + * its next call instead of permanently capturing `undefined`. */ export function useKiloChatTokenGetter(): () => Promise { - const { token: authToken } = useAuth(); return useCallback(async () => { + const authToken = await SecureStore.getItemAsync(AUTH_TOKEN_KEY); if (!authToken) { throw new Error('Cannot fetch kilo-chat token: not authenticated'); } @@ -45,7 +51,7 @@ export function useKiloChatTokenGetter(): () => Promise { inFlight = null; } } - }, [authToken]); + }, []); } async function fetchAndCacheToken(authToken: string): Promise { From b89c4d9659a470ad1f08ca1e89435306bd860409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:02:31 +0200 Subject: [PATCH 45/75] feat(mobile): add usePresenceSubscription primitive --- .../kilo-chat/hooks/use-presence-subscription.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-presence-subscription.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-presence-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-presence-subscription.ts new file mode 100644 index 0000000000..de2e53b5a1 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-presence-subscription.ts @@ -0,0 +1,14 @@ +import { useEffect } from 'react'; + +import { useEventServiceClient } from './use-kilo-chat-client'; + +export function usePresenceSubscription(context: string | null, active: boolean) { + const eventService = useEventServiceClient(); + useEffect(() => { + if (!active || !context) return; + eventService.subscribe([context]); + return () => { + eventService.unsubscribe([context]); + }; + }, [eventService, context, active]); +} From 87a74ed56140a7dbb5a181fb84fc56687eac40ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:03:02 +0200 Subject: [PATCH 46/75] feat(mobile): subscribe to /presence/app while app is active --- apps/mobile/src/app/(app)/_layout.tsx | 136 ++++++++++-------- .../kilo-chat/hooks/use-app-presence.ts | 19 +++ 2 files changed, 92 insertions(+), 63 deletions(-) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-app-presence.ts diff --git a/apps/mobile/src/app/(app)/_layout.tsx b/apps/mobile/src/app/(app)/_layout.tsx index d47c26095a..97bbe8abd3 100644 --- a/apps/mobile/src/app/(app)/_layout.tsx +++ b/apps/mobile/src/app/(app)/_layout.tsx @@ -1,5 +1,8 @@ +import { type ReactNode } from 'react'; + import { Stack } from 'expo-router'; +import { useAppPresence } from '@/components/kilo-chat/hooks/use-app-presence'; import { KiloChatProvider } from '@/components/kilo-chat/kilo-chat-provider'; import { useThemeColors } from '@/lib/hooks/use-theme-colors'; @@ -8,71 +11,78 @@ export default function AppLayout() { return ( - - - - - - - - - - - + - + > + + + + + + + + + + + + ); } + +function PresenceMount({ children }: { children: ReactNode }) { + useAppPresence(); + return <>{children}; +} diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-app-presence.ts b/apps/mobile/src/components/kilo-chat/hooks/use-app-presence.ts new file mode 100644 index 0000000000..5d8c0745a5 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-app-presence.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react'; +import { AppState } from 'react-native'; + +import { presenceContextForPlatform } from '@kilocode/event-service'; + +import { usePresenceSubscription } from './use-presence-subscription'; + +export function useAppPresence() { + const [active, setActive] = useState(AppState.currentState === 'active'); + + useEffect(() => { + const sub = AppState.addEventListener('change', state => { + setActive(state === 'active'); + }); + return () => sub.remove(); + }, []); + + usePresenceSubscription(presenceContextForPlatform('app'), active); +} From 7e20f1c4de79ad1ca797b2f0f7871fa8407a56d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:03:12 +0200 Subject: [PATCH 47/75] feat(mobile): add useInstancePresence hook --- .../kilo-chat/hooks/use-instance-presence.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts b/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts new file mode 100644 index 0000000000..dda9686233 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts @@ -0,0 +1,31 @@ +import { useCallback, useEffect, useState } from 'react'; +import { AppState } from 'react-native'; +import { useFocusEffect } from 'expo-router'; + +import { presenceContextForInstance } from '@kilocode/event-service'; + +import { usePresenceSubscription } from './use-presence-subscription'; + +export function useInstancePresence(sandboxId: string | undefined) { + const [appActive, setAppActive] = useState(AppState.currentState === 'active'); + const [focused, setFocused] = useState(false); + + useEffect(() => { + const sub = AppState.addEventListener('change', state => { + setAppActive(state === 'active'); + }); + return () => sub.remove(); + }, []); + + useFocusEffect( + useCallback(() => { + setFocused(true); + return () => setFocused(false); + }, []) + ); + + usePresenceSubscription( + sandboxId ? presenceContextForInstance(sandboxId) : null, + Boolean(sandboxId) && appActive && focused + ); +} From 6d4e6c2c010418573d231eebf254e4b59729fab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:03:19 +0200 Subject: [PATCH 48/75] feat(mobile): add useConversationPresence hook --- .../hooks/use-conversation-presence.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts b/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts new file mode 100644 index 0000000000..080aebfd85 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts @@ -0,0 +1,36 @@ +import { useCallback, useEffect, useState } from 'react'; +import { AppState } from 'react-native'; +import { useFocusEffect } from 'expo-router'; + +import { presenceContextForConversation } from '@kilocode/event-service'; + +import { usePresenceSubscription } from './use-presence-subscription'; + +export function useConversationPresence( + sandboxId: string | undefined, + conversationId: string | undefined +) { + const [appActive, setAppActive] = useState(AppState.currentState === 'active'); + const [focused, setFocused] = useState(false); + + useEffect(() => { + const sub = AppState.addEventListener('change', state => { + setAppActive(state === 'active'); + }); + return () => sub.remove(); + }, []); + + useFocusEffect( + useCallback(() => { + setFocused(true); + return () => setFocused(false); + }, []) + ); + + usePresenceSubscription( + sandboxId && conversationId + ? presenceContextForConversation(sandboxId, conversationId) + : null, + Boolean(sandboxId && conversationId) && appActive && focused + ); +} From 4898f4339d189527d711f21ac3b540ed9f83551b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:04:06 +0200 Subject: [PATCH 49/75] fix(mobile): fix lint errors in presence hooks --- .../components/kilo-chat/hooks/use-app-presence.ts | 4 +++- .../kilo-chat/hooks/use-conversation-presence.ts | 12 +++++++----- .../kilo-chat/hooks/use-instance-presence.ts | 8 ++++++-- .../kilo-chat/hooks/use-presence-subscription.ts | 4 +++- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-app-presence.ts b/apps/mobile/src/components/kilo-chat/hooks/use-app-presence.ts index 5d8c0745a5..79389de2a0 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-app-presence.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-app-presence.ts @@ -12,7 +12,9 @@ export function useAppPresence() { const sub = AppState.addEventListener('change', state => { setActive(state === 'active'); }); - return () => sub.remove(); + return () => { + sub.remove(); + }; }, []); usePresenceSubscription(presenceContextForPlatform('app'), active); diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts b/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts index 080aebfd85..437534318f 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts @@ -17,20 +17,22 @@ export function useConversationPresence( const sub = AppState.addEventListener('change', state => { setAppActive(state === 'active'); }); - return () => sub.remove(); + return () => { + sub.remove(); + }; }, []); useFocusEffect( useCallback(() => { setFocused(true); - return () => setFocused(false); + return () => { + setFocused(false); + }; }, []) ); usePresenceSubscription( - sandboxId && conversationId - ? presenceContextForConversation(sandboxId, conversationId) - : null, + sandboxId && conversationId ? presenceContextForConversation(sandboxId, conversationId) : null, Boolean(sandboxId && conversationId) && appActive && focused ); } diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts b/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts index dda9686233..e81be747fe 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts @@ -14,13 +14,17 @@ export function useInstancePresence(sandboxId: string | undefined) { const sub = AppState.addEventListener('change', state => { setAppActive(state === 'active'); }); - return () => sub.remove(); + return () => { + sub.remove(); + }; }, []); useFocusEffect( useCallback(() => { setFocused(true); - return () => setFocused(false); + return () => { + setFocused(false); + }; }, []) ); diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-presence-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-presence-subscription.ts index de2e53b5a1..b3c860bb89 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-presence-subscription.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-presence-subscription.ts @@ -5,7 +5,9 @@ import { useEventServiceClient } from './use-kilo-chat-client'; export function usePresenceSubscription(context: string | null, active: boolean) { const eventService = useEventServiceClient(); useEffect(() => { - if (!active || !context) return; + if (!active || !context) { + return undefined; + } eventService.subscribe([context]); return () => { eventService.unsubscribe([context]); From 1a6b2413480b3273e6ffb227ded5844dae081d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:06:31 +0200 Subject: [PATCH 50/75] feat(mobile): add useEventSubscription primitive --- .../kilo-chat/hooks/use-event-subscription.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts new file mode 100644 index 0000000000..1c9bd401fe --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; + +import { useEventServiceClient } from './use-kilo-chat-client'; + +export function useEventSubscription( + context: string | null, + events: readonly string[], + onEvent: (event: { event: string; payload: unknown }) => void +) { + const eventService = useEventServiceClient(); + useEffect(() => { + if (!context) return undefined; + eventService.subscribe([context]); + const offs = events.map(eventName => + eventService.on(eventName, (ctx, payload) => { + if (ctx === context) onEvent({ event: eventName, payload }); + }) + ); + return () => { + for (const off of offs) off(); + eventService.unsubscribe([context]); + }; + // events is meant to be a stable array literal at the call site; + // join to use as a dependency without forcing memoization on callers. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [eventService, context, events.join('|'), onEvent]); +} From f0563e876e944fc4fb9c8c0cf62a7f8cfacd5a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:06:45 +0200 Subject: [PATCH 51/75] feat(mobile): add useInstanceEventSubscription --- .../hooks/use-instance-event-subscription.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts new file mode 100644 index 0000000000..04c3c67ac2 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts @@ -0,0 +1,46 @@ +import { useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; + +import { kiloclawInstanceContext } from '@kilocode/event-service'; + +import { useEventSubscription } from './use-event-subscription'; + +const INSTANCE_EVENTS = [ + 'conversation.created', + 'conversation.left', + 'message.created', + 'message.updated', + 'message.deleted', + 'bot.status', +] as const; + +export function useInstanceEventSubscription(sandboxId: string | undefined) { + const qc = useQueryClient(); + const onEvent = useCallback( + ({ event }: { event: string; payload: unknown }) => { + switch (event) { + case 'conversation.created': + case 'conversation.left': + case 'message.created': + case 'message.updated': + case 'message.deleted': + // message.* invalidates the conversations list so last-message + // preview and unread counts stay current while the user is on + // the list (instance-level presence, not viewing a specific conv). + void qc.invalidateQueries({ queryKey: ['conversations', sandboxId] }); + break; + case 'bot.status': + void qc.invalidateQueries({ queryKey: ['bot-status', sandboxId] }); + break; + default: + break; + } + }, + [qc, sandboxId] + ); + useEventSubscription( + sandboxId ? kiloclawInstanceContext(sandboxId) : null, + INSTANCE_EVENTS, + onEvent + ); +} From b939c3f2013e488bac703e841bd90408dbfcd05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:07:21 +0200 Subject: [PATCH 52/75] fix(mobile): apply curly/switch-case-braces lint rules to event hooks --- .../kilo-chat/hooks/use-event-subscription.ts | 12 +++++++++--- .../hooks/use-instance-event-subscription.ts | 9 ++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts index 1c9bd401fe..e4ae65fa31 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts @@ -9,15 +9,21 @@ export function useEventSubscription( ) { const eventService = useEventServiceClient(); useEffect(() => { - if (!context) return undefined; + if (!context) { + return undefined; + } eventService.subscribe([context]); const offs = events.map(eventName => eventService.on(eventName, (ctx, payload) => { - if (ctx === context) onEvent({ event: eventName, payload }); + if (ctx === context) { + onEvent({ event: eventName, payload }); + } }) ); return () => { - for (const off of offs) off(); + for (const off of offs) { + off(); + } eventService.unsubscribe([context]); }; // events is meant to be a stable array literal at the call site; diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts index 04c3c67ac2..524639c8cc 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts @@ -23,17 +23,20 @@ export function useInstanceEventSubscription(sandboxId: string | undefined) { case 'conversation.left': case 'message.created': case 'message.updated': - case 'message.deleted': + case 'message.deleted': { // message.* invalidates the conversations list so last-message // preview and unread counts stay current while the user is on // the list (instance-level presence, not viewing a specific conv). void qc.invalidateQueries({ queryKey: ['conversations', sandboxId] }); break; - case 'bot.status': + } + case 'bot.status': { void qc.invalidateQueries({ queryKey: ['bot-status', sandboxId] }); break; - default: + } + default: { break; + } } }, [qc, sandboxId] From d40b216216de144914eccd1fce57e8e9cc27f2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:10:58 +0200 Subject: [PATCH 53/75] feat(kilo-chat-hooks): create shared package; extract useConversations --- apps/mobile/package.json | 1 + apps/web/package.json | 1 + .../claw/kilo-chat/hooks/useConversations.ts | 135 ++---------------- apps/web/src/contexts/EventServiceContext.tsx | 9 +- packages/kilo-chat-hooks/package.json | 27 ++++ packages/kilo-chat-hooks/src/context.tsx | 26 ++++ packages/kilo-chat-hooks/src/index.ts | 2 + .../kilo-chat-hooks/src/use-conversations.ts | 124 ++++++++++++++++ packages/kilo-chat-hooks/tsconfig.json | 20 +++ pnpm-lock.yaml | 31 ++++ 10 files changed, 251 insertions(+), 125 deletions(-) create mode 100644 packages/kilo-chat-hooks/package.json create mode 100644 packages/kilo-chat-hooks/src/context.tsx create mode 100644 packages/kilo-chat-hooks/src/index.ts create mode 100644 packages/kilo-chat-hooks/src/use-conversations.ts create mode 100644 packages/kilo-chat-hooks/tsconfig.json diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 75cea06b0a..a59df0c47e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -25,6 +25,7 @@ "@expo/react-native-action-sheet": "^4.1.1", "@kilocode/event-service": "workspace:*", "@kilocode/kilo-chat": "workspace:*", + "@kilocode/kilo-chat-hooks": "workspace:*", "@kilocode/notifications": "workspace:*", "@kilocode/trpc": "workspace:*", "@react-native-community/netinfo": "11.5.2", diff --git a/apps/web/package.json b/apps/web/package.json index 15a3dd7aad..eb8afda600 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -45,6 +45,7 @@ "@kilocode/encryption": "workspace:*", "@kilocode/event-service": "workspace:*", "@kilocode/kilo-chat": "workspace:*", + "@kilocode/kilo-chat-hooks": "workspace:*", "@kilocode/kiloclaw-secret-catalog": "workspace:*", "@kilocode/worker-utils": "workspace:*", "@lottiefiles/dotlottie-react": "^0.17.15", diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useConversations.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useConversations.ts index faaf06f759..221f56e395 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useConversations.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useConversations.ts @@ -1,124 +1,11 @@ -import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; -import type { InfiniteData } from '@tanstack/react-query'; -import type { KiloChatClient } from '@kilocode/kilo-chat'; -import type { CreateConversationRequest, ConversationListResponse } from '@kilocode/kilo-chat'; - -const CONVERSATIONS_PAGE_SIZE = 50; - -export function useConversations(client: KiloChatClient, sandboxId: string | null) { - return useInfiniteQuery({ - queryKey: ['kilo-chat', 'conversations', sandboxId], - queryFn: ({ pageParam }) => - client.listConversations({ - sandboxId: sandboxId ?? undefined, - limit: CONVERSATIONS_PAGE_SIZE, - cursor: pageParam ?? undefined, - }), - initialPageParam: null as string | null, - getNextPageParam: lastPage => lastPage.nextCursor, - enabled: !!sandboxId, - select: data => ({ - ...data, - conversations: data.pages.flatMap(p => p.conversations), - }), - }); -} - -export function useConversationDetail(client: KiloChatClient, conversationId: string | null) { - return useQuery({ - queryKey: ['kilo-chat', 'conversation', conversationId], - queryFn: () => client.getConversation(conversationId ?? ''), - enabled: !!conversationId, - }); -} - -export function useCreateConversation(client: KiloChatClient) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (req: CreateConversationRequest) => client.createConversation(req), - onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); - }, - }); -} - -export function useRenameConversation(client: KiloChatClient) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ conversationId, title }: { conversationId: string; title: string }) => - client.renameConversation(conversationId, { title }), - onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); - }, - }); -} - -export function useLeaveConversation(client: KiloChatClient) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (conversationId: string) => client.leaveConversation(conversationId), - onSuccess: (_data, conversationId) => { - queryClient.removeQueries({ queryKey: ['kilo-chat', 'conversation', conversationId] }); - queryClient.removeQueries({ queryKey: ['kilo-chat', 'messages', conversationId] }); - void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); - }, - }); -} - -export type ConversationListInfiniteData = InfiniteData; - -export function updateConversationPages( - data: ConversationListInfiniteData | undefined, - mapItem: ( - c: ConversationListResponse['conversations'][number] - ) => ConversationListResponse['conversations'][number] -): ConversationListInfiniteData | undefined { - if (!data) return data; - return { - ...data, - pages: data.pages.map(page => ({ - ...page, - conversations: page.conversations.map(mapItem), - })), - }; -} - -export function filterConversationPages( - data: ConversationListInfiniteData | undefined, - predicate: (c: ConversationListResponse['conversations'][number]) => boolean -): ConversationListInfiniteData | undefined { - if (!data) return data; - return { - ...data, - pages: data.pages.map(page => ({ - ...page, - conversations: page.conversations.filter(predicate), - })), - }; -} - -export function useMarkConversationRead(client: KiloChatClient) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (conversationId: string) => client.markConversationRead(conversationId), - onMutate: conversationId => { - // Optimistically set lastReadAt = now in all cached conversation lists - const now = Date.now(); - const queryKey = ['kilo-chat', 'conversations']; - const previous = queryClient.getQueriesData({ queryKey }); - queryClient.setQueriesData({ queryKey }, old => - updateConversationPages(old, c => - c.conversationId === conversationId ? { ...c, lastReadAt: now } : c - ) - ); - return { previous }; - }, - onError: (_err, _variables, context) => { - if (context?.previous) { - for (const [key, data] of context.previous) { - queryClient.setQueryData(key, data); - } - } - }, - }); -} +export { + useConversations, + useConversationDetail, + useCreateConversation, + useRenameConversation, + useLeaveConversation, + useMarkConversationRead, + updateConversationPages, + filterConversationPages, +} from '@kilocode/kilo-chat-hooks'; +export type { ConversationListInfiniteData } from '@kilocode/kilo-chat-hooks'; diff --git a/apps/web/src/contexts/EventServiceContext.tsx b/apps/web/src/contexts/EventServiceContext.tsx index 03bc458faa..31b0318c6c 100644 --- a/apps/web/src/contexts/EventServiceContext.tsx +++ b/apps/web/src/contexts/EventServiceContext.tsx @@ -3,6 +3,7 @@ import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react'; import { EventServiceClient } from '@kilocode/event-service'; import { KiloChatClient } from '@kilocode/kilo-chat'; +import { KiloChatHooksProvider } from '@kilocode/kilo-chat-hooks'; import { EVENT_SERVICE_URL, KILO_CHAT_URL } from '@/lib/constants'; import { getKiloChatToken, clearKiloChatToken } from '@/app/(app)/claw/kilo-chat/token'; @@ -60,7 +61,13 @@ export function EventServiceProvider({ children }: EventServiceProviderProps) { [eventService, kiloChatClient] ); - return {children}; + return ( + + + {children} + + + ); } export function useEventServiceClient(): EventServiceContextValue { diff --git a/packages/kilo-chat-hooks/package.json b/packages/kilo-chat-hooks/package.json new file mode 100644 index 0000000000..e15161978f --- /dev/null +++ b/packages/kilo-chat-hooks/package.json @@ -0,0 +1,27 @@ +{ + "name": "@kilocode/kilo-chat-hooks", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { ".": "./src/index.ts" }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "peerDependencies": { + "react": "*", + "@tanstack/react-query": "*" + }, + "dependencies": { + "@kilocode/kilo-chat": "workspace:*", + "@kilocode/event-service": "workspace:*" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "react": "^19.2.4", + "@tanstack/react-query": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/kilo-chat-hooks/src/context.tsx b/packages/kilo-chat-hooks/src/context.tsx new file mode 100644 index 0000000000..55c017fbd7 --- /dev/null +++ b/packages/kilo-chat-hooks/src/context.tsx @@ -0,0 +1,26 @@ +import { createContext, useContext, type ReactNode } from 'react'; +import type { KiloChatClient } from '@kilocode/kilo-chat'; +import type { EventServiceClient } from '@kilocode/event-service'; + +type Value = { + kiloChatClient: KiloChatClient; + eventService: EventServiceClient; +}; + +const Ctx = createContext(null); + +export function KiloChatHooksProvider(props: { value: Value; children: ReactNode }) { + return {props.children}; +} + +export function useKiloChatClient(): KiloChatClient { + const v = useContext(Ctx); + if (!v) throw new Error('useKiloChatClient: missing KiloChatHooksProvider'); + return v.kiloChatClient; +} + +export function useEventServiceClient(): EventServiceClient { + const v = useContext(Ctx); + if (!v) throw new Error('useEventServiceClient: missing KiloChatHooksProvider'); + return v.eventService; +} diff --git a/packages/kilo-chat-hooks/src/index.ts b/packages/kilo-chat-hooks/src/index.ts new file mode 100644 index 0000000000..eaf921a29d --- /dev/null +++ b/packages/kilo-chat-hooks/src/index.ts @@ -0,0 +1,2 @@ +export * from './context'; +export * from './use-conversations'; diff --git a/packages/kilo-chat-hooks/src/use-conversations.ts b/packages/kilo-chat-hooks/src/use-conversations.ts new file mode 100644 index 0000000000..faaf06f759 --- /dev/null +++ b/packages/kilo-chat-hooks/src/use-conversations.ts @@ -0,0 +1,124 @@ +import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; +import type { InfiniteData } from '@tanstack/react-query'; +import type { KiloChatClient } from '@kilocode/kilo-chat'; +import type { CreateConversationRequest, ConversationListResponse } from '@kilocode/kilo-chat'; + +const CONVERSATIONS_PAGE_SIZE = 50; + +export function useConversations(client: KiloChatClient, sandboxId: string | null) { + return useInfiniteQuery({ + queryKey: ['kilo-chat', 'conversations', sandboxId], + queryFn: ({ pageParam }) => + client.listConversations({ + sandboxId: sandboxId ?? undefined, + limit: CONVERSATIONS_PAGE_SIZE, + cursor: pageParam ?? undefined, + }), + initialPageParam: null as string | null, + getNextPageParam: lastPage => lastPage.nextCursor, + enabled: !!sandboxId, + select: data => ({ + ...data, + conversations: data.pages.flatMap(p => p.conversations), + }), + }); +} + +export function useConversationDetail(client: KiloChatClient, conversationId: string | null) { + return useQuery({ + queryKey: ['kilo-chat', 'conversation', conversationId], + queryFn: () => client.getConversation(conversationId ?? ''), + enabled: !!conversationId, + }); +} + +export function useCreateConversation(client: KiloChatClient) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (req: CreateConversationRequest) => client.createConversation(req), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); + }, + }); +} + +export function useRenameConversation(client: KiloChatClient) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ conversationId, title }: { conversationId: string; title: string }) => + client.renameConversation(conversationId, { title }), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); + }, + }); +} + +export function useLeaveConversation(client: KiloChatClient) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (conversationId: string) => client.leaveConversation(conversationId), + onSuccess: (_data, conversationId) => { + queryClient.removeQueries({ queryKey: ['kilo-chat', 'conversation', conversationId] }); + queryClient.removeQueries({ queryKey: ['kilo-chat', 'messages', conversationId] }); + void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); + }, + }); +} + +export type ConversationListInfiniteData = InfiniteData; + +export function updateConversationPages( + data: ConversationListInfiniteData | undefined, + mapItem: ( + c: ConversationListResponse['conversations'][number] + ) => ConversationListResponse['conversations'][number] +): ConversationListInfiniteData | undefined { + if (!data) return data; + return { + ...data, + pages: data.pages.map(page => ({ + ...page, + conversations: page.conversations.map(mapItem), + })), + }; +} + +export function filterConversationPages( + data: ConversationListInfiniteData | undefined, + predicate: (c: ConversationListResponse['conversations'][number]) => boolean +): ConversationListInfiniteData | undefined { + if (!data) return data; + return { + ...data, + pages: data.pages.map(page => ({ + ...page, + conversations: page.conversations.filter(predicate), + })), + }; +} + +export function useMarkConversationRead(client: KiloChatClient) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (conversationId: string) => client.markConversationRead(conversationId), + onMutate: conversationId => { + // Optimistically set lastReadAt = now in all cached conversation lists + const now = Date.now(); + const queryKey = ['kilo-chat', 'conversations']; + const previous = queryClient.getQueriesData({ queryKey }); + queryClient.setQueriesData({ queryKey }, old => + updateConversationPages(old, c => + c.conversationId === conversationId ? { ...c, lastReadAt: now } : c + ) + ); + return { previous }; + }, + onError: (_err, _variables, context) => { + if (context?.previous) { + for (const [key, data] of context.previous) { + queryClient.setQueryData(key, data); + } + } + }, + }); +} diff --git a/packages/kilo-chat-hooks/tsconfig.json b/packages/kilo-chat-hooks/tsconfig.json new file mode 100644 index 0000000000..e480ae3539 --- /dev/null +++ b/packages/kilo-chat-hooks/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["dom", "dom.iterable", "esnext"], + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "isolatedModules": true, + "resolveJsonModule": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85dd420104..b252a8a276 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: '@kilocode/kilo-chat': specifier: workspace:* version: link:../../packages/kilo-chat + '@kilocode/kilo-chat-hooks': + specifier: workspace:* + version: link:../../packages/kilo-chat-hooks '@kilocode/notifications': specifier: workspace:* version: link:../../packages/notifications @@ -514,6 +517,9 @@ importers: '@kilocode/kilo-chat': specifier: workspace:* version: link:../../packages/kilo-chat + '@kilocode/kilo-chat-hooks': + specifier: workspace:* + version: link:../../packages/kilo-chat-hooks '@kilocode/kiloclaw-secret-catalog': specifier: workspace:* version: link:../../packages/kiloclaw-secret-catalog @@ -1005,6 +1011,31 @@ importers: specifier: ~3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(@vitest/ui@3.2.4)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + packages/kilo-chat-hooks: + dependencies: + '@kilocode/event-service': + specifier: workspace:* + version: link:../event-service + '@kilocode/kilo-chat': + specifier: workspace:* + version: link:../kilo-chat + devDependencies: + '@tanstack/react-query': + specifier: 'catalog:' + version: 5.90.21(react@19.2.4) + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@typescript/native-preview': + specifier: 'catalog:' + version: 7.0.0-dev.20260319.1 + react: + specifier: ^19.2.4 + version: 19.2.4 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/kiloclaw-secret-catalog: dependencies: zod: From c829254bb7d0d645dd313aa9024cb366983aeaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:13:59 +0200 Subject: [PATCH 54/75] =?UTF-8?q?feat(kilo-chat-hooks):=20extract=20useMes?= =?UTF-8?q?sages=20=E2=80=94=20base=20query=20+=20optimistic=20send?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move PAGE_SIZE, helper functions (applyReactionAdded/Removed, restoreMessageInCache, removeMessageFromCache, findMessageInCache), useMessages infinite-query hook, and useSendMessage mutation into @kilocode/kilo-chat-hooks. Web's useMessages.ts re-exports the moved hooks and retains local helper copies for remaining mutations (37b will collapse). --- .../(app)/claw/kilo-chat/hooks/useMessages.ts | 80 +-------- packages/kilo-chat-hooks/package.json | 4 +- packages/kilo-chat-hooks/src/index.ts | 1 + packages/kilo-chat-hooks/src/use-messages.ts | 165 ++++++++++++++++++ 4 files changed, 173 insertions(+), 77 deletions(-) create mode 100644 packages/kilo-chat-hooks/src/use-messages.ts diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts index de70a427c5..ff5f484f2a 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts @@ -1,10 +1,12 @@ -import { useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; +export { useMessages, useSendMessage } from '@kilocode/kilo-chat-hooks'; +export type { SendMessageVariables } from '@kilocode/kilo-chat-hooks'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { InfiniteData } from '@tanstack/react-query'; import type { KiloChatClient } from '@kilocode/kilo-chat'; import type { Message, ReactionSummary, - CreateMessageRequest, EditMessageRequest, MessageCreatedEvent, MessageUpdatedEvent, @@ -19,8 +21,6 @@ import { useEffect } from 'react'; import { kiloclawConversationContext } from '@kilocode/event-service'; import { toast } from 'sonner'; -const PAGE_SIZE = 50; - function applyReactionAdded( reactions: ReactionSummary[], emoji: string, @@ -108,78 +108,6 @@ function findMessageInCache( return undefined; } -export function useMessages(client: KiloChatClient, conversationId: string | null) { - return useInfiniteQuery({ - queryKey: ['kilo-chat', 'messages', conversationId], - queryFn: async ({ pageParam }) => { - return client.listMessages(conversationId ?? '', { before: pageParam, limit: PAGE_SIZE }); - }, - initialPageParam: undefined as string | undefined, - getNextPageParam: lastPage => { - if (lastPage.length < PAGE_SIZE) return undefined; - return lastPage[lastPage.length - 1]?.id; - }, - enabled: !!conversationId, - select: data => ({ - ...data, - messages: data.pages.flatMap(p => p).reverse(), - }), - }); -} - -export type SendMessageVariables = CreateMessageRequest & { clientId: string }; - -export function useSendMessage( - client: KiloChatClient, - conversationId: string | null, - currentUserId: string -) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (req: SendMessageVariables) => client.sendMessage(req), - onMutate: async (variables: SendMessageVariables) => { - if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; - await queryClient.cancelQueries({ queryKey }); - const pendingId = `pending-${variables.clientId}`; - const optimisticMessage: Message = { - id: pendingId, - senderId: currentUserId, - content: variables.content, - inReplyToMessageId: variables.inReplyToMessageId ?? null, - updatedAt: null, - clientUpdatedAt: null, - deleted: false, - deliveryFailed: false, - reactions: [], - }; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - const firstPage = old.pages[0] ?? []; - return { ...old, pages: [[optimisticMessage, ...firstPage], ...old.pages.slice(1)] }; - }); - return { queryKey, pendingId }; - }, - onSuccess: (response, _variables, context) => { - if (!context) return; - const { queryKey, pendingId } = context; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => (msg.id === pendingId ? { ...msg, id: response.messageId } : msg)) - ), - }; - }); - }, - onError: (_err, _variables, context) => { - if (!context) return; - removeMessageFromCache(queryClient, context.queryKey, context.pendingId); - }, - }); -} - export function useEditMessage(client: KiloChatClient, conversationId: string | null) { const queryClient = useQueryClient(); return useMutation({ diff --git a/packages/kilo-chat-hooks/package.json b/packages/kilo-chat-hooks/package.json index e15161978f..9d61da112d 100644 --- a/packages/kilo-chat-hooks/package.json +++ b/packages/kilo-chat-hooks/package.json @@ -5,7 +5,9 @@ "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", - "exports": { ".": "./src/index.ts" }, + "exports": { + ".": "./src/index.ts" + }, "scripts": { "typecheck": "tsgo --noEmit" }, diff --git a/packages/kilo-chat-hooks/src/index.ts b/packages/kilo-chat-hooks/src/index.ts index eaf921a29d..aa0315d79d 100644 --- a/packages/kilo-chat-hooks/src/index.ts +++ b/packages/kilo-chat-hooks/src/index.ts @@ -1,2 +1,3 @@ export * from './context'; export * from './use-conversations'; +export * from './use-messages'; diff --git a/packages/kilo-chat-hooks/src/use-messages.ts b/packages/kilo-chat-hooks/src/use-messages.ts new file mode 100644 index 0000000000..bdcdb70c0c --- /dev/null +++ b/packages/kilo-chat-hooks/src/use-messages.ts @@ -0,0 +1,165 @@ +import { useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; +import type { InfiniteData } from '@tanstack/react-query'; +import type { KiloChatClient } from '@kilocode/kilo-chat'; +import type { Message, ReactionSummary, CreateMessageRequest } from '@kilocode/kilo-chat'; + +export const PAGE_SIZE = 50; + +export function applyReactionAdded( + reactions: ReactionSummary[], + emoji: string, + memberId: string +): ReactionSummary[] { + const existing = reactions.find(r => r.emoji === emoji); + if (existing) { + if (existing.memberIds.includes(memberId)) return reactions; + return reactions.map(r => + r.emoji === emoji ? { ...r, count: r.count + 1, memberIds: [...r.memberIds, memberId] } : r + ); + } + return [...reactions, { emoji, count: 1, memberIds: [memberId] }]; +} + +export function applyReactionRemoved( + reactions: ReactionSummary[], + emoji: string, + memberId: string +): ReactionSummary[] { + return reactions + .map(r => { + if (r.emoji !== emoji) return r; + const memberIds = r.memberIds.filter(id => id !== memberId); + return { ...r, count: memberIds.length, memberIds }; + }) + .filter(r => r.count > 0); +} + +/** + * Splice a snapshotted message back into the current cache state. If the + * message no longer exists in any page (e.g. a concurrent delete event), the + * cache is left unchanged so we do not resurrect it. + */ +export function restoreMessageInCache( + queryClient: ReturnType, + queryKey: readonly unknown[], + snapshot: Message +): void { + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + let replaced = false; + const pages = old.pages.map(page => + page.map(msg => { + if (msg.id !== snapshot.id) return msg; + replaced = true; + return snapshot; + }) + ); + if (!replaced) return old; + return { ...old, pages }; + }); +} + +/** + * Remove a message by id from the current cache state. Used to roll back the + * optimistic insert performed by `useSendMessage` without touching any other + * concurrently-optimistic messages. + */ +export function removeMessageFromCache( + queryClient: ReturnType, + queryKey: readonly unknown[], + messageId: string +): void { + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => page.filter(msg => msg.id !== messageId)), + }; + }); +} + +export function findMessageInCache( + queryClient: ReturnType, + queryKey: readonly unknown[], + messageId: string +): Message | undefined { + const data = queryClient.getQueryData>(queryKey); + if (!data) return undefined; + for (const page of data.pages) { + const match = page.find(msg => msg.id === messageId); + if (match) return match; + } + return undefined; +} + +export function useMessages(client: KiloChatClient, conversationId: string | null) { + return useInfiniteQuery({ + queryKey: ['kilo-chat', 'messages', conversationId], + queryFn: async ({ pageParam }) => { + return client.listMessages(conversationId ?? '', { before: pageParam, limit: PAGE_SIZE }); + }, + initialPageParam: undefined as string | undefined, + getNextPageParam: lastPage => { + if (lastPage.length < PAGE_SIZE) return undefined; + return lastPage[lastPage.length - 1]?.id; + }, + enabled: !!conversationId, + select: data => ({ + ...data, + messages: data.pages.flatMap(p => p).reverse(), + }), + }); +} + +export type SendMessageVariables = CreateMessageRequest & { clientId: string }; + +export function useSendMessage( + client: KiloChatClient, + conversationId: string | null, + currentUserId: string +) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (req: SendMessageVariables) => client.sendMessage(req), + onMutate: async (variables: SendMessageVariables) => { + if (!conversationId) return; + const queryKey = ['kilo-chat', 'messages', conversationId]; + await queryClient.cancelQueries({ queryKey }); + const pendingId = `pending-${variables.clientId}`; + const optimisticMessage: Message = { + id: pendingId, + senderId: currentUserId, + content: variables.content, + inReplyToMessageId: variables.inReplyToMessageId ?? null, + updatedAt: null, + clientUpdatedAt: null, + deleted: false, + deliveryFailed: false, + reactions: [], + }; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + const firstPage = old.pages[0] ?? []; + return { ...old, pages: [[optimisticMessage, ...firstPage], ...old.pages.slice(1)] }; + }); + return { queryKey, pendingId }; + }, + onSuccess: (response, _variables, context) => { + if (!context) return; + const { queryKey, pendingId } = context; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => (msg.id === pendingId ? { ...msg, id: response.messageId } : msg)) + ), + }; + }); + }, + onError: (_err, _variables, context) => { + if (!context) return; + removeMessageFromCache(queryClient, context.queryKey, context.pendingId); + }, + }); +} From aa560afb7dfe1e08755e71de21f204ac03c8de24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:16:04 +0200 Subject: [PATCH 55/75] feat(kilo-chat-hooks): useMessages adds edit/delete/react mutations --- .../(app)/claw/kilo-chat/hooks/useMessages.ts | 267 +----------------- packages/kilo-chat-hooks/src/use-messages.ts | 203 ++++++++++++- 2 files changed, 212 insertions(+), 258 deletions(-) diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts index ff5f484f2a..f0cda78522 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts @@ -1,13 +1,20 @@ -export { useMessages, useSendMessage } from '@kilocode/kilo-chat-hooks'; +export { + useMessages, + useSendMessage, + useEditMessage, + useDeleteMessage, + useAddReaction, + useRemoveReaction, + useExecuteAction, +} from '@kilocode/kilo-chat-hooks'; export type { SendMessageVariables } from '@kilocode/kilo-chat-hooks'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import type { InfiniteData } from '@tanstack/react-query'; import type { KiloChatClient } from '@kilocode/kilo-chat'; import type { Message, ReactionSummary, - EditMessageRequest, MessageCreatedEvent, MessageUpdatedEvent, MessageDeletedEvent, @@ -15,7 +22,6 @@ import type { ActionDeliveryFailedEvent, ReactionAddedEvent, ReactionRemovedEvent, - ExecApprovalDecision, } from '@kilocode/kilo-chat'; import { useEffect } from 'react'; import { kiloclawConversationContext } from '@kilocode/event-service'; @@ -50,259 +56,6 @@ function applyReactionRemoved( .filter(r => r.count > 0); } -/** - * Splice a snapshotted message back into the current cache state. If the - * message no longer exists in any page (e.g. a concurrent delete event), the - * cache is left unchanged so we do not resurrect it. - */ -function restoreMessageInCache( - queryClient: ReturnType, - queryKey: readonly unknown[], - snapshot: Message -): void { - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - let replaced = false; - const pages = old.pages.map(page => - page.map(msg => { - if (msg.id !== snapshot.id) return msg; - replaced = true; - return snapshot; - }) - ); - if (!replaced) return old; - return { ...old, pages }; - }); -} - -/** - * Remove a message by id from the current cache state. Used to roll back the - * optimistic insert performed by `useSendMessage` without touching any other - * concurrently-optimistic messages. - */ -function removeMessageFromCache( - queryClient: ReturnType, - queryKey: readonly unknown[], - messageId: string -): void { - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => page.filter(msg => msg.id !== messageId)), - }; - }); -} - -function findMessageInCache( - queryClient: ReturnType, - queryKey: readonly unknown[], - messageId: string -): Message | undefined { - const data = queryClient.getQueryData>(queryKey); - if (!data) return undefined; - for (const page of data.pages) { - const match = page.find(msg => msg.id === messageId); - if (match) return match; - } - return undefined; -} - -export function useEditMessage(client: KiloChatClient, conversationId: string | null) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ messageId, ...req }: EditMessageRequest & { messageId: string }) => - client.editMessage(messageId, req), - onMutate: async variables => { - if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; - await queryClient.cancelQueries({ queryKey }); - const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => - msg.id === variables.messageId - ? { ...msg, content: variables.content, clientUpdatedAt: variables.timestamp } - : msg - ) - ), - }; - }); - return { queryKey, snapshot }; - }, - onError: (_err, _variables, context) => { - if (!context?.snapshot) return; - restoreMessageInCache(queryClient, context.queryKey, context.snapshot); - }, - }); -} - -export function useDeleteMessage(client: KiloChatClient, conversationId: string | null) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ messageId, conversationId }: { messageId: string; conversationId: string }) => - client.deleteMessage(messageId, { conversationId }), - onMutate: async variables => { - if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; - await queryClient.cancelQueries({ queryKey }); - const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => (msg.id === variables.messageId ? { ...msg, deleted: true } : msg)) - ), - }; - }); - return { queryKey, snapshot }; - }, - onError: (_err, _variables, context) => { - if (!context?.snapshot) return; - restoreMessageInCache(queryClient, context.queryKey, context.snapshot); - }, - }); -} - -export function useAddReaction( - client: KiloChatClient, - conversationId: string | null, - currentUserId: string -) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ messageId, emoji }: { messageId: string; emoji: string }) => - client.addReaction(messageId, { conversationId: conversationId ?? '', emoji }), - onMutate: async variables => { - if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; - await queryClient.cancelQueries({ queryKey }); - const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => - msg.id !== variables.messageId - ? msg - : { - ...msg, - reactions: applyReactionAdded(msg.reactions, variables.emoji, currentUserId), - } - ) - ), - }; - }); - return { queryKey, snapshot }; - }, - onError: (_err, _variables, context) => { - if (!context?.snapshot) return; - restoreMessageInCache(queryClient, context.queryKey, context.snapshot); - }, - }); -} - -export function useRemoveReaction( - client: KiloChatClient, - conversationId: string | null, - currentUserId: string -) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ messageId, emoji }: { messageId: string; emoji: string }) => - client.removeReaction(messageId, { conversationId: conversationId ?? '', emoji }), - onMutate: async variables => { - if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; - await queryClient.cancelQueries({ queryKey }); - const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => - msg.id !== variables.messageId - ? msg - : { - ...msg, - reactions: applyReactionRemoved(msg.reactions, variables.emoji, currentUserId), - } - ) - ), - }; - }); - return { queryKey, snapshot }; - }, - onError: (_err, _variables, context) => { - if (!context?.snapshot) return; - restoreMessageInCache(queryClient, context.queryKey, context.snapshot); - }, - }); -} - -export function useExecuteAction( - client: KiloChatClient, - conversationId: string | null, - currentUserId: string -) { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ - messageId, - groupId, - value, - }: { - messageId: string; - groupId: string; - value: ExecApprovalDecision; - }) => client.executeAction(conversationId ?? '', messageId, { groupId, value }), - onMutate: async variables => { - if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; - await queryClient.cancelQueries({ queryKey }); - const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); - // Optimistically mark the action as resolved - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => { - if (msg.id !== variables.messageId) return msg; - return { - ...msg, - content: msg.content.map(block => { - if (block.type !== 'actions') return block; - if (block.groupId !== variables.groupId) return block; - return { - ...block, - resolved: { - value: variables.value, - resolvedBy: currentUserId, - resolvedAt: Date.now(), - }, - }; - }), - }; - }) - ), - }; - }); - return { queryKey, snapshot }; - }, - onError: (_err, _variables, context) => { - if (!context?.snapshot) return; - restoreMessageInCache(queryClient, context.queryKey, context.snapshot); - }, - }); -} - /** * Subscribes to real-time kilo-chat events on the shared client and applies * them to the React Query message cache for the active conversation. diff --git a/packages/kilo-chat-hooks/src/use-messages.ts b/packages/kilo-chat-hooks/src/use-messages.ts index bdcdb70c0c..f948fcd090 100644 --- a/packages/kilo-chat-hooks/src/use-messages.ts +++ b/packages/kilo-chat-hooks/src/use-messages.ts @@ -1,7 +1,13 @@ import { useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; import type { InfiniteData } from '@tanstack/react-query'; import type { KiloChatClient } from '@kilocode/kilo-chat'; -import type { Message, ReactionSummary, CreateMessageRequest } from '@kilocode/kilo-chat'; +import type { + Message, + ReactionSummary, + CreateMessageRequest, + EditMessageRequest, + ExecApprovalDecision, +} from '@kilocode/kilo-chat'; export const PAGE_SIZE = 50; @@ -163,3 +169,198 @@ export function useSendMessage( }, }); } + +export function useEditMessage(client: KiloChatClient, conversationId: string | null) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ messageId, ...req }: EditMessageRequest & { messageId: string }) => + client.editMessage(messageId, req), + onMutate: async variables => { + if (!conversationId) return; + const queryKey = ['kilo-chat', 'messages', conversationId]; + await queryClient.cancelQueries({ queryKey }); + const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => + msg.id === variables.messageId + ? { ...msg, content: variables.content, clientUpdatedAt: variables.timestamp } + : msg + ) + ), + }; + }); + return { queryKey, snapshot }; + }, + onError: (_err, _variables, context) => { + if (!context?.snapshot) return; + restoreMessageInCache(queryClient, context.queryKey, context.snapshot); + }, + }); +} + +export function useDeleteMessage(client: KiloChatClient, conversationId: string | null) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ messageId, conversationId }: { messageId: string; conversationId: string }) => + client.deleteMessage(messageId, { conversationId }), + onMutate: async variables => { + if (!conversationId) return; + const queryKey = ['kilo-chat', 'messages', conversationId]; + await queryClient.cancelQueries({ queryKey }); + const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => (msg.id === variables.messageId ? { ...msg, deleted: true } : msg)) + ), + }; + }); + return { queryKey, snapshot }; + }, + onError: (_err, _variables, context) => { + if (!context?.snapshot) return; + restoreMessageInCache(queryClient, context.queryKey, context.snapshot); + }, + }); +} + +export function useAddReaction( + client: KiloChatClient, + conversationId: string | null, + currentUserId: string +) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ messageId, emoji }: { messageId: string; emoji: string }) => + client.addReaction(messageId, { conversationId: conversationId ?? '', emoji }), + onMutate: async variables => { + if (!conversationId) return; + const queryKey = ['kilo-chat', 'messages', conversationId]; + await queryClient.cancelQueries({ queryKey }); + const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => + msg.id !== variables.messageId + ? msg + : { + ...msg, + reactions: applyReactionAdded(msg.reactions, variables.emoji, currentUserId), + } + ) + ), + }; + }); + return { queryKey, snapshot }; + }, + onError: (_err, _variables, context) => { + if (!context?.snapshot) return; + restoreMessageInCache(queryClient, context.queryKey, context.snapshot); + }, + }); +} + +export function useRemoveReaction( + client: KiloChatClient, + conversationId: string | null, + currentUserId: string +) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ messageId, emoji }: { messageId: string; emoji: string }) => + client.removeReaction(messageId, { conversationId: conversationId ?? '', emoji }), + onMutate: async variables => { + if (!conversationId) return; + const queryKey = ['kilo-chat', 'messages', conversationId]; + await queryClient.cancelQueries({ queryKey }); + const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => + msg.id !== variables.messageId + ? msg + : { + ...msg, + reactions: applyReactionRemoved(msg.reactions, variables.emoji, currentUserId), + } + ) + ), + }; + }); + return { queryKey, snapshot }; + }, + onError: (_err, _variables, context) => { + if (!context?.snapshot) return; + restoreMessageInCache(queryClient, context.queryKey, context.snapshot); + }, + }); +} + +export function useExecuteAction( + client: KiloChatClient, + conversationId: string | null, + currentUserId: string +) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ + messageId, + groupId, + value, + }: { + messageId: string; + groupId: string; + value: ExecApprovalDecision; + }) => client.executeAction(conversationId ?? '', messageId, { groupId, value }), + onMutate: async variables => { + if (!conversationId) return; + const queryKey = ['kilo-chat', 'messages', conversationId]; + await queryClient.cancelQueries({ queryKey }); + const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); + // Optimistically mark the action as resolved + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => { + if (msg.id !== variables.messageId) return msg; + return { + ...msg, + content: msg.content.map(block => { + if (block.type !== 'actions') return block; + if (block.groupId !== variables.groupId) return block; + return { + ...block, + resolved: { + value: variables.value, + resolvedBy: currentUserId, + resolvedAt: Date.now(), + }, + }; + }), + }; + }) + ), + }; + }); + return { queryKey, snapshot }; + }, + onError: (_err, _variables, context) => { + if (!context?.snapshot) return; + restoreMessageInCache(queryClient, context.queryKey, context.snapshot); + }, + }); +} From 1dd065a63a3f0f294c5254ec8f518aa828c00ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:19:39 +0200 Subject: [PATCH 56/75] feat(kilo-chat-hooks): extract useMessageCacheUpdater into shared package Moves the live event-stream cache patcher from the web-only useMessages file into @kilocode/kilo-chat-hooks. Adds an optional onActionFailed callback so platform wrappers inject toasts; web passes toast.error. --- .../claw/kilo-chat/components/MessageArea.tsx | 4 +- .../(app)/claw/kilo-chat/hooks/useMessages.ts | 239 +----------------- packages/kilo-chat-hooks/src/use-messages.ts | 201 +++++++++++++++ 3 files changed, 205 insertions(+), 239 deletions(-) diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx index e8d373a230..4f5d96e97e 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx @@ -127,7 +127,9 @@ export function MessageArea({ conversationId }: MessageAreaProps) { // Bots are excluded inside the hook because their streaming uses // message.created for every token chunk and relies on typing.stopped to // signal stream completion. - useMessageCacheUpdater(kiloChatClient, sandboxId, conversationId, clearTypingForMember); + useMessageCacheUpdater(kiloChatClient, sandboxId, conversationId, clearTypingForMember, msg => + toast.error(msg) + ); const sendTyping = useTypingSender(kiloChatClient, conversationId); const markRead = useMarkConversationRead(kiloChatClient); diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts index f0cda78522..2f5fb31ec6 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts @@ -6,243 +6,6 @@ export { useAddReaction, useRemoveReaction, useExecuteAction, + useMessageCacheUpdater, } from '@kilocode/kilo-chat-hooks'; export type { SendMessageVariables } from '@kilocode/kilo-chat-hooks'; - -import { useQueryClient } from '@tanstack/react-query'; -import type { InfiniteData } from '@tanstack/react-query'; -import type { KiloChatClient } from '@kilocode/kilo-chat'; -import type { - Message, - ReactionSummary, - MessageCreatedEvent, - MessageUpdatedEvent, - MessageDeletedEvent, - MessageDeliveryFailedEvent, - ActionDeliveryFailedEvent, - ReactionAddedEvent, - ReactionRemovedEvent, -} from '@kilocode/kilo-chat'; -import { useEffect } from 'react'; -import { kiloclawConversationContext } from '@kilocode/event-service'; -import { toast } from 'sonner'; - -function applyReactionAdded( - reactions: ReactionSummary[], - emoji: string, - memberId: string -): ReactionSummary[] { - const existing = reactions.find(r => r.emoji === emoji); - if (existing) { - if (existing.memberIds.includes(memberId)) return reactions; - return reactions.map(r => - r.emoji === emoji ? { ...r, count: r.count + 1, memberIds: [...r.memberIds, memberId] } : r - ); - } - return [...reactions, { emoji, count: 1, memberIds: [memberId] }]; -} - -function applyReactionRemoved( - reactions: ReactionSummary[], - emoji: string, - memberId: string -): ReactionSummary[] { - return reactions - .map(r => { - if (r.emoji !== emoji) return r; - const memberIds = r.memberIds.filter(id => id !== memberId); - return { ...r, count: memberIds.length, memberIds }; - }) - .filter(r => r.count > 0); -} - -/** - * Subscribes to real-time kilo-chat events on the shared client and applies - * them to the React Query message cache for the active conversation. - * - * Each subscription receives the fully validated typed payload from the - * client (Zod-checked inside `KiloChatClient.on`), so no casts are needed. - * - * Event Service delivers every subscribed context to every handler, so we - * also validate `ctx` against the expected conversation context before - * mutating the cache. This protects against stale subscriptions, context - * leaks, or server-side routing drift. - */ -export function useMessageCacheUpdater( - client: KiloChatClient, - sandboxId: string | null, - conversationId: string | null, - // Called with the event context and sender id when a human sender's - // message lands. Bots stream tokens through message.created events and - // end their own typing state via explicit typing.stopped, so we must not - // clear on bot messages or the indicator disappears mid-stream. - onHumanMessageCreated?: (ctx: string, senderId: string) => void -): void { - const queryClient = useQueryClient(); - - useEffect(() => { - if (!conversationId || !sandboxId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; - const expectedContext = kiloclawConversationContext(sandboxId, conversationId); - - const onCreated = (ctx: string, e: MessageCreatedEvent) => { - if (ctx !== expectedContext) return; - if (!e.senderId.startsWith('bot:')) { - onHumanMessageCreated?.(ctx, e.senderId); - } - const newMessage: Message = { - id: e.messageId, - senderId: e.senderId, - content: e.content, - inReplyToMessageId: e.inReplyToMessageId, - updatedAt: null, - clientUpdatedAt: null, - deleted: false, - deliveryFailed: false, - reactions: [], - }; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - // Skip if this messageId already exists - for (const page of old.pages) { - if (page.some(msg => msg.id === e.messageId)) return old; - } - // Replace the matching pending optimistic message if clientId correlates - if (e.clientId) { - const pendingId = `pending-${e.clientId}`; - for (const page of old.pages) { - if (page.some(msg => msg.id === pendingId)) { - return { - ...old, - pages: old.pages.map(p => p.map(msg => (msg.id === pendingId ? newMessage : msg))), - }; - } - } - } - const firstPage = old.pages[0] ?? []; - return { ...old, pages: [[newMessage, ...firstPage], ...old.pages.slice(1)] }; - }); - }; - - const onUpdated = (ctx: string, e: MessageUpdatedEvent) => { - if (ctx !== expectedContext) return; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => - msg.id === e.messageId - ? { - ...msg, - content: e.content, - clientUpdatedAt: e.clientUpdatedAt, - } - : msg - ) - ), - }; - }); - }; - - const onDeleted = (ctx: string, e: MessageDeletedEvent) => { - if (ctx !== expectedContext) return; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => (msg.id === e.messageId ? { ...msg, deleted: true } : msg)) - ), - }; - }); - }; - - const onDeliveryFailed = (ctx: string, e: MessageDeliveryFailedEvent) => { - if (ctx !== expectedContext) return; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => (msg.id === e.messageId ? { ...msg, deliveryFailed: true } : msg)) - ), - }; - }); - }; - - const onActionFailed = (ctx: string, e: ActionDeliveryFailedEvent) => { - if (ctx !== expectedContext) return; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => { - if (msg.id !== e.messageId) return msg; - return { - ...msg, - content: msg.content.map(block => { - if (block.type !== 'actions') return block; - if (block.groupId !== e.groupId) return block; - return { ...block, resolved: undefined }; - }), - }; - }) - ), - }; - }); - toast.error("Couldn't reach the bot — please try again"); - }; - - const onReactionAdded = (ctx: string, e: ReactionAddedEvent) => { - if (ctx !== expectedContext) return; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => - msg.id !== e.messageId - ? msg - : { ...msg, reactions: applyReactionAdded(msg.reactions, e.emoji, e.memberId) } - ) - ), - }; - }); - }; - - const onReactionRemoved = (ctx: string, e: ReactionRemovedEvent) => { - if (ctx !== expectedContext) return; - queryClient.setQueryData>(queryKey, old => { - if (!old) return old; - return { - ...old, - pages: old.pages.map(page => - page.map(msg => - msg.id !== e.messageId - ? msg - : { - ...msg, - reactions: applyReactionRemoved(msg.reactions, e.emoji, e.memberId), - } - ) - ), - }; - }); - }; - - const offs = [ - client.onMessageCreated(onCreated), - client.onMessageUpdated(onUpdated), - client.onMessageDeleted(onDeleted), - client.onMessageDeliveryFailed(onDeliveryFailed), - client.onActionDeliveryFailed(onActionFailed), - client.onReactionAdded(onReactionAdded), - client.onReactionRemoved(onReactionRemoved), - ]; - return () => { - for (const off of offs) off(); - }; - }, [client, sandboxId, conversationId, queryClient, onHumanMessageCreated]); -} diff --git a/packages/kilo-chat-hooks/src/use-messages.ts b/packages/kilo-chat-hooks/src/use-messages.ts index f948fcd090..d0acf0908b 100644 --- a/packages/kilo-chat-hooks/src/use-messages.ts +++ b/packages/kilo-chat-hooks/src/use-messages.ts @@ -7,7 +7,16 @@ import type { CreateMessageRequest, EditMessageRequest, ExecApprovalDecision, + MessageCreatedEvent, + MessageUpdatedEvent, + MessageDeletedEvent, + MessageDeliveryFailedEvent, + ActionDeliveryFailedEvent, + ReactionAddedEvent, + ReactionRemovedEvent, } from '@kilocode/kilo-chat'; +import { useEffect } from 'react'; +import { kiloclawConversationContext } from '@kilocode/event-service'; export const PAGE_SIZE = 50; @@ -364,3 +373,195 @@ export function useExecuteAction( }, }); } + +/** + * Subscribes to real-time kilo-chat events on the shared client and applies + * them to the React Query message cache for the active conversation. + * + * Each subscription receives the fully validated typed payload from the + * client (Zod-checked inside `KiloChatClient.on`), so no casts are needed. + * + * Event Service delivers every subscribed context to every handler, so we + * also validate `ctx` against the expected conversation context before + * mutating the cache. This protects against stale subscriptions, context + * leaks, or server-side routing drift. + */ +export function useMessageCacheUpdater( + client: KiloChatClient, + sandboxId: string | null, + conversationId: string | null, + // Called with the event context and sender id when a human sender's + // message lands. Bots stream tokens through message.created events and + // end their own typing state via explicit typing.stopped, so we must not + // clear on bot messages or the indicator disappears mid-stream. + onHumanMessageCreated?: (ctx: string, senderId: string) => void, + onActionFailed?: (message: string) => void +): void { + const queryClient = useQueryClient(); + + useEffect(() => { + if (!conversationId || !sandboxId) return; + const queryKey = ['kilo-chat', 'messages', conversationId]; + const expectedContext = kiloclawConversationContext(sandboxId, conversationId); + + const onCreated = (ctx: string, e: MessageCreatedEvent) => { + if (ctx !== expectedContext) return; + if (!e.senderId.startsWith('bot:')) { + onHumanMessageCreated?.(ctx, e.senderId); + } + const newMessage: Message = { + id: e.messageId, + senderId: e.senderId, + content: e.content, + inReplyToMessageId: e.inReplyToMessageId, + updatedAt: null, + clientUpdatedAt: null, + deleted: false, + deliveryFailed: false, + reactions: [], + }; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + // Skip if this messageId already exists + for (const page of old.pages) { + if (page.some(msg => msg.id === e.messageId)) return old; + } + // Replace the matching pending optimistic message if clientId correlates + if (e.clientId) { + const pendingId = `pending-${e.clientId}`; + for (const page of old.pages) { + if (page.some(msg => msg.id === pendingId)) { + return { + ...old, + pages: old.pages.map(p => p.map(msg => (msg.id === pendingId ? newMessage : msg))), + }; + } + } + } + const firstPage = old.pages[0] ?? []; + return { ...old, pages: [[newMessage, ...firstPage], ...old.pages.slice(1)] }; + }); + }; + + const onUpdated = (ctx: string, e: MessageUpdatedEvent) => { + if (ctx !== expectedContext) return; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => + msg.id === e.messageId + ? { + ...msg, + content: e.content, + clientUpdatedAt: e.clientUpdatedAt, + } + : msg + ) + ), + }; + }); + }; + + const onDeleted = (ctx: string, e: MessageDeletedEvent) => { + if (ctx !== expectedContext) return; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => (msg.id === e.messageId ? { ...msg, deleted: true } : msg)) + ), + }; + }); + }; + + const onDeliveryFailed = (ctx: string, e: MessageDeliveryFailedEvent) => { + if (ctx !== expectedContext) return; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => (msg.id === e.messageId ? { ...msg, deliveryFailed: true } : msg)) + ), + }; + }); + }; + + const onActionDeliveryFailed = (ctx: string, e: ActionDeliveryFailedEvent) => { + if (ctx !== expectedContext) return; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => { + if (msg.id !== e.messageId) return msg; + return { + ...msg, + content: msg.content.map(block => { + if (block.type !== 'actions') return block; + if (block.groupId !== e.groupId) return block; + return { ...block, resolved: undefined }; + }), + }; + }) + ), + }; + }); + onActionFailed?.("Couldn't reach the bot — please try again"); + }; + + const onReactionAdded = (ctx: string, e: ReactionAddedEvent) => { + if (ctx !== expectedContext) return; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => + msg.id !== e.messageId + ? msg + : { ...msg, reactions: applyReactionAdded(msg.reactions, e.emoji, e.memberId) } + ) + ), + }; + }); + }; + + const onReactionRemoved = (ctx: string, e: ReactionRemovedEvent) => { + if (ctx !== expectedContext) return; + queryClient.setQueryData>(queryKey, old => { + if (!old) return old; + return { + ...old, + pages: old.pages.map(page => + page.map(msg => + msg.id !== e.messageId + ? msg + : { + ...msg, + reactions: applyReactionRemoved(msg.reactions, e.emoji, e.memberId), + } + ) + ), + }; + }); + }; + + const offs = [ + client.onMessageCreated(onCreated), + client.onMessageUpdated(onUpdated), + client.onMessageDeleted(onDeleted), + client.onMessageDeliveryFailed(onDeliveryFailed), + client.onActionDeliveryFailed(onActionDeliveryFailed), + client.onReactionAdded(onReactionAdded), + client.onReactionRemoved(onReactionRemoved), + ]; + return () => { + for (const off of offs) off(); + }; + }, [client, sandboxId, conversationId, queryClient, onHumanMessageCreated, onActionFailed]); +} From ad591d9fb0ffbac704808f513a19dea1fd1fe577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:22:14 +0200 Subject: [PATCH 57/75] feat(mobile): wire shared kilo-chat-hooks + platform adapters --- .../kilo-chat/hooks/use-conversations.ts | 11 +++++++ .../kilo-chat/hooks/use-kilo-chat-client.ts | 21 +------------- .../kilo-chat/hooks/use-mark-read.ts | 29 +++++++++++++++++++ .../kilo-chat/hooks/use-messages.ts | 11 +++++++ .../kilo-chat/kilo-chat-provider.tsx | 11 ++++++- 5 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-conversations.ts create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-mark-read.ts create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-messages.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-conversations.ts b/apps/mobile/src/components/kilo-chat/hooks/use-conversations.ts new file mode 100644 index 0000000000..221f56e395 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-conversations.ts @@ -0,0 +1,11 @@ +export { + useConversations, + useConversationDetail, + useCreateConversation, + useRenameConversation, + useLeaveConversation, + useMarkConversationRead, + updateConversationPages, + filterConversationPages, +} from '@kilocode/kilo-chat-hooks'; +export type { ConversationListInfiniteData } from '@kilocode/kilo-chat-hooks'; diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts index 32e7402aa3..cef730a6dc 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-kilo-chat-client.ts @@ -1,20 +1 @@ -import { type EventServiceClient } from '@kilocode/event-service'; -import { type KiloChatClient } from '@kilocode/kilo-chat'; - -import { useKiloChatContext } from '../kilo-chat-provider'; - -/** - * Returns the {@link KiloChatClient} instance from the nearest - * {@link KiloChatProvider}. Throws if called outside a provider. - */ -export function useKiloChatClient(): KiloChatClient { - return useKiloChatContext().kiloChatClient; -} - -/** - * Returns the {@link EventServiceClient} instance from the nearest - * {@link KiloChatProvider}. Throws if called outside a provider. - */ -export function useEventServiceClient(): EventServiceClient { - return useKiloChatContext().eventService; -} +export { useKiloChatClient, useEventServiceClient } from '@kilocode/kilo-chat-hooks'; diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-mark-read.ts b/apps/mobile/src/components/kilo-chat/hooks/use-mark-read.ts new file mode 100644 index 0000000000..4123d96972 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-mark-read.ts @@ -0,0 +1,29 @@ +import { useCallback } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import * as Notifications from 'expo-notifications'; + +import { badgeBucketForConversation } from '@kilocode/notifications'; + +import { useTRPC } from '@/lib/trpc'; + +export function useMarkRead() { + const trpc = useTRPC(); + const mutation = useMutation( + trpc.user.markChatRead.mutationOptions({ + onSuccess: result => { + if (typeof result.badgeCount === 'number') { + void Notifications.setBadgeCountAsync(result.badgeCount); + } + }, + }) + ); + + return useCallback( + (sandboxId: string, conversationId: string) => { + mutation.mutate({ + badgeBucket: badgeBucketForConversation(sandboxId, conversationId), + }); + }, + [mutation] + ); +} diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-messages.ts b/apps/mobile/src/components/kilo-chat/hooks/use-messages.ts new file mode 100644 index 0000000000..2f5fb31ec6 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-messages.ts @@ -0,0 +1,11 @@ +export { + useMessages, + useSendMessage, + useEditMessage, + useDeleteMessage, + useAddReaction, + useRemoveReaction, + useExecuteAction, + useMessageCacheUpdater, +} from '@kilocode/kilo-chat-hooks'; +export type { SendMessageVariables } from '@kilocode/kilo-chat-hooks'; diff --git a/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx b/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx index f255f96a2b..95ed84f647 100644 --- a/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx +++ b/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, useEffect, useState } from 'react'; import { EventServiceClient } from '@kilocode/event-service'; import { KiloChatClient } from '@kilocode/kilo-chat'; +import { KiloChatHooksProvider } from '@kilocode/kilo-chat-hooks'; import { EVENT_SERVICE_URL, KILO_CHAT_URL } from '@/lib/config'; @@ -49,5 +50,13 @@ export function KiloChatProvider({ children }: KiloChatProviderProps) { }; }, [value]); - return {children}; + return ( + + + {children} + + + ); } From 66713ba9df38bc05976c2a8b22c56c8790fb1433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:37:08 +0200 Subject: [PATCH 58/75] fix(kilo-chat-hooks): centralize query keys; tighten event-subscription API - Add packages/kilo-chat-hooks/src/query-keys.ts with conversations/ conversation/messages/bot-status helpers; route every hook + invalidator through it. Fixes the mobile useInstanceEventSubscription bug where invalidations used ['conversations', sandboxId] but the queries register under ['kilo-chat', 'conversations', sandboxId], so list previews and unread counts never refreshed on incoming events. - useEventSubscription now takes a single event name; callers register one hook per event. Drops the events.join('|') dependency hack and the eslint-disable. useInstanceEventSubscription becomes six explicit registrations. - Drop the hardcoded English toast string from useMessageCacheUpdater; onActionFailed is () => void and the message lives at each call site. - Extract useAppActiveAndFocused to deduplicate AppState+focus boilerplate shared by useInstancePresence and useConversationPresence. --- .../hooks/use-app-active-and-focused.ts | 33 +++++++++++ .../hooks/use-conversation-presence.ts | 29 +-------- .../kilo-chat/hooks/use-event-subscription.ts | 29 ++++----- .../hooks/use-instance-event-subscription.ts | 59 +++++++------------ .../kilo-chat/hooks/use-instance-presence.ts | 29 +-------- .../claw/kilo-chat/components/MessageArea.tsx | 4 +- .../claw/kilo-chat/hooks/useBotStatus.ts | 7 +-- packages/kilo-chat-hooks/src/index.ts | 1 + packages/kilo-chat-hooks/src/query-keys.ts | 17 ++++++ .../kilo-chat-hooks/src/use-conversations.ts | 18 +++--- packages/kilo-chat-hooks/src/use-messages.ts | 26 ++++---- 11 files changed, 121 insertions(+), 131 deletions(-) create mode 100644 apps/mobile/src/components/kilo-chat/hooks/use-app-active-and-focused.ts create mode 100644 packages/kilo-chat-hooks/src/query-keys.ts diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-app-active-and-focused.ts b/apps/mobile/src/components/kilo-chat/hooks/use-app-active-and-focused.ts new file mode 100644 index 0000000000..f1066b1b03 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/hooks/use-app-active-and-focused.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useState } from 'react'; +import { AppState } from 'react-native'; +import { useFocusEffect } from 'expo-router'; + +/** + * True only when the app is in the foreground AND the current expo-router + * route is focused. Used to gate presence subscriptions so we hold them only + * while the user is genuinely on a surface. + */ +export function useAppActiveAndFocused(): boolean { + const [appActive, setAppActive] = useState(AppState.currentState === 'active'); + const [focused, setFocused] = useState(false); + + useEffect(() => { + const sub = AppState.addEventListener('change', state => { + setAppActive(state === 'active'); + }); + return () => { + sub.remove(); + }; + }, []); + + useFocusEffect( + useCallback(() => { + setFocused(true); + return () => { + setFocused(false); + }; + }, []) + ); + + return appActive && focused; +} diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts b/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts index 437534318f..ed2b68dacc 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-conversation-presence.ts @@ -1,38 +1,15 @@ -import { useCallback, useEffect, useState } from 'react'; -import { AppState } from 'react-native'; -import { useFocusEffect } from 'expo-router'; - import { presenceContextForConversation } from '@kilocode/event-service'; +import { useAppActiveAndFocused } from './use-app-active-and-focused'; import { usePresenceSubscription } from './use-presence-subscription'; export function useConversationPresence( sandboxId: string | undefined, conversationId: string | undefined ) { - const [appActive, setAppActive] = useState(AppState.currentState === 'active'); - const [focused, setFocused] = useState(false); - - useEffect(() => { - const sub = AppState.addEventListener('change', state => { - setAppActive(state === 'active'); - }); - return () => { - sub.remove(); - }; - }, []); - - useFocusEffect( - useCallback(() => { - setFocused(true); - return () => { - setFocused(false); - }; - }, []) - ); - + const activeAndFocused = useAppActiveAndFocused(); usePresenceSubscription( sandboxId && conversationId ? presenceContextForConversation(sandboxId, conversationId) : null, - Boolean(sandboxId && conversationId) && appActive && focused + Boolean(sandboxId && conversationId) && activeAndFocused ); } diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts index e4ae65fa31..e3b783bdad 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-event-subscription.ts @@ -2,10 +2,14 @@ import { useEffect } from 'react'; import { useEventServiceClient } from './use-kilo-chat-client'; +/** + * Subscribe to a single event-service event for one context. Call this hook + * once per event name when you need multiple events on the same context. + */ export function useEventSubscription( context: string | null, - events: readonly string[], - onEvent: (event: { event: string; payload: unknown }) => void + eventName: string, + onEvent: (payload: unknown) => void ) { const eventService = useEventServiceClient(); useEffect(() => { @@ -13,21 +17,14 @@ export function useEventSubscription( return undefined; } eventService.subscribe([context]); - const offs = events.map(eventName => - eventService.on(eventName, (ctx, payload) => { - if (ctx === context) { - onEvent({ event: eventName, payload }); - } - }) - ); - return () => { - for (const off of offs) { - off(); + const off = eventService.on(eventName, (ctx, payload) => { + if (ctx === context) { + onEvent(payload); } + }); + return () => { + off(); eventService.unsubscribe([context]); }; - // events is meant to be a stable array literal at the call site; - // join to use as a dependency without forcing memoization on callers. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [eventService, context, events.join('|'), onEvent]); + }, [eventService, context, eventName, onEvent]); } diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts index 524639c8cc..b2c9036b6d 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts @@ -2,48 +2,29 @@ import { useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { kiloclawInstanceContext } from '@kilocode/event-service'; +import { botStatusKey, conversationsKey } from '@kilocode/kilo-chat-hooks'; import { useEventSubscription } from './use-event-subscription'; -const INSTANCE_EVENTS = [ - 'conversation.created', - 'conversation.left', - 'message.created', - 'message.updated', - 'message.deleted', - 'bot.status', -] as const; - export function useInstanceEventSubscription(sandboxId: string | undefined) { const qc = useQueryClient(); - const onEvent = useCallback( - ({ event }: { event: string; payload: unknown }) => { - switch (event) { - case 'conversation.created': - case 'conversation.left': - case 'message.created': - case 'message.updated': - case 'message.deleted': { - // message.* invalidates the conversations list so last-message - // preview and unread counts stay current while the user is on - // the list (instance-level presence, not viewing a specific conv). - void qc.invalidateQueries({ queryKey: ['conversations', sandboxId] }); - break; - } - case 'bot.status': { - void qc.invalidateQueries({ queryKey: ['bot-status', sandboxId] }); - break; - } - default: { - break; - } - } - }, - [qc, sandboxId] - ); - useEventSubscription( - sandboxId ? kiloclawInstanceContext(sandboxId) : null, - INSTANCE_EVENTS, - onEvent - ); + const ctx = sandboxId ? kiloclawInstanceContext(sandboxId) : null; + + // message.* / conversation.* invalidate the conversations list so + // last-message preview and unread counts stay current while the user is on + // the list (instance-level presence, not viewing a specific conv). + const invalidateConversations = useCallback(() => { + void qc.invalidateQueries({ queryKey: conversationsKey(sandboxId ?? null) }); + }, [qc, sandboxId]); + + const invalidateBotStatus = useCallback(() => { + void qc.invalidateQueries({ queryKey: botStatusKey(sandboxId ?? null) }); + }, [qc, sandboxId]); + + useEventSubscription(ctx, 'conversation.created', invalidateConversations); + useEventSubscription(ctx, 'conversation.left', invalidateConversations); + useEventSubscription(ctx, 'message.created', invalidateConversations); + useEventSubscription(ctx, 'message.updated', invalidateConversations); + useEventSubscription(ctx, 'message.deleted', invalidateConversations); + useEventSubscription(ctx, 'bot.status', invalidateBotStatus); } diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts b/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts index e81be747fe..1c04d9eaca 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-instance-presence.ts @@ -1,35 +1,12 @@ -import { useCallback, useEffect, useState } from 'react'; -import { AppState } from 'react-native'; -import { useFocusEffect } from 'expo-router'; - import { presenceContextForInstance } from '@kilocode/event-service'; +import { useAppActiveAndFocused } from './use-app-active-and-focused'; import { usePresenceSubscription } from './use-presence-subscription'; export function useInstancePresence(sandboxId: string | undefined) { - const [appActive, setAppActive] = useState(AppState.currentState === 'active'); - const [focused, setFocused] = useState(false); - - useEffect(() => { - const sub = AppState.addEventListener('change', state => { - setAppActive(state === 'active'); - }); - return () => { - sub.remove(); - }; - }, []); - - useFocusEffect( - useCallback(() => { - setFocused(true); - return () => { - setFocused(false); - }; - }, []) - ); - + const activeAndFocused = useAppActiveAndFocused(); usePresenceSubscription( sandboxId ? presenceContextForInstance(sandboxId) : null, - Boolean(sandboxId) && appActive && focused + Boolean(sandboxId) && activeAndFocused ); } diff --git a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx index 4f5d96e97e..2789beeb0e 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx +++ b/apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx @@ -127,8 +127,8 @@ export function MessageArea({ conversationId }: MessageAreaProps) { // Bots are excluded inside the hook because their streaming uses // message.created for every token chunk and relies on typing.stopped to // signal stream completion. - useMessageCacheUpdater(kiloChatClient, sandboxId, conversationId, clearTypingForMember, msg => - toast.error(msg) + useMessageCacheUpdater(kiloChatClient, sandboxId, conversationId, clearTypingForMember, () => + toast.error("Couldn't reach the bot — please try again") ); const sendTyping = useTypingSender(kiloChatClient, conversationId); diff --git a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useBotStatus.ts b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useBotStatus.ts index 1ed0553523..c4ca080e6a 100644 --- a/apps/web/src/app/(app)/claw/kilo-chat/hooks/useBotStatus.ts +++ b/apps/web/src/app/(app)/claw/kilo-chat/hooks/useBotStatus.ts @@ -3,10 +3,9 @@ import { useEffect } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { BotStatusRecord, KiloChatEventOf } from '@kilocode/kilo-chat'; +import { botStatusKey } from '@kilocode/kilo-chat-hooks'; import { useKiloChatContext } from '../components/kiloChatContext'; -const botKey = (sandboxId: string) => ['kilo-chat', 'bot-status', sandboxId] as const; - // Matches the bot's old heartbeat cadence so UI staleness thresholds keep // working unchanged. Server-side dedupe absorbs multi-tab / multi-device // polling so this stays at ~1 webhook per sandbox per interval. @@ -20,7 +19,7 @@ export function useBotStatus(): BotStatusRecord | null { if (!sandboxId) return; return kiloChatClient.onBotStatus((_ctx: string, e: KiloChatEventOf<'bot.status'>) => { if (e.sandboxId !== sandboxId) return; - queryClient.setQueryData(botKey(sandboxId), prev => + queryClient.setQueryData(botStatusKey(sandboxId), prev => prev && prev.at >= e.at ? prev : { online: e.online, at: e.at, updatedAt: e.at } ); }); @@ -48,7 +47,7 @@ export function useBotStatus(): BotStatusRecord | null { }, [kiloChatClient, sandboxId]); const { data } = useQuery({ - queryKey: botKey(sandboxId ?? ''), + queryKey: botStatusKey(sandboxId), queryFn: async () => { if (!sandboxId) return null; const res = await kiloChatClient.getBotStatus(sandboxId); diff --git a/packages/kilo-chat-hooks/src/index.ts b/packages/kilo-chat-hooks/src/index.ts index aa0315d79d..599b343147 100644 --- a/packages/kilo-chat-hooks/src/index.ts +++ b/packages/kilo-chat-hooks/src/index.ts @@ -1,3 +1,4 @@ export * from './context'; +export * from './query-keys'; export * from './use-conversations'; export * from './use-messages'; diff --git a/packages/kilo-chat-hooks/src/query-keys.ts b/packages/kilo-chat-hooks/src/query-keys.ts new file mode 100644 index 0000000000..08a7c24b13 --- /dev/null +++ b/packages/kilo-chat-hooks/src/query-keys.ts @@ -0,0 +1,17 @@ +// Shared React Query key builders so subscribers (event handlers, mutations) +// invalidate exactly the keys the queries register under. Drift here silently +// breaks live updates — keep all kilo-chat keys in this file. + +export const conversationsKey = (sandboxId: string | null) => + ['kilo-chat', 'conversations', sandboxId] as const; + +export const conversationsKeyAll = () => ['kilo-chat', 'conversations'] as const; + +export const conversationKey = (conversationId: string | null) => + ['kilo-chat', 'conversation', conversationId] as const; + +export const messagesKey = (conversationId: string | null) => + ['kilo-chat', 'messages', conversationId] as const; + +export const botStatusKey = (sandboxId: string | null) => + ['kilo-chat', 'bot-status', sandboxId] as const; diff --git a/packages/kilo-chat-hooks/src/use-conversations.ts b/packages/kilo-chat-hooks/src/use-conversations.ts index faaf06f759..430e797fc0 100644 --- a/packages/kilo-chat-hooks/src/use-conversations.ts +++ b/packages/kilo-chat-hooks/src/use-conversations.ts @@ -3,11 +3,13 @@ import type { InfiniteData } from '@tanstack/react-query'; import type { KiloChatClient } from '@kilocode/kilo-chat'; import type { CreateConversationRequest, ConversationListResponse } from '@kilocode/kilo-chat'; +import { conversationKey, conversationsKey, conversationsKeyAll, messagesKey } from './query-keys'; + const CONVERSATIONS_PAGE_SIZE = 50; export function useConversations(client: KiloChatClient, sandboxId: string | null) { return useInfiniteQuery({ - queryKey: ['kilo-chat', 'conversations', sandboxId], + queryKey: conversationsKey(sandboxId), queryFn: ({ pageParam }) => client.listConversations({ sandboxId: sandboxId ?? undefined, @@ -26,7 +28,7 @@ export function useConversations(client: KiloChatClient, sandboxId: string | nul export function useConversationDetail(client: KiloChatClient, conversationId: string | null) { return useQuery({ - queryKey: ['kilo-chat', 'conversation', conversationId], + queryKey: conversationKey(conversationId), queryFn: () => client.getConversation(conversationId ?? ''), enabled: !!conversationId, }); @@ -37,7 +39,7 @@ export function useCreateConversation(client: KiloChatClient) { return useMutation({ mutationFn: (req: CreateConversationRequest) => client.createConversation(req), onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); + void queryClient.invalidateQueries({ queryKey: conversationsKeyAll() }); }, }); } @@ -48,7 +50,7 @@ export function useRenameConversation(client: KiloChatClient) { mutationFn: ({ conversationId, title }: { conversationId: string; title: string }) => client.renameConversation(conversationId, { title }), onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); + void queryClient.invalidateQueries({ queryKey: conversationsKeyAll() }); }, }); } @@ -58,9 +60,9 @@ export function useLeaveConversation(client: KiloChatClient) { return useMutation({ mutationFn: (conversationId: string) => client.leaveConversation(conversationId), onSuccess: (_data, conversationId) => { - queryClient.removeQueries({ queryKey: ['kilo-chat', 'conversation', conversationId] }); - queryClient.removeQueries({ queryKey: ['kilo-chat', 'messages', conversationId] }); - void queryClient.invalidateQueries({ queryKey: ['kilo-chat', 'conversations'] }); + queryClient.removeQueries({ queryKey: conversationKey(conversationId) }); + queryClient.removeQueries({ queryKey: messagesKey(conversationId) }); + void queryClient.invalidateQueries({ queryKey: conversationsKeyAll() }); }, }); } @@ -104,7 +106,7 @@ export function useMarkConversationRead(client: KiloChatClient) { onMutate: conversationId => { // Optimistically set lastReadAt = now in all cached conversation lists const now = Date.now(); - const queryKey = ['kilo-chat', 'conversations']; + const queryKey = conversationsKeyAll(); const previous = queryClient.getQueriesData({ queryKey }); queryClient.setQueriesData({ queryKey }, old => updateConversationPages(old, c => diff --git a/packages/kilo-chat-hooks/src/use-messages.ts b/packages/kilo-chat-hooks/src/use-messages.ts index d0acf0908b..b2302e1903 100644 --- a/packages/kilo-chat-hooks/src/use-messages.ts +++ b/packages/kilo-chat-hooks/src/use-messages.ts @@ -18,6 +18,8 @@ import type { import { useEffect } from 'react'; import { kiloclawConversationContext } from '@kilocode/event-service'; +import { messagesKey } from './query-keys'; + export const PAGE_SIZE = 50; export function applyReactionAdded( @@ -109,7 +111,7 @@ export function findMessageInCache( export function useMessages(client: KiloChatClient, conversationId: string | null) { return useInfiniteQuery({ - queryKey: ['kilo-chat', 'messages', conversationId], + queryKey: messagesKey(conversationId), queryFn: async ({ pageParam }) => { return client.listMessages(conversationId ?? '', { before: pageParam, limit: PAGE_SIZE }); }, @@ -138,7 +140,7 @@ export function useSendMessage( mutationFn: (req: SendMessageVariables) => client.sendMessage(req), onMutate: async (variables: SendMessageVariables) => { if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; + const queryKey = messagesKey(conversationId); await queryClient.cancelQueries({ queryKey }); const pendingId = `pending-${variables.clientId}`; const optimisticMessage: Message = { @@ -186,7 +188,7 @@ export function useEditMessage(client: KiloChatClient, conversationId: string | client.editMessage(messageId, req), onMutate: async variables => { if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; + const queryKey = messagesKey(conversationId); await queryClient.cancelQueries({ queryKey }); const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); queryClient.setQueryData>(queryKey, old => { @@ -218,7 +220,7 @@ export function useDeleteMessage(client: KiloChatClient, conversationId: string client.deleteMessage(messageId, { conversationId }), onMutate: async variables => { if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; + const queryKey = messagesKey(conversationId); await queryClient.cancelQueries({ queryKey }); const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); queryClient.setQueryData>(queryKey, old => { @@ -250,7 +252,7 @@ export function useAddReaction( client.addReaction(messageId, { conversationId: conversationId ?? '', emoji }), onMutate: async variables => { if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; + const queryKey = messagesKey(conversationId); await queryClient.cancelQueries({ queryKey }); const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); queryClient.setQueryData>(queryKey, old => { @@ -289,7 +291,7 @@ export function useRemoveReaction( client.removeReaction(messageId, { conversationId: conversationId ?? '', emoji }), onMutate: async variables => { if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; + const queryKey = messagesKey(conversationId); await queryClient.cancelQueries({ queryKey }); const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); queryClient.setQueryData>(queryKey, old => { @@ -335,7 +337,7 @@ export function useExecuteAction( }) => client.executeAction(conversationId ?? '', messageId, { groupId, value }), onMutate: async variables => { if (!conversationId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; + const queryKey = messagesKey(conversationId); await queryClient.cancelQueries({ queryKey }); const snapshot = findMessageInCache(queryClient, queryKey, variables.messageId); // Optimistically mark the action as resolved @@ -395,13 +397,17 @@ export function useMessageCacheUpdater( // end their own typing state via explicit typing.stopped, so we must not // clear on bot messages or the indicator disappears mid-stream. onHumanMessageCreated?: (ctx: string, senderId: string) => void, - onActionFailed?: (message: string) => void + // Fires when the server reports an action.delivery_failed for a message in + // this conversation, after the optimistic resolved-state has been rolled + // back. The shared package is platform-agnostic, so the user-visible + // message lives at the call site (web: sonner toast; mobile: native toast). + onActionFailed?: () => void ): void { const queryClient = useQueryClient(); useEffect(() => { if (!conversationId || !sandboxId) return; - const queryKey = ['kilo-chat', 'messages', conversationId]; + const queryKey = messagesKey(conversationId); const expectedContext = kiloclawConversationContext(sandboxId, conversationId); const onCreated = (ctx: string, e: MessageCreatedEvent) => { @@ -511,7 +517,7 @@ export function useMessageCacheUpdater( ), }; }); - onActionFailed?.("Couldn't reach the bot — please try again"); + onActionFailed?.(); }; const onReactionAdded = (ctx: string, e: ReactionAddedEvent) => { From ffeb4b6d3a62918fc054297bf10590bc4ca55d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:54:00 +0200 Subject: [PATCH 59/75] fix(mobile): subscribe to conversation.* events on instance context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instance-level subscription was listening for message.created/updated/ deleted, which are published on conversation contexts and never fire here. Replace them with conversation.renamed, conversation.read, and conversation.activity — the events kilo-chat actually pushes to the instance context — so list updates (title, unread, last-activity) invalidate the conversations query as intended. --- .../hooks/use-instance-event-subscription.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts b/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts index b2c9036b6d..8b6f57449e 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-instance-event-subscription.ts @@ -10,9 +10,10 @@ export function useInstanceEventSubscription(sandboxId: string | undefined) { const qc = useQueryClient(); const ctx = sandboxId ? kiloclawInstanceContext(sandboxId) : null; - // message.* / conversation.* invalidate the conversations list so - // last-message preview and unread counts stay current while the user is on - // the list (instance-level presence, not viewing a specific conv). + // conversation.* events are published on the instance context to keep the + // conversation list (last-activity, unread, title, membership) current while + // the user is on the list. message.* events fire on conversation contexts, + // not here. const invalidateConversations = useCallback(() => { void qc.invalidateQueries({ queryKey: conversationsKey(sandboxId ?? null) }); }, [qc, sandboxId]); @@ -23,8 +24,8 @@ export function useInstanceEventSubscription(sandboxId: string | undefined) { useEventSubscription(ctx, 'conversation.created', invalidateConversations); useEventSubscription(ctx, 'conversation.left', invalidateConversations); - useEventSubscription(ctx, 'message.created', invalidateConversations); - useEventSubscription(ctx, 'message.updated', invalidateConversations); - useEventSubscription(ctx, 'message.deleted', invalidateConversations); + useEventSubscription(ctx, 'conversation.renamed', invalidateConversations); + useEventSubscription(ctx, 'conversation.read', invalidateConversations); + useEventSubscription(ctx, 'conversation.activity', invalidateConversations); useEventSubscription(ctx, 'bot.status', invalidateBotStatus); } From c259c9567eb317fd8cd361f93a9e1fd05cfb5392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:03:26 +0200 Subject: [PATCH 60/75] chore(mobile): add @shopify/flash-list dependency Required by the kilo-chat MessageList and ConversationListScreen components. --- apps/mobile/package.json | 1 + pnpm-lock.yaml | 60 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index a59df0c47e..c210d87966 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -32,6 +32,7 @@ "@rn-primitives/portal": "^1.3.0", "@rn-primitives/slot": "^1.2.0", "@sentry/react-native": "~7.11.0", + "@shopify/flash-list": "2.0.2", "@tailwindcss/postcss": "^4.2.2", "@tanstack/react-query": "catalog:", "@trpc/client": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b252a8a276..fde8db827f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,9 @@ importers: '@sentry/react-native': specifier: ~7.11.0 version: 7.11.0(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) + '@shopify/flash-list': + specifier: 2.0.2 + version: 2.0.2(@babel/runtime@7.29.2)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -328,7 +331,7 @@ importers: version: 9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) stream-chat-expo: specifier: ^8.13.7 - version: 8.13.7(f3af0588b693ec71c0fc67ae0290618e) + version: 8.13.7(e673e8bffb1896cc06607271df6a38dc) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 @@ -1601,7 +1604,7 @@ importers: version: 7.0.0-dev.20260319.1 jest: specifier: ^30.3.0 - version: 30.3.0(@types/node@24.12.0)(esbuild-register@3.6.0(esbuild@0.27.4)) + version: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) typescript: specifier: 'catalog:' version: 5.9.3 @@ -6814,6 +6817,13 @@ packages: peerDependencies: webpack: '>=5.0.0' + '@shopify/flash-list@2.0.2': + resolution: {integrity: sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w==} + peerDependencies: + '@babel/runtime': '*' + react: '*' + react-native: '*' + '@sideway/address@4.1.5': resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} @@ -20523,6 +20533,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@shopify/flash-list@2.0.2(@babel/runtime@7.29.2)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.0 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) + tslib: 2.8.1 + '@sideway/address@4.1.5': dependencies: '@hapi/hoek': 9.3.0 @@ -25729,6 +25746,25 @@ snapshots: - supports-color - ts-node + jest-cli@30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)): + dependencies: + '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.4)) + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) + jest-util: 30.3.0 + jest-validate: 30.3.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jest-config@29.7.0(@types/node@24.12.0): dependencies: '@babel/core': 7.29.0 @@ -26380,6 +26416,19 @@ snapshots: - supports-color - ts-node + jest@30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)): + dependencies: + '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.4)) + '@jest/types': 30.3.0 + import-local: 3.2.0 + jest-cli: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jimp-compact@0.16.1: {} jiti@2.6.1: {} @@ -29668,12 +29717,12 @@ snapshots: stream-buffers@2.2.0: {} - stream-chat-expo@8.13.7(f3af0588b693ec71c0fc67ae0290618e): + stream-chat-expo@8.13.7(e673e8bffb1896cc06607271df6a38dc): dependencies: expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(bufferutil@4.1.0)(expo-router@55.0.11)(react-dom@19.2.4(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6) expo-image-manipulator: 55.0.14(expo@55.0.12) mime: 4.1.0 - stream-chat-react-native-core: 8.13.7(patch_hash=6fa2fe7a3ddb0ab3312ee1adc4e07d3fc9c46f7bc5bb861675bef4078808b8c0)(ab8a9f6f835746e18b11cebeae560e08) + stream-chat-react-native-core: 8.13.7(patch_hash=6fa2fe7a3ddb0ab3312ee1adc4e07d3fc9c46f7bc5bb861675bef4078808b8c0)(f6393708cf819e255e90d2b806f318a8) optionalDependencies: expo-audio: 55.0.12(expo-asset@55.0.13(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3))(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) expo-clipboard: 55.0.12(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) @@ -29702,7 +29751,7 @@ snapshots: - typescript - utf-8-validate - stream-chat-react-native-core@8.13.7(patch_hash=6fa2fe7a3ddb0ab3312ee1adc4e07d3fc9c46f7bc5bb861675bef4078808b8c0)(ab8a9f6f835746e18b11cebeae560e08): + stream-chat-react-native-core@8.13.7(patch_hash=6fa2fe7a3ddb0ab3312ee1adc4e07d3fc9c46f7bc5bb861675bef4078808b8c0)(f6393708cf819e255e90d2b806f318a8): dependencies: '@gorhom/bottom-sheet': 5.1.8(patch_hash=c5eaae9a28f5662f32d66e0129609680309eddc44720d6a2b1e02bbc2b5dd11f)(@types/react@19.2.14)(react-native-gesture-handler@2.30.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) '@react-native-community/netinfo': 11.5.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) @@ -29726,6 +29775,7 @@ snapshots: use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: '@emoji-mart/data': 1.2.1 + '@shopify/flash-list': 2.0.2(@babel/runtime@7.29.2)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) emoji-mart: 5.6.0 transitivePeerDependencies: - '@types/react' From 41bb5650fdf6bcd315a6cb7f8d5a5cec9d6035cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:03:26 +0200 Subject: [PATCH 61/75] chore(mobile): add EXPO_PUBLIC_KILO_CHAT_URL and EXPO_PUBLIC_EVENT_SERVICE_URL These were declared in env-keys.js by PR 5a but never added to apps/mobile/.env, which broke the dev build. --- apps/mobile/.env | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/mobile/.env b/apps/mobile/.env index 11c9f66e17..5882750319 100644 --- a/apps/mobile/.env +++ b/apps/mobile/.env @@ -5,3 +5,5 @@ CLOUD_AGENT_WS_URL=wss://cloud-agent-next.kilosessions.ai SESSION_INGEST_WS_URL=wss://ingest.kilosessions.ai APPSFLYER_DEV_KEY=jnoVs6KzXanpbKrqXckPu9 APPSFLYER_APP_ID=6761193135 +EXPO_PUBLIC_KILO_CHAT_URL=https://kilo-chat.kilosessions.ai +EXPO_PUBLIC_EVENT_SERVICE_URL=wss://event-service.kilosessions.ai From 5fd8c698811e4cb72d19d9d73bbce6f50da80b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:46:50 +0200 Subject: [PATCH 62/75] feat(mobile): add EmptyConversationList --- .../kilo-chat/empty-conversation-list.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/empty-conversation-list.tsx diff --git a/apps/mobile/src/components/kilo-chat/empty-conversation-list.tsx b/apps/mobile/src/components/kilo-chat/empty-conversation-list.tsx new file mode 100644 index 0000000000..332fee2c3d --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/empty-conversation-list.tsx @@ -0,0 +1,28 @@ +import { MessageSquarePlus } from 'lucide-react-native'; +import { View } from 'react-native'; + +import { EmptyState } from '@/components/empty-state'; +import { Button } from '@/components/ui/button'; +import { Text } from '@/components/ui/text'; + +type Props = { + onStart: () => void; + isStarting: boolean; +}; + +export function EmptyConversationList({ onStart, isStarting }: Props) { + return ( + + + {isStarting ? 'Starting…' : 'Start a conversation'} + + } + /> + + ); +} From a4c1b99cff5cc2b3191b9ae6fcfe557aa7c2134b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:48:06 +0200 Subject: [PATCH 63/75] feat(mobile): add ConversationHeader --- .../components/kilo-chat/conversation-header.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/conversation-header.tsx diff --git a/apps/mobile/src/components/kilo-chat/conversation-header.tsx b/apps/mobile/src/components/kilo-chat/conversation-header.tsx new file mode 100644 index 0000000000..bd69c1f476 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/conversation-header.tsx @@ -0,0 +1,15 @@ +import { ScreenHeader } from '@/components/screen-header'; +import { Text } from '@/components/ui/text'; + +type Props = { title: string; subtitle?: string }; + +export function ConversationHeader({ title, subtitle }: Props) { + return ( + {subtitle} : undefined + } + /> + ); +} From e3b7d04ac89f6a4b95c6ea20eb01f40340419b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:49:00 +0200 Subject: [PATCH 64/75] feat(mobile): add TypingIndicator placeholder --- .../src/components/kilo-chat/typing-indicator.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/typing-indicator.tsx diff --git a/apps/mobile/src/components/kilo-chat/typing-indicator.tsx b/apps/mobile/src/components/kilo-chat/typing-indicator.tsx new file mode 100644 index 0000000000..c909788c14 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/typing-indicator.tsx @@ -0,0 +1,15 @@ +import { View } from 'react-native'; +import { Text } from '@/components/ui/text'; + +type Props = { isTyping: boolean; name?: string }; + +export function TypingIndicator({ isTyping, name }: Props) { + if (!isTyping) { + return null; + } + return ( + + {name ?? 'Bot'} is typing… + + ); +} From 0f910d65d902e8ac9e4ede3f59a65dd99872cbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:50:15 +0200 Subject: [PATCH 65/75] feat(mobile): add MessageInput --- .../components/kilo-chat/message-input.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/message-input.tsx diff --git a/apps/mobile/src/components/kilo-chat/message-input.tsx b/apps/mobile/src/components/kilo-chat/message-input.tsx new file mode 100644 index 0000000000..9e6d0bf5eb --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/message-input.tsx @@ -0,0 +1,56 @@ +import { Send } from 'lucide-react-native'; +import { useRef, useState } from 'react'; +import { Pressable, TextInput, View } from 'react-native'; + +import { cn } from '@/lib/utils'; +import { useThemeColors } from '@/lib/hooks/use-theme-colors'; + +type Props = { + onSend: (text: string) => void; + disabled?: boolean; +}; + +export function MessageInput({ onSend, disabled }: Props) { + const colors = useThemeColors(); + const valueRef = useRef(''); + const [canSend, setCanSend] = useState(false); + const inputRef = useRef(null); + + const submit = () => { + const text = valueRef.current.trim(); + if (!text) { + return; + } + onSend(text); + valueRef.current = ''; + inputRef.current?.clear(); + setCanSend(false); + }; + + return ( + + { + valueRef.current = t; + setCanSend(t.trim().length > 0); + }} + onSubmitEditing={submit} + /> + + + + + ); +} From 682e87b83b15bcda1bb83f77caa85cfed0dd220c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:53:06 +0200 Subject: [PATCH 66/75] feat(mobile): add MessageBubble --- .../components/kilo-chat/message-bubble.tsx | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/message-bubble.tsx diff --git a/apps/mobile/src/components/kilo-chat/message-bubble.tsx b/apps/mobile/src/components/kilo-chat/message-bubble.tsx new file mode 100644 index 0000000000..af63b2189a --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/message-bubble.tsx @@ -0,0 +1,194 @@ +import { + useAddReaction, + useExecuteAction, + useKiloChatClient, + useRemoveReaction, +} from '@kilocode/kilo-chat-hooks'; +import { type ExecApprovalDecision, type Message } from '@kilocode/kilo-chat'; +import { Pressable, Text, View } from 'react-native'; + +import { Button } from '@/components/ui/button'; +import { useCurrentUserId } from '@/components/kilo-chat/hooks/use-current-user-id'; +import { cn } from '@/lib/utils'; + +type Props = { + message: Message; + conversationId: string; + isFromMe: boolean; + showAuthor: boolean; + onLongPress?: (m: Message) => void; +}; + +function formatTimestamp(ms: number): string { + return new Date(ms).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); +} + +function actionStyleToVariant( + style: 'primary' | 'danger' | 'secondary' +): 'default' | 'destructive' | 'secondary' { + if (style === 'danger') { + return 'destructive'; + } + if (style === 'secondary') { + return 'secondary'; + } + return 'default'; +} + +export function MessageBubble({ + message, + conversationId, + isFromMe, + showAuthor, + onLongPress, +}: Props) { + const client = useKiloChatClient(); + const currentUserId = useCurrentUserId(); + + const executeAction = useExecuteAction(client, conversationId, currentUserId ?? ''); + const addReaction = useAddReaction(client, conversationId, currentUserId ?? ''); + const removeReaction = useRemoveReaction(client, conversationId, currentUserId ?? ''); + + const isPending = message.id.startsWith('pending-'); + const timestamp = message.clientUpdatedAt ?? message.updatedAt; + + function handleReactionPress(emoji: string) { + if (!currentUserId) { + return; + } + const hasReacted = message.reactions + .find(r => r.emoji === emoji) + ?.memberIds.includes(currentUserId); + if (hasReacted) { + removeReaction.mutate({ messageId: message.id, emoji }); + } else { + addReaction.mutate({ messageId: message.id, emoji }); + } + } + + function handleExecuteAction(groupId: string, value: ExecApprovalDecision) { + executeAction.mutate({ messageId: message.id, groupId, value }); + } + + const textColor = isFromMe ? 'text-primary-foreground' : 'text-foreground'; + + return ( + { + onLongPress(message); + } + : undefined + } + className={cn('px-4 py-1', isFromMe ? 'items-end' : 'items-start', isPending && 'opacity-50')} + > + {showAuthor && ( + + {message.senderId} + {timestamp !== null && ( + {formatTimestamp(timestamp)} + )} + + )} + + + {message.deleted ? ( + [deleted message] + ) : ( + <> + {message.content.map((block, index) => { + if (block.type === 'text') { + return ( + + {block.text} + + ); + } + + // block.type === 'actions' + if (block.resolved) { + const resolvedAction = block.actions.find(a => a.value === block.resolved?.value); + const label = resolvedAction?.label ?? block.resolved.value; + return ( + + {label} + + ); + } + + return ( + + {block.actions.map(action => ( + + ))} + + ); + })} + + )} + + {!showAuthor && timestamp !== null && ( + + {formatTimestamp(timestamp)} + + )} + + + {message.reactions.length > 0 && ( + + {message.reactions.map(reaction => { + const hasReacted = currentUserId ? reaction.memberIds.includes(currentUserId) : false; + return ( + { + handleReactionPress(reaction.emoji); + }} + className={cn( + 'flex-row items-center gap-0.5 rounded-full px-2 py-0.5', + hasReacted ? 'bg-primary' : 'bg-neutral-200 dark:bg-neutral-700' + )} + > + {reaction.emoji} + + {reaction.count} + + + ); + })} + + )} + + ); +} From a0f8c840ae0bbc9618d064b456e9a005270aae1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:55:57 +0200 Subject: [PATCH 67/75] feat(mobile): add MessageList Implement MessageList using FlashList v2 with maintainVisibleContentPosition and startRenderingFromBottom for chat layout; wire fetchOlder via onStartReached. --- .../src/components/kilo-chat/message-list.tsx | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/message-list.tsx diff --git a/apps/mobile/src/components/kilo-chat/message-list.tsx b/apps/mobile/src/components/kilo-chat/message-list.tsx new file mode 100644 index 0000000000..b418e76961 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/message-list.tsx @@ -0,0 +1,67 @@ +import { FlashList } from '@shopify/flash-list'; +import { type Message } from '@kilocode/kilo-chat'; +import { View } from 'react-native'; + +import { MessageBubble } from '@/components/kilo-chat/message-bubble'; +import { Skeleton } from '@/components/ui/skeleton'; + +type Props = { + messages: Message[]; + conversationId: string; + currentUserId: string | null; + fetchOlder?: () => void; + hasOlder?: boolean; + onLongPressMessage?: (m: Message) => void; +}; + +export function MessageList({ + messages, + conversationId, + currentUserId, + fetchOlder, + hasOlder, + onLongPressMessage, +}: Props) { + // useMessages returns messages newest-first (result of .reverse() in the hook). + // FlashList v2 does not support `inverted`; instead we use maintainVisibleContentPosition + // with startRenderingFromBottom. That requires data in chronological order (oldest first), + // so we reverse once to get oldest→newest. + const chronological = messages.toReversed(); + + return ( + { + // In chronological order, the previous message in time is data[index - 1]. + // showAuthor is true when the sender changes relative to the prior message, + // or when this is the oldest message (index 0). + const previousItem = chronological[index - 1]; + const showAuthor = previousItem === undefined || previousItem.senderId !== item.senderId; + + return ( + + ); + }} + keyExtractor={item => item.id} + onStartReached={fetchOlder} + onStartReachedThreshold={0.5} + maintainVisibleContentPosition={{ + // Start rendering from the bottom so the newest message is visible on first render. + startRenderingFromBottom: true, + }} + ListHeaderComponent={ + hasOlder ? ( + + + + ) : null + } + /> + ); +} From 497613a2cb47c12b9b340c2b356b42ad2f4d5ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 12:58:08 +0200 Subject: [PATCH 68/75] feat(mobile): add ConversationScreen --- .../kilo-chat/conversation-screen.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/conversation-screen.tsx diff --git a/apps/mobile/src/components/kilo-chat/conversation-screen.tsx b/apps/mobile/src/components/kilo-chat/conversation-screen.tsx new file mode 100644 index 0000000000..21470d0e95 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/conversation-screen.tsx @@ -0,0 +1,71 @@ +import { useCallback } from 'react'; +import { KeyboardAvoidingView, Platform, View } from 'react-native'; +import { useFocusEffect } from 'expo-router'; + +import { ConversationHeader } from './conversation-header'; +import { MessageInput } from './message-input'; +import { MessageList } from './message-list'; +import { TypingIndicator } from './typing-indicator'; +import { useConversationPresence } from './hooks/use-conversation-presence'; +import { useKiloChatClient } from './hooks/use-kilo-chat-client'; +import { useMarkRead } from './hooks/use-mark-read'; +import { useMessages, useSendMessage } from './hooks/use-messages'; +import { useCurrentUserId } from './hooks/use-current-user-id'; + +type Props = { sandboxId: string; conversationId: string; conversationTitle: string }; + +export function ConversationScreen({ sandboxId, conversationId, conversationTitle }: Props) { + const client = useKiloChatClient(); + const currentUserId = useCurrentUserId(); + + const messagesQuery = useMessages(client, conversationId); + const messages = messagesQuery.data?.messages ?? []; + const hasOlder = messagesQuery.hasNextPage; + const fetchOlder = useCallback(() => { + if (messagesQuery.hasNextPage && !messagesQuery.isFetchingNextPage) { + void messagesQuery.fetchNextPage(); + } + }, [messagesQuery]); + + const sendMutation = useSendMessage(client, conversationId, currentUserId ?? ''); + const handleSend = useCallback( + (text: string) => { + sendMutation.mutate({ + conversationId, + content: [{ type: 'text', text }], + clientId: crypto.randomUUID(), + }); + }, + [sendMutation, conversationId] + ); + + useConversationPresence(sandboxId, conversationId); + + const markRead = useMarkRead(); + useFocusEffect( + useCallback(() => { + markRead(sandboxId, conversationId); + // Active-conversation suppression wiring added in PR 5d (Task 50). + }, [sandboxId, conversationId, markRead]) + ); + + return ( + + + + + + + + + ); +} From 0d78ce70f915b6e8df3efeb83993538ea3b99367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:01:20 +0200 Subject: [PATCH 69/75] feat(mobile): add ConversationListScreen --- .../kilo-chat/conversation-list-screen.tsx | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 apps/mobile/src/components/kilo-chat/conversation-list-screen.tsx diff --git a/apps/mobile/src/components/kilo-chat/conversation-list-screen.tsx b/apps/mobile/src/components/kilo-chat/conversation-list-screen.tsx new file mode 100644 index 0000000000..a0b9d01953 --- /dev/null +++ b/apps/mobile/src/components/kilo-chat/conversation-list-screen.tsx @@ -0,0 +1,126 @@ +import { FlashList } from '@shopify/flash-list'; +import { type Href, useRouter } from 'expo-router'; +import { Pressable, View } from 'react-native'; +import Animated, { FadeIn } from 'react-native-reanimated'; + +import { ScreenHeader } from '@/components/screen-header'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Text } from '@/components/ui/text'; +import { timeAgo } from '@/lib/utils'; + +import { EmptyConversationList } from './empty-conversation-list'; +import { useKiloChatClient } from './hooks/use-kilo-chat-client'; +import { useConversations, useCreateConversation } from './hooks/use-conversations'; +import { useInstanceEventSubscription } from './hooks/use-instance-event-subscription'; +import { useInstancePresence } from './hooks/use-instance-presence'; + +type Props = { + sandboxId: string; + sandboxLabel: string; +}; + +type ConversationItem = { + conversationId: string; + title: string | null; + lastActivityAt: number | null; + lastReadAt: number | null; + joinedAt: number; +}; + +type ConversationRowProps = { + item: ConversationItem; + onPress: (id: string) => void; +}; + +function ConversationRow({ item, onPress }: ConversationRowProps) { + const hasUnread = + item.lastActivityAt !== null && + (item.lastReadAt === null || item.lastReadAt < item.lastActivityAt); + + return ( + { + onPress(item.conversationId); + }} + > + + + {item.title ?? 'Untitled conversation'} + + {item.lastActivityAt !== null ? ( + + {timeAgo(new Date(item.lastActivityAt))} + + ) : null} + + {hasUnread ? ( + + ) : ( + // Reserve space so rows stay the same width whether the dot is shown or not + + )} + + ); +} + +export function ConversationListScreen({ sandboxId, sandboxLabel }: Props) { + const router = useRouter(); + const client = useKiloChatClient(); + const listQuery = useConversations(client, sandboxId); + const createConversation = useCreateConversation(client); + + const conversations = listQuery.data?.conversations ?? []; + const isFetchingNextPage = listQuery.isFetchingNextPage; + const fetchNextPage = listQuery.fetchNextPage; + + useInstanceEventSubscription(sandboxId); + useInstancePresence(sandboxId); + + function handleRowPress(conversationId: string) { + // Route lands in PR 5d (Task 47) + router.push(`/(app)/chat/${sandboxId}/${conversationId}` as unknown as Href); + } + + function handleCreateAndNavigate() { + createConversation.mutate( + { sandboxId }, + { + onSuccess: result => { + // Route lands in PR 5d (Task 47) + router.push(`/(app)/chat/${sandboxId}/${result.conversationId}` as unknown as Href); + }, + } + ); + } + + return ( + + + + c.conversationId} + renderItem={({ item }) => } + ListEmptyComponent={ + + } + ListFooterComponent={ + isFetchingNextPage ? ( + + + + ) : null + } + onEndReached={() => { + void fetchNextPage(); + }} + onEndReachedThreshold={0.5} + /> + + + ); +} From 1bf1b34a4897ddd093d508ac3cd85b612b222eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:16:38 +0200 Subject: [PATCH 70/75] fix(mobile): address review feedback on kilo-chat components - Drop double-cast `as unknown as Href` in favor of `as Href` - Use themed `Text` from `@/components/ui/text` and local `useKiloChatClient` re-export in `MessageBubble` - Switch `crypto.randomUUID()` to `expo-crypto`'s `Crypto.randomUUID` to match existing usage in `cloud-agent-runtime.ts` --- .../kilo-chat/conversation-list-screen.tsx | 4 ++-- .../components/kilo-chat/conversation-screen.tsx | 3 ++- .../src/components/kilo-chat/message-bubble.tsx | 13 +++++-------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/conversation-list-screen.tsx b/apps/mobile/src/components/kilo-chat/conversation-list-screen.tsx index a0b9d01953..029e6c5e64 100644 --- a/apps/mobile/src/components/kilo-chat/conversation-list-screen.tsx +++ b/apps/mobile/src/components/kilo-chat/conversation-list-screen.tsx @@ -79,7 +79,7 @@ export function ConversationListScreen({ sandboxId, sandboxLabel }: Props) { function handleRowPress(conversationId: string) { // Route lands in PR 5d (Task 47) - router.push(`/(app)/chat/${sandboxId}/${conversationId}` as unknown as Href); + router.push(`/(app)/chat/${sandboxId}/${conversationId}` as Href); } function handleCreateAndNavigate() { @@ -88,7 +88,7 @@ export function ConversationListScreen({ sandboxId, sandboxLabel }: Props) { { onSuccess: result => { // Route lands in PR 5d (Task 47) - router.push(`/(app)/chat/${sandboxId}/${result.conversationId}` as unknown as Href); + router.push(`/(app)/chat/${sandboxId}/${result.conversationId}` as Href); }, } ); diff --git a/apps/mobile/src/components/kilo-chat/conversation-screen.tsx b/apps/mobile/src/components/kilo-chat/conversation-screen.tsx index 21470d0e95..ac42c770a8 100644 --- a/apps/mobile/src/components/kilo-chat/conversation-screen.tsx +++ b/apps/mobile/src/components/kilo-chat/conversation-screen.tsx @@ -1,3 +1,4 @@ +import * as Crypto from 'expo-crypto'; import { useCallback } from 'react'; import { KeyboardAvoidingView, Platform, View } from 'react-native'; import { useFocusEffect } from 'expo-router'; @@ -33,7 +34,7 @@ export function ConversationScreen({ sandboxId, conversationId, conversationTitl sendMutation.mutate({ conversationId, content: [{ type: 'text', text }], - clientId: crypto.randomUUID(), + clientId: Crypto.randomUUID(), }); }, [sendMutation, conversationId] diff --git a/apps/mobile/src/components/kilo-chat/message-bubble.tsx b/apps/mobile/src/components/kilo-chat/message-bubble.tsx index af63b2189a..211264261a 100644 --- a/apps/mobile/src/components/kilo-chat/message-bubble.tsx +++ b/apps/mobile/src/components/kilo-chat/message-bubble.tsx @@ -1,14 +1,11 @@ -import { - useAddReaction, - useExecuteAction, - useKiloChatClient, - useRemoveReaction, -} from '@kilocode/kilo-chat-hooks'; +import { useAddReaction, useExecuteAction, useRemoveReaction } from '@kilocode/kilo-chat-hooks'; import { type ExecApprovalDecision, type Message } from '@kilocode/kilo-chat'; -import { Pressable, Text, View } from 'react-native'; +import { Pressable, View } from 'react-native'; -import { Button } from '@/components/ui/button'; import { useCurrentUserId } from '@/components/kilo-chat/hooks/use-current-user-id'; +import { useKiloChatClient } from '@/components/kilo-chat/hooks/use-kilo-chat-client'; +import { Button } from '@/components/ui/button'; +import { Text } from '@/components/ui/text'; import { cn } from '@/lib/utils'; type Props = { From ce577c4e9b7ec88eaefa7bdba088c111d1f2b4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:24:37 +0200 Subject: [PATCH 71/75] feat(mobile): add chat sandbox stack layout --- apps/mobile/src/app/(app)/chat/[sandbox-id]/_layout.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 apps/mobile/src/app/(app)/chat/[sandbox-id]/_layout.tsx diff --git a/apps/mobile/src/app/(app)/chat/[sandbox-id]/_layout.tsx b/apps/mobile/src/app/(app)/chat/[sandbox-id]/_layout.tsx new file mode 100644 index 0000000000..6d1a690211 --- /dev/null +++ b/apps/mobile/src/app/(app)/chat/[sandbox-id]/_layout.tsx @@ -0,0 +1,5 @@ +import { Stack } from 'expo-router'; + +export default function ChatSandboxLayout() { + return ; +} From a25a8df7722b3badb904e5f28710ff1f44a37f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:24:40 +0200 Subject: [PATCH 72/75] feat(mobile): add conversation list route --- apps/mobile/src/app/(app)/chat/[sandbox-id]/index.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 apps/mobile/src/app/(app)/chat/[sandbox-id]/index.tsx diff --git a/apps/mobile/src/app/(app)/chat/[sandbox-id]/index.tsx b/apps/mobile/src/app/(app)/chat/[sandbox-id]/index.tsx new file mode 100644 index 0000000000..d99324b468 --- /dev/null +++ b/apps/mobile/src/app/(app)/chat/[sandbox-id]/index.tsx @@ -0,0 +1,11 @@ +import { useLocalSearchParams } from 'expo-router'; + +import { ConversationListScreen } from '@/components/kilo-chat/conversation-list-screen'; +import { useAllKiloClawInstances } from '@/lib/hooks/use-instance-context'; + +export default function ChatSandboxIndex() { + const { 'sandbox-id': sandboxId } = useLocalSearchParams<{ 'sandbox-id': string }>(); + const { data: instances } = useAllKiloClawInstances(); + const sandboxLabel = instances?.find(i => i.sandboxId === sandboxId)?.name ?? 'Chat'; + return ; +} From 4ed5cb832617f46a52ebd9f5436fa94953bb2d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:24:43 +0200 Subject: [PATCH 73/75] feat(mobile): add conversation message route --- .../chat/[sandbox-id]/[conversation-id].tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 apps/mobile/src/app/(app)/chat/[sandbox-id]/[conversation-id].tsx diff --git a/apps/mobile/src/app/(app)/chat/[sandbox-id]/[conversation-id].tsx b/apps/mobile/src/app/(app)/chat/[sandbox-id]/[conversation-id].tsx new file mode 100644 index 0000000000..a81779108f --- /dev/null +++ b/apps/mobile/src/app/(app)/chat/[sandbox-id]/[conversation-id].tsx @@ -0,0 +1,20 @@ +import { useLocalSearchParams } from 'expo-router'; + +import { ConversationScreen } from '@/components/kilo-chat/conversation-screen'; +import { useConversationDetail } from '@/components/kilo-chat/hooks/use-conversations'; +import { useKiloChatClient } from '@/components/kilo-chat/hooks/use-kilo-chat-client'; + +export default function ChatConversationRoute() { + const params = useLocalSearchParams<{ 'sandbox-id': string; 'conversation-id': string }>(); + const sandboxId = params['sandbox-id']; + const conversationId = params['conversation-id']; + const client = useKiloChatClient(); + const { data } = useConversationDetail(client, conversationId); + return ( + + ); +} From 381ad05f9ad1bddd689a301dcfb9631e88b5e144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:28:28 +0200 Subject: [PATCH 74/75] feat(mobile): wire chat deep links and active-conversation suppression --- .../kilo-chat/conversation-screen.tsx | 6 ++- apps/mobile/src/components/kiloclaw/chat.tsx | 6 +-- .../hooks/use-unread-counts-invalidation.ts | 2 +- apps/mobile/src/lib/notifications.ts | 38 +++++++++---------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/apps/mobile/src/components/kilo-chat/conversation-screen.tsx b/apps/mobile/src/components/kilo-chat/conversation-screen.tsx index ac42c770a8..000c4a76a7 100644 --- a/apps/mobile/src/components/kilo-chat/conversation-screen.tsx +++ b/apps/mobile/src/components/kilo-chat/conversation-screen.tsx @@ -12,6 +12,7 @@ import { useKiloChatClient } from './hooks/use-kilo-chat-client'; import { useMarkRead } from './hooks/use-mark-read'; import { useMessages, useSendMessage } from './hooks/use-messages'; import { useCurrentUserId } from './hooks/use-current-user-id'; +import { setActiveChatLocation } from '@/lib/notifications'; type Props = { sandboxId: string; conversationId: string; conversationTitle: string }; @@ -46,7 +47,10 @@ export function ConversationScreen({ sandboxId, conversationId, conversationTitl useFocusEffect( useCallback(() => { markRead(sandboxId, conversationId); - // Active-conversation suppression wiring added in PR 5d (Task 50). + setActiveChatLocation({ sandboxId, conversationId }); + return () => { + setActiveChatLocation(null); + }; }, [sandboxId, conversationId, markRead]) ); diff --git a/apps/mobile/src/components/kiloclaw/chat.tsx b/apps/mobile/src/components/kiloclaw/chat.tsx index 626cde8230..f38ea731a4 100644 --- a/apps/mobile/src/components/kiloclaw/chat.tsx +++ b/apps/mobile/src/components/kiloclaw/chat.tsx @@ -18,7 +18,7 @@ import { useStreamChatTheme } from '@/components/kiloclaw/chat-theme'; import { useAppLifecycle } from '@/lib/hooks/use-app-lifecycle'; import { useStreamChatCredentials } from '@/lib/hooks/use-kiloclaw-queries'; import { setLastActiveInstance } from '@/lib/last-active-instance'; -import { parseNotificationData, setActiveChatInstance } from '@/lib/notifications'; +import { parseNotificationData } from '@/lib/notifications'; import { useTRPC } from '@/lib/trpc'; type KiloClawChatProps = { @@ -72,7 +72,6 @@ export function KiloClawChat({ useFocusEffect( useCallback(() => { isFocusedRef.current = true; - setActiveChatInstance(instanceId); setLastActiveInstance(instanceId); markChatRead({ channelId: instanceId }); @@ -81,14 +80,13 @@ export function KiloClawChat({ // it immediately so the badge never drifts above 0 while the user is reading. const subscription = Notifications.addNotificationReceivedListener(notification => { const data = parseNotificationData(notification.request.content.data); - if (data?.type === 'chat' && data.instanceId === instanceId) { + if (data?.type === 'chat.message' && data.sandboxId === instanceId) { markChatRead({ channelId: instanceId }); } }); return () => { isFocusedRef.current = false; - setActiveChatInstance(null); subscription.remove(); }; }, [instanceId, markChatRead]) diff --git a/apps/mobile/src/lib/hooks/use-unread-counts-invalidation.ts b/apps/mobile/src/lib/hooks/use-unread-counts-invalidation.ts index b6b361ae9c..4d9846244e 100644 --- a/apps/mobile/src/lib/hooks/use-unread-counts-invalidation.ts +++ b/apps/mobile/src/lib/hooks/use-unread-counts-invalidation.ts @@ -33,7 +33,7 @@ export function useUnreadCountsInvalidation() { const received = Notifications.addNotificationReceivedListener(notification => { const data = parseNotificationData(notification.request.content.data); - if (data?.type === 'chat') { + if (data?.type === 'chat.message') { invalidate(); } }); diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts index c20858c069..273680ff92 100644 --- a/apps/mobile/src/lib/notifications.ts +++ b/apps/mobile/src/lib/notifications.ts @@ -2,7 +2,8 @@ import expoConstants from 'expo-constants'; import * as Notifications from 'expo-notifications'; import { type Href, router } from 'expo-router'; import { Platform } from 'react-native'; -import { z } from 'zod'; + +import { pushDataSchema } from '@kilocode/notifications'; function getProjectId(): string { const eas = expoConstants.expoConfig?.extra?.eas as { projectId?: string } | undefined; @@ -13,30 +14,24 @@ function getProjectId(): string { return projectId; } -// Tracks which chat instance screen is currently focused. +// Tracks which conversation screen is currently focused. // Read by the foreground notification handler to suppress notifications -// when the user is already viewing that chat. +// when the user is already viewing that conversation. // A module-level variable (not React state) because the notification handler // is registered once and must always read the latest value without stale closures. -let activeChatInstanceId: string | null = null; +let activeChatLocation: { sandboxId: string; conversationId: string } | null = null; -export function setActiveChatInstance(instanceId: string | null) { - activeChatInstanceId = instanceId; +export function setActiveChatLocation( + location: { sandboxId: string; conversationId: string } | null +) { + activeChatLocation = location; } -// Keep in sync with data field in services/notifications/src/dos/NotificationChannelDO.ts -const notificationDataSchema = z.object({ - type: z.literal('chat'), - instanceId: z.string().min(1), -}); - -type NotificationData = z.infer; - // Runtime-validates that an arbitrary notification `data` payload matches the // shape we care about. Push producers can evolve independently of the app, so // always parse before reading fields from the OS-provided notification content. -export function parseNotificationData(data: unknown): NotificationData | null { - const parsed = notificationDataSchema.safeParse(data); +export function parseNotificationData(data: unknown) { + const parsed = pushDataSchema.safeParse(data); return parsed.success ? parsed.data : null; } @@ -62,8 +57,11 @@ export function setupNotificationHandler() { handleNotification: async notification => { const data = parseNotificationData(notification.request.content.data); - // Suppress only if the user is already viewing this exact chat - if (data && data.instanceId === activeChatInstanceId) { + if ( + data?.type === 'chat.message' && + activeChatLocation?.sandboxId === data.sandboxId && + activeChatLocation.conversationId === data.conversationId + ) { return suppressed; } @@ -87,7 +85,7 @@ export function setupNotificationResponseHandler() { const data = parseNotificationData(response.notification.request.content.data); if (data) { - const path = `/(app)/chat/${data.instanceId}`; + const path = `/(app)/chat/${data.sandboxId}/${data.conversationId}`; // If the router is ready (has segments), navigate immediately. // Otherwise store as pending for consumption after auth completes. try { @@ -109,7 +107,7 @@ export function checkInitialNotification(): void { } const data = parseNotificationData(response.notification.request.content.data); if (data) { - pendingNotificationLink = `/(app)/chat/${data.instanceId}`; + pendingNotificationLink = `/(app)/chat/${data.sandboxId}/${data.conversationId}`; } } From d4e68ccd9687d74fc3206472ec39da7a97042c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 30 Apr 2026 13:42:04 +0200 Subject: [PATCH 75/75] fix(mobile): clear correct badge bucket on legacy chat foreground push --- apps/mobile/src/components/kiloclaw/chat.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/components/kiloclaw/chat.tsx b/apps/mobile/src/components/kiloclaw/chat.tsx index f38ea731a4..30e17ae7b4 100644 --- a/apps/mobile/src/components/kiloclaw/chat.tsx +++ b/apps/mobile/src/components/kiloclaw/chat.tsx @@ -9,6 +9,8 @@ import { type Channel as StreamChannel, StreamChat } from 'stream-chat'; import { Channel, Chat, MessageInput, MessageList, OverlayProvider } from 'stream-chat-expo'; import { toast } from 'sonner-native'; +import { badgeBucketForConversation } from '@kilocode/notifications'; + import { KiloClawMessageAvatar } from '@/components/kiloclaw/chat-avatar'; import { ChatPlaceholder } from '@/components/kiloclaw/chat-placeholder'; import { ChatHeader, ChatShell } from '@/components/kiloclaw/chat-shell'; @@ -81,7 +83,9 @@ export function KiloClawChat({ const subscription = Notifications.addNotificationReceivedListener(notification => { const data = parseNotificationData(notification.request.content.data); if (data?.type === 'chat.message' && data.sandboxId === instanceId) { - markChatRead({ channelId: instanceId }); + markChatRead({ + badgeBucket: badgeBucketForConversation(data.sandboxId, data.conversationId), + }); } });