diff --git a/apps/api/src/api/activity/activity-retry.test.ts b/apps/api/src/api/activity/activity-retry.test.ts new file mode 100644 index 00000000..a33f687f --- /dev/null +++ b/apps/api/src/api/activity/activity-retry.test.ts @@ -0,0 +1,251 @@ +import { afterAll, describe, expect, it, mock, spyOn } from "bun:test"; +import { resolve } from "node:path"; +import type { ActivityLogEntryJson } from "@proval/db"; + +// Keep the module mocks below isolated from the other API tests +if (process.env.PROVAL_ACTIVITY_RETRY_TEST_CHILD !== "1") { + it("passes activity retry integration in an isolated process", () => { + const result = Bun.spawnSync([process.execPath, "test", import.meta.path], { + env: { ...process.env, PROVAL_ACTIVITY_RETRY_TEST_CHILD: "1", DB_FILE_NAME: ":memory:" }, + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) { + throw new Error(result.stdout.toString() + result.stderr.toString()); + } + expect(result.exitCode).toBe(0); + }, 30000); +} else { + process.env.DB_FILE_NAME = ":memory:"; + process.env.ENCRYPTION_KEY = Buffer.alloc(32, 13).toString("base64"); + + const usage = { inputToken: 1, cachedInputToken: 0, outputToken: 1 }; + const runPullRequestReviewMock = mock(async () => usage); + const runPullRequestReplyMock = mock(async () => usage); + const runIssueReplyOnOpenMock = mock(async () => usage); + const runIssueReplyMock = mock(async () => usage); + + mock.module("../../agent/pull-request/index.js", () => ({ + runPullRequestReview: runPullRequestReviewMock, + runPullRequestReply: runPullRequestReplyMock, + })); + mock.module("../../agent/issue/index.js", () => ({ + runIssueReplyOnOpen: runIssueReplyOnOpenMock, + runIssueReply: runIssueReplyMock, + })); + + const { and, desc, eq, gt } = await import("drizzle-orm"); + const { migrate } = await import("drizzle-orm/bun-sqlite/migrator"); + const { default: db } = await import("../../db/index.js"); + const { activityTable, gitProviderAccessTable, modelProviderTable, repositoryTable } = await import("@proval/db"); + const { encrypt } = await import("../../util/encrypt.js"); + const { ActivityService, recoverLegacyReplyTargetComment } = await import("./activity.service.js"); + + migrate(db, { migrationsFolder: resolve(import.meta.dir, "../../../../../packages/db/src/migration") }); + + const consoleError = spyOn(console, "error").mockImplementation(() => {}); + afterAll(() => { + consoleError.mockRestore(); + }); + + const [modelProvider] = await db + .insert(modelProviderTable) + .values({ + provider: "openai", + label: "Test", + baseUrl: "http://localhost:1234/v1", + apiKey: encrypt("test key"), + }) + .returning({ id: modelProviderTable.id }); + const [access] = await db + .insert(gitProviderAccessTable) + .values({ + provider: "gitlab", + name: "Test", + baseUrl: "https://gitlab.example.com", + accessToken: encrypt("glpat-test"), + }) + .returning({ id: gitProviderAccessTable.id }); + const [repository] = await db + .insert(repositoryTable) + .values({ + path: "group/repo", + provider: "gitlab", + gitProviderAccessId: access.id, + gitProviderRepositoryId: 5, + accessToken: encrypt("glpat-repository"), + modelProviderId: modelProvider.id, + modelName: "test-model", + }) + .returning({ id: repositoryTable.id }); + + const service = new ActivityService(); + + async function insertFailedActivity(values: { + type: "pr_review" | "pr_reply" | "issue_open" | "issue_reply"; + targetIid: number; + targetCommentId?: number | null; + targetInlineReviewId?: string | null; + logList?: ActivityLogEntryJson[]; + }): Promise { + const [activity] = await db + .insert(activityTable) + .values({ + repositoryId: repository.id, + repositoryPath: "group/repo", + provider: "gitlab", + modelProviderId: modelProvider.id, + modelName: "test-model", + type: values.type, + status: "failed", + targetIid: values.targetIid, + targetCommentId: values.targetCommentId ?? null, + targetInlineReviewId: values.targetInlineReviewId ?? null, + logVersion: "1", + logs: values.logList ?? [], + errorMessage: "boom", + }) + .returning({ id: activityTable.id }); + return activity.id; + } + + function toolCallEntry(toolName: string, message: string): ActivityLogEntryJson { + return { + type: "tool-call", + level: "info", + label: "[PR #21] Reply", + toolName, + toolCallId: "call-1", + message, + timestamp: new Date(0).toISOString(), + }; + } + + async function waitForRetriedActivity(previousId: number) { + const deadline = Date.now() + 3000; + while (Date.now() < deadline) { + const [activity] = await db + .select() + .from(activityTable) + .where(and(eq(activityTable.repositoryId, repository.id), gt(activityTable.id, previousId))) + .orderBy(desc(activityTable.id)) + .limit(1); + if (activity?.status === "completed") { + return activity; + } + await Bun.sleep(10); + } + throw new Error("Timed out waiting for the retried activity"); + } + + describe("activity retry", () => { + it("replies to the same pull request comment", async () => { + const failedId = await insertFailedActivity({ + type: "pr_reply", + targetIid: 7, + targetCommentId: 4242, + targetInlineReviewId: "discussion-1", + }); + + await service.retry(failedId); + + const retried = await waitForRetriedActivity(failedId); + expect(runPullRequestReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ prIid: 7, commentId: 4242, inlineReviewId: "discussion-1" }), + ); + expect(retried.targetCommentId).toBe(4242); + expect(retried.targetInlineReviewId).toBe("discussion-1"); + }); + + it("replies to the same issue comment", async () => { + const failedId = await insertFailedActivity({ + type: "issue_reply", + targetIid: 11, + targetCommentId: 5151, + }); + + await service.retry(failedId); + + const retried = await waitForRetriedActivity(failedId); + expect(runIssueReplyMock).toHaveBeenCalledWith(expect.objectContaining({ issueIid: 11, commentId: 5151 })); + expect(retried.targetCommentId).toBe(5151); + }); + + it("still runs the issue open workflow", async () => { + const failedId = await insertFailedActivity({ type: "issue_open", targetIid: 13 }); + + await service.retry(failedId); + + const retried = await waitForRetriedActivity(failedId); + expect(runIssueReplyOnOpenMock).toHaveBeenCalledWith(expect.objectContaining({ issueIid: 13 })); + expect(retried.type).toBe("issue_open"); + }); + + it("refuses a reply without a stored comment", async () => { + const callCount = runPullRequestReplyMock.mock.calls.length; + const failedId = await insertFailedActivity({ type: "pr_reply", targetIid: 7 }); + + await expect(service.retry(failedId)).rejects.toThrow("This activity has no comment to reply to"); + expect(runPullRequestReplyMock.mock.calls.length).toBe(callCount); + }); + }); + + describe("legacy reply recovery", () => { + it("reads the replied comment back from the agent log", async () => { + const legacyId = await insertFailedActivity({ + type: "pr_reply", + targetIid: 21, + logList: [toolCallEntry("get_pull_request_comment", '{"commentId":6097072063}')], + }); + + expect(recoverLegacyReplyTargetComment()).toBe(1); + + const [recovered] = await db.select().from(activityTable).where(eq(activityTable.id, legacyId)); + expect(recovered.targetCommentId).toBe(6097072063); + + await service.retry(legacyId); + + const retried = await waitForRetriedActivity(legacyId); + expect(runPullRequestReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ prIid: 21, commentId: 6097072063 }), + ); + expect(retried.targetCommentId).toBe(6097072063); + }); + + it("leaves an inline review reply alone and ignores a truncated log", async () => { + const inlineId = await insertFailedActivity({ + type: "pr_reply", + targetIid: 22, + logList: [toolCallEntry("get_pull_request_inline_review_comment", '{"commentId":42}')], + }); + const truncatedId = await insertFailedActivity({ + type: "pr_reply", + targetIid: 23, + logList: [toolCallEntry("get_pull_request_comment", '{"commentId":43…')], + }); + + expect(recoverLegacyReplyTargetComment()).toBe(0); + + for (const id of [inlineId, truncatedId]) { + const [row] = await db.select().from(activityTable).where(eq(activityTable.id, id)); + expect(row.targetCommentId).toBeNull(); + } + }); + + it("skips an unreadable fetch and reads the later one", async () => { + const mixedId = await insertFailedActivity({ + type: "pr_reply", + targetIid: 24, + logList: [ + toolCallEntry("get_pull_request_comment", '{"commentId":43…'), + toolCallEntry("get_pull_request_comment", '{"commentId":77}'), + ], + }); + + expect(recoverLegacyReplyTargetComment()).toBe(1); + + const [row] = await db.select().from(activityTable).where(eq(activityTable.id, mixedId)); + expect(row.targetCommentId).toBe(77); + }); + }); +} diff --git a/apps/api/src/api/activity/activity.controller.ts b/apps/api/src/api/activity/activity.controller.ts index 1915f0fa..aadb67a4 100644 --- a/apps/api/src/api/activity/activity.controller.ts +++ b/apps/api/src/api/activity/activity.controller.ts @@ -55,9 +55,7 @@ export const getActivitySummary: Handler = async (c) => { activityService.findRecent(since, 5, repositoryId), activityService.getTokenSeries(since, bucket, new Date(), repositoryId), activityService.getTokenBreakdownByModel(since, 5, repositoryId), - repositoryId != null - ? Promise.resolve([]) - : activityService.getTokenBreakdownByRepository(since, 5), + repositoryId != null ? Promise.resolve([]) : activityService.getTokenBreakdownByRepository(since, 5), activityService.findInProgress(10), ]); return c.json({ range, stats, recent, tokenSeries, tokensByModel, tokensByRepository, inProgress }, 200); @@ -110,6 +108,7 @@ const RETRY_NOT_FOUND_ERRORS = new Set(["Activity not found", "Repository not fo const RETRY_CLIENT_ERRORS = new Set([ "Only failed or canceled activities can be retried", "This activity type cannot be retried", + "This activity has no comment to reply to", "Repository is no longer linked to this activity", "Model provider is no longer linked to this activity", ]); diff --git a/apps/api/src/api/activity/activity.service.ts b/apps/api/src/api/activity/activity.service.ts index cbe2b122..54ad145b 100644 --- a/apps/api/src/api/activity/activity.service.ts +++ b/apps/api/src/api/activity/activity.service.ts @@ -15,8 +15,8 @@ import type { import db from "../../db/index.js"; import { and, count, desc, eq, getTableColumns, gte, inArray, isNotNull, lt, sql, type SQL } from "drizzle-orm"; import { createSender } from "../../agent/llm/factory.js"; -import { runPullRequestReview } from "../../agent/pull-request/index.js"; -import { runIssueReplyOnOpen } from "../../agent/issue/index.js"; +import { runPullRequestReply, runPullRequestReview } from "../../agent/pull-request/index.js"; +import { runIssueReply, runIssueReplyOnOpen } from "../../agent/issue/index.js"; import { Workspace } from "../../git-provider/workspace.js"; import { decrypt } from "../../util/encrypt.js"; import { logError } from "../../util/log.js"; @@ -24,7 +24,7 @@ import { ModelProviderService } from "../model/model.service.js"; import { RepositoryService } from "../repository/repository.service.js"; import { runWithActivity } from "./activity.runner.js"; -const RETRY_TYPES = ["pr_review", "issue_open"] as const; +const RETRY_TYPES = ["pr_review", "pr_reply", "issue_open", "issue_reply"] as const; type RetryActivityType = (typeof RETRY_TYPES)[number]; const FINISHED_STATUSES = ["completed", "failed", "canceled"] as const; @@ -176,6 +176,8 @@ export type ActivityStartInput = { modelName: string; type: Activity["type"]; targetIid: number; + targetCommentId?: number | null; + targetInlineReviewId?: string | null; headSha?: string | null; }; @@ -243,6 +245,54 @@ const listOrderBy = [ desc(activityTable.id), ] as const; +function requireReplyTargetComment(activity: ActivityResponse): number { + if (activity.targetCommentId == null) { + throw new Error("This activity has no comment to reply to"); + } + return activity.targetCommentId; +} + +/** + * A reply created before the retry support does not carry the comment it answered. + * Recover it from the stored agent log so an old failed reply can still be replayed. + * Only the conversation fetch tools are read, an inline review needs a discussion id + * that the log does not keep. + */ +export function recoverLegacyReplyTargetComment(): number { + const result = db.$client.run(` + UPDATE activity + SET target_comment_id = ( + SELECT CASE + WHEN json_valid(json_extract(entry.value, '$.message')) + THEN json_extract(json_extract(entry.value, '$.message'), '$.commentId') + END + FROM json_each(activity.logs) AS entry + WHERE json_extract(entry.value, '$.type') = 'tool-call' + AND json_extract(entry.value, '$.toolName') IN ('get_pull_request_comment', 'get_issue_comment') + AND CASE + WHEN json_valid(json_extract(entry.value, '$.message')) + THEN json_extract(json_extract(entry.value, '$.message'), '$.commentId') + END IS NOT NULL + ORDER BY entry.key + LIMIT 1 + ) + WHERE type IN ('pr_reply', 'issue_reply') + AND target_comment_id IS NULL + AND json_valid(logs) + AND EXISTS ( + SELECT 1 + FROM json_each(activity.logs) AS entry + WHERE json_extract(entry.value, '$.type') = 'tool-call' + AND json_extract(entry.value, '$.toolName') IN ('get_pull_request_comment', 'get_issue_comment') + AND CASE + WHEN json_valid(json_extract(entry.value, '$.message')) + THEN json_extract(json_extract(entry.value, '$.message'), '$.commentId') + END IS NOT NULL + ) + `); + return result.changes; +} + export class ActivityService { public async findAll( input: { page: number; limit: number } & ActivityListFilter, @@ -604,6 +654,12 @@ export class ActivityService { if (!RETRY_TYPES.includes(activity.type as RetryActivityType)) { throw new Error("This activity type cannot be retried"); } + // A reply without its comment has nothing to replay. Resolve it here, before the + // provider setup below, which reaches the Git provider API for a GitHub repository. + const replyTargetCommentId = + activity.type === "pr_reply" || activity.type === "issue_reply" + ? requireReplyTargetComment(activity) + : null; if (activity.repositoryId == null) { throw new Error("Repository is no longer linked to this activity"); } @@ -631,6 +687,8 @@ export class ActivityService { modelName: repository.modelName, type: activity.type, targetIid: activity.targetIid, + targetCommentId: activity.targetCommentId, + targetInlineReviewId: activity.targetInlineReviewId, }; if (activity.type === "pr_review") { @@ -659,6 +717,45 @@ export class ActivityService { return; } + if (replyTargetCommentId != null) { + const targetIid = activity.targetIid; + + if (activity.type === "pr_reply") { + runWithActivity(startInput, (activityId) => + runPullRequestReply({ + provider: gitProvider, + workspace, + llmSender, + prIid: targetIid, + commentId: replyTargetCommentId, + inlineReviewId: activity.targetInlineReviewId, + language: repository.language, + userPrompt: repository.userPrompt, + activityId, + }), + ).catch((error) => { + logError("Activity retry failed", error); + }); + return; + } + + runWithActivity(startInput, (activityId) => + runIssueReply({ + provider: gitProvider, + workspace, + llmSender, + issueIid: targetIid, + commentId: replyTargetCommentId, + language: repository.language, + userPrompt: repository.userPrompt, + activityId, + }), + ).catch((error) => { + logError("Activity retry failed", error); + }); + return; + } + const issueIid = activity.targetIid; runWithActivity(startInput, (activityId) => runIssueReplyOnOpen({ diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 057edddf..9d91a796 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -8,6 +8,7 @@ import { activityTable } from "@proval/db"; import { eq } from "drizzle-orm"; import { loadEncryptionKey } from "./util/encrypt.js"; import { clearWorkspaceRoot } from "./git-provider/workspace.js"; +import { recoverLegacyReplyTargetComment } from "./api/activity/activity.service.js"; loadEncryptionKey(); @@ -48,6 +49,11 @@ if (process.env.NODE_ENV === "production") { await db.update(activityTable).set({ status: "failed" }).where(eq(activityTable.status, "started")); +const recoveredReplyTargetCount = recoverLegacyReplyTargetComment(); +if (recoveredReplyTargetCount > 0) { + log(pc.bgGreen(pc.bold(` Recovered ${recoveredReplyTargetCount} legacy reply comment link(s) `))); +} + Bun.serve({ fetch: apiApp.fetch, port: 7900, diff --git a/apps/api/src/webhook/forgejo/forgejo.controller.ts b/apps/api/src/webhook/forgejo/forgejo.controller.ts index faf2279e..c554dc87 100644 --- a/apps/api/src/webhook/forgejo/forgejo.controller.ts +++ b/apps/api/src/webhook/forgejo/forgejo.controller.ts @@ -426,6 +426,7 @@ const handleForgejoCommentWebhook = async ( modelName: repository.modelName, type: "issue_reply", targetIid, + targetCommentId: comment.id, }, (activityId) => runIssueReply({ @@ -559,6 +560,8 @@ async function startForgejoPrReply( modelName: repository.modelName, type: "pr_reply", targetIid: prNumber, + targetCommentId: commentId, + targetInlineReviewId: inlineReviewId, }, (activityId) => runPullRequestReply({ diff --git a/apps/api/src/webhook/github/github.controller.ts b/apps/api/src/webhook/github/github.controller.ts index cb9e0756..4847c96c 100644 --- a/apps/api/src/webhook/github/github.controller.ts +++ b/apps/api/src/webhook/github/github.controller.ts @@ -378,6 +378,7 @@ async function handleIssueCommentWebhook( modelName: repository.modelName, type: "pr_reply", targetIid: issueNumber, + targetCommentId: commentId, }, (activityId) => runPullRequestReply({ @@ -434,6 +435,7 @@ async function handleIssueCommentWebhook( modelName: repository.modelName, type: "issue_reply", targetIid: issueNumber, + targetCommentId: commentId, }, (activityId) => runIssueReply({ @@ -520,6 +522,8 @@ async function handlePullRequestReviewCommentWebhook( modelName: repository.modelName, type: "pr_reply", targetIid: prNumber, + targetCommentId: comment.id, + targetInlineReviewId: inlineReviewId, }, (activityId) => runPullRequestReply({ diff --git a/apps/api/src/webhook/gitlab/gitlab.controller.ts b/apps/api/src/webhook/gitlab/gitlab.controller.ts index cf496971..1a0d4c78 100644 --- a/apps/api/src/webhook/gitlab/gitlab.controller.ts +++ b/apps/api/src/webhook/gitlab/gitlab.controller.ts @@ -296,7 +296,7 @@ const handleGitLabPullRequestNoteWebhook: HandleGitLabPullRequestNoteWebhook = a reasoningEffort: repository.reasoningEffort, }); - const inlineReviewId = payload.object_attributes.discussion_id ?? null; + const inlineReviewId = isInlineReviewComment ? (payload.object_attributes.discussion_id ?? null) : null; const workspace = new Workspace(gitlabProvider); runWithActivity( @@ -306,6 +306,8 @@ const handleGitLabPullRequestNoteWebhook: HandleGitLabPullRequestNoteWebhook = a modelName: repository.modelName, type: "pr_reply", targetIid: prIid, + targetCommentId: commentId, + targetInlineReviewId: inlineReviewId, }, (activityId) => runPullRequestReply({ @@ -314,7 +316,7 @@ const handleGitLabPullRequestNoteWebhook: HandleGitLabPullRequestNoteWebhook = a llmSender, prIid, commentId, - inlineReviewId: isInlineReviewComment ? inlineReviewId : null, + inlineReviewId, language: repository.language, userPrompt: repository.userPrompt, activityId, @@ -490,6 +492,7 @@ const handleGitLabIssueNoteWebhook: HandleGitLabIssueNoteWebhook = async ( modelName: repository.modelName, type: "issue_reply", targetIid: issueIid, + targetCommentId: commentId, }, (activityId) => runIssueReply({ diff --git a/apps/api/src/webhook/webhook-signing.test.ts b/apps/api/src/webhook/webhook-signing.test.ts index 174bd908..24d445ff 100644 --- a/apps/api/src/webhook/webhook-signing.test.ts +++ b/apps/api/src/webhook/webhook-signing.test.ts @@ -62,7 +62,11 @@ if (process.env.PROVAL_WEBHOOK_TEST_CHILD !== "1") { "INSERT INTO git_provider_access (provider, name, base_url, access_token, default_webhook_secret) VALUES ('gitlab', 'Old access', 'https://old.example', ?, ?)", [encrypt("access"), legacySecret], ); - const activityBefore = db.$client.query("SELECT *, NULL AS log_version FROM activity").all(); + const activityBefore = db.$client + .query( + "SELECT *, NULL AS log_version, NULL AS target_comment_id, NULL AS target_inline_review_id FROM activity", + ) + .all(); const commentBefore = db.$client.query("SELECT * FROM comment").all(); migrate(db, { migrationsFolder: migrationPath }); const migrationResult = { diff --git a/apps/client/src/lib/demo/fixtures.ts b/apps/client/src/lib/demo/fixtures.ts index 28aa42e1..ddde517c 100644 --- a/apps/client/src/lib/demo/fixtures.ts +++ b/apps/client/src/lib/demo/fixtures.ts @@ -307,8 +307,20 @@ function pickType(rng: () => number): ActivityResponse["type"] { return "issue_open"; } +type DemoActivity = Omit; + +function withReplyTarget(activity: DemoActivity): ActivityResponse { + const isReply = activity.type === "pr_reply" || activity.type === "issue_reply"; + return { + ...activity, + targetCommentId: isReply ? 700000000 + activity.id : null, + targetInlineReviewId: + activity.type === "pr_reply" && activity.id % 3 === 0 ? `discussion-${activity.id}` : null, + }; +} + /** Recent showcase rows for the activity feed (last ~14h). */ -const recentActivityList: ActivityResponse[] = [ +const recentActivitySeedList: DemoActivity[] = [ { id: 1, repositoryId: 2, @@ -551,6 +563,8 @@ const recentActivityList: ActivityResponse[] = [ }, ]; +const recentActivityList: ActivityResponse[] = recentActivitySeedList.map(withReplyTarget); + /** * Year-to-date history so 30d / year charts look like a real small team * (weekday-heavy). Starts Jan 1 or 90 days ago, whichever is earlier. @@ -602,28 +616,30 @@ function buildHistoricalActivities(): ActivityResponse[] { const nextIid = (targetIidByRepo.get(repo.id) ?? 1) + 1; targetIidByRepo.set(repo.id, nextIid); - activities.push({ - id: nextId++, - repositoryId: repo.id, - repositoryPath: repo.path, - provider: repo.provider, - modelProviderId: repo.modelProviderId, - modelName: pickModelName(repo, rng), - type, - status: failed ? "failed" : "completed", - targetIid: nextIid, - headSha: type === "pr_review" ? `deadbeef${String(nextId).padStart(32, "0")}`.slice(0, 40) : null, - logVersion: "1", - ...tokens, - errorMessage: failed - ? rng() < 0.5 - ? "LLM request timed out after 120s" - : "Git provider API rate limit exceeded" - : null, - completedAt, - createdAt, - updatedAt: completedAt, - }); + activities.push( + withReplyTarget({ + id: nextId++, + repositoryId: repo.id, + repositoryPath: repo.path, + provider: repo.provider, + modelProviderId: repo.modelProviderId, + modelName: pickModelName(repo, rng), + type, + status: failed ? "failed" : "completed", + targetIid: nextIid, + headSha: type === "pr_review" ? `deadbeef${String(nextId).padStart(32, "0")}`.slice(0, 40) : null, + logVersion: "1", + ...tokens, + errorMessage: failed + ? rng() < 0.5 + ? "LLM request timed out after 120s" + : "Git provider API rate limit exceeded" + : null, + completedAt, + createdAt, + updatedAt: completedAt, + }), + ); } cursor.setDate(cursor.getDate() + 1); diff --git a/apps/client/src/routes/review/[id]/+page.svelte b/apps/client/src/routes/review/[id]/+page.svelte index e8d9201b..4d5ff8ab 100644 --- a/apps/client/src/routes/review/[id]/+page.svelte +++ b/apps/client/src/routes/review/[id]/+page.svelte @@ -41,7 +41,9 @@ const canRetry = $derived( (review.status === "failed" || review.status === "canceled") && review.repositoryId != null && - (review.type === "pr_review" || review.type === "issue_open"), + (review.type === "pr_review" || + review.type === "issue_open" || + ((review.type === "pr_reply" || review.type === "issue_reply") && review.targetCommentId != null)), ); const canCancel = $derived(review.status === "started"); const isRunning = $derived(review.status === "started"); @@ -56,10 +58,18 @@ async function onRetry(): Promise { if (isRetrying || !canRetry) return; - const message = - review.type === "pr_review" - ? "Start a new activity and run the pull request review again? This may post another review or comment on the pull request." - : "Start a new activity and run the issue open workflow again? This may post another comment on the issue."; + const retryMessageRecord: Record = { + pr_review: + "Start a new activity and run the pull request review again? This may post another review or comment on the pull request.", + pr_reply: + "Start a new activity and reply to the same pull request comment again? This may post another comment on the pull request.", + issue_open: + "Start a new activity and run the issue open workflow again? This may post another comment on the issue.", + issue_reply: + "Start a new activity and reply to the same issue comment again? This may post another comment on the issue.", + }; + + const message = retryMessageRecord[review.type]; const confirmed = await openConfirm(message, { title: "Retry activity", confirmText: "Retry" }); if (!confirmed) return; diff --git a/packages/db/src/migration/0046_fearless_vermin.sql b/packages/db/src/migration/0046_fearless_vermin.sql new file mode 100644 index 00000000..1e8ea944 --- /dev/null +++ b/packages/db/src/migration/0046_fearless_vermin.sql @@ -0,0 +1,2 @@ +ALTER TABLE `activity` ADD `target_comment_id` integer;--> statement-breakpoint +ALTER TABLE `activity` ADD `target_inline_review_id` text; \ No newline at end of file diff --git a/packages/db/src/migration/meta/0046_snapshot.json b/packages/db/src/migration/meta/0046_snapshot.json new file mode 100644 index 00000000..dcaad293 --- /dev/null +++ b/packages/db/src/migration/meta/0046_snapshot.json @@ -0,0 +1,1244 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "e7b49a03-4dee-4b6e-a742-b331a248dc7a", + "prevId": "d54cf2e8-40e0-4bb1-a75f-a8efbc571842", + "tables": { + "activity": { + "name": "activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_path": { + "name": "repository_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_provider_id": { + "name": "model_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_name": { + "name": "model_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_iid": { + "name": "target_iid", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_comment_id": { + "name": "target_comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_inline_review_id": { + "name": "target_inline_review_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_token": { + "name": "input_token", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_input_token": { + "name": "cached_input_token", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_token": { + "name": "output_token", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "log_version": { + "name": "log_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "activity_repository_id_created_at_idx": { + "name": "activity_repository_id_created_at_idx", + "columns": [ + "repository_id", + "created_at" + ], + "isUnique": false + }, + "activity_model_provider_id_created_at_idx": { + "name": "activity_model_provider_id_created_at_idx", + "columns": [ + "model_provider_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "activity_repository_id_repository_id_fk": { + "name": "activity_repository_id_repository_id_fk", + "tableFrom": "activity", + "tableTo": "repository", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_model_provider_id_model_provider_id_fk": { + "name": "activity_model_provider_id_model_provider_id_fk", + "tableFrom": "activity", + "tableTo": "model_provider", + "columnsFrom": [ + "model_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comment": { + "name": "comment", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "activity_id": { + "name": "activity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "comment_comment_id_type_idx": { + "name": "comment_comment_id_type_idx", + "columns": [ + "comment_id", + "type" + ], + "isUnique": false + }, + "comment_activityId_type_commentId_unique": { + "name": "comment_activityId_type_commentId_unique", + "columns": [ + "activity_id", + "type", + "comment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "comment_activity_id_activity_id_fk": { + "name": "comment_activity_id_activity_id_fk", + "tableFrom": "comment", + "tableTo": "activity", + "columnsFrom": [ + "activity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_provider_access": { + "name": "git_provider_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_create_enabled": { + "name": "auto_create_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_webhook_secret": { + "name": "default_webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_webhook_signing_token": { + "name": "default_webhook_signing_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model_provider_id": { + "name": "default_model_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model_name": { + "name": "default_model_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_language": { + "name": "default_language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_pr_enabled": { + "name": "default_pr_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_pr_min_access_level": { + "name": "default_pr_min_access_level", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_pr_review_enabled": { + "name": "default_pr_review_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_pr_inline_review": { + "name": "default_pr_inline_review", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_pr_review_on_push": { + "name": "default_pr_review_on_push", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_pr_ignore_draft": { + "name": "default_pr_ignore_draft", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_pr_reply_enabled": { + "name": "default_pr_reply_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_pr_mention_only": { + "name": "default_pr_mention_only", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_issue_enabled": { + "name": "default_issue_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_issue_min_access_level": { + "name": "default_issue_min_access_level", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_issue_comment_on_open_enabled": { + "name": "default_issue_comment_on_open_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_issue_label_on_open_enabled": { + "name": "default_issue_label_on_open_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_issue_reply_enabled": { + "name": "default_issue_reply_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_issue_mention_only": { + "name": "default_issue_mention_only", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "git_provider_access_provider_baseUrl_unique": { + "name": "git_provider_access_provider_baseUrl_unique", + "columns": [ + "provider", + "base_url" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_provider_access_default_model_provider_id_model_provider_id_fk": { + "name": "git_provider_access_default_model_provider_id_model_provider_id_fk", + "tableFrom": "git_provider_access", + "tableTo": "model_provider", + "columnsFrom": [ + "default_model_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "github_app": { + "name": "github_app", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "github_app_appId_unique": { + "name": "github_app_appId_unique", + "columns": [ + "app_id" + ], + "isUnique": true + }, + "github_app_slug_unique": { + "name": "github_app_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "github_installation": { + "name": "github_installation", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "installation_id": { + "name": "installation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "github_installation_installationId_unique": { + "name": "github_installation_installationId_unique", + "columns": [ + "installation_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "github_installation_app_id_github_app_id_fk": { + "name": "github_installation_app_id_github_app_id_fk", + "tableFrom": "github_installation", + "tableTo": "github_app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "instance_setting": { + "name": "instance_setting", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registration_enabled": { + "name": "registration_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "model_provider": { + "name": "model_provider", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeout_second": { + "name": "timeout_second", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 600 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repository": { + "name": "repository", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "webhook_signing_token": { + "name": "webhook_signing_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'English'" + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_provider_access_id": { + "name": "git_provider_access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_provider_repository_id": { + "name": "git_provider_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_id": { + "name": "access_token_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_enabled": { + "name": "pr_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "pr_min_access_level": { + "name": "pr_min_access_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "pr_review_enabled": { + "name": "pr_review_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "pr_inline_review": { + "name": "pr_inline_review", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "pr_review_on_push": { + "name": "pr_review_on_push", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'on_every_push'" + }, + "pr_ignore_draft": { + "name": "pr_ignore_draft", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "user_prompt": { + "name": "user_prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_reply_enabled": { + "name": "pr_reply_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "pr_mention_only": { + "name": "pr_mention_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "issue_enabled": { + "name": "issue_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "issue_min_access_level": { + "name": "issue_min_access_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "issue_comment_on_open_enabled": { + "name": "issue_comment_on_open_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "issue_label_on_open_enabled": { + "name": "issue_label_on_open_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "issue_reply_enabled": { + "name": "issue_reply_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "issue_mention_only": { + "name": "issue_mention_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "model_provider_id": { + "name": "model_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_name": { + "name": "model_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "reasoning_effort": { + "name": "reasoning_effort", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "repository_gitProviderRepositoryId_gitProviderAccessId_unique": { + "name": "repository_gitProviderRepositoryId_gitProviderAccessId_unique", + "columns": [ + "git_provider_repository_id", + "git_provider_access_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "repository_github_installation_id_github_installation_id_fk": { + "name": "repository_github_installation_id_github_installation_id_fk", + "tableFrom": "repository", + "tableTo": "github_installation", + "columnsFrom": [ + "github_installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "repository_git_provider_access_id_git_provider_access_id_fk": { + "name": "repository_git_provider_access_id_git_provider_access_id_fk", + "tableFrom": "repository", + "tableTo": "git_provider_access", + "columnsFrom": [ + "git_provider_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "repository_model_provider_id_model_provider_id_fk": { + "name": "repository_model_provider_id_model_provider_id_fk", + "tableFrom": "repository", + "tableTo": "model_provider", + "columnsFrom": [ + "model_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migration/meta/_journal.json b/packages/db/src/migration/meta/_journal.json index 3629c9cd..3d372c0c 100644 --- a/packages/db/src/migration/meta/_journal.json +++ b/packages/db/src/migration/meta/_journal.json @@ -323,6 +323,13 @@ "when": 1791276789272, "tag": "0045_activity_log_version", "breakpoints": true + }, + { + "idx": 46, + "version": "6", + "when": 1791640261334, + "tag": "0046_fearless_vermin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 9a3e7136..f792650e 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -195,6 +195,8 @@ export const activityTable = sqliteTable( type: text({ enum: ["pr_review", "pr_reply", "issue_open", "issue_reply"] }).notNull(), status: text({ enum: ["started", "completed", "failed", "canceled"] }).notNull(), targetIid: integer().notNull(), + targetCommentId: integer(), + targetInlineReviewId: text(), headSha: text(), inputToken: integer(), cachedInputToken: integer(),