@rudderhq/db 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/backup-lib.d.ts.map +1 -1
  2. package/dist/backup-lib.js +11 -3
  3. package/dist/backup-lib.js.map +1 -1
  4. package/dist/client.d.ts +25 -1
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/client.js +304 -20
  7. package/dist/client.js.map +1 -1
  8. package/dist/client.test.js +217 -29
  9. package/dist/client.test.js.map +1 -1
  10. package/dist/embedded-postgres-recovery.d.ts +11 -0
  11. package/dist/embedded-postgres-recovery.d.ts.map +1 -1
  12. package/dist/embedded-postgres-recovery.js +50 -11
  13. package/dist/embedded-postgres-recovery.js.map +1 -1
  14. package/dist/embedded-postgres-recovery.test.js +50 -2
  15. package/dist/embedded-postgres-recovery.test.js.map +1 -1
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/local-postgres-provider.d.ts +3 -0
  21. package/dist/local-postgres-provider.d.ts.map +1 -1
  22. package/dist/local-postgres-provider.js +92 -10
  23. package/dist/local-postgres-provider.js.map +1 -1
  24. package/dist/local-postgres-provider.test.js +24 -1
  25. package/dist/local-postgres-provider.test.js.map +1 -1
  26. package/dist/migration-manifest.d.ts +41 -0
  27. package/dist/migration-manifest.d.ts.map +1 -0
  28. package/dist/migration-manifest.js +186 -0
  29. package/dist/migration-manifest.js.map +1 -0
  30. package/dist/migration-manifest.test.d.ts +2 -0
  31. package/dist/migration-manifest.test.d.ts.map +1 -0
  32. package/dist/migration-manifest.test.js +88 -0
  33. package/dist/migration-manifest.test.js.map +1 -0
  34. package/dist/migration-runtime.d.ts.map +1 -1
  35. package/dist/migration-runtime.js +18 -33
  36. package/dist/migration-runtime.js.map +1 -1
  37. package/dist/migrations/0143_goal_contract.sql +97 -0
  38. package/dist/migrations/0144_eminent_umar.sql +16 -0
  39. package/dist/migrations/0145_public_nehzno.sql +96 -0
  40. package/dist/migrations/0146_medical_roulette.sql +1 -0
  41. package/dist/migrations/0147_github_mcp_provider.sql +4 -0
  42. package/dist/migrations/0148_chat_message_transcript_entries.sql +20 -0
  43. package/dist/migrations/chat-transcript-storage.test.ts +211 -0
  44. package/dist/migrations/github-mcp-provider.test.ts +20 -0
  45. package/dist/migrations/meta/0143_snapshot.json +25318 -0
  46. package/dist/migrations/meta/0144_snapshot.json +25336 -0
  47. package/dist/migrations/meta/0145_snapshot.json +27010 -0
  48. package/dist/migrations/meta/0146_snapshot.json +27016 -0
  49. package/dist/migrations/meta/0147_snapshot.json +27016 -0
  50. package/dist/migrations/meta/_journal.json +42 -0
  51. package/dist/schema/chat_message_transcript_entries.d.ts +82 -0
  52. package/dist/schema/chat_message_transcript_entries.d.ts.map +1 -0
  53. package/dist/schema/chat_message_transcript_entries.js +20 -0
  54. package/dist/schema/chat_message_transcript_entries.js.map +1 -0
  55. package/dist/schema/goals.d.ts +2110 -0
  56. package/dist/schema/goals.d.ts.map +1 -1
  57. package/dist/schema/goals.js +169 -1
  58. package/dist/schema/goals.js.map +1 -1
  59. package/dist/schema/index.d.ts +2 -1
  60. package/dist/schema/index.d.ts.map +1 -1
  61. package/dist/schema/index.js +2 -1
  62. package/dist/schema/index.js.map +1 -1
  63. package/dist/schema/mcp_connections.js +2 -2
  64. package/dist/schema/mcp_connections.js.map +1 -1
  65. package/package.json +2 -2
@@ -0,0 +1,97 @@
1
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "outcome_statement" text;
2
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "objective_mode" text DEFAULT 'target' NOT NULL;
3
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "lifecycle" text DEFAULT 'draft' NOT NULL;
4
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "contract_revision" integer DEFAULT 1 NOT NULL;
5
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "criteria" jsonb DEFAULT '[]'::jsonb NOT NULL;
6
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "autonomy_envelope" jsonb DEFAULT '{}'::jsonb NOT NULL;
7
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "human_authorities" jsonb DEFAULT '{}'::jsonb NOT NULL;
8
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "evaluation_policy" jsonb DEFAULT '{}'::jsonb NOT NULL;
9
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "action_deadline" timestamp with time zone;
10
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "evaluation_deadline" timestamp with time zone;
11
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "evaluation_result" jsonb;
12
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "close_reason" text;
13
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "result_payload" jsonb;
14
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "focus" boolean DEFAULT false NOT NULL;
15
+ ALTER TABLE "goals" ADD COLUMN IF NOT EXISTS "plan_revision" integer DEFAULT 0 NOT NULL;
16
+
17
+ UPDATE "goals"
18
+ SET
19
+ "lifecycle" = CASE
20
+ -- Legacy active rows have no canonical Contract/Owner/Plan proof. They
21
+ -- must be explicitly activated after migration instead of bypassing Draft.
22
+ WHEN "status" = 'active' THEN 'draft'
23
+ WHEN "status" IN ('achieved', 'cancelled') THEN 'closed'
24
+ ELSE 'draft'
25
+ END,
26
+ "close_reason" = CASE
27
+ WHEN "status" = 'achieved' THEN 'evaluated'
28
+ WHEN "status" = 'cancelled' THEN 'cancelled'
29
+ ELSE "close_reason"
30
+ END;
31
+
32
+ CREATE UNIQUE INDEX IF NOT EXISTS "goals_org_focus_uq"
33
+ ON "goals" USING btree ("org_id")
34
+ WHERE "focus" = true;
35
+
36
+ CREATE TABLE IF NOT EXISTS "goal_owner_assignments" (
37
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
38
+ "org_id" uuid NOT NULL REFERENCES "organizations"("id") ON DELETE cascade,
39
+ "goal_id" uuid NOT NULL REFERENCES "goals"("id") ON DELETE cascade,
40
+ "agent_id" uuid NOT NULL REFERENCES "agents"("id"),
41
+ "assignment_revision" integer DEFAULT 1 NOT NULL,
42
+ "assigned_by_authority_ref" text,
43
+ "starts_at" timestamp with time zone DEFAULT now() NOT NULL,
44
+ "ends_at" timestamp with time zone,
45
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
46
+ );
47
+ CREATE INDEX IF NOT EXISTS "goal_owner_assignments_goal_idx"
48
+ ON "goal_owner_assignments" USING btree ("goal_id", "starts_at");
49
+ CREATE UNIQUE INDEX IF NOT EXISTS "goal_owner_assignments_current_goal_uq"
50
+ ON "goal_owner_assignments" USING btree ("goal_id")
51
+ WHERE "ends_at" IS NULL;
52
+
53
+ CREATE TABLE IF NOT EXISTS "goal_plans" (
54
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
55
+ "org_id" uuid NOT NULL REFERENCES "organizations"("id") ON DELETE cascade,
56
+ "goal_id" uuid NOT NULL REFERENCES "goals"("id") ON DELETE cascade,
57
+ "revision" integer NOT NULL,
58
+ "summary" text NOT NULL,
59
+ "hypotheses" jsonb DEFAULT '[]'::jsonb NOT NULL,
60
+ "selected_paths" jsonb DEFAULT '[]'::jsonb NOT NULL,
61
+ "rejected_paths" jsonb DEFAULT '[]'::jsonb NOT NULL,
62
+ "sequencing" jsonb DEFAULT '[]'::jsonb NOT NULL,
63
+ "budget_allocations" jsonb DEFAULT '{}'::jsonb NOT NULL,
64
+ "invalidation_conditions" jsonb DEFAULT '[]'::jsonb NOT NULL,
65
+ "created_by_agent_id" uuid REFERENCES "agents"("id") ON DELETE set null,
66
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
67
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
68
+ );
69
+ CREATE UNIQUE INDEX IF NOT EXISTS "goal_plans_goal_revision_uq"
70
+ ON "goal_plans" USING btree ("goal_id", "revision");
71
+ CREATE INDEX IF NOT EXISTS "goal_plans_goal_revision_idx"
72
+ ON "goal_plans" USING btree ("goal_id", "revision");
73
+
74
+ CREATE TABLE IF NOT EXISTS "goal_activities" (
75
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
76
+ "org_id" uuid NOT NULL REFERENCES "organizations"("id") ON DELETE cascade,
77
+ "goal_id" uuid NOT NULL REFERENCES "goals"("id") ON DELETE cascade,
78
+ "contract_revision" integer NOT NULL,
79
+ "submitted_by_agent_id" uuid REFERENCES "agents"("id") ON DELETE set null,
80
+ "agent_owner_ref_at_time" uuid REFERENCES "agents"("id") ON DELETE set null,
81
+ "commitment_ref" text,
82
+ "run_ref" uuid,
83
+ "activity_kind" text,
84
+ "summary" text NOT NULL,
85
+ "evidence_refs" jsonb DEFAULT '[]'::jsonb NOT NULL,
86
+ "idempotency_key" text,
87
+ "occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
88
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
89
+ );
90
+ CREATE INDEX IF NOT EXISTS "goal_activities_goal_occurred_idx"
91
+ ON "goal_activities" USING btree ("goal_id", "occurred_at");
92
+ CREATE UNIQUE INDEX IF NOT EXISTS "goal_activities_closeout_run_uq"
93
+ ON "goal_activities" USING btree ("goal_id", "run_ref", "activity_kind")
94
+ WHERE "activity_kind" = 'closeout' AND "run_ref" IS NOT NULL;
95
+ CREATE UNIQUE INDEX IF NOT EXISTS "goal_activities_idempotency_uq"
96
+ ON "goal_activities" USING btree ("goal_id", "idempotency_key")
97
+ WHERE "idempotency_key" IS NOT NULL;
@@ -0,0 +1,16 @@
1
+ ALTER TABLE "goals" ADD COLUMN "continuation_kind" text;--> statement-breakpoint
2
+ ALTER TABLE "goals" ADD COLUMN "continuation_summary" text;--> statement-breakpoint
3
+ ALTER TABLE "goals" ADD COLUMN "wake_condition" text;--> statement-breakpoint
4
+
5
+ UPDATE "goals"
6
+ SET
7
+ "continuation_kind" = COALESCE("continuation_kind", 'verification'),
8
+ "continuation_summary" = COALESCE(
9
+ "continuation_summary",
10
+ 'Review the legacy Goal contract and define the next bounded verification'
11
+ )
12
+ WHERE "status" = 'active'
13
+ AND "lifecycle" = 'draft'
14
+ AND "outcome_statement" IS NOT NULL
15
+ AND jsonb_array_length("criteria") > 0
16
+ AND "owner_agent_id" IS NOT NULL;--> statement-breakpoint
@@ -0,0 +1,96 @@
1
+ CREATE TABLE "goal_change_proposals" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "org_id" uuid NOT NULL,
4
+ "goal_id" uuid NOT NULL,
5
+ "expected_contract_revision" integer NOT NULL,
6
+ "before_contract" jsonb NOT NULL,
7
+ "after_contract" jsonb NOT NULL,
8
+ "rationale" text NOT NULL,
9
+ "evidence_refs" jsonb DEFAULT '[]'::jsonb NOT NULL,
10
+ "approval_id" uuid NOT NULL,
11
+ "status" text DEFAULT 'pending' NOT NULL,
12
+ "idempotency_key" text NOT NULL,
13
+ "proposed_by_agent_id" uuid NOT NULL,
14
+ "applied_revision" integer,
15
+ "applied_at" timestamp with time zone,
16
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
17
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
18
+ );
19
+ --> statement-breakpoint
20
+ CREATE TABLE "goal_feedback_entries" (
21
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
22
+ "org_id" uuid NOT NULL,
23
+ "goal_id" uuid NOT NULL,
24
+ "actor_type" text NOT NULL,
25
+ "actor_id" text NOT NULL,
26
+ "body" text NOT NULL,
27
+ "attachments" jsonb DEFAULT '[]'::jsonb NOT NULL,
28
+ "content_hash" text NOT NULL,
29
+ "feedback_kind" text DEFAULT 'ordinary' NOT NULL,
30
+ "idempotency_key" text NOT NULL,
31
+ "routed_wakeup_request_id" uuid,
32
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
33
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
34
+ );
35
+ --> statement-breakpoint
36
+ CREATE TABLE "goal_result_proposals" (
37
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
38
+ "org_id" uuid NOT NULL,
39
+ "goal_id" uuid NOT NULL,
40
+ "contract_revision" integer NOT NULL,
41
+ "candidate" jsonb NOT NULL,
42
+ "candidate_hash" text NOT NULL,
43
+ "preflight" jsonb NOT NULL,
44
+ "risk_summary" text NOT NULL,
45
+ "status" text DEFAULT 'ready' NOT NULL,
46
+ "idempotency_key" text NOT NULL,
47
+ "proposed_by_agent_id" uuid NOT NULL,
48
+ "accepted_by_actor_type" text,
49
+ "accepted_by_actor_id" text,
50
+ "accepted_at" timestamp with time zone,
51
+ "rejected_by_actor_type" text,
52
+ "rejected_by_actor_id" text,
53
+ "rejected_at" timestamp with time zone,
54
+ "rejection_feedback" text,
55
+ "consumed_at" timestamp with time zone,
56
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
57
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
58
+ );
59
+ --> statement-breakpoint
60
+ CREATE TABLE "goal_start_requests" (
61
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
62
+ "org_id" uuid NOT NULL,
63
+ "request_key" text NOT NULL,
64
+ "packet_hash" text NOT NULL,
65
+ "packet" jsonb NOT NULL,
66
+ "draft_goal_id" uuid,
67
+ "goal_id" uuid,
68
+ "status" text DEFAULT 'pending' NOT NULL,
69
+ "completed_at" timestamp with time zone,
70
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
71
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
72
+ );
73
+ --> statement-breakpoint
74
+ ALTER TABLE "goals" ADD COLUMN "alignment_question" text;--> statement-breakpoint
75
+ ALTER TABLE "goal_change_proposals" ADD CONSTRAINT "goal_change_proposals_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
76
+ ALTER TABLE "goal_change_proposals" ADD CONSTRAINT "goal_change_proposals_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
77
+ ALTER TABLE "goal_change_proposals" ADD CONSTRAINT "goal_change_proposals_approval_id_approvals_id_fk" FOREIGN KEY ("approval_id") REFERENCES "public"."approvals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
78
+ ALTER TABLE "goal_change_proposals" ADD CONSTRAINT "goal_change_proposals_proposed_by_agent_id_agents_id_fk" FOREIGN KEY ("proposed_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
79
+ ALTER TABLE "goal_feedback_entries" ADD CONSTRAINT "goal_feedback_entries_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
80
+ ALTER TABLE "goal_feedback_entries" ADD CONSTRAINT "goal_feedback_entries_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
81
+ ALTER TABLE "goal_feedback_entries" ADD CONSTRAINT "goal_feedback_entries_routed_wakeup_request_id_agent_wakeup_requests_id_fk" FOREIGN KEY ("routed_wakeup_request_id") REFERENCES "public"."agent_wakeup_requests"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
82
+ ALTER TABLE "goal_result_proposals" ADD CONSTRAINT "goal_result_proposals_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
83
+ ALTER TABLE "goal_result_proposals" ADD CONSTRAINT "goal_result_proposals_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
84
+ ALTER TABLE "goal_result_proposals" ADD CONSTRAINT "goal_result_proposals_proposed_by_agent_id_agents_id_fk" FOREIGN KEY ("proposed_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
85
+ ALTER TABLE "goal_start_requests" ADD CONSTRAINT "goal_start_requests_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
86
+ ALTER TABLE "goal_start_requests" ADD CONSTRAINT "goal_start_requests_draft_goal_id_goals_id_fk" FOREIGN KEY ("draft_goal_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
87
+ ALTER TABLE "goal_start_requests" ADD CONSTRAINT "goal_start_requests_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
88
+ CREATE INDEX "goal_change_proposals_goal_status_created_idx" ON "goal_change_proposals" USING btree ("goal_id","status","created_at");--> statement-breakpoint
89
+ CREATE UNIQUE INDEX "goal_change_proposals_goal_idempotency_uq" ON "goal_change_proposals" USING btree ("goal_id","idempotency_key");--> statement-breakpoint
90
+ CREATE UNIQUE INDEX "goal_change_proposals_approval_uq" ON "goal_change_proposals" USING btree ("approval_id");--> statement-breakpoint
91
+ CREATE INDEX "goal_feedback_entries_goal_created_idx" ON "goal_feedback_entries" USING btree ("goal_id","created_at");--> statement-breakpoint
92
+ CREATE UNIQUE INDEX "goal_feedback_entries_goal_idempotency_uq" ON "goal_feedback_entries" USING btree ("goal_id","idempotency_key");--> statement-breakpoint
93
+ CREATE INDEX "goal_result_proposals_goal_status_created_idx" ON "goal_result_proposals" USING btree ("goal_id","status","created_at");--> statement-breakpoint
94
+ CREATE UNIQUE INDEX "goal_result_proposals_goal_idempotency_uq" ON "goal_result_proposals" USING btree ("goal_id","idempotency_key");--> statement-breakpoint
95
+ CREATE UNIQUE INDEX "goal_start_requests_org_request_key_uq" ON "goal_start_requests" USING btree ("org_id","request_key");--> statement-breakpoint
96
+ CREATE INDEX "goal_start_requests_org_status_updated_idx" ON "goal_start_requests" USING btree ("org_id","status","updated_at");
@@ -0,0 +1 @@
1
+ ALTER TABLE "goal_result_proposals" ADD COLUMN "acceptance_idempotency_key" text;
@@ -0,0 +1,4 @@
1
+ DROP INDEX "mcp_connections_org_official_canonical_uq";--> statement-breakpoint
2
+ DROP INDEX "mcp_connections_agent_official_canonical_uq";--> statement-breakpoint
3
+ CREATE UNIQUE INDEX "mcp_connections_org_official_canonical_uq" ON "mcp_connections" USING btree ("org_id","provider") WHERE "mcp_connections"."provider" in ('supabase', 'linear', 'notion', 'github') and "mcp_connections"."canonical_state" = 'canonical' and "mcp_connections"."scope" = 'organization';--> statement-breakpoint
4
+ CREATE UNIQUE INDEX "mcp_connections_agent_official_canonical_uq" ON "mcp_connections" USING btree ("org_id","owner_agent_id","provider") WHERE "mcp_connections"."provider" in ('supabase', 'linear', 'notion', 'github') and "mcp_connections"."canonical_state" = 'canonical' and "mcp_connections"."scope" = 'agent';
@@ -0,0 +1,20 @@
1
+ CREATE TABLE "chat_message_transcript_entries" (
2
+ "org_id" uuid NOT NULL,
3
+ "message_id" uuid NOT NULL,
4
+ "entry_seq" integer NOT NULL,
5
+ "payload" jsonb NOT NULL,
6
+ CONSTRAINT "chat_message_transcript_entries_message_seq_pk" PRIMARY KEY("message_id","entry_seq")
7
+ );
8
+ --> statement-breakpoint
9
+ ALTER TABLE "chat_message_transcript_entries" ADD CONSTRAINT "chat_message_transcript_entries_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;
10
+ --> statement-breakpoint
11
+ ALTER TABLE "chat_message_transcript_entries" ADD CONSTRAINT "chat_message_transcript_entries_message_id_chat_messages_id_fk" FOREIGN KEY ("message_id") REFERENCES "public"."chat_messages"("id") ON DELETE cascade ON UPDATE no action;
12
+ --> statement-breakpoint
13
+ CREATE INDEX "chat_message_transcript_entries_org_message_seq_idx" ON "chat_message_transcript_entries" USING btree ("org_id","message_id","entry_seq");
14
+ --> statement-breakpoint
15
+ ALTER TABLE "chat_messages" SET (
16
+ autovacuum_vacuum_scale_factor = 0.05,
17
+ autovacuum_vacuum_threshold = 500,
18
+ autovacuum_analyze_threshold = 500,
19
+ autovacuum_analyze_scale_factor = 0.02
20
+ );
@@ -0,0 +1,211 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import net from "node:net";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import postgres from "postgres";
9
+ import { describe, expect, it } from "vitest";
10
+ import { applyPendingMigrations, ensurePostgresDatabase } from "../client.js";
11
+ import { createLocalPostgresInstance } from "../local-postgres-provider.js";
12
+
13
+ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
14
+
15
+ type BackfillReport = {
16
+ legacyCandidates: number;
17
+ legacyEligible: number;
18
+ legacyMigrated: number;
19
+ legacySkipped: unknown[];
20
+ forksChecked: number;
21
+ forksEligible: number;
22
+ forksRepaired: number;
23
+ };
24
+
25
+ type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
26
+
27
+ async function availablePort(): Promise<number> {
28
+ return await new Promise((resolve, reject) => {
29
+ const server = net.createServer();
30
+ server.unref();
31
+ server.once("error", reject);
32
+ server.listen(0, "127.0.0.1", () => {
33
+ const address = server.address();
34
+ if (!address || typeof address === "string") return reject(new Error("port allocation failed"));
35
+ server.close((error) => error ? reject(error) : resolve(address.port));
36
+ });
37
+ });
38
+ }
39
+
40
+ async function runBackfill(connectionString: string, args: string[]) {
41
+ return await new Promise<{ code: number | null; report: BackfillReport }>((resolve, reject) => {
42
+ const child = spawn(process.execPath, [
43
+ path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"),
44
+ "scripts/backfill-chat-transcripts.ts",
45
+ ...args,
46
+ ], {
47
+ cwd: repositoryRoot,
48
+ env: { ...process.env, DATABASE_URL: connectionString },
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ });
51
+ let stdout = "";
52
+ let stderr = "";
53
+ child.stdout.on("data", (chunk) => { stdout += String(chunk); });
54
+ child.stderr.on("data", (chunk) => { stderr += String(chunk); });
55
+ const timeout = setTimeout(() => child.kill("SIGTERM"), 30_000);
56
+ child.once("error", reject);
57
+ child.once("close", (code) => {
58
+ clearTimeout(timeout);
59
+ if (!stdout.trim()) return reject(new Error(stderr || "backfill produced no report"));
60
+ try {
61
+ resolve({ code, report: (JSON.parse(stdout) as { report: BackfillReport }).report });
62
+ } catch {
63
+ reject(new Error(`${stderr}\n${stdout}`));
64
+ }
65
+ });
66
+ });
67
+ }
68
+
69
+ describe("chat transcript storage migration", () => {
70
+ it("backfills safely, prefers generation events, and is repeatable", async () => {
71
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "rudder-chat-transcript-storage-"));
72
+ const port = await availablePort();
73
+ const { instance } = await createLocalPostgresInstance({
74
+ databaseDir: path.join(tempRoot, "postgres"),
75
+ user: "rudder",
76
+ password: "rudder",
77
+ port,
78
+ persistent: true,
79
+ initdbFlags: ["--encoding=UTF8", "--locale=C"],
80
+ onLog: () => {},
81
+ onError: () => {},
82
+ });
83
+ let sql: postgres.Sql | null = null;
84
+ try {
85
+ await instance.initialise();
86
+ await instance.start();
87
+ const adminUrl = `postgres://rudder:rudder@127.0.0.1:${port}/postgres`;
88
+ await ensurePostgresDatabase(adminUrl, "rudder");
89
+ const connectionString = `postgres://rudder:rudder@127.0.0.1:${port}/rudder`;
90
+ await applyPendingMigrations(connectionString);
91
+ sql = postgres(connectionString, { max: 1, onnotice: () => {} });
92
+ const db = sql;
93
+
94
+ const orgId = randomUUID();
95
+ const conversationId = randomUUID();
96
+ await db`
97
+ INSERT INTO organizations (id, url_key, name, issue_prefix)
98
+ VALUES (${orgId}, ${`transcript-${orgId}`}, 'Transcript migration', ${`T${orgId.replaceAll("-", "").slice(0, 8)}`})
99
+ `;
100
+ await db`
101
+ INSERT INTO chat_conversations (id, org_id, title)
102
+ VALUES (${conversationId}, ${orgId}, 'Transcript migration')
103
+ `;
104
+ const makeMessage = async (
105
+ targetConversationId: string,
106
+ role: string,
107
+ body: string,
108
+ payload: JsonValue,
109
+ createdAt?: Date,
110
+ ) => {
111
+ const id = randomUUID();
112
+ await db`
113
+ INSERT INTO chat_messages (id, org_id, conversation_id, role, kind, status, body, structured_payload)
114
+ VALUES (${id}, ${orgId}, ${targetConversationId}, ${role}, 'message', 'completed', ${body}, ${db.json(payload)})
115
+ `;
116
+ if (createdAt) {
117
+ await db`UPDATE chat_messages SET created_at = ${createdAt}, updated_at = ${createdAt} WHERE id = ${id}`;
118
+ }
119
+ return id;
120
+ };
121
+ await makeMessage(conversationId, "user", "user", { __chatTranscript: [{ kind: "stdout", ts: "2026-08-08T00:00:01.000Z", text: "one" }] });
122
+ await makeMessage(conversationId, "user", "user", { __chatTranscript: [{ kind: "stdout", ts: "2026-08-08T00:00:02.000Z", text: "two" }] });
123
+ await makeMessage(conversationId, "user", "user", { __chatTranscript: "malformed" });
124
+ const assistantMessageId = await makeMessage(conversationId, "assistant", "assistant", {
125
+ __chatTranscript: [{ kind: "thinking", ts: "2026-08-08T00:00:03.000Z", text: "legacy" }],
126
+ });
127
+ const generationId = randomUUID();
128
+ const generationAt = new Date("2026-08-08T00:00:00.000Z");
129
+ await db`
130
+ INSERT INTO chat_generations (id, org_id, conversation_id, status, started_at, created_at, updated_at)
131
+ VALUES (${generationId}, ${orgId}, ${conversationId}, 'completed', ${generationAt}, ${generationAt}, ${generationAt})
132
+ `;
133
+ await db`
134
+ INSERT INTO chat_generation_events (org_id, generation_id, generation_seq, attempt_epoch, event_kind, payload, assistant_message_id)
135
+ VALUES (${orgId}, ${generationId}, 1, 1, 'transcript', ${db.json({ entry: { kind: "thinking", ts: "2026-08-08T00:00:04.000Z", text: "ledger" } })}, ${assistantMessageId})
136
+ `;
137
+
138
+ const forkSourceConversationId = randomUUID();
139
+ const forkTargetConversationId = randomUUID();
140
+ const forkUserCreatedAt = new Date("2026-08-08T00:01:00.000Z");
141
+ const forkAssistantCreatedAt = new Date("2026-08-08T00:01:01.000Z");
142
+ const forkBoundaryCreatedAt = new Date("2026-08-08T00:01:02.000Z");
143
+ await db`
144
+ INSERT INTO chat_conversations (id, org_id, title)
145
+ VALUES (${forkSourceConversationId}, ${orgId}, 'Old fork source'), (${forkTargetConversationId}, ${orgId}, 'Old fork target')
146
+ `;
147
+ await makeMessage(
148
+ forkSourceConversationId,
149
+ "user",
150
+ "fork user",
151
+ { __chatTranscript: [{ kind: "stdout", ts: "2026-08-08T00:01:00.100Z", text: "fork user transcript" }] },
152
+ forkUserCreatedAt,
153
+ );
154
+ const forkSourceAssistantId = await makeMessage(
155
+ forkSourceConversationId,
156
+ "assistant",
157
+ "fork assistant",
158
+ { __chatTranscript: [{ kind: "thinking", ts: "2026-08-08T00:01:01.100Z", text: "fork assistant transcript" }] },
159
+ forkAssistantCreatedAt,
160
+ );
161
+ await db`
162
+ UPDATE chat_conversations
163
+ SET forked_from_conversation_id = ${forkSourceConversationId},
164
+ forked_from_message_id = ${forkSourceAssistantId}
165
+ WHERE id = ${forkTargetConversationId}
166
+ `;
167
+ await makeMessage(forkTargetConversationId, "user", "fork user", null, forkUserCreatedAt);
168
+ await makeMessage(forkTargetConversationId, "assistant", "fork assistant", null, forkAssistantCreatedAt);
169
+ await db`
170
+ INSERT INTO chat_messages (id, org_id, conversation_id, role, kind, status, body, structured_payload, created_at, updated_at)
171
+ VALUES (${randomUUID()}, ${orgId}, ${forkTargetConversationId}, 'system', 'system_event', 'completed', 'fork event', ${db.json({ eventType: "chat_fork" })}, ${forkBoundaryCreatedAt}, ${forkBoundaryCreatedAt})
172
+ `;
173
+
174
+ const dryRun = await runBackfill(connectionString, ["--dry-run", "--batch-size", "1"]);
175
+ expect(dryRun.code).toBe(2);
176
+ expect(dryRun.report).toMatchObject({ legacyCandidates: 6, legacyEligible: 5, legacyMigrated: 0, forksChecked: 1, forksEligible: 2 });
177
+ expect(dryRun.report.legacySkipped).toHaveLength(1);
178
+
179
+ const firstRun = await runBackfill(connectionString, ["--batch-size", "1"]);
180
+ expect(firstRun.code).toBe(2);
181
+ expect(firstRun.report).toMatchObject({ legacyCandidates: 6, legacyEligible: 5, legacyMigrated: 5, forksChecked: 1, forksRepaired: 1 });
182
+
183
+ const secondRun = await runBackfill(connectionString, ["--batch-size", "1"]);
184
+ expect(secondRun.code).toBe(2);
185
+ expect(secondRun.report).toMatchObject({ legacyCandidates: 1, legacyMigrated: 0, forksChecked: 1, forksRepaired: 0 });
186
+
187
+ const remaining = await db<{ count: number }[]>`SELECT count(*)::int AS count FROM chat_messages WHERE structured_payload ? '__chatTranscript'`;
188
+ const entries = await db<{ count: number }[]>`SELECT count(*)::int AS count FROM chat_message_transcript_entries`;
189
+ const assistantEntries = await db<{ count: number }[]>`
190
+ SELECT count(*)::int AS count FROM chat_message_transcript_entries WHERE message_id = ${assistantMessageId}
191
+ `;
192
+ expect(Number(remaining[0]?.count)).toBe(1);
193
+ expect(Number(entries[0]?.count)).toBe(6);
194
+ expect(Number(assistantEntries[0]?.count)).toBe(0);
195
+ const forkEntries = await db<{ text: string }[]>`
196
+ SELECT payload->>'text' AS text
197
+ FROM chat_message_transcript_entries
198
+ WHERE message_id = (
199
+ SELECT id FROM chat_messages
200
+ WHERE conversation_id = ${forkTargetConversationId} AND role = 'assistant'
201
+ )
202
+ ORDER BY entry_seq
203
+ `;
204
+ expect(forkEntries).toEqual([{ text: "fork assistant transcript" }]);
205
+ } finally {
206
+ await sql?.end();
207
+ await instance.stop().catch(() => undefined);
208
+ fs.rmSync(tempRoot, { recursive: true, force: true });
209
+ }
210
+ }, 60_000);
211
+ });
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const migrationSql = fs.readFileSync(
6
+ path.join(path.dirname(new URL(import.meta.url).pathname), "0147_github_mcp_provider.sql"),
7
+ "utf8",
8
+ );
9
+
10
+ describe("GitHub MCP provider migration", () => {
11
+ it("adds GitHub to organization and agent canonical uniqueness without widening custom MCP", () => {
12
+ expect(migrationSql).toMatch(
13
+ /create unique index\s+"mcp_connections_org_official_canonical_uq"[\s\S]*"provider"\s+in\s*\('supabase',\s*'linear',\s*'notion',\s*'github'\)[\s\S]*"scope"\s*=\s*'organization'/i,
14
+ );
15
+ expect(migrationSql).toMatch(
16
+ /create unique index\s+"mcp_connections_agent_official_canonical_uq"[\s\S]*"provider"\s+in\s*\('supabase',\s*'linear',\s*'notion',\s*'github'\)[\s\S]*"scope"\s*=\s*'agent'/i,
17
+ );
18
+ expect(migrationSql).not.toMatch(/provider"\s*=\s*'custom'/i);
19
+ });
20
+ });