From 317144cbd275d5a739a55cb0e65bbc2e0dbb527c Mon Sep 17 00:00:00 2001 From: Noext Date: Sat, 10 Oct 2026 12:42:36 +0000 Subject: [PATCH] fix(activity): retry a failed reply from the stored comment A failed pr_reply or issue_reply never offered Retry because the retry path only accepted pr_review and issue_open, and because the activity row did not keep the comment that started the reply. The webhook handlers now store targetCommentId and targetInlineReviewId at start, retry resolves the target comment before any provider setup and replays runPullRequestReply or runIssueReply with it, and the review page shows the button for both reply types. The boot repair reads the comment id back from the agent log of a reply created before this change, so an old failed reply becomes retryable too. --- .../src/api/activity/activity-retry.test.ts | 251 ++++ .../src/api/activity/activity.controller.ts | 5 +- apps/api/src/api/activity/activity.service.ts | 103 +- apps/api/src/index.ts | 6 + .../src/webhook/forgejo/forgejo.controller.ts | 3 + .../src/webhook/github/github.controller.ts | 4 + .../src/webhook/gitlab/gitlab.controller.ts | 7 +- apps/api/src/webhook/webhook-signing.test.ts | 6 +- apps/client/src/lib/demo/fixtures.ts | 62 +- .../src/routes/review/[id]/+page.svelte | 20 +- .../db/src/migration/0046_fearless_vermin.sql | 2 + .../db/src/migration/meta/0046_snapshot.json | 1244 +++++++++++++++++ packages/db/src/migration/meta/_journal.json | 7 + packages/db/src/schema.ts | 2 + 14 files changed, 1685 insertions(+), 37 deletions(-) create mode 100644 apps/api/src/api/activity/activity-retry.test.ts create mode 100644 packages/db/src/migration/0046_fearless_vermin.sql create mode 100644 packages/db/src/migration/meta/0046_snapshot.json 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(),