diff --git a/apps/app/src/components/promptbox/banner/QueuedMessagesList.test.tsx b/apps/app/src/components/promptbox/banner/QueuedMessagesList.test.tsx index 551312a5d54..18904dbf077 100644 --- a/apps/app/src/components/promptbox/banner/QueuedMessagesList.test.tsx +++ b/apps/app/src/components/promptbox/banner/QueuedMessagesList.test.tsx @@ -169,6 +169,41 @@ afterEach(() => { }); describe("QueuedMessagesList", () => { + it.each([ + { kind: "host-offline", hostName: "M4" }, + { kind: "provisioning" }, + { kind: "interaction" }, + { kind: "turn-starting" }, + { kind: "stopping" }, + ] as const)("offers Send now for a failed $kind row", (waitingOn) => { + const onSend = vi.fn(); + const { getByRole, getByText } = render( + , + ); + expect(getByText("Provider unavailable")).toBeTruthy(); + const button = getByRole("button", { name: "Send queued message 1 now" }); + expect(button.hasAttribute("disabled")).toBe(false); + fireEvent.click(button); + expect(onSend).toHaveBeenCalledWith("failed-row"); + }); + it("labels non-user senders and refreshes their names from the thread cache", async () => { const queryClient = new QueryClient(); const messages = [ diff --git a/apps/app/src/components/promptbox/banner/QueuedMessagesList.tsx b/apps/app/src/components/promptbox/banner/QueuedMessagesList.tsx index 3357fa7e79d..7231e0923ce 100644 --- a/apps/app/src/components/promptbox/banner/QueuedMessagesList.tsx +++ b/apps/app/src/components/promptbox/banner/QueuedMessagesList.tsx @@ -803,7 +803,7 @@ const QueuedMessageRow = memo(function QueuedMessageRow({ const hasWaitLine = queuedMessageHasWaitLine(queuedMessage); const sendAllowed = sendAction === "steer-when-ready" || - isQueuedMessageSendNowAllowed(queuedMessage.waitingOn); + isQueuedMessageSendNowAllowed(queuedMessage); const sendAriaLabel = sendAction === "steer-when-ready" ? `Steer queued message ${index + 1} when ready` diff --git a/apps/app/src/lib/queued-message-wait.test.ts b/apps/app/src/lib/queued-message-wait.test.ts index 4427009392a..03951a75eb2 100644 --- a/apps/app/src/lib/queued-message-wait.test.ts +++ b/apps/app/src/lib/queued-message-wait.test.ts @@ -201,27 +201,33 @@ describe("queuedMessageFallbackTitle", () => { }); describe("isQueuedMessageSendNowAllowed", () => { - it("hides send-now only for the waits a re-attempt cannot clear", () => { - expect(isQueuedMessageSendNowAllowed({ kind: "time" })).toBe(true); - expect( - isQueuedMessageSendNowAllowed({ - kind: "plugin", - pluginId: "limiter", - reason: "busy", - }), - ).toBe(true); - expect(isQueuedMessageSendNowAllowed({ kind: "thread-busy" })).toBe(true); - expect(isQueuedMessageSendNowAllowed({ kind: "stopping" })).toBe(false); - expect(isQueuedMessageSendNowAllowed({ kind: "turn-starting" })).toBe( - false, - ); - expect(isQueuedMessageSendNowAllowed(null)).toBe(true); - expect(isQueuedMessageSendNowAllowed({ kind: "provisioning" })).toBe(false); - expect(isQueuedMessageSendNowAllowed({ kind: "interaction" })).toBe(false); - expect( - isQueuedMessageSendNowAllowed({ kind: "host-offline", hostName: "M4" }), - ).toBe(false); - }); + it.each([ + { waitingOn: null, allowed: true }, + { waitingOn: { kind: "time" }, allowed: true }, + { + waitingOn: { kind: "plugin", pluginId: "limiter", reason: "busy" }, + allowed: true, + }, + { waitingOn: { kind: "thread-busy" }, allowed: true }, + { waitingOn: { kind: "stopping" }, allowed: false }, + { waitingOn: { kind: "turn-starting" }, allowed: false }, + { waitingOn: { kind: "provisioning" }, allowed: false }, + { waitingOn: { kind: "interaction" }, allowed: false }, + { waitingOn: { kind: "host-offline", hostName: "M4" }, allowed: false }, + ] as const)( + "allows manual recovery for $waitingOn", + ({ waitingOn, allowed }) => { + expect( + isQueuedMessageSendNowAllowed({ waitingOn, failureReason: null }), + ).toBe(allowed); + expect( + isQueuedMessageSendNowAllowed({ + waitingOn, + failureReason: "Provider unavailable", + }), + ).toBe(true); + }, + ); }); describe("formatQueuedMessageCountdown", () => { diff --git a/apps/app/src/lib/queued-message-wait.ts b/apps/app/src/lib/queued-message-wait.ts index a3aac025938..dea6a2a4db3 100644 --- a/apps/app/src/lib/queued-message-wait.ts +++ b/apps/app/src/lib/queued-message-wait.ts @@ -22,9 +22,14 @@ export function formatQueuedMessageCountdown( return `in ${Math.floor(remainingMs / DAY_MS)}d`; } -export function isQueuedMessageSendNowAllowed( - waitingOn: QueuedMessageWaitingOn | null, -): boolean { +export function isQueuedMessageSendNowAllowed({ + waitingOn, + failureReason, +}: { + waitingOn: QueuedMessageWaitingOn | null; + failureReason: string | null; +}): boolean { + if (failureReason !== null) return true; if (waitingOn === null) return true; switch (waitingOn.kind) { case "provisioning": diff --git a/apps/cli/src/__tests__/command-output/thread-organization.test.ts b/apps/cli/src/__tests__/command-output/thread-organization.test.ts index 5c2edcab641..fe27735580d 100644 --- a/apps/cli/src/__tests__/command-output/thread-organization.test.ts +++ b/apps/cli/src/__tests__/command-output/thread-organization.test.ts @@ -105,6 +105,36 @@ describe("bb thread organization commands", () => { expect(output).toContain("System"); }); + it.each([false, true])( + "shows failures and recovery commands in queue list (scoped: %s)", + async (scoped) => { + const failureReason = + "The provider bridge is unavailable while the plugin is still building"; + const list = vi.fn(async () => [ + queuedMessage({ + waitingOn: { kind: "host-offline", hostName: "Michael-M4" }, + failureReason, + }), + ]); + stubServerApi({ + [scoped + ? "v1.threads.:id.queued-messages.$get" + : "v1.queued-messages.$get"]: list, + }); + await runCommand( + ["thread", "queue", "list", ...(scoped ? ["thread-1"] : [])], + register, + ); + const output = vi + .mocked(console.log) + .mock.calls.map((args) => args.join(" ")) + .join("\n"); + expect(output).toContain(`Failed queued-1: ${failureReason}`); + expect(output).toContain("bb thread queue send thread-1 queued-1"); + expect(output).not.toContain("waiting for Michael-M4 to reconnect"); + }, + ); + it("updates a queued message in place", async () => { const list = vi.fn(async () => [ { id: "queued-1", updatedAt: 42 }, diff --git a/apps/cli/src/commands/thread/organization.ts b/apps/cli/src/commands/thread/organization.ts index f5e53141b51..d6ddadcf1b0 100644 --- a/apps/cli/src/commands/thread/organization.ts +++ b/apps/cli/src/commands/thread/organization.ts @@ -120,7 +120,12 @@ function printQueueTable(rows: ThreadQueuedMessagesResult): void { ? "System" : (row.senderThreadId ?? "Agent"), truncateCell(queuedMessagePreview(row.content), MAX_QUEUE_TEXT_WIDTH), - truncateCell(describeQueueWait(row), MAX_QUEUE_TEXT_WIDTH), + truncateCell( + row.failureReason === null + ? describeQueueWait(row) + : `Failed: ${row.failureReason}`, + MAX_QUEUE_TEXT_WIDTH, + ), formatQueueSendCountdown(row.sendAt, now), ]); printBorderlessTable( @@ -130,6 +135,11 @@ function printQueueTable(rows: ThreadQueuedMessagesResult): void { }, table, ); + for (const row of rows) { + if (row.failureReason === null) continue; + console.log(`Failed ${row.id}: ${row.failureReason}`); + console.log(`Retry: bb thread queue send ${row.threadId} ${row.id}`); + } } function queuedMessagePreview(content: PromptInput[]): string { diff --git a/apps/server/src/services/system/periodic-sweeps.ts b/apps/server/src/services/system/periodic-sweeps.ts index 5028ae72e5c..5f213bd25d7 100644 --- a/apps/server/src/services/system/periodic-sweeps.ts +++ b/apps/server/src/services/system/periodic-sweeps.ts @@ -591,6 +591,13 @@ const PERIODIC_SWEEP_JOBS: PeriodicSweepJob[] = [ run: (deps, now) => runQueuedMessageDispatch(deps, { kind: "time-reached", now }), }, + { + cadenceMs: 0, + category: "durable-intent-retry", + name: "failed-queue-message-retry", + run: (deps, now) => + runQueuedMessageDispatch(deps, { kind: "failed-retry", now }), + }, { cadenceMs: 0, category: "durable-intent-retry", diff --git a/apps/server/src/services/threads/queue-drain-failure.ts b/apps/server/src/services/threads/queue-drain-failure.ts index 583c61038ab..392e4fc88bd 100644 --- a/apps/server/src/services/threads/queue-drain-failure.ts +++ b/apps/server/src/services/threads/queue-drain-failure.ts @@ -12,6 +12,10 @@ import { dispatchEnvironmentAndHost } from "./dispatch-hooks.js"; type QueueDrainFailureDeps = Pick; +export const QUEUED_MESSAGE_RETRY_DELAYS_MS: readonly number[] = [ + 15_000, 60_000, 300_000, +]; + /** * What a failed dispatch says to the person whose message did not go. * @@ -39,7 +43,10 @@ export function describeDispatchFailure(error: unknown): string { * host-reconnect drain clears when the machine comes back. Any other failure * is recorded as the row's failure reason, leaving its existing wait alone — * the row is still waiting on whatever it was waiting on, and what went wrong - * last time is a different fact from what it is waiting for. + * last time is a different fact from what it is waiting for — and spends one + * of the row's attempts, booking the next on + * {@link QUEUED_MESSAGE_RETRY_DELAYS_MS}. The row gives up only once that + * budget runs out. * * Only the drain calls this. An inline attempt has a caller still listening * and surfaces its error to them instead, which is why a queued row never @@ -49,6 +56,7 @@ export function recordQueuedMessageDrainFailure( deps: QueueDrainFailureDeps, args: { error: unknown; + now: number; row: { id: string; threadId: string }; thread: Thread; }, @@ -71,5 +79,7 @@ export function recordQueuedMessageDrainFailure( id: args.row.id, threadId: args.row.threadId, failureReason: describeDispatchFailure(args.error), + now: args.now, + retryDelaysMs: QUEUED_MESSAGE_RETRY_DELAYS_MS, }); } diff --git a/apps/server/src/services/threads/queued-message-dispatch.ts b/apps/server/src/services/threads/queued-message-dispatch.ts index 12a86f9d332..499675377b3 100644 --- a/apps/server/src/services/threads/queued-message-dispatch.ts +++ b/apps/server/src/services/threads/queued-message-dispatch.ts @@ -10,6 +10,7 @@ import { listQueuedThreadMessagePluginWaitRefs, listQueuedThreadMessagesByWaitHolder, listQueuedThreadMessagesWaitingOnKind, + listRetryableFailedQueuedThreadMessages, listThreadIdsWithHostOfflineQueueWaits, } from "@bb/db"; import { @@ -47,6 +48,7 @@ export type QueuedMessageDispatchWake = | { kind: "plugin-recheck" } | { kind: "plugin-unregistered"; pluginId: string } | { kind: "idle-recovery"; now: number } + | { kind: "failed-retry"; now: number } | { kind: "orphaned-plugin-recovery"; plugins: QueueWaitPluginDirectory; @@ -116,6 +118,7 @@ function dispatchWakeContext( case "interaction-settled": return { threadId: wake.threadId, wake: wake.kind }; case "idle-recovery": + case "failed-retry": return { now: wake.now, wake: wake.kind }; case "orphaned-plugin-recovery": return { wake: wake.kind }; @@ -231,6 +234,7 @@ async function executePreparedQueuedMessageDispatch( await attemptAutomaticQueuedMessage(deps, row, { now: Date.now(), respectRequeuePacing: false, + retryingFailure: false, }); } } @@ -260,6 +264,9 @@ async function executePreparedQueuedMessageDispatch( releaseStaleQueuedMessageDispatchClaims(deps, wake.now); await runIdleThreadRecovery(deps); return; + case "failed-retry": + await runFailedRetryDispatch(deps, wake.now); + return; case "orphaned-plugin-recovery": await runOrphanedPluginWaitRecovery(deps, wake.plugins); return; @@ -319,6 +326,7 @@ async function runTurnStartedDispatch( await attemptAutomaticQueuedMessage(deps, row, { now: Date.now(), respectRequeuePacing: false, + retryingFailure: false, }); } } @@ -343,6 +351,7 @@ async function runWorkspaceReadyDispatch( await attemptAutomaticQueuedMessage(deps, row, { now: Date.now(), respectRequeuePacing: false, + retryingFailure: false, }); } } @@ -364,7 +373,11 @@ async function runInteractionSettledDispatch( async function attemptAutomaticQueuedMessage( deps: QueueDispatchDeps, row: QueuedMessageDispatchRef, - args: { now: number; respectRequeuePacing: boolean }, + args: { + now: number; + respectRequeuePacing: boolean; + retryingFailure: boolean; + }, ): Promise { if (args.respectRequeuePacing && isDispatchRequeuedRecently(row.threadId)) return; @@ -377,8 +390,10 @@ async function attemptAutomaticQueuedMessage( kind: "automatic", isGroupEligible: createAutomaticQueuedMessageGroupEligibility(deps, { now: args.now, + retryingFailure: args.retryingFailure, thread, }), + retryingFailure: args.retryingFailure, }, mode: "auto", queuedMessageId: row.id, @@ -396,7 +411,12 @@ async function attemptAutomaticQueuedMessage( ); return; } - recordQueuedMessageDrainFailure(deps, { error, row, thread }); + recordQueuedMessageDrainFailure(deps, { + error, + now: args.now, + row, + thread, + }); deps.logger.warn( { queuedMessageId: row.id, @@ -416,6 +436,7 @@ async function runPluginRecheckDispatch( await attemptAutomaticQueuedMessage(deps, row, { now, respectRequeuePacing: true, + retryingFailure: false, }); } } @@ -449,6 +470,40 @@ async function runDueScheduledDispatch( await attemptAutomaticQueuedMessage(deps, row, { now, respectRequeuePacing: true, + retryingFailure: false, + }); + } +} + +/** + * Re-attempts rows whose booked retry has come due. + * + * This is the only automatic path that may claim a row with a recorded + * failure, and it exists because a failure is not a verdict about the message: + * it is what the server was able to do at one instant, usually an instant + * during a restart. Every other wake asks "did the thing this row waits for + * happen?", which a row that failed can no longer be asked — its wait may have + * gone stale while it sat there, and the edge that would have cleared it has + * passed. So this one re-asks the whole question instead, and the row's + * remaining attempts are what stop it asking forever. + */ +async function runFailedRetryDispatch( + deps: QueueDispatchDeps, + now: number, +): Promise { + for (const row of listRetryableFailedQueuedThreadMessages(deps.db, now)) { + deps.logger.info( + { + failureCount: row.failureCount, + queuedMessageId: row.id, + threadId: row.threadId, + }, + "Retrying a queued message whose dispatch failed", + ); + await attemptAutomaticQueuedMessage(deps, row, { + now, + respectRequeuePacing: true, + retryingFailure: true, }); } } diff --git a/apps/server/src/services/threads/queued-messages.ts b/apps/server/src/services/threads/queued-messages.ts index e2fa2fb4689..a2fab087d9e 100644 --- a/apps/server/src/services/threads/queued-messages.ts +++ b/apps/server/src/services/threads/queued-messages.ts @@ -130,12 +130,12 @@ interface SendClaimedQueuedMessageForThreadArgs { export function createAutomaticQueuedMessageGroupEligibility( deps: Pick, - args: { now: number; thread: Thread }, + args: { now: number; retryingFailure: boolean; thread: Thread }, ): QueuedThreadMessageGroupEligibility { const activeTurnId = getActiveTurnId(deps, args.thread.id); return (group) => group.every((member) => { - if (member.failureReason !== null) return false; + if (member.failureReason !== null && !args.retryingFailure) return false; const waitingOn = parseStoredQueuedThreadMessageWaitingOn(member); switch (waitingOn?.kind) { case undefined: @@ -855,6 +855,7 @@ export async function sendNextQueuedMessageIfPresent( args.threadId, createAutomaticQueuedMessageGroupEligibility(deps, { now: Date.now(), + retryingFailure: false, thread: initialThread, }), ); @@ -896,6 +897,7 @@ export async function sendNextQueuedMessageIfPresent( if (!isCommandTimeoutError(error)) { recordQueuedMessageDrainFailure(deps, { error, + now: Date.now(), row: nextQueuedMessages[0]!, thread, }); diff --git a/apps/server/test/public/public-thread-fork.test.ts b/apps/server/test/public/public-thread-fork.test.ts index 3133aedd85a..859075a46b0 100644 --- a/apps/server/test/public/public-thread-fork.test.ts +++ b/apps/server/test/public/public-thread-fork.test.ts @@ -808,6 +808,7 @@ describe("public thread fork route", () => { claimPolicy: { kind: "automatic", isGroupEligible: () => true, + retryingFailure: false, }, threadId: fork.id, queuedMessageId: first.id, diff --git a/apps/server/test/services/plugins/plugin-mention-providers.test.ts b/apps/server/test/services/plugins/plugin-mention-providers.test.ts index 6ed8452ac1b..389ccc08e53 100644 --- a/apps/server/test/services/plugins/plugin-mention-providers.test.ts +++ b/apps/server/test/services/plugins/plugin-mention-providers.test.ts @@ -460,6 +460,7 @@ describe("plugin mention providers (bb.ui.registerMentionProvider)", () => { claimPolicy: { kind: "automatic", isGroupEligible: () => true, + retryingFailure: false, }, threadId: thread.id, queuedMessageId: queued.id, diff --git a/apps/server/test/services/threads/thread-runtime-display.test.ts b/apps/server/test/services/threads/thread-runtime-display.test.ts index 2deaaf650c0..6d70a3b39f7 100644 --- a/apps/server/test/services/threads/thread-runtime-display.test.ts +++ b/apps/server/test/services/threads/thread-runtime-display.test.ts @@ -474,6 +474,8 @@ describe("thread runtime display", () => { id: failedRow.id, threadId: failed.thread.id, failureReason: "The message could not be sent.", + now: Date.now(), + retryDelaysMs: [], }); const entries = toThreadListEntryResponses( diff --git a/apps/server/test/threads/dispatch-hooks.test.ts b/apps/server/test/threads/dispatch-hooks.test.ts index 9953b894e97..ef587475d9d 100644 --- a/apps/server/test/threads/dispatch-hooks.test.ts +++ b/apps/server/test/threads/dispatch-hooks.test.ts @@ -1202,8 +1202,9 @@ describe("message.dispatch grouped authors", () => { kind: "automatic", isGroupEligible: createAutomaticQueuedMessageGroupEligibility( harness.deps, - { now: Date.now(), thread }, + { now: Date.now(), retryingFailure: false, thread }, ), + retryingFailure: false, }, }); expect(seen).toHaveLength(2); diff --git a/apps/server/test/threads/queue-drain-failure.test.ts b/apps/server/test/threads/queue-drain-failure.test.ts index 47f1e96413b..924ee4c4332 100644 --- a/apps/server/test/threads/queue-drain-failure.test.ts +++ b/apps/server/test/threads/queue-drain-failure.test.ts @@ -12,7 +12,10 @@ import { type PluginHookRegistration, } from "../../src/services/plugins/plugin-hook-registry.js"; import { noteDispatchRequeued } from "../../src/services/threads/dispatch-hooks.js"; -import { recordQueuedMessageDrainFailure } from "../../src/services/threads/queue-drain-failure.js"; +import { + QUEUED_MESSAGE_RETRY_DELAYS_MS, + recordQueuedMessageDrainFailure, +} from "../../src/services/threads/queue-drain-failure.js"; import { runQueuedMessageDispatch } from "../../src/services/threads/queued-message-dispatch.js"; import { toThreadQueuedMessage } from "../../src/services/threads/thread-queued-messages.js"; import { textInput } from "../helpers/prompt-input.js"; @@ -71,10 +74,14 @@ function seedQueuedRow( return { host, thread, row }; } -function reread(harness: TestAppHarness, queuedMessageId: string) { +function rereadRow(harness: TestAppHarness, queuedMessageId: string) { const row = getQueuedThreadMessage(harness.db, queuedMessageId); if (row === null) throw new Error("the queued row vanished"); - return toThreadQueuedMessage(row); + return row; +} + +function reread(harness: TestAppHarness, queuedMessageId: string) { + return toThreadQueuedMessage(rereadRow(harness, queuedMessageId)); } describe("host-connected queue dispatch", () => { @@ -91,6 +98,7 @@ describe("host-connected queue dispatch", () => { for (const seeded of [away, otherAway]) { recordQueuedMessageDrainFailure(harness.deps, { error: new ApiError(502, "host_unavailable", "Host is not connected"), + now: Date.now(), row: seeded.row, thread: seeded.thread, }); @@ -124,7 +132,7 @@ describe("host-connected queue dispatch", () => { }); describe("recordQueuedMessageDrainFailure", () => { - it("does not automatically re-attempt a terminally failed row", async () => { + it("hides a failed row from the wakes that are not its booked retry", async () => { await withTestHarness(async (harness) => { let attempts = 0; const registry: HookRegistry = { "message.dispatch": [] }; @@ -256,6 +264,7 @@ describe("recordQueuedMessageDrainFailure", () => { recordQueuedMessageDrainFailure(harness.deps, { error: new ApiError(502, "host_unavailable", "Host is not connected"), + now: Date.now(), row, thread, }); @@ -283,6 +292,7 @@ describe("recordQueuedMessageDrainFailure", () => { recordQueuedMessageDrainFailure(harness.deps, { error: new ApiError(409, "thread_not_writable", "Thread is archived"), + now: Date.now(), row, thread, }); @@ -306,6 +316,7 @@ describe("recordQueuedMessageDrainFailure", () => { recordQueuedMessageDrainFailure(harness.deps, { error: new Error("Cannot read properties of undefined (reading 'id')"), + now: Date.now(), row, thread, }); @@ -329,10 +340,13 @@ describe("recordQueuedMessageDrainFailure", () => { id: row.id, threadId: row.threadId, failureReason: "Thread is archived", + now: Date.now(), + retryDelaysMs: [], }); recordQueuedMessageDrainFailure(harness.deps, { error: new ApiError(502, "host_unavailable", "Host is not connected"), + now: Date.now(), row, thread, }); @@ -349,3 +363,225 @@ describe("recordQueuedMessageDrainFailure", () => { }); }); }); + +describe("a failed row's booked retry", () => { + const [FIRST_DELAY_MS, SECOND_DELAY_MS] = QUEUED_MESSAGE_RETRY_DELAYS_MS; + + /** + * A dispatch hook that refuses everything, counting the attempts. Rejection + * is the shortest route to a recorded failure that is not the host being + * away, which is the one failure the drain turns into a wait instead. + */ + function installRejector(): { attempts: () => number; dispose(): void } { + let attempts = 0; + const registry: HookRegistry = { "message.dispatch": [] }; + registry["message.dispatch"].push({ + pluginId: "rejector", + handler: () => { + attempts += 1; + return { action: "reject", message: "Rejected for testing" } as const; + }, + }); + setPluginHookProvider({ + listHooks: (hook) => registry[hook], + invokeHook: async (_pluginId, _label, run) => ({ + ok: true, + value: await run(), + }), + decisionTimeoutMs: 10_000, + }); + return { + attempts: () => attempts, + dispose: () => setPluginHookProvider(undefined), + }; + } + + it("books the next attempt rather than giving up on the first failure", async () => { + await withTestHarness(async (harness) => { + const { thread, row } = seedQueuedRow(harness, { + hostConnected: true, + hostName: "M4", + }); + const now = Date.now(); + + recordQueuedMessageDrainFailure(harness.deps, { + error: new ApiError(409, "thread_not_writable", "Thread is archived"), + now, + row, + thread, + }); + + const failed = rereadRow(harness, row.id); + expect(failed.failureReason).toBe("Thread is archived"); + expect(failed.failureCount).toBe(1); + expect(failed.nextAttemptAt).toBe(now + FIRST_DELAY_MS!); + }); + }); + + it("waits for the booked instant before trying again", async () => { + await withTestHarness(async (harness) => { + const rejector = installRejector(); + try { + const { thread, row } = seedQueuedRow(harness, { + hostConnected: true, + hostName: "M4", + }); + + await runQueuedMessageDispatch(harness.deps, { + kind: "thread-ready", + threadId: thread.id, + }); + expect(rejector.attempts()).toBe(1); + const booked = rereadRow(harness, row.id).nextAttemptAt!; + + await runQueuedMessageDispatch(harness.deps, { + kind: "failed-retry", + now: booked - 1, + }); + expect(rejector.attempts()).toBe(1); + + await runQueuedMessageDispatch(harness.deps, { + kind: "failed-retry", + now: booked, + }); + expect(rejector.attempts()).toBe(2); + + // A second failure spends a second attempt and books a later one, so a + // row that keeps failing backs off instead of spinning on every tick. + const retried = rereadRow(harness, row.id); + expect(retried.failureCount).toBe(2); + expect(retried.nextAttemptAt).toBe(booked + SECOND_DELAY_MS!); + } finally { + rejector.dispose(); + } + }); + }); + + it("sends a row whose wait went stale while it sat failed", async () => { + await withTestHarness(async (harness) => { + const { host, thread, row } = seedQueuedRow(harness, { + hostConnected: false, + hostName: "M4", + }); + const now = Date.now(); + + // How the stuck row is actually made: the host is away, so the drain + // parks the row on `host-offline`; the machine comes back, and the + // attempt that follows fails for its own reason and leaves that wait in + // place. From then on the wait describes a condition that has already + // cleared, and the host-reconnect wake it names has been and gone. + recordQueuedMessageDrainFailure(harness.deps, { + error: new ApiError(502, "host_unavailable", "Host is not connected"), + now, + row, + thread, + }); + expect(reread(harness, row.id).waitingOn).toEqual({ + kind: "host-offline", + hostName: "M4", + }); + + seedHostSession(harness.deps, { id: host.id, name: "M4" }); + seedThreadRuntimeState(harness.deps, { + environmentId: thread.environmentId, + providerThreadId: "returning-machine-thread", + threadId: thread.id, + }); + recordQueuedMessageDrainFailure(harness.deps, { + error: new ApiError( + 409, + "provider_bridge_unavailable", + 'Provider "claude-code" has no bridge to run on.', + ), + now, + row, + thread, + }); + const stale = rereadRow(harness, row.id); + expect(stale.waitingOn).toContain("host-offline"); + expect(stale.nextAttemptAt).toBe(now + FIRST_DELAY_MS!); + + await runQueuedMessageDispatch(harness.deps, { + kind: "failed-retry", + now: stale.nextAttemptAt!, + }); + + // The retry re-asks the whole question instead of waiting on an edge + // that already passed, so the row goes out. + expect(getQueuedThreadMessage(harness.db, row.id)).toBeNull(); + }); + }); + + it("stops once the row's attempts are spent", async () => { + await withTestHarness(async (harness) => { + const rejector = installRejector(); + try { + const { thread, row } = seedQueuedRow(harness, { + hostConnected: true, + hostName: "M4", + }); + + await runQueuedMessageDispatch(harness.deps, { + kind: "thread-ready", + threadId: thread.id, + }); + for (const _delay of QUEUED_MESSAGE_RETRY_DELAYS_MS) { + const booked = rereadRow(harness, row.id).nextAttemptAt; + if (booked === null) break; + await runQueuedMessageDispatch(harness.deps, { + kind: "failed-retry", + now: booked, + }); + } + + const spent = rereadRow(harness, row.id); + expect(spent.failureCount).toBe(4); + expect(spent.nextAttemptAt).toBeNull(); + expect(rejector.attempts()).toBe(4); + + // Nothing automatic can reach it now: it is a row for a person, and + // the thread list has been showing it as failed the whole time. + await runQueuedMessageDispatch(harness.deps, { + kind: "failed-retry", + now: Date.now() + 86_400_000, + }); + expect(rejector.attempts()).toBe(4); + } finally { + rejector.dispose(); + } + }); + }); + + it("gives the attempts back when the row queues again", async () => { + await withTestHarness(async (harness) => { + const { thread, row } = seedQueuedRow(harness, { + hostConnected: false, + hostName: "M4", + }); + + setQueuedThreadMessageFailureReason(harness.db, harness.deps.hub, { + id: row.id, + threadId: row.threadId, + failureReason: "Thread is archived", + now: Date.now(), + retryDelaysMs: QUEUED_MESSAGE_RETRY_DELAYS_MS, + }); + expect(rereadRow(harness, row.id).failureCount).toBe(1); + + recordQueuedMessageDrainFailure(harness.deps, { + error: new ApiError(502, "host_unavailable", "Host is not connected"), + now: Date.now(), + row, + thread, + }); + + // Re-queueing is a fresh, successful statement of why the row waits, so + // the row starts its budget over rather than carrying attempts it spent + // against a condition it has since got past. + const requeued = rereadRow(harness, row.id); + expect(requeued.failureReason).toBeNull(); + expect(requeued.failureCount).toBe(0); + expect(requeued.nextAttemptAt).toBeNull(); + }); + }); +}); diff --git a/apps/server/test/threads/requested-queue-drain.test.ts b/apps/server/test/threads/requested-queue-drain.test.ts index 284c555e894..04356a16f0d 100644 --- a/apps/server/test/threads/requested-queue-drain.test.ts +++ b/apps/server/test/threads/requested-queue-drain.test.ts @@ -534,6 +534,8 @@ describe("the requested queue drain", () => { id: failed.id, threadId: thread.id, failureReason: "Terminal failure", + now: Date.now(), + retryDelaysMs: [], }); if (drain === "scheduled") await runTimeWake(harness, Date.now()); diff --git a/apps/server/test/threads/thread-send-dispatch.test.ts b/apps/server/test/threads/thread-send-dispatch.test.ts index 87246e4a001..acab5bdc3a1 100644 --- a/apps/server/test/threads/thread-send-dispatch.test.ts +++ b/apps/server/test/threads/thread-send-dispatch.test.ts @@ -163,8 +163,9 @@ describe("queued message dispatch hook", () => { kind: "automatic", isGroupEligible: createAutomaticQueuedMessageGroupEligibility( harness.deps, - { now: Date.now(), thread }, + { now: Date.now(), retryingFailure: false, thread }, ), + retryingFailure: false, }, threadId: thread.id, queuedMessageId: queued.id, @@ -206,8 +207,9 @@ describe("queued message dispatch hook", () => { kind: "automatic", isGroupEligible: createAutomaticQueuedMessageGroupEligibility( harness.deps, - { now: Date.now(), thread }, + { now: Date.now(), retryingFailure: false, thread }, ), + retryingFailure: false, }, threadId: thread.id, queuedMessageId: queued.id, @@ -245,8 +247,9 @@ describe("queued message auto-send notification", () => { kind: "automatic", isGroupEligible: createAutomaticQueuedMessageGroupEligibility( harness.deps, - { now: Date.now(), thread }, + { now: Date.now(), retryingFailure: false, thread }, ), + retryingFailure: false, }, threadId: thread.id, queuedMessageId: queued.id, @@ -620,6 +623,8 @@ describe("startup queue waits", () => { id: failed.id, threadId: thread.id, failureReason: "Terminal failure", + now: Date.now(), + retryDelaysMs: [], }); setQueuedThreadMessageGroupBoundary({ db: harness.db, @@ -1385,8 +1390,9 @@ describe("service tier execution lifecycle", () => { kind: "automatic", isGroupEligible: createAutomaticQueuedMessageGroupEligibility( harness.deps, - { now: Date.now(), thread }, + { now: Date.now(), retryingFailure: false, thread }, ), + retryingFailure: false, }, threadId: thread.id, queuedMessageId: older.id, diff --git a/packages/db/drizzle/0128_burly_jazinda.sql b/packages/db/drizzle/0128_burly_jazinda.sql new file mode 100644 index 00000000000..c0b6f976183 --- /dev/null +++ b/packages/db/drizzle/0128_burly_jazinda.sql @@ -0,0 +1,2 @@ +ALTER TABLE `queued_thread_messages` ADD `failure_count` integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `queued_thread_messages` ADD `next_attempt_at` integer; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0128_snapshot.json b/packages/db/drizzle/meta/0128_snapshot.json new file mode 100644 index 00000000000..21abfa9eb20 --- /dev/null +++ b/packages/db/drizzle/meta/0128_snapshot.json @@ -0,0 +1,5083 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "4c665fb9-6b38-402a-99fc-4cda889c9090", + "prevId": "6c59f5c6-5254-4535-8f08-708287d8c133", + "tables": { + "app_settings": { + "name": "app_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "caffeinate": { + "name": "caffeinate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_keyboard_hints": { + "name": "show_keyboard_hints", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "steer_active_thread_on_enter": { + "name": "steer_active_thread_on_enter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_unhandled_provider_events": { + "name": "show_unhandled_provider_events", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "codex_memory_enabled": { + "name": "codex_memory_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "claude_code_memory_enabled": { + "name": "claude_code_memory_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "codex_subagents_disabled": { + "name": "codex_subagents_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claude_code_subagents_disabled": { + "name": "claude_code_subagents_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claude_code_workflows_disabled": { + "name": "claude_code_workflows_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "keybinding_overrides": { + "name": "keybinding_overrides", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_settings_values": { + "name": "app_settings_values", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_theme": { + "name": "app_theme", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "theme_id": { + "name": "theme_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "favicon_color": { + "name": "favicon_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "referenceId": { + "name": "referenceId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refillInterval": { + "name": "refillInterval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refillAmount": { + "name": "refillAmount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastRefillAt": { + "name": "lastRefillAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitEnabled": { + "name": "rateLimitEnabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitTimeWindow": { + "name": "rateLimitTimeWindow", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitMax": { + "name": "rateLimitMax", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requestCount": { + "name": "requestCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastRequest": { + "name": "lastRequest", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configId": { + "name": "configId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apikey_key_unique": { + "name": "apikey_key_unique", + "columns": [ + "key" + ], + "isUnique": true + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + "referenceId" + ], + "isUnique": false + }, + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + "configId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "apikey_referenceId_user_id_fk": { + "name": "apikey_referenceId_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "referenceId" + ], + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_hook_operations": { + "name": "environment_hook_operations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_variables": { + "name": "environment_variables", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encryption_version": { + "name": "encryption_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_variables_global_name": { + "name": "environment_variables_global_name", + "columns": [ + "name" + ], + "isUnique": true, + "where": "\"environment_variables\".\"project_id\" IS NULL" + }, + "environment_variables_project_name": { + "name": "environment_variables_project_name", + "columns": [ + "project_id", + "name" + ], + "isUnique": true, + "where": "\"environment_variables\".\"project_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "environment_variables_project_id_projects_id_fk": { + "name": "environment_variables_project_id_projects_id_fk", + "tableFrom": "environment_variables", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_git_repo": { + "name": "is_git_repo", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_worktree": { + "name": "is_worktree", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "merge_base_branch": { + "name": "merge_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_provider_id": { + "name": "environment_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_provider_plugin_id": { + "name": "environment_provider_plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_owns_path": { + "name": "provider_owns_path", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "environment_provider_selection": { + "name": "environment_provider_selection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_provider_instance_key": { + "name": "environment_provider_instance_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retire_at": { + "name": "retire_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teardown_attempt": { + "name": "teardown_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "teardown_status": { + "name": "teardown_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teardown_message": { + "name": "teardown_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_thread_id": { + "name": "owner_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status_message": { + "name": "status_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_log": { + "name": "pending_log", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "claim_path": { + "name": "claim_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisioning'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environments_project_host_path_idx": { + "name": "environments_project_host_path_idx", + "columns": [ + "project_id", + "host_id", + "path" + ], + "isUnique": true + }, + "environments_host_path_lookup_idx": { + "name": "environments_host_path_lookup_idx", + "columns": [ + "host_id", + "path" + ], + "isUnique": false + }, + "environments_owner_thread_idx": { + "name": "environments_owner_thread_idx", + "columns": [ + "owner_thread_id" + ], + "isUnique": true + }, + "environments_claim_idx": { + "name": "environments_claim_idx", + "columns": [ + "host_id", + "claim_path" + ], + "isUnique": false + }, + "environments_project_idx": { + "name": "environments_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "environments_status_idx": { + "name": "environments_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "environments_provider_instance_idx": { + "name": "environments_provider_instance_idx", + "columns": [ + "environment_provider_id", + "environment_provider_instance_key" + ], + "isUnique": false + } + }, + "foreignKeys": { + "environments_project_id_projects_id_fk": { + "name": "environments_project_id_projects_id_fk", + "tableFrom": "environments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environments_host_id_hosts_id_fk": { + "name": "environments_host_id_hosts_id_fk", + "tableFrom": "environments", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "events": { + "name": "events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_thread_id": { + "name": "provider_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_tool_call_id": { + "name": "parent_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "events_thread_sequence_idx": { + "name": "events_thread_sequence_idx", + "columns": [ + "thread_id", + "sequence" + ], + "isUnique": true + }, + "events_delegating_item_lookup_idx": { + "name": "events_delegating_item_lookup_idx", + "columns": [ + "thread_id", + "item_id", + "sequence", + "item_kind" + ], + "isUnique": false, + "where": "\"events\".\"item_kind\" IN ('toolCall', 'delegation')" + }, + "events_plan_steps_thread_sequence_idx": { + "name": "events_plan_steps_thread_sequence_idx", + "columns": [ + "thread_id", + "sequence" + ], + "isUnique": false, + "where": "(\"events\".\"item_kind\" = 'planSteps' AND \"events\".\"type\" = 'item/completed') OR \"events\".\"type\" = 'turn/plan/updated'" + }, + "events_parent_tool_call_thread_parent_sequence_idx": { + "name": "events_parent_tool_call_thread_parent_sequence_idx", + "columns": [ + "thread_id", + "parent_tool_call_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"parent_tool_call_id\" IS NOT NULL" + }, + "events_thread_type_item_kind_sequence_idx": { + "name": "events_thread_type_item_kind_sequence_idx", + "columns": [ + "thread_id", + "type", + "item_kind", + "sequence" + ], + "isUnique": false + }, + "events_background_task_thread_type_item_sequence_idx": { + "name": "events_background_task_thread_type_item_sequence_idx", + "columns": [ + "thread_id", + "type", + "item_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"item_kind\" = 'backgroundTask'" + }, + "events_thread_type_sequence_idx": { + "name": "events_thread_type_sequence_idx", + "columns": [ + "thread_id", + "type", + "sequence" + ], + "isUnique": false + }, + "events_thread_turn_type_item_sequence_idx": { + "name": "events_thread_turn_type_item_sequence_idx", + "columns": [ + "thread_id", + "turn_id", + "type", + "item_id", + "sequence" + ], + "isUnique": false + }, + "events_item_lifecycle_thread_item_sequence_idx": { + "name": "events_item_lifecycle_thread_item_sequence_idx", + "columns": [ + "thread_id", + "item_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"type\" IN ('item/started', 'item/completed', 'item/backgroundTask/completed')" + }, + "events_environment_idx": { + "name": "events_environment_idx", + "columns": [ + "environment_id" + ], + "isUnique": false + }, + "events_provider_identity_idx": { + "name": "events_provider_identity_idx", + "columns": [ + "provider_thread_id", + "created_at" + ], + "isUnique": false, + "where": "\"events\".\"type\" = 'thread/identity'" + }, + "events_completed_item_truncation_idx": { + "name": "events_completed_item_truncation_idx", + "columns": [ + "item_kind", + "created_at", + "id" + ], + "isUnique": false, + "where": "\"events\".\"type\" = 'item/completed'" + }, + "events_thread_state_thread_sequence_idx": { + "name": "events_thread_state_thread_sequence_idx", + "columns": [ + "thread_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"type\" IN ('thread/goal/updated', 'thread/goal/cleared', 'thread/extensionState/updated')" + } + }, + "foreignKeys": { + "events_thread_id_threads_id_fk": { + "name": "events_thread_id_threads_id_fk", + "tableFrom": "events", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "events_environment_id_environments_id_fk": { + "name": "events_environment_id_environments_id_fk", + "tableFrom": "events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "events_scope_shape_check": { + "name": "events_scope_shape_check", + "value": "(\n (\"events\".\"scope_kind\" = 'turn' AND \"events\".\"turn_id\" IS NOT NULL)\n OR\n (\"events\".\"scope_kind\" = 'thread' AND \"events\".\"turn_id\" IS NULL)\n )" + } + } + }, + "host_daemon_sessions": { + "name": "host_daemon_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data_dir": { + "name": "data_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_interval_ms": { + "name": "heartbeat_interval_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lease_timeout_ms": { + "name": "lease_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "closed_at": { + "name": "closed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "host_daemon_sessions_host_status_idx": { + "name": "host_daemon_sessions_host_status_idx", + "columns": [ + "host_id", + "status" + ], + "isUnique": false + }, + "host_daemon_sessions_host_latest_idx": { + "name": "host_daemon_sessions_host_latest_idx", + "columns": [ + "host_id", + "updated_at", + "created_at", + "id" + ], + "isUnique": false + }, + "host_daemon_sessions_closed_prune_idx": { + "name": "host_daemon_sessions_closed_prune_idx", + "columns": [ + "status", + "closed_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_daemon_sessions_host_id_hosts_id_fk": { + "name": "host_daemon_sessions_host_id_hosts_id_fk", + "tableFrom": "host_daemon_sessions", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hosts": { + "name": "hosts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connect_machine_id": { + "name": "connect_machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "machine_provider_id": { + "name": "machine_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "launch_key": { + "name": "launch_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "machine_inputs": { + "name": "machine_inputs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "machine_attempt": { + "name": "machine_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "pending_log": { + "name": "pending_log", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "machine_operation_id": { + "name": "machine_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_access_provider_id": { + "name": "server_access_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_access_grant_id": { + "name": "server_access_grant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "suspended_at": { + "name": "suspended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_message": { + "name": "status_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "suspend_retry_at": { + "name": "suspend_retry_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remove_retry_at": { + "name": "remove_retry_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teardown_attempt": { + "name": "teardown_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "teardown_status": { + "name": "teardown_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_permission_mode": { + "name": "max_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'full'" + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_rejected_protocol_version": { + "name": "last_rejected_protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "hosts_last_seen_idx": { + "name": "hosts_last_seen_idx", + "columns": [ + "last_seen_at" + ], + "isUnique": false + }, + "hosts_live_launch_key_idx": { + "name": "hosts_live_launch_key_idx", + "columns": [ + "launch_key" + ], + "isUnique": true, + "where": "\"hosts\".\"destroyed_at\" is null" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provenance": { + "name": "provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "catalog_marketplace_name": { + "name": "catalog_marketplace_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'path'" + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_builtin_name": { + "name": "source_builtin_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_package": { + "name": "source_npm_package", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_registry": { + "name": "source_npm_registry", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_requested_spec": { + "name": "source_npm_requested_spec", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_spec_kind": { + "name": "source_npm_spec_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_url": { + "name": "source_git_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_subdirectory": { + "name": "source_git_subdirectory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_requested_ref": { + "name": "source_git_requested_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_ref_kind": { + "name": "source_git_ref_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_range": { + "name": "source_git_range", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_tag_prefix": { + "name": "source_git_tag_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_resolved_tag": { + "name": "source_git_resolved_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npm_resolved_version": { + "name": "npm_resolved_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npm_integrity": { + "name": "npm_integrity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_resolved_commit": { + "name": "git_resolved_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_update_check_at": { + "name": "last_update_check_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "available_compatible_version": { + "name": "available_compatible_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "newest_incompatible_version": { + "name": "newest_incompatible_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "update_status_detail": { + "name": "update_status_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_version": { + "name": "last_failure_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_detail": { + "name": "last_failure_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_artifact_id": { + "name": "active_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "normalization_version": { + "name": "normalization_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "root_dir": { + "name": "root_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "removed_at": { + "name": "removed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "plugins_active_artifact_id_plugin_artifacts_id_fk": { + "name": "plugins_active_artifact_id_plugin_artifacts_id_fk", + "tableFrom": "plugins", + "tableTo": "plugin_artifacts", + "columnsFrom": [ + "active_artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "maintenance_scan_cursors": { + "name": "maintenance_scan_cursors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_path": { + "name": "output_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_created_at": { + "name": "last_created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_event_id": { + "name": "last_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "maintenance_scan_cursors_path_idx": { + "name": "maintenance_scan_cursors_path_idx", + "columns": [ + "policy", + "version", + "item_kind", + "output_path" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_interactions": { + "name": "pending_interactions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provider'" + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_thread_id": { + "name": "provider_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_request_id": { + "name": "provider_request_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "renderer_id": { + "name": "renderer_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "pending_interactions_provider_request_idx": { + "name": "pending_interactions_provider_request_idx", + "columns": [ + "provider_id", + "provider_thread_id", + "provider_request_id" + ], + "isUnique": true + }, + "pending_interactions_thread_created_idx": { + "name": "pending_interactions_thread_created_idx", + "columns": [ + "thread_id", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_thread_status_created_idx": { + "name": "pending_interactions_thread_status_created_idx", + "columns": [ + "thread_id", + "status", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_status_created_idx": { + "name": "pending_interactions_status_created_idx", + "columns": [ + "status", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_plugin_status_created_idx": { + "name": "pending_interactions_plugin_status_created_idx", + "columns": [ + "plugin_id", + "status", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "pending_interactions_thread_id_threads_id_fk": { + "name": "pending_interactions_thread_id_threads_id_fk", + "tableFrom": "pending_interactions", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_artifacts": { + "name": "plugin_artifacts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "npm_resolved_version": { + "name": "npm_resolved_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_resolved_commit": { + "name": "git_resolved_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_checkout_root": { + "name": "git_checkout_root", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integrity": { + "name": "integrity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validation_result": { + "name": "validation_result", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validated_at": { + "name": "validated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "plugin_artifacts_plugin_idx": { + "name": "plugin_artifacts_plugin_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_kv": { + "name": "plugin_kv", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_kv_plugin_id_key_pk": { + "columns": [ + "plugin_id", + "key" + ], + "name": "plugin_kv_plugin_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_marketplace_icons": { + "name": "plugin_marketplace_icons", + "columns": { + "marketplace_name": { + "name": "marketplace_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_marketplace_icons_marketplace_name_entry_id_pk": { + "columns": [ + "marketplace_name", + "entry_id" + ], + "name": "plugin_marketplace_icons_marketplace_name_entry_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_marketplaces": { + "name": "plugin_marketplaces", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https'" + }, + "manifest_url": { + "name": "manifest_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_git_ref": { + "name": "source_git_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_commit": { + "name": "source_git_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stats_json": { + "name": "stats_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified": { + "name": "last_modified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_successful_refresh_at": { + "name": "last_successful_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_attempted_refresh_at": { + "name": "last_attempted_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_schedules": { + "name": "plugin_schedules", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_schedules_plugin_id_name_pk": { + "columns": [ + "plugin_id", + "name" + ], + "name": "plugin_schedules_plugin_id_name_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_settings_plugin_id_key_pk": { + "columns": [ + "plugin_id", + "key" + ], + "name": "plugin_settings_plugin_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_state_snapshots": { + "name": "plugin_state_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_artifact_id": { + "name": "from_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "to_artifact_id": { + "name": "to_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_path": { + "name": "snapshot_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "database_path": { + "name": "database_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state_path": { + "name": "state_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets_path": { + "name": "secrets_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registration_path": { + "name": "registration_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rollback_candidate_version": { + "name": "rollback_candidate_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_source_fingerprint": { + "name": "rollback_source_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_bb_version": { + "name": "rollback_bb_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_sdk_version": { + "name": "rollback_sdk_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_detail": { + "name": "rollback_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retained_until": { + "name": "retained_until", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "plugin_state_snapshots_plugin_idx": { + "name": "plugin_state_snapshots_plugin_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "plugin_state_snapshots_retention_idx": { + "name": "plugin_state_snapshots_retention_idx", + "columns": [ + "retained_until" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_attachment_backfills": { + "name": "project_attachment_backfills", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thread_cursor": { + "name": "thread_cursor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_cursor": { + "name": "input_cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_id": { + "name": "input_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_sequence": { + "name": "input_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempted_at": { + "name": "attempted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_attachment_backfills_project_id_projects_id_fk": { + "name": "project_attachment_backfills_project_id_projects_id_fk", + "tableFrom": "project_attachment_backfills", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_attachment_threads": { + "name": "project_attachment_threads", + "columns": { + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_attachment_threads_thread_idx": { + "name": "project_attachment_threads_thread_idx", + "columns": [ + "thread_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "project_attachment_threads_attachment_id_project_attachments_id_fk": { + "name": "project_attachment_threads_attachment_id_project_attachments_id_fk", + "tableFrom": "project_attachment_threads", + "tableTo": "project_attachments", + "columnsFrom": [ + "attachment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_attachment_threads_thread_id_threads_id_fk": { + "name": "project_attachment_threads_thread_id_threads_id_fk", + "tableFrom": "project_attachment_threads", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_attachment_threads_attachment_id_thread_id_pk": { + "columns": [ + "attachment_id", + "thread_id" + ], + "name": "project_attachment_threads_attachment_id_thread_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_attachments": { + "name": "project_attachments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stored_path": { + "name": "stored_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ready_at": { + "name": "ready_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deletion_claimed_at": { + "name": "deletion_claimed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_attachments_project_path_idx": { + "name": "project_attachments_project_path_idx", + "columns": [ + "project_id", + "stored_path" + ], + "isUnique": true + }, + "project_attachments_project_created_idx": { + "name": "project_attachments_project_created_idx", + "columns": [ + "project_id", + "created_at" + ], + "isUnique": false + }, + "project_attachments_deletion_idx": { + "name": "project_attachments_deletion_idx", + "columns": [ + "project_id", + "deletion_claimed_at", + "id" + ], + "isUnique": false, + "where": "\"project_attachments\".\"deletion_claimed_at\" IS NOT NULL" + } + }, + "foreignKeys": { + "project_attachments_project_id_projects_id_fk": { + "name": "project_attachments_project_id_projects_id_fk", + "tableFrom": "project_attachments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_attachments_size_check": { + "name": "project_attachments_size_check", + "value": "\"project_attachments\".\"size_bytes\" >= 0" + } + } + }, + "project_execution_defaults": { + "name": "project_execution_defaults", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_tier": { + "name": "service_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_level": { + "name": "reasoning_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_mode": { + "name": "permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_execution_defaults_project_idx": { + "name": "project_execution_defaults_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_execution_defaults_project_id_projects_id_fk": { + "name": "project_execution_defaults_project_id_projects_id_fk", + "tableFrom": "project_execution_defaults", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_sources": { + "name": "project_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owns_path": { + "name": "owns_path", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_sources_project_idx": { + "name": "project_sources_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "project_sources_host_idx": { + "name": "project_sources_host_idx", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "project_sources_project_host_idx": { + "name": "project_sources_project_host_idx", + "columns": [ + "project_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_sources_project_id_projects_id_fk": { + "name": "project_sources_project_id_projects_id_fk", + "tableFrom": "project_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_sources_host_id_hosts_id_fk": { + "name": "project_sources_host_id_hosts_id_fk", + "tableFrom": "project_sources", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_sources_shape_check": { + "name": "project_sources_shape_check", + "value": "(\n \"project_sources\".\"type\" = 'local_path' AND \"project_sources\".\"host_id\" IS NOT NULL AND \"project_sources\".\"path\" IS NOT NULL\n )" + } + } + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'standard'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "git_remote_url": { + "name": "git_remote_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_key": { + "name": "sort_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'V'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_updated_idx": { + "name": "projects_updated_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "projects_deleted_idx": { + "name": "projects_deleted_idx", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "projects_sort_idx": { + "name": "projects_sort_idx", + "columns": [ + "sort_key", + "id" + ], + "isUnique": false + }, + "projects_personal_singleton_idx": { + "name": "projects_personal_singleton_idx", + "columns": [ + "kind" + ], + "isUnique": true, + "where": "\"projects\".\"kind\" = 'personal'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prompt_history_entries": { + "name": "prompt_history_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "prompt_history_entries_thread_request_idx": { + "name": "prompt_history_entries_thread_request_idx", + "columns": [ + "thread_id", + "request_sequence" + ], + "isUnique": true + }, + "prompt_history_entries_project_scope_created_idx": { + "name": "prompt_history_entries_project_scope_created_idx", + "columns": [ + "project_id", + "scope", + "created_at", + "request_sequence", + "id" + ], + "isUnique": false + }, + "prompt_history_entries_thread_scope_created_idx": { + "name": "prompt_history_entries_thread_scope_created_idx", + "columns": [ + "thread_id", + "scope", + "created_at", + "request_sequence", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "prompt_history_entries_project_id_projects_id_fk": { + "name": "prompt_history_entries_project_id_projects_id_fk", + "tableFrom": "prompt_history_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "prompt_history_entries_thread_id_threads_id_fk": { + "name": "prompt_history_entries_thread_id_threads_id_fk", + "tableFrom": "prompt_history_entries", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "provider_model_catalogs": { + "name": "provider_model_catalogs", + "columns": { + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "models_json": { + "name": "models_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "selected_only_models_json": { + "name": "selected_only_models_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "provider_model_catalogs_host_id_hosts_id_fk": { + "name": "provider_model_catalogs_host_id_hosts_id_fk", + "tableFrom": "provider_model_catalogs", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "provider_model_catalogs_host_id_provider_id_scope_key_pk": { + "columns": [ + "host_id", + "provider_id", + "scope_key" + ], + "name": "provider_model_catalogs_host_id_provider_id_scope_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "queued_thread_messages": { + "name": "queued_thread_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "system_notice": { + "name": "system_notice", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender_thread_id": { + "name": "sender_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_plugin_id": { + "name": "origin_plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_by_initiator": { + "name": "requested_by_initiator", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_by_thread_id": { + "name": "requested_by_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_level": { + "name": "reasoning_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_mode": { + "name": "permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_tier": { + "name": "service_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_with_next": { + "name": "group_with_next", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "send_at": { + "name": "send_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "waiting_on": { + "name": "waiting_on", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wait_holder": { + "name": "wait_holder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_kind": { + "name": "payload_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'inline'" + }, + "retry_of_turn_request_id": { + "name": "retry_of_turn_request_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_attempt": { + "name": "retry_attempt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_reason": { + "name": "retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_key": { + "name": "sort_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "queued_thread_messages_thread_created_idx": { + "name": "queued_thread_messages_thread_created_idx", + "columns": [ + "thread_id", + "created_at", + "id" + ], + "isUnique": false + }, + "queued_thread_messages_thread_sort_idx": { + "name": "queued_thread_messages_thread_sort_idx", + "columns": [ + "thread_id", + "sort_key", + "id" + ], + "isUnique": false + }, + "queued_thread_messages_due_idx": { + "name": "queued_thread_messages_due_idx", + "columns": [ + "send_at", + "id" + ], + "isUnique": false, + "where": "\"queued_thread_messages\".\"send_at\" IS NOT NULL AND \"queued_thread_messages\".\"claimed_at\" IS NULL AND \"queued_thread_messages\".\"claim_token\" IS NULL" + }, + "queued_thread_messages_wait_holder_idx": { + "name": "queued_thread_messages_wait_holder_idx", + "columns": [ + "wait_holder", + "id" + ], + "isUnique": false, + "where": "\"queued_thread_messages\".\"wait_holder\" IS NOT NULL" + } + }, + "foreignKeys": { + "queued_thread_messages_thread_id_threads_id_fk": { + "name": "queued_thread_messages_thread_id_threads_id_fk", + "tableFrom": "queued_thread_messages", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "retained_event_outputs": { + "name": "retained_event_outputs", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "output_path": { + "name": "output_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "retained_event_outputs_expiry_idx": { + "name": "retained_event_outputs_expiry_idx", + "columns": [ + "expires_at", + "event_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "retained_event_outputs_event_id_events_id_fk": { + "name": "retained_event_outputs_event_id_events_id_fk", + "tableFrom": "retained_event_outputs", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_experiments": { + "name": "system_experiments", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "terminal_sessions": { + "name": "terminal_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "daemon_session_id": { + "name": "daemon_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initial_cwd": { + "name": "initial_cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cols": { + "name": "cols", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rows": { + "name": "rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_user_input_at": { + "name": "last_user_input_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "terminal_sessions_thread_status_updated_idx": { + "name": "terminal_sessions_thread_status_updated_idx", + "columns": [ + "thread_id", + "status", + "updated_at" + ], + "isUnique": false + }, + "terminal_sessions_environment_status_idx": { + "name": "terminal_sessions_environment_status_idx", + "columns": [ + "environment_id", + "status" + ], + "isUnique": false + }, + "terminal_sessions_host_status_idx": { + "name": "terminal_sessions_host_status_idx", + "columns": [ + "host_id", + "status" + ], + "isUnique": false + }, + "terminal_sessions_daemon_session_idx": { + "name": "terminal_sessions_daemon_session_idx", + "columns": [ + "daemon_session_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "terminal_sessions_thread_id_threads_id_fk": { + "name": "terminal_sessions_thread_id_threads_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_environment_id_environments_id_fk": { + "name": "terminal_sessions_environment_id_environments_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_host_id_hosts_id_fk": { + "name": "terminal_sessions_host_id_hosts_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_daemon_session_id_host_daemon_sessions_id_fk": { + "name": "terminal_sessions_daemon_session_id_host_daemon_sessions_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "host_daemon_sessions", + "columnsFrom": [ + "daemon_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_conversation_outlines": { + "name": "thread_conversation_outlines", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "projection_key": { + "name": "projection_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "items_json": { + "name": "items_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_conversation_outlines_thread_id_threads_id_fk": { + "name": "thread_conversation_outlines_thread_id_threads_id_fk", + "tableFrom": "thread_conversation_outlines", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_dynamic_context_file_states": { + "name": "thread_dynamic_context_file_states", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_key": { + "name": "file_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_status": { + "name": "content_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shown_at": { + "name": "shown_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_dynamic_context_file_states_thread_file_idx": { + "name": "thread_dynamic_context_file_states_thread_file_idx", + "columns": [ + "thread_id", + "file_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "thread_dynamic_context_file_states_thread_id_threads_id_fk": { + "name": "thread_dynamic_context_file_states_thread_id_threads_id_fk", + "tableFrom": "thread_dynamic_context_file_states", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_image_metadata": { + "name": "thread_image_metadata", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_image_metadata_thread_id_threads_id_fk": { + "name": "thread_image_metadata_thread_id_threads_id_fk", + "tableFrom": "thread_image_metadata", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "thread_image_metadata_thread_id_source_pk": { + "columns": [ + "thread_id", + "source" + ], + "name": "thread_image_metadata_thread_id_source_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_plugin_metadata": { + "name": "thread_plugin_metadata", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_plugin_metadata_thread_id_threads_id_fk": { + "name": "thread_plugin_metadata_thread_id_threads_id_fk", + "tableFrom": "thread_plugin_metadata", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "thread_plugin_metadata_thread_id_plugin_id_pk": { + "columns": [ + "thread_id", + "plugin_id" + ], + "name": "thread_plugin_metadata_thread_id_plugin_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_pruning_cursors": { + "name": "thread_pruning_cursors", + "columns": { + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_thread_id": { + "name": "last_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "current_thread_id": { + "name": "current_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upper_sequence": { + "name": "upper_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cycle": { + "name": "cycle", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "latest_root_sequence": { + "name": "latest_root_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "latest_context_sequence": { + "name": "latest_context_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "probe_event_id": { + "name": "probe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "probe_phase": { + "name": "probe_phase", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "probe_sequence": { + "name": "probe_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "probe_witness_id": { + "name": "probe_witness_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_pruning_cursors_thread_idx": { + "name": "thread_pruning_cursors_thread_idx", + "columns": [ + "thread_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "thread_pruning_cursors_thread_id_threads_id_fk": { + "name": "thread_pruning_cursors_thread_id_threads_id_fk", + "tableFrom": "thread_pruning_cursors", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "thread_pruning_cursors_policy_scope_pk": { + "columns": [ + "policy", + "scope" + ], + "name": "thread_pruning_cursors_policy_scope_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": { + "thread_pruning_cursors_scope_check": { + "name": "thread_pruning_cursors_scope_check", + "value": "\"thread_pruning_cursors\".\"scope\" = coalesce(\"thread_pruning_cursors\".\"thread_id\", '')" + } + } + }, + "thread_search_segments": { + "name": "thread_search_segments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_seq": { + "name": "source_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_search_segments_source_idx": { + "name": "thread_search_segments_source_idx", + "columns": [ + "thread_id", + "source_kind", + "source_key" + ], + "isUnique": true + }, + "thread_search_segments_thread_source_seq_idx": { + "name": "thread_search_segments_thread_source_seq_idx", + "columns": [ + "thread_id", + "source_seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "thread_search_segments_thread_id_threads_id_fk": { + "name": "thread_search_segments_thread_id_threads_id_fk", + "tableFrom": "thread_search_segments", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_sections": { + "name": "thread_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_sections_name_idx": { + "name": "thread_sections_name_idx", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_tabs": { + "name": "thread_tabs", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tabs_json": { + "name": "tabs_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_tabs_thread_id_threads_id_fk": { + "name": "thread_tabs_thread_id_threads_id_fk", + "tableFrom": "thread_tabs", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "threads": { + "name": "threads", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_override": { + "name": "model_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning_level_override": { + "name": "reasoning_level_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title_fallback": { + "name": "title_fallback", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'starting'" + }, + "startup_context": { + "name": "startup_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_thread_id": { + "name": "parent_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lifecycle_owner_thread_id": { + "name": "lifecycle_owner_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_thread_id": { + "name": "source_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_plugin_id": { + "name": "origin_plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'visible'" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_sort_key": { + "name": "pin_sort_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_deleted_at": { + "name": "storage_deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_read_at": { + "name": "last_read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_attention_at": { + "name": "latest_attention_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "threads_project_id_idx": { + "name": "threads_project_id_idx", + "columns": [ + "project_id", + "id" + ], + "isUnique": false + }, + "threads_project_updated_idx": { + "name": "threads_project_updated_idx", + "columns": [ + "project_id", + "updated_at" + ], + "isUnique": false + }, + "threads_project_archived_deleted_idx": { + "name": "threads_project_archived_deleted_idx", + "columns": [ + "project_id", + "archived_at", + "deleted_at", + "id" + ], + "isUnique": false + }, + "threads_pin_sort_idx": { + "name": "threads_pin_sort_idx", + "columns": [ + "archived_at", + "deleted_at", + "pin_sort_key", + "id" + ], + "isUnique": false, + "where": "\"threads\".\"pinned_at\" IS NOT NULL" + }, + "threads_environment_idx": { + "name": "threads_environment_idx", + "columns": [ + "environment_id" + ], + "isUnique": false + }, + "threads_lifecycle_owner_idx": { + "name": "threads_lifecycle_owner_idx", + "columns": [ + "lifecycle_owner_thread_id" + ], + "isUnique": false + }, + "threads_parent_idx": { + "name": "threads_parent_idx", + "columns": [ + "parent_thread_id" + ], + "isUnique": false + }, + "threads_source_origin_idx": { + "name": "threads_source_origin_idx", + "columns": [ + "source_thread_id", + "origin_kind" + ], + "isUnique": false + }, + "threads_origin_plugin_archived_idx": { + "name": "threads_origin_plugin_archived_idx", + "columns": [ + "origin_plugin_id", + "archived_at" + ], + "isUnique": false + }, + "threads_section_archived_deleted_idx": { + "name": "threads_section_archived_deleted_idx", + "columns": [ + "section_id", + "archived_at", + "deleted_at", + "id" + ], + "isUnique": false + }, + "threads_archived_status_idx": { + "name": "threads_archived_status_idx", + "columns": [ + "archived_at", + "status" + ], + "isUnique": false + }, + "threads_environment_archived_deleted_idx": { + "name": "threads_environment_archived_deleted_idx", + "columns": [ + "environment_id", + "archived_at", + "deleted_at" + ], + "isUnique": false + }, + "threads_active_maintenance_idx": { + "name": "threads_active_maintenance_idx", + "columns": [ + "status" + ], + "isUnique": false, + "where": "\"threads\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": { + "threads_project_id_projects_id_fk": { + "name": "threads_project_id_projects_id_fk", + "tableFrom": "threads", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "threads_environment_id_environments_id_fk": { + "name": "threads_environment_id_environments_id_fk", + "tableFrom": "threads", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_section_id_thread_sections_id_fk": { + "name": "threads_section_id_thread_sections_id_fk", + "tableFrom": "threads", + "tableTo": "thread_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_parent_thread_id_threads_id_fk": { + "name": "threads_parent_thread_id_threads_id_fk", + "tableFrom": "threads", + "tableTo": "threads", + "columnsFrom": [ + "parent_thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_lifecycle_owner_thread_id_threads_id_fk": { + "name": "threads_lifecycle_owner_thread_id_threads_id_fk", + "tableFrom": "threads", + "tableTo": "threads", + "columnsFrom": [ + "lifecycle_owner_thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "threads_source_thread_id_threads_id_fk": { + "name": "threads_source_thread_id_threads_id_fk", + "tableFrom": "threads", + "tableTo": "threads", + "columnsFrom": [ + "source_thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value_json": { + "name": "value_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 0b470d2ea80..8a18070fc12 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -897,6 +897,13 @@ "when": 1789925496816, "tag": "0127_thread_image_metadata", "breakpoints": true + }, + { + "idx": 128, + "version": "6", + "when": 1790008317938, + "tag": "0128_burly_jazinda", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/data/index.ts b/packages/db/src/data/index.ts index 288f68fbff5..507db715316 100644 --- a/packages/db/src/data/index.ts +++ b/packages/db/src/data/index.ts @@ -431,6 +431,7 @@ export { listQueuedThreadMessagesForApi, listQueuedThreadMessagesByWaitHolder, listQueuedThreadMessagesWaitingOnKind, + listRetryableFailedQueuedThreadMessages, listThreadIdsWithHostOfflineQueueWaits, releaseQueuedMessageClaim, requeueClaimedQueuedThreadMessages, diff --git a/packages/db/src/data/queued-thread-messages.ts b/packages/db/src/data/queued-thread-messages.ts index 79a029c6a60..05147f2d1c9 100644 --- a/packages/db/src/data/queued-thread-messages.ts +++ b/packages/db/src/data/queued-thread-messages.ts @@ -957,15 +957,23 @@ export type QueuedThreadMessageGroupClaimPolicy = | { kind: "automatic"; isGroupEligible: QueuedThreadMessageGroupEligibility; + /** + * True only for the retry the row's own `next_attempt_at` booked. A recorded + * failure hides a row from every other automatic claim — otherwise the + * idle drain would re-run a failing send every sweep tick — and the one + * claim that must see through it is the retry of that failure. + */ + retryingFailure: boolean; } | { kind: "explicit-send" }; function isAutomaticQueuedThreadMessageGroupClaimAllowed( rows: readonly QueuedThreadMessageRow[], pauseOrdinaryMessages: boolean, + retryingFailure: boolean, ): boolean { return ( - rows.every((row) => row.failureReason === null) && + (retryingFailure || rows.every((row) => row.failureReason === null)) && (!pauseOrdinaryMessages || rows.every((row) => !isOrdinaryTurnEndQueuedMessage(row))) ); @@ -997,6 +1005,7 @@ export function claimQueuedThreadMessageGroup( !isAutomaticQueuedThreadMessageGroupClaimAllowed( group, isThreadQueueAutoSendPaused(tx, existing.threadId), + policy.retryingFailure, )) || (policy.kind === "automatic" && !policy.isGroupEligible(group)) ) { @@ -1049,6 +1058,7 @@ export function claimNextQueuedThreadMessageGroup( isAutomaticQueuedThreadMessageGroupClaimAllowed( rows, pauseOrdinaryMessages, + false, ) ); }) ?? null; @@ -1301,6 +1311,8 @@ export function requeueClaimedQueuedThreadMessages( waitHolder: waitHolderFor(args.waitingOn), sendAt: args.sendAt, failureReason: null, + failureCount: 0, + nextAttemptAt: null, updatedAt: now, }) .where( @@ -1321,8 +1333,12 @@ export function requeueClaimedQueuedThreadMessages( // A re-queue is a fresh, successful statement of why this row is // waiting, which supersedes whatever the previous attempt failed // with. Leaving a stale failure next to a current wait would show - // the user two contradictory explanations of the same row. + // the user two contradictory explanations of the same row, and + // would spend the row's remaining attempts against a failure it + // has since got past. failureReason: null, + failureCount: 0, + nextAttemptAt: null, updatedAt: now, }) .where( @@ -1674,8 +1690,11 @@ export function setQueuedThreadMessageWaitingOn( // successful statement of why this row is waiting supersedes whatever // a previous attempt failed with. Leaving a stale failure beside a // current wait would show the reader two contradictory explanations of - // one row. + // one row, and would spend the row's remaining attempts against a + // failure it has since got past. failureReason: null, + failureCount: 0, + nextAttemptAt: null, updatedAt: Date.now(), }) .where( @@ -1698,6 +1717,14 @@ export interface SetQueuedThreadMessageFailureReasonArgs { id: string; threadId: string; failureReason: string; + now: number; + /** + * How long to wait before each further automatic attempt, indexed by the + * failures already recorded. Running off the end is what makes a failure + * terminal, so the caller decides how many attempts a row gets and how far + * apart — the policy is the server's, the counting is this row's. + */ + retryDelaysMs: readonly number[]; } /** @@ -1708,28 +1735,52 @@ export interface SetQueuedThreadMessageFailureReasonArgs { * is still waiting on whatever it was waiting on, and the failure is a separate * fact about the last attempt rather than a new reason to wait. A later * successful re-queue clears it (see `requeueClaimedQueuedThreadMessages`). + * + * Recording a failure also spends one of the row's attempts and books the next + * one. What failed a dispatch is usually a condition with an end — a provider + * whose plugin is still loading, a workspace mid-rebuild — so the row is owed + * another try before anybody is asked to look at it. It is terminal only once + * `retryDelaysMs` runs out, which is the state `next_attempt_at` NULL records. */ export function setQueuedThreadMessageFailureReason( db: DbConnection, notifier: DbNotifier, args: SetQueuedThreadMessageFailureReasonArgs, ): QueuedThreadMessageRow | null { - const updated = - db - .update(queuedThreadMessages) - .set({ - failureReason: args.failureReason, - updatedAt: Date.now(), - }) - .where( - and( - eq(queuedThreadMessages.id, args.id), - eq(queuedThreadMessages.threadId, args.threadId), - liveQueuedThreadMessage(), - ), - ) - .returning() - .get() ?? null; + const updated = db.transaction( + (tx): QueuedThreadMessageRow | null => { + const existing = getQueuedThreadMessage(tx, args.id); + if ( + !existing || + existing.threadId !== args.threadId || + isQueuedThreadMessageClaimed(existing) + ) { + return null; + } + const failureCount = existing.failureCount + 1; + const delayMs = args.retryDelaysMs[failureCount - 1]; + return ( + tx + .update(queuedThreadMessages) + .set({ + failureReason: args.failureReason, + failureCount, + nextAttemptAt: delayMs === undefined ? null : args.now + delayMs, + updatedAt: args.now, + }) + .where( + and( + eq(queuedThreadMessages.id, args.id), + eq(queuedThreadMessages.threadId, args.threadId), + liveQueuedThreadMessage(), + ), + ) + .returning() + .get() ?? null + ); + }, + { behavior: "immediate" }, + ); if (updated) { notifier.notifyThread(args.threadId, ["queue-changed"]); @@ -1737,6 +1788,47 @@ export function setQueuedThreadMessageFailureReason( return updated; } +/** + * Every live row whose booked retry has come due, oldest first. + * + * Deliberately not filtered by wait: the retry is not the wait's wake firing + * again, it is core re-asking the whole question from scratch, which is the + * only thing that can move a row whose wait went stale while it sat failed + * (a `host-offline` row whose host came back during the failure, say). Rows + * on archived or deleted threads are excluded for the same reason the due + * sweep excludes them: nobody is waiting for those to send. + */ +export function listRetryableFailedQueuedThreadMessages( + db: DbQueryConnection, + now: number, +): QueuedThreadMessageRow[] { + return db + .select() + .from(queuedThreadMessages) + .where( + and( + isNotNull(queuedThreadMessages.failureReason), + isNotNull(queuedThreadMessages.nextAttemptAt), + lte(queuedThreadMessages.nextAttemptAt, now), + liveQueuedThreadMessage(), + exists( + db + .select({ live: sql`1` }) + .from(threads) + .where( + and( + eq(threads.id, queuedThreadMessages.threadId), + isNull(threads.archivedAt), + isNull(threads.deletedAt), + ), + ), + ), + ), + ) + .orderBy(asc(queuedThreadMessages.nextAttemptAt), asc(queuedThreadMessages.id)) + .all(); +} + /** * Drop a live row's wait, leaving it an ordinary queued row eligible at the * next drain. `sendAt` is cleared with it: a row with no wait is not waiting diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 5ff43ebcb9e..857d59806de 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -1030,6 +1030,17 @@ export const queuedThreadMessages = sqliteTable( // row stays waiting on whatever it was waiting on; this only says what went // wrong the last time the drain tried to send it. failureReason: text("failure_reason"), + // How many drain attempts in a row have failed, and when the next + // automatic one may run. Together they make a failure a bounded retry + // instead of a terminal state: the condition that failed a dispatch is + // usually the one a restart just created, so the row goes again on a + // widening delay and only stops when the budget is spent. `next_attempt_at` + // NULL beside a non-NULL `failure_reason` IS that spent budget — the row + // now waits for a person. A fresh, successful statement of the row's wait + // resets both, because the attempt that wrote it learned something newer + // than the failure did. + failureCount: integer("failure_count").notNull().default(0), + nextAttemptAt: integer("next_attempt_at"), payloadKind: text("payload_kind") .$type() .notNull() diff --git a/packages/db/test/data/queued-thread-messages.test.ts b/packages/db/test/data/queued-thread-messages.test.ts index c50ff78b906..dd294c42c6d 100644 --- a/packages/db/test/data/queued-thread-messages.test.ts +++ b/packages/db/test/data/queued-thread-messages.test.ts @@ -868,6 +868,7 @@ describe("queued thread messages", () => { claimQueuedThreadMessageGroup(db, noopNotifier, ordinary.id, { kind: "automatic", isGroupEligible: () => true, + retryingFailure: false, }), ).toBeNull(); expect( @@ -931,6 +932,7 @@ describe("queued thread messages", () => { claimQueuedThreadMessageGroup(db, noopNotifier, heldBack.id, { kind: "automatic", isGroupEligible: () => true, + retryingFailure: false, }), ).toBeNull(); expect( diff --git a/packages/db/test/migrate.test.ts b/packages/db/test/migrate.test.ts index 55ad087a00b..32bab3f4480 100644 --- a/packages/db/test/migrate.test.ts +++ b/packages/db/test/migrate.test.ts @@ -780,7 +780,20 @@ function rewindEnvironmentProvisioningMigration(db: DbConnection): void { ); } +function dropQueuedMessageAttemptColumns(db: DbConnection): void { + const columns = db.$client + .prepare<[], TableInfoRow>("PRAGMA table_info(queued_thread_messages)") + .all(); + for (const name of ["failure_count", "next_attempt_at"]) { + if (!columns.some((column) => column.name === name)) continue; + db.$client + .prepare(`ALTER TABLE queued_thread_messages DROP COLUMN ${name}`) + .run(); + } +} + function dropQueueReworkSchema(db: DbConnection): void { + dropQueuedMessageAttemptColumns(db); rewindEnvironmentProvisioningMigration(db); // Indexes first: SQLite refuses to drop a column an existing index names. for (const index of [ @@ -5708,6 +5721,7 @@ describe("environment providers migration", () => { db.$client.prepare("DROP TABLE retained_event_outputs").run(); rewindEnvironmentRowFactsMigration(db); rewindEnvironmentProvidersMigration(db); + dropQueuedMessageAttemptColumns(db); db.$client .prepare<[number]>( "DELETE FROM __drizzle_migrations WHERE created_at >= ?", @@ -6121,6 +6135,7 @@ describe("environment and thread startup ownership migration", () => { try { rewindMachineProvidersMigration(db); rewindEnvironmentProvisioningMigration(db); + dropQueuedMessageAttemptColumns(db); const legacySchema = readFileSync( resolve( dirname(fileURLToPath(import.meta.url)), diff --git a/packages/templates/src/templates/bb-guide-threads.md b/packages/templates/src/templates/bb-guide-threads.md index e4853f55c1e..027dbd8da5e 100644 --- a/packages/templates/src/templates/bb-guide-threads.md +++ b/packages/templates/src/templates/bb-guide-threads.md @@ -367,6 +367,11 @@ Queued messages: queued row in the workspace; `--wait-holder plugin:` narrows it to the rows one plugin is holding. + Failed rows show their failure reason instead of their previous wait, followed + by a recovery command: `bb thread queue send `. + Use it to retry immediately, including after automatic retries are exhausted. + Editing the message does not clear its failure or trigger a retry. + `queue send` dispatches a row now, bypassing every plugin wait and its own schedule — the invariants (a running turn, an unfinished workspace, an unanswered interaction) still apply, and a message that hits one simply queues diff --git a/plugins/bb-guide/skills/bb-cli/references/thread-operation.md b/plugins/bb-guide/skills/bb-cli/references/thread-operation.md index 88984cd9adf..f96de70f6af 100644 --- a/plugins/bb-guide/skills/bb-cli/references/thread-operation.md +++ b/plugins/bb-guide/skills/bb-cli/references/thread-operation.md @@ -58,6 +58,10 @@ and `Send at` columns. Several queued rows on one thread are normal. The SDK equivalents are `threads.queue.list` (cross-thread) and `threads.queuedMessages.list/send/update/delete` (one thread). +- Failed queue rows show the failure reason and an exact recovery command. + Use `bb thread queue send ` to retry immediately, + including after automatic retries are exhausted. Editing does not clear a + failure or trigger a retry; send still respects core readiness requirements. - `bb thread queue send --mode steer` re-attempts the row as a steer with the same send-now behavior: it bypasses the row's schedule and plugin waits, while core waits still apply. During provisioning it reports