diff --git a/.gitattributes b/.gitattributes index 6313b56c578..673d126db59 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * text=auto eol=lf +packages/db/drizzle/meta/*_snapshot.json linguist-generated=true diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index 4bc8707e8c0..291e32d344b 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -69,6 +69,12 @@ import { ProviderCliInstallLogDialogHost } from "./components/provider-cli/provi import { ServerMoveOverlay } from "./components/machines/ServerMoveOverlay"; import { RouteLoadingSkeleton } from "./components/ui/route-loading-skeleton"; +const PendingThreadMessagesSync = lazy(() => + import("@/lib/PendingThreadMessagesSync").then((module) => ({ + default: module.PendingThreadMessagesSync, + })), +); + const SettingsView = lazy(() => import("./views/SettingsView").then((m) => ({ default: m.SettingsView, @@ -433,6 +439,9 @@ export function App() { + + + entry.row.id === queuedMessage.id, + ) || queuedMessageHasWaitLine(queuedMessage) || queuedMessage.id === processingMessageId ? queuedMessage.initiator === "agent" && @@ -703,18 +711,27 @@ function QueuedMessageWaitLine({ pluginDisplayName: string; queuedMessage: ThreadQueuedMessage; }) { + const pending = usePendingThreadMessages().find( + (entry) => entry.row.id === queuedMessage.id, + ); const now = useSecondTick(); - const label = describeQueuedMessageWait({ - failureReason: queuedMessage.failureReason, - now, - payload: queuedMessage.payload, - pluginDisplayName, - sendAt: queuedMessage.sendAt, - waitingOn: queuedMessage.waitingOn, - }); + const label = pending + ? (pending.error ?? "Connecting to server") + : describeQueuedMessageWait({ + failureReason: queuedMessage.failureReason, + now, + payload: queuedMessage.payload, + pluginDisplayName, + sendAt: queuedMessage.sendAt, + waitingOn: queuedMessage.waitingOn, + }); if (label === null) return null; - const failed = queuedMessage.failureReason !== null; - const icon = queuedMessageWaitIcon(queuedMessage); + const failed = pending?.error != null || queuedMessage.failureReason !== null; + const icon = pending + ? failed + ? "AlertCircle" + : "Loading" + : queuedMessageWaitIcon(queuedMessage); const countdownInstant = queuedMessageCountdownInstant(queuedMessage); const countdown = countdownInstant === null @@ -730,7 +747,11 @@ function QueuedMessageWaitLine({ )} > {icon !== null ? ( - + ) : queuedMessage.waitingOn?.kind === "plugin" ? ( entry.row.id === queuedMessage.id, + ); + sendDisabled ||= pending !== undefined; + dragDisabled ||= pending !== undefined; + const deleteMessage = pending + ? () => removePendingThreadMessage(queuedMessage.id) + : () => onDelete(queuedMessage.id); const actionsRef = useRef(null); const focusActionsOnExpandRef = useRef(false); useLayoutEffect(() => { @@ -800,7 +829,8 @@ const QueuedMessageRow = memo(function QueuedMessageRow({ ? queuedMessage.waitingOn.pluginId : "", ); - const hasWaitLine = queuedMessageHasWaitLine(queuedMessage); + const hasWaitLine = + pending !== undefined || queuedMessageHasWaitLine(queuedMessage); const sendAllowed = sendAction === "steer-when-ready" || isQueuedMessageSendNowAllowed(queuedMessage.waitingOn); @@ -997,7 +1027,7 @@ const QueuedMessageRow = memo(function QueuedMessageRow({ "shrink-0 text-muted-foreground", compact ? "size-7" : "size-8", )} - disabled={actionDisabled} + disabled={actionDisabled || pending !== undefined} onClick={() => onEdit({ queuedMessageId: queuedMessage.id, @@ -1024,8 +1054,11 @@ const QueuedMessageRow = memo(function QueuedMessageRow({ "shrink-0 text-muted-foreground hover:text-destructive max-md:text-destructive", compact ? "size-7" : "size-8", )} - disabled={actionDisabled} - onClick={() => onDelete(queuedMessage.id)} + disabled={ + actionDisabled || + (pending !== undefined && pending.error === null) + } + onClick={deleteMessage} aria-label={`Delete queued message ${index + 1}`} > @@ -1240,6 +1273,7 @@ export function QueuedMessagesList({ onEdit, onDelete, }: QueuedMessagesListProps) { + const pendingMessages = usePendingThreadMessages(); const senderThreadMetadataById = useSenderThreadMetadataById(); const processingLabel = processingAction === "edit" @@ -1455,7 +1489,12 @@ export function QueuedMessagesList({ ]; }, [groupBoundaryIndex, orderedMessages]); const sortingDisabled = - actionDisabled || processingMessageId !== null || queuedMessages.length < 2; + actionDisabled || + processingMessageId !== null || + queuedMessages.length < 2 || + queuedMessages.some((row) => + pendingMessages.some((entry) => entry.row.id === row.id), + ); const sortableIds = useMemo( () => inlineEditor diff --git a/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx b/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx index e3da67a42a0..8a540447f52 100644 --- a/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx +++ b/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx @@ -1,6 +1,13 @@ // @vitest-environment jsdom -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { + act, + cleanup, + fireEvent, + render, + screen, +} from "@testing-library/react"; +import { createDeferredPromise } from "@bb/test-helpers"; import { useEffect, useLayoutEffect, type ReactNode } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { FollowUpComposerProps } from "@/components/promptbox/FollowUpPromptBox"; @@ -295,6 +302,9 @@ vi.mock("@/hooks/queries/system-queries", () => ({ }), })); +vi.mock("@/hooks/useRetainThreadMessage", () => ({ + useRetainThreadMessage: () => ({ connected: true, retain: () => false }), +})); vi.mock("@/hooks/mutations/thread-runtime-mutations", () => ({ useCreateThreadQueuedMessage: () => ({ mutateAsync: mocks.createQueuedMessageMutateAsync, @@ -512,9 +522,39 @@ describe("EmbeddedThreadChat", () => { }), ); expect(mocks.sendThreadMessageMutateAsync).not.toHaveBeenCalled(); + await vi.waitFor(() => { + expect( + screen.getByTestId("embedded-chat-composer").value, + ).toBe(""); + }); + }); + + it("keeps a queued draft when the request fails after the composer closes", async () => { + mocks.threadRuntimeDisplayStatus = "active"; + const pending = createDeferredPromise(); + mocks.createQueuedMessageMutateAsync.mockReturnValueOnce(pending.promise); + const first = renderEmbeddedChat(); + fireEvent.change(screen.getByTestId("embedded-chat-composer"), { + target: { value: "Do not lose this" }, + }); + fireEvent.click(screen.getByText("Send")); expect( screen.getByTestId("embedded-chat-composer").value, - ).toBe(""); + ).toBe("Do not lose this"); + first.unmount(); + await act(async () => { + pending.reject(new Error("Connection lost")); + }); + renderEmbeddedChat(); + expect( + screen.getByTestId("embedded-chat-composer").value, + ).toBe("Do not lose this"); + fireEvent.click(screen.getByText("Send")); + await vi.waitFor(() => { + expect( + screen.getByTestId("embedded-chat-composer").value, + ).toBe(""); + }); }); it("sends directly when the thread runtime is idle", async () => { diff --git a/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.tsx b/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.tsx index 63a54c76816..e38f8ae5e02 100644 --- a/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.tsx +++ b/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.tsx @@ -55,6 +55,8 @@ import { } from "@/hooks/queries/thread-queries"; import { useThreadDefaultExecutionOptions } from "@/hooks/queries/thread-default-execution-options-query"; import { useSystemConfig } from "@/hooks/queries/system-queries"; +import { useRetainThreadMessage } from "@/hooks/useRetainThreadMessage"; +import { usePendingQueuedMessages } from "@/lib/pending-thread-messages"; import { useCreateThreadQueuedMessage, useSendThreadMessage, @@ -231,6 +233,7 @@ function EmbeddedThreadChatWithComposer({ composer, }: EmbeddedThreadChatComposerModeProps) { const systemConfigQuery = useSystemConfig(); + const pendingMessages = useRetainThreadMessage(); const steerActiveThreadOnEnter = systemConfigQuery.data?.generalSettings.steerActiveThreadOnEnter ?? defaultAppSettings.steerActiveThreadOnEnter; @@ -238,7 +241,7 @@ function EmbeddedThreadChatWithComposer({ const markThreadRead = useMarkThreadRead(); const stopThread = useStopThread(); const sendThreadMessage = useSendThreadMessage(); - const createQueuedMessage = useCreateThreadQueuedMessage(); + const createQueuedMessage = useCreateThreadQueuedMessage(threadId); const threadQuery = useThread(threadId); const pendingInteractionsQuery = useThreadPendingInteractions(threadId); const activePendingInteraction = getLatestPendingInteraction( @@ -261,7 +264,11 @@ function EmbeddedThreadChatWithComposer({ markThreadRead, thread: threadQuery.data, }); - const { data: queuedMessages = [] } = useThreadQueuedMessages(threadId); + const { data: serverQueuedMessages } = useThreadQueuedMessages(threadId); + const queuedMessages = usePendingQueuedMessages( + threadId, + serverQueuedMessages, + ); const executionOptionsQuery = useThreadDefaultExecutionOptions( composer.executionDefaultsThreadId, @@ -342,7 +349,8 @@ function EmbeddedThreadChatWithComposer({ ); const [composerFocusNonce, setComposerFocusNonce] = useState(0); const [inlineComposerFocusNonce, setInlineComposerFocusNonce] = useState(0); - const [isTurnSubmitting, setIsTurnSubmitting] = useState(false); + const [isSending, setIsTurnSubmitting] = useState(false); + const isTurnSubmitting = isSending || createQueuedMessage.isPending; const isMountedRef = useRef(false); useEffect(() => { isMountedRef.current = true; @@ -440,17 +448,21 @@ function EmbeddedThreadChatWithComposer({ const submitMode = useMemo( () => - buildSideChatSubmitMode({ - childThreadId: threadId, - hasPendingInteraction: hasComposerBlockingPendingInteraction, - isDefaultExecutionOptionsLoading, - isPendingInteractionsInitialLoading: - pendingInteractionsInitialLoading || pendingInteractionsUnavailable, - isStopRequested, - onStop: handleStopThread, - runtimeDisplayStatus: displayStatus, - }), + !pendingMessages.connected && !shouldQueueFollowUpMessage(displayStatus) + ? { kind: "blocked", reason: "unavailable" } + : buildSideChatSubmitMode({ + childThreadId: threadId, + hasPendingInteraction: hasComposerBlockingPendingInteraction, + isDefaultExecutionOptionsLoading, + isPendingInteractionsInitialLoading: + pendingInteractionsInitialLoading || + pendingInteractionsUnavailable, + isStopRequested, + onStop: handleStopThread, + runtimeDisplayStatus: displayStatus, + }), [ + pendingMessages.connected, displayStatus, hasComposerBlockingPendingInteraction, handleStopThread, @@ -494,15 +506,43 @@ function EmbeddedThreadChatWithComposer({ if (submittedInput.length === 0 || isTurnSubmitting) { return; } - promptDraft.clearIfCurrentMatches(submittedDraft); + const isQueuingMessage = shouldQueueFollowUpMessage(displayStatus); + if (!isQueuingMessage && !pendingMessages.connected) return; + try { + if ( + pendingMessages.retain({ + request: { + id: threadId, + input: submittedInput, + ...executionRequestFields, + }, + operation: isQueuingMessage ? "queue" : "send", + }) + ) { + promptDraft.clearIfCurrentMatches(submittedDraft); + setBottomAttachmentError(null); + return; + } + } catch (error) { + showMutationErrorToast({ + error, + fallbackMessage: "Could not save message on this device", + lifecycleOperation: "queue_message", + }); + return; + } + if (!isQueuingMessage) promptDraft.clearIfCurrentMatches(submittedDraft); setBottomAttachmentError(null); setIsTurnSubmitting(true); void defaultSendOrQueueInput(submittedInput) + .then(() => { + if (isQueuingMessage) promptDraft.clearIfCurrentMatches(submittedDraft); + }) .catch((error) => { if (!isMountedRef.current) { return; } - promptDraft.restoreIfEmpty(submittedDraft); + if (!isQueuingMessage) promptDraft.restoreIfEmpty(submittedDraft); showMutationErrorToast({ error, fallbackMessage: "Failed to send message", @@ -517,6 +557,9 @@ function EmbeddedThreadChatWithComposer({ } }); }, [ + pendingMessages, + threadId, + executionRequestFields, currentPromptDraft, currentPromptDraftInput, defaultSendOrQueueInput, @@ -566,6 +609,29 @@ function EmbeddedThreadChatWithComposer({ return; } + try { + if ( + pendingMessages.retain({ + request: { + id: threadId, + input: submittedInput, + ...executionRequestFields, + }, + operation: "steer", + }) + ) { + promptDraft.clearIfCurrentMatches(submittedDraft); + setBottomAttachmentError(null); + return; + } + } catch (error) { + showMutationErrorToast({ + error, + fallbackMessage: "Could not save message on this device", + lifecycleOperation: "send_message", + }); + return; + } promptDraft.clearIfCurrentMatches(submittedDraft); setBottomAttachmentError(null); setIsTurnSubmitting(true); @@ -600,6 +666,7 @@ function EmbeddedThreadChatWithComposer({ currentPromptDraft, currentPromptDraftInput, executionRequestFields, + pendingMessages, promptDraft, queuedMessages, sendQueuedMessageById, diff --git a/apps/app/src/hooks/cache-owners/thread-runtime-cache-owner.ts b/apps/app/src/hooks/cache-owners/thread-runtime-cache-owner.ts index 695a69c1c41..97a3011ea67 100644 --- a/apps/app/src/hooks/cache-owners/thread-runtime-cache-owner.ts +++ b/apps/app/src/hooks/cache-owners/thread-runtime-cache-owner.ts @@ -390,7 +390,7 @@ function getCachedDefaultExecutionOptions( ); } -function buildOptimisticQueuedMessage({ +export function buildOptimisticQueuedMessage({ createdAt, queryClient, request, diff --git a/apps/app/src/hooks/mutations/thread-runtime-mutations.test.tsx b/apps/app/src/hooks/mutations/thread-runtime-mutations.test.tsx index 78052714adb..b9be11c6f90 100644 --- a/apps/app/src/hooks/mutations/thread-runtime-mutations.test.tsx +++ b/apps/app/src/hooks/mutations/thread-runtime-mutations.test.tsx @@ -453,6 +453,47 @@ describe("thread runtime mutations", () => { ).toEqual([]); }); + it("keeps queue submission pending across remounts and permits retry after failure", async () => { + const { wrapper } = createQueryClientTestHarness(); + const pending = createDeferredPromise(); + vi.mocked(sdk.threads.queuedMessages.create).mockReturnValueOnce( + pending.promise, + ); + const first = renderHook(() => useCreateThreadQueuedMessage("thread-1"), { + wrapper, + }); + const request = { + id: "thread-1", + input: [{ type: "text" as const, text: "Keep me", mentions: [] }], + }; + let sent: Promise; + act(() => { + sent = first.result.current.mutateAsync(request).catch((error) => error); + }); + await waitFor(() => + expect(sdk.threads.queuedMessages.create).toHaveBeenCalledTimes(1), + ); + first.unmount(); + const reopened = renderHook( + () => useCreateThreadQueuedMessage("thread-1"), + { wrapper }, + ); + expect(reopened.result.current.isPending).toBe(true); + await expect(reopened.result.current.mutateAsync(request)).rejects.toThrow( + "still being queued", + ); + expect(sdk.threads.queuedMessages.create).toHaveBeenCalledTimes(1); + await act(async () => { + pending.reject(new Error("Connection lost")); + await sent; + }); + await waitFor(() => expect(reopened.result.current.isPending).toBe(false)); + await act(async () => { + await reopened.result.current.mutateAsync(request); + }); + expect(sdk.threads.queuedMessages.create).toHaveBeenCalledTimes(2); + }); + it("forwards execution input sources and sender thread when queueing a message", async () => { const { wrapper } = createQueryClientTestHarness(); const { result } = renderHook(() => useCreateThreadQueuedMessage(), { diff --git a/apps/app/src/hooks/mutations/thread-runtime-mutations.ts b/apps/app/src/hooks/mutations/thread-runtime-mutations.ts index 5494d1e7ce0..707e1c928b9 100644 --- a/apps/app/src/hooks/mutations/thread-runtime-mutations.ts +++ b/apps/app/src/hooks/mutations/thread-runtime-mutations.ts @@ -1,5 +1,9 @@ import { notifyComposerSubmitted } from "@/lib/composer-submissions"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + useIsMutating, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; import type { ThreadQueuedMessage } from "@bb/domain"; import type { CreateQueuedMessageRequest, @@ -245,10 +249,13 @@ export function useEditThreadMessage() { }); } -export function useCreateThreadQueuedMessage() { +export function useCreateThreadQueuedMessage(threadId?: string) { const queryClient = useQueryClient(); + const mutationKey = ["create-thread-queued-message", threadId]; + const isPending = useIsMutating({ mutationKey, exact: true }) > 0; - return useMutation({ + const mutation = useMutation({ + mutationKey, meta: { errorMessage: "Failed to queue message.", lifecycleOperation: "queue_message", @@ -296,6 +303,20 @@ export function useCreateThreadQueuedMessage() { }); }, }); + + return { + ...mutation, + isPending: threadId === undefined ? mutation.isPending : isPending, + mutateAsync: (...args: Parameters) => { + if ( + threadId !== undefined && + queryClient.isMutating({ mutationKey, exact: true }) + ) { + return Promise.reject(new Error("A message is still being queued.")); + } + return mutation.mutateAsync(...args); + }, + }; } export function useUpdateThreadQueuedMessage() { diff --git a/apps/app/src/hooks/useRetainThreadMessage.ts b/apps/app/src/hooks/useRetainThreadMessage.ts new file mode 100644 index 00000000000..161650ddaea --- /dev/null +++ b/apps/app/src/hooks/useRetainThreadMessage.ts @@ -0,0 +1,34 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useSystemConfig } from "./queries/system-queries"; +import { useServerConnectionState } from "./useServerConnectionState"; +import { retainThreadMessage } from "@/lib/pending-thread-messages"; +import { notifyComposerSubmitted } from "@/lib/composer-submissions"; + +export function useRetainThreadMessage() { + const queryClient = useQueryClient(); + const { data: config } = useSystemConfig(); + const connection = useServerConnectionState(); + return { + connected: !config?.messageSubmissionKeys || connection === "connected", + retain( + args: Omit[0], "queryClient">, + ): boolean { + if ( + !config?.messageSubmissionKeys || + args.request.input.some( + (block) => + block.type === "text" && + block.mentions.some( + (mention) => + mention.resource.kind === "command" && + mention.resource.source === "command", + ), + ) + ) + return false; + retainThreadMessage({ ...args, queryClient }); + notifyComposerSubmitted({ kind: "thread", threadId: args.request.id }); + return true; + }, + }; +} diff --git a/apps/app/src/lib/PendingThreadMessagesSync.test.tsx b/apps/app/src/lib/PendingThreadMessagesSync.test.tsx new file mode 100644 index 00000000000..c45b41e4efd --- /dev/null +++ b/apps/app/src/lib/PendingThreadMessagesSync.test.tsx @@ -0,0 +1,78 @@ +// @vitest-environment jsdom +import { act, cleanup, render, waitFor } from "@testing-library/react"; +import { afterEach, expect, it, vi } from "vitest"; +import { createQueryClientTestHarness } from "@/test/queryClientTestHarness"; +import { PendingThreadMessagesSync } from "./PendingThreadMessagesSync"; +import { + getPendingThreadMessages, + retainThreadMessage, +} from "./pending-thread-messages"; +import { sdk } from "./sdk"; + +vi.mock("@/hooks/queries/system-queries", () => ({ + useSystemConfig: () => ({ data: { messageSubmissionKeys: true } }), +})); +vi.mock("@/hooks/useServerConnectionState", () => ({ + useServerConnectionState: () => "connected", +})); +vi.mock("./ws", () => ({ + wsManager: { getConnectionState: () => "connected" }, +})); +vi.mock("./sdk", async (importOriginal) => ({ + ...(await importOriginal()), + sdk: { threads: { send: vi.fn(), queuedMessages: { create: vi.fn() } } }, +})); + +afterEach(() => { + cleanup(); + localStorage.clear(); + window.dispatchEvent(new StorageEvent("storage", { key: null })); + vi.clearAllMocks(); +}); + +it.each(["queue", "send", "steer"] as const)( + "retains an uncertain %s and retries with the same submission key until confirmed", + async (operation) => { + const { queryClient, wrapper } = createQueryClientTestHarness(); + vi.mocked(sdk.threads.queuedMessages.create) + .mockRejectedValueOnce(new TypeError("Response lost")) + .mockImplementationOnce(async (request) => ({ + ...getPendingThreadMessages()[0]!.row, + clientSubmissionId: request.clientSubmissionId, + })); + vi.mocked(sdk.threads.send).mockImplementation(async (request) => ({ + ok: true, + delivery: "queued", + queuedMessage: await sdk.threads.queuedMessages.create(request), + })); + render(, { wrapper }); + act(() => + retainThreadMessage({ + queryClient, + operation, + request: { + id: "thread-1", + input: [{ type: "text", text: "saved locally", mentions: [] }], + }, + }), + ); + await waitFor(() => + expect(sdk.threads.queuedMessages.create).toHaveBeenCalledTimes(1), + ); + expect(getPendingThreadMessages()).toHaveLength(1); + await waitFor(() => expect(getPendingThreadMessages()).toHaveLength(0), { + timeout: 3000, + }); + const calls = vi.mocked(sdk.threads.queuedMessages.create).mock.calls; + expect(calls).toHaveLength(2); + expect(calls[1]![0].clientSubmissionId).toBe(calls[0]![0].clientSubmissionId); + if (operation !== "queue") { + expect(sdk.threads.send).toHaveBeenCalledWith( + expect.objectContaining({ + mode: operation === "steer" ? "steer-if-active" : "queue-if-active", + }), + ); + } + queryClient.clear(); + }, +); diff --git a/apps/app/src/lib/PendingThreadMessagesSync.tsx b/apps/app/src/lib/PendingThreadMessagesSync.tsx new file mode 100644 index 00000000000..3703e81af8a --- /dev/null +++ b/apps/app/src/lib/PendingThreadMessagesSync.tsx @@ -0,0 +1,96 @@ +import { useEffect } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useServerConnectionState } from "@/hooks/useServerConnectionState"; +import { useSystemConfig } from "@/hooks/queries/system-queries"; +import { invalidateThreadQueuedMessageListQuery } from "@/hooks/cache-owners/mutation-cache-effects"; +import { BbHttpError, sdk } from "./sdk"; +import { wsManager } from "./ws"; +import { + removePendingThreadMessage, + savePendingThreadMessage, + usePendingThreadMessages, + type PendingThreadMessage, +} from "./pending-thread-messages"; + +function retryable(error: unknown): boolean { + return ( + !(error instanceof BbHttpError) || + error.status >= 500 || + error.status === 429 || + error.status === 408 + ); +} + +function PendingDelivery({ entry }: { entry: PendingThreadMessage }) { + const queryClient = useQueryClient(); + const connection = useServerConnectionState(); + const mutation = useMutation({ + mutationKey: ["deliver-pending-thread-message", entry.row.id], + meta: { showErrorToast: false }, + retry: (_count, error) => + retryable(error) && wsManager.getConnectionState() === "connected", + mutationFn: async () => { + if (wsManager.getConnectionState() !== "connected") + throw new Error("Waiting for connection"); + const { id, ...request } = entry.request; + const signal = AbortSignal.timeout(30_000); + const result = + entry.operation === "queue" + ? await sdk.threads.queuedMessages.create({ + ...request, + threadId: id, + signal, + }) + : await sdk.threads + .send({ + ...request, + threadId: id, + mode: + entry.operation === "steer" + ? "steer-if-active" + : "queue-if-active", + signal, + }) + .then((result) => + result.delivery === "queued" ? result.queuedMessage : null, + ); + if (result?.clientSubmissionId !== request.clientSubmissionId) + throw new Error("Waiting for server confirmation"); + invalidateThreadQueuedMessageListQuery({ queryClient, threadId: id }); + removePendingThreadMessage(entry.row.id); + }, + onError: (error) => { + if (!retryable(error)) + savePendingThreadMessage({ + ...entry, + error: + error instanceof Error ? error.message : "Message was rejected", + }); + }, + }); + const { mutate } = mutation; + useEffect(() => { + if ( + connection === "connected" && + queryClient.isMutating({ + mutationKey: ["deliver-pending-thread-message", entry.row.id], + }) === 0 + ) + mutate(); + }, [connection, mutate, queryClient, entry.row.id]); + return null; +} + +export function PendingThreadMessagesSync() { + const entries = usePendingThreadMessages(); + const { data: config } = useSystemConfig(); + if (!config?.messageSubmissionKeys) return null; + const threads = new Set(); + return entries + .filter((entry) => { + if (entry.error || threads.has(entry.request.id)) return false; + threads.add(entry.request.id); + return true; + }) + .map((entry) => ); +} diff --git a/apps/app/src/lib/pending-thread-messages.test.tsx b/apps/app/src/lib/pending-thread-messages.test.tsx new file mode 100644 index 00000000000..95b866e93a3 --- /dev/null +++ b/apps/app/src/lib/pending-thread-messages.test.tsx @@ -0,0 +1,57 @@ +// @vitest-environment jsdom +import { afterEach, expect, it, vi } from "vitest"; +import { QueryClient } from "@tanstack/react-query"; +import { + getPendingThreadMessages, + retainThreadMessage, + removePendingThreadMessage, +} from "./pending-thread-messages"; + +afterEach(() => { + vi.restoreAllMocks(); + localStorage.clear(); + window.dispatchEvent(new StorageEvent("storage", { key: null })); +}); + +it("persists separate submissions and retains them across storage refreshes", () => { + const queryClient = new QueryClient(); + const submit = (text: string) => + retainThreadMessage({ + queryClient, + operation: "queue", + request: { + id: "thread-1", + input: [{ type: "text", text, mentions: [] }], + }, + }); + submit("first draft"); + submit("second draft"); + const before = getPendingThreadMessages(); + expect(before).toHaveLength(2); + expect(before[0]?.request.clientSubmissionId).not.toBe( + before[1]?.request.clientSubmissionId, + ); + window.dispatchEvent(new StorageEvent("storage", { key: null })); + expect(getPendingThreadMessages()).toEqual(before); + removePendingThreadMessage(before[0]!.row.id); + expect(getPendingThreadMessages().map((entry) => entry.row.content)).toEqual([ + before[1]!.row.content, + ]); +}); + +it("reports storage failure before a caller can clear its composer", () => { + vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new DOMException("Full", "QuotaExceededError"); + }); + expect(() => + retainThreadMessage({ + queryClient: new QueryClient(), + operation: "queue", + request: { + id: "thread-1", + input: [{ type: "text", text: "keep me", mentions: [] }], + }, + }), + ).toThrow("Full"); + expect(getPendingThreadMessages()).toHaveLength(0); +}); diff --git a/apps/app/src/lib/pending-thread-messages.ts b/apps/app/src/lib/pending-thread-messages.ts new file mode 100644 index 00000000000..f3390fae407 --- /dev/null +++ b/apps/app/src/lib/pending-thread-messages.ts @@ -0,0 +1,128 @@ +import { useMemo, useSyncExternalStore } from "react"; +import { z } from "zod"; +import { nanoid } from "nanoid"; +import { + threadQueuedMessageSchema, + type ThreadQueuedMessage, +} from "@bb/domain"; +import { createQueuedMessageRequestSchema } from "@bb/server-contract"; +import { buildOptimisticQueuedMessage } from "@/hooks/cache-owners/thread-runtime-cache-owner"; +import type { QueryClient } from "@tanstack/react-query"; + +const prefix = "bb.pending-thread-message.v1."; +const entrySchema = z.object({ + operation: z.enum(["send", "queue", "steer"]), + request: createQueuedMessageRequestSchema.extend({ + id: z.string(), + clientSubmissionId: z.string(), + }), + row: threadQueuedMessageSchema, + error: z.string().nullable(), +}); +export type PendingThreadMessage = z.infer; +let entries: PendingThreadMessage[] = []; +let initialized = false; +const listeners = new Set<() => void>(); + +function refresh(): void { + const next: PendingThreadMessage[] = []; + let keys: string[]; + try { + keys = Object.keys(localStorage); + } catch { + return; + } + for (const key of keys) { + if (!key?.startsWith(prefix)) continue; + try { + const parsed = entrySchema.safeParse( + JSON.parse(localStorage.getItem(key) ?? "null"), + ); + if (parsed.success) next.push(parsed.data); + } catch {} + } + entries = next.sort( + (a, b) => + a.row.createdAt - b.row.createdAt || a.row.id.localeCompare(b.row.id), + ); + for (const listener of listeners) listener(); +} + +function initialize(): void { + if (initialized || typeof window === "undefined") return; + initialized = true; + refresh(); + window.addEventListener("storage", (event) => { + if (event.key === null || event.key.startsWith(prefix)) refresh(); + }); +} + +function subscribe(listener: () => void): () => void { + initialize(); + listeners.add(listener); + return () => listeners.delete(listener); +} + +export function getPendingThreadMessages(): readonly PendingThreadMessage[] { + initialize(); + return entries; +} + +export function usePendingThreadMessages(): readonly PendingThreadMessage[] { + return useSyncExternalStore( + subscribe, + getPendingThreadMessages, + getPendingThreadMessages, + ); +} + +const emptyQueue: readonly ThreadQueuedMessage[] = []; +export function usePendingQueuedMessages( + threadId: string, + serverRows: readonly ThreadQueuedMessage[] = emptyQueue, +): readonly ThreadQueuedMessage[] { + const pending = usePendingThreadMessages(); + return useMemo(() => { + const local = pending.filter((entry) => entry.request.id === threadId); + if (local.length === 0) return serverRows; + const ids = new Set(local.map((entry) => entry.row.id)); + return [ + ...serverRows.filter((row) => !ids.has(row.id)), + ...local.map((entry) => entry.row), + ]; + }, [pending, threadId, serverRows]); +} + +export function savePendingThreadMessage(entry: PendingThreadMessage): void { + localStorage.setItem( + prefix + entry.row.id, + JSON.stringify(entrySchema.parse(entry)), + ); + refresh(); +} + +export function removePendingThreadMessage(id: string): void { + localStorage.removeItem(prefix + id); + refresh(); +} + +export function retainThreadMessage(args: { + queryClient: QueryClient; + operation: PendingThreadMessage["operation"]; + request: z.infer & { id: string }; +}): void { + initialize(); + const request = { ...args.request, clientSubmissionId: nanoid() }; + const row = buildOptimisticQueuedMessage({ + queryClient: args.queryClient, + request, + createdAt: Date.now(), + }); + row.id = `qmsg_${request.id}_${request.clientSubmissionId}`; + savePendingThreadMessage({ + operation: args.operation, + request, + row, + error: null, + }); +} diff --git a/apps/app/src/views/thread-detail/ThreadDetailPromptArea.keystrokes.test.tsx b/apps/app/src/views/thread-detail/ThreadDetailPromptArea.keystrokes.test.tsx index 5991fb5ece5..e4059ce1a93 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailPromptArea.keystrokes.test.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailPromptArea.keystrokes.test.tsx @@ -216,6 +216,9 @@ vi.mock("@/hooks/mutations/project-mutations", () => ({ }), })); +vi.mock("@/hooks/useRetainThreadMessage", () => ({ + useRetainThreadMessage: () => ({ connected: true, retain: () => false }), +})); vi.mock("@/hooks/mutations/thread-runtime-mutations", () => { const idleMutation = () => ({ isPending: false, diff --git a/apps/app/src/views/thread-detail/ThreadDetailPromptArea.test.tsx b/apps/app/src/views/thread-detail/ThreadDetailPromptArea.test.tsx index 6ee92ca8a67..59a488eba2a 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailPromptArea.test.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailPromptArea.test.tsx @@ -653,6 +653,9 @@ vi.mock("@/hooks/mutations/project-mutations", () => ({ }), })); +vi.mock("@/hooks/useRetainThreadMessage", () => ({ + useRetainThreadMessage: () => ({ connected: true, retain: () => false }), +})); vi.mock("@/hooks/mutations/thread-runtime-mutations", () => ({ useCancelThreadPlan: () => ({ isPending: false, @@ -1151,6 +1154,37 @@ describe("ThreadDetailPromptArea", () => { expect(onSubmit).not.toHaveBeenCalled(); }); + it.each(["accepted", "failed"])( + "retains the queued draft until the request is %s", + async (outcome) => { + mocks.promptDraft.text = "Keep this queued prompt"; + const pending = createDeferredPromise(); + mocks.createQueuedMessageMutateAsync.mockReturnValueOnce(pending.promise); + renderPromptArea({ + thread: makeThread({ + status: "active", + runtime: { + displayStatus: "active", + hostReconnectGraceExpiresAt: null, + }, + }), + }); + fireEvent.click(screen.getByRole("button", { name: "Submit composer" })); + await waitFor(() => + expect(mocks.createQueuedMessageMutateAsync).toHaveBeenCalledTimes(1), + ); + expect(mocks.promptDraft.clearIfCurrentMatches).not.toHaveBeenCalled(); + await act(async () => { + if (outcome === "accepted") pending.resolve(makeQueuedMessage()); + else pending.reject(new Error("Connection lost")); + }); + expect(mocks.promptDraft.clearIfCurrentMatches).toHaveBeenCalledTimes( + outcome === "accepted" ? 1 : 0, + ); + expect(mocks.promptDraft.restoreIfEmpty).not.toHaveBeenCalled(); + }, + ); + it("keeps the queued drawer adjacent to the bottom composer", () => { mocks.queuedMessages = [makeQueuedMessage()]; diff --git a/apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx b/apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx index e82d078afaf..1c4f2ac1d50 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx @@ -1,4 +1,6 @@ import { ThreadMachineStatus } from "@/components/promptbox/banner/ThreadMachineStatus"; +import { useRetainThreadMessage } from "@/hooks/useRetainThreadMessage"; +import { usePendingQueuedMessages } from "@/lib/pending-thread-messages"; import { useCallback, useEffect, @@ -428,6 +430,7 @@ export function ThreadDetailPromptArea({ thread, }: ThreadDetailPromptAreaProps) { const navigate = useNavigate(); + const pendingMessages = useRetainThreadMessage(); const defaultExecutionOptionsQuery = useThreadDefaultExecutionOptions( thread.id, { @@ -454,7 +457,10 @@ export function ThreadDetailPromptArea({ const queuedMessagesQuery = useThreadQueuedMessages(thread.id, { enabled: true, }); - const queuedMessages = queuedMessagesQuery.data ?? EMPTY_QUEUED_MESSAGES; + const queuedMessages = usePendingQueuedMessages( + thread.id, + queuedMessagesQuery.data ?? EMPTY_QUEUED_MESSAGES, + ); const queuedMessagesPending = queuedMessagesQuery.data === undefined && queuedMessageCount > 0; const queuedMessagesRef = @@ -493,7 +499,7 @@ export function ThreadDetailPromptArea({ enabled: promptHistoryEnabled, }, ); - const createQueuedMessage = useCreateThreadQueuedMessage(); + const createQueuedMessage = useCreateThreadQueuedMessage(thread.id); const stopThread = useStopThread(); const cancelThreadPlan = useCancelThreadPlan(); const clearThreadGoal = useClearThreadGoal(); @@ -913,6 +919,11 @@ export function ThreadDetailPromptArea({ clearThreadGoal.mutate(thread.id); }, [clearThreadGoal, thread.id]); const submitMode = useMemo(() => { + if ( + !pendingMessages.connected && + !shouldQueueFollowUpMessage(runtimeDisplayStatus) + ) + return { kind: "blocked", reason: "unavailable" }; if (isHandoffSelection && !isStopRequested) { if (effectiveSelectedModel.length > 0) { return { kind: "ready" }; @@ -931,6 +942,7 @@ export function ThreadDetailPromptArea({ runtimeDisplayStatus, }); }, [ + pendingMessages.connected, effectiveSelectedModel, handleStopThread, hasPendingInteraction, @@ -1170,6 +1182,7 @@ export function ThreadDetailPromptArea({ ); const handleSend = useCallback(async () => { + if (isFollowUpSubmitting) return; const submittedDraft = currentPromptDraft; const submittedInput = currentPromptDraftInput; if (isHandoffSelection) { @@ -1186,18 +1199,30 @@ export function ThreadDetailPromptArea({ return; } - promptDraft.clearIfCurrentMatches(submittedDraft); - setBottomAttachmentError(null); - try { + if (!isQueuingMessage && !pendingMessages.connected) return; + const retainedRequest = buildCreateQueuedFollowUpRequest({ + threadId: thread.id, + input: submittedInput, + execution: followUpExecutionSelection, + }); + if ( + retainedRequest && + pendingMessages.retain({ + request: retainedRequest, + operation: isQueuingMessage ? "queue" : "send", + }) + ) { + promptDraft.clearIfCurrentMatches(submittedDraft); + setBottomAttachmentError(null); + return; + } + if (!isQueuingMessage) promptDraft.clearIfCurrentMatches(submittedDraft); + setBottomAttachmentError(null); if (isQueuingMessage) { - const request = buildCreateQueuedFollowUpRequest({ - threadId: thread.id, - input: submittedInput, - execution: followUpExecutionSelection, - }); - if (request) { - await createQueuedMessage.mutateAsync(request); + if (retainedRequest) { + await createQueuedMessage.mutateAsync(retainedRequest); + promptDraft.clearIfCurrentMatches(submittedDraft); } } else { const request = buildAutoFollowUpRequest({ @@ -1210,7 +1235,7 @@ export function ThreadDetailPromptArea({ } } } catch (nextError) { - promptDraft.restoreIfEmpty(submittedDraft); + if (!isQueuingMessage) promptDraft.restoreIfEmpty(submittedDraft); showMutationErrorToast({ error: nextError, fallbackMessage: isQueuingMessage @@ -1220,12 +1245,14 @@ export function ThreadDetailPromptArea({ }); } }, [ + pendingMessages, createHandoffThread, createQueuedMessage, currentPromptDraft, currentPromptDraftInput, followUpExecutionSelection, isDefaultExecutionOptionsLoading, + isFollowUpSubmitting, isHandoffSelection, promptDraft, sendMessage, @@ -1333,6 +1360,25 @@ export function ThreadDetailPromptArea({ } if (shortcutRequest.kind === "draft") { + try { + if ( + pendingMessages.retain({ + request: shortcutRequest.request, + operation: "steer", + }) + ) { + promptDraft.clearIfCurrentMatches(submittedDraft); + setBottomAttachmentError(null); + return; + } + } catch (error) { + showMutationErrorToast({ + error, + fallbackMessage: "Could not save message on this device", + lifecycleOperation: "send_message", + }); + return; + } promptDraft.clearIfCurrentMatches(submittedDraft); setBottomAttachmentError(null); await runWhileFollowUpShortcutSending( @@ -1373,6 +1419,7 @@ export function ThreadDetailPromptArea({ currentPromptDraft, currentPromptDraftInput, followUpExecutionSelection, + pendingMessages, promptDraft, queuedMessagesRef, sendMessage, diff --git a/apps/cli/src/commands/thread/actions.ts b/apps/cli/src/commands/thread/actions.ts index f7a4aff90b0..37d35788564 100644 --- a/apps/cli/src/commands/thread/actions.ts +++ b/apps/cli/src/commands/thread/actions.ts @@ -69,6 +69,7 @@ interface ThreadDeleteCommandOptions { } interface ThreadTellCommandOptions { + submissionId?: string; json?: boolean; messageFile?: string; model?: string; @@ -106,6 +107,7 @@ interface ThreadEditMessageCommandOptions { type ThreadTellDeliveryMode = "auto" | "queue" | "steer"; interface PostThreadMessageArgs { + submissionId?: string; getUrl: () => string; threadId: string; message: string; @@ -457,6 +459,10 @@ export function registerActionsCommands( "Message mode: steer (default), queue, or auto (steer a live turn, else start one)", ) .option("--send-at ", SEND_AT_HELP) + .option( + "--submission-id ", + "Reuse this ID for safe retries of an ordinary --mode queue message", + ) .option("--plan", PLAN_HELP) .option( "--file ", @@ -484,6 +490,7 @@ export function registerActionsCommands( inlineLabel: "", }); const response = await postThreadMessage({ + submissionId: opts.submissionId, getUrl, threadId: id, message, @@ -604,6 +611,9 @@ async function postThreadMessage( sdk, }); const response = await sdk.threads.send({ + ...(args.submissionId === undefined + ? {} + : { clientSubmissionId: args.submissionId }), threadId: args.threadId, input, mode: diff --git a/apps/cli/src/commands/thread/organization.ts b/apps/cli/src/commands/thread/organization.ts index f5e53141b51..f57a4b86c72 100644 --- a/apps/cli/src/commands/thread/organization.ts +++ b/apps/cli/src/commands/thread/organization.ts @@ -49,6 +49,7 @@ interface QueueListOptions extends JsonOptions { } interface QueueCreateOptions extends JsonOptions { + submissionId?: string; messageFile?: string; model?: string; } @@ -342,6 +343,10 @@ export function registerOrganizationCommands( queue .command("create [message]") .description("Create a queued text message") + .option( + "--submission-id ", + "Reuse this ID to avoid duplicates when retrying", + ) .option("--model ", "Model override for the queued message") .option( "--message-file ", @@ -364,6 +369,9 @@ export function registerOrganizationCommands( const result = await createCliBbSdk( getUrl(), ).threads.queuedMessages.create({ + ...(opts.submissionId === undefined + ? {} + : { clientSubmissionId: opts.submissionId }), threadId, input: [{ type: "text", text: message, mentions: [] }], ...(opts.model ? { model: opts.model } : {}), diff --git a/apps/server/src/routes/system.ts b/apps/server/src/routes/system.ts index 9953591c12f..c760065238d 100644 --- a/apps/server/src/routes/system.ts +++ b/apps/server/src/routes/system.ts @@ -208,6 +208,7 @@ export function registerSystemRoutes( customThemes: listCustomThemeNames(themeRoot), pluginThemes: pluginService.listThemes(), featureFlags: deps.config.featureFlags, + messageSubmissionKeys: true, hostDaemonPort: deps.config.hostDaemonPort, localHelperPorts, serverUrl, diff --git a/apps/server/src/services/threads/queued-messages.ts b/apps/server/src/services/threads/queued-messages.ts index e2fa2fb4689..8e74b617851 100644 --- a/apps/server/src/services/threads/queued-messages.ts +++ b/apps/server/src/services/threads/queued-messages.ts @@ -1,5 +1,8 @@ +import { createHash } from "node:crypto"; +import { and, eq } from "drizzle-orm"; import { claimNextQueuedThreadMessageGroup, + claimQueuedThreadMessage, claimQueuedThreadMessageGroup, createQueuedThreadMessageInTransaction, deleteClaimedQueuedThreadMessageBatchInTransaction, @@ -12,6 +15,7 @@ import { isThreadQueueAutoSendPaused, releaseQueuedMessageClaim, releaseStaleQueuedMessageClaims, + threadSubmissionReceipts, type DbQueryConnection, type QueuedThreadMessageGroupClaimPolicy, type QueuedThreadMessageGroupEligibility, @@ -19,6 +23,7 @@ import { import { flattenPromptInputGroups, queuedMessageSystemNoticeSchema, + threadQueuedMessageSchema, } from "@bb/domain"; import type { PromptInput, @@ -114,7 +119,7 @@ type ClaimedQueuedMessage = Exclude< >[number]; interface SendClaimedQueuedMessageArgs { - mode: SendQueuedMessageMode; + mode: SendMessageRequest["mode"]; queuedMessages: ClaimedQueuedMessage[]; /** True for an explicit "send now"; false for an ordinary drain. */ sendNow: boolean; @@ -122,7 +127,7 @@ interface SendClaimedQueuedMessageArgs { } interface SendClaimedQueuedMessageForThreadArgs { - mode: SendQueuedMessageMode; + mode: SendMessageRequest["mode"]; queuedMessages: ClaimedQueuedMessage[]; sendNow: boolean; thread: Thread; @@ -185,6 +190,8 @@ async function requireReadyQueuedMessageEnvironment( export interface CreateQueuedMessageForThreadArgs { payload: CreateQueuedMessageRequest; thread: Thread; + startWhenIdle?: boolean; + steerWhenActive?: boolean; } function admitQueuedMessage( @@ -217,6 +224,39 @@ export async function createQueuedMessageForThread( args: CreateQueuedMessageForThreadArgs, ): Promise { const { payload, thread } = args; + const submissionId = payload.clientSubmissionId; + const fingerprint = createHash("sha256") + .update( + JSON.stringify({ + payload, + startWhenIdle: args.startWhenIdle === true, + ...(args.steerWhenActive ? { steerWhenActive: true } : {}), + }), + ) + .digest("hex"); + const readReceipt = (db: DbQueryConnection): ThreadQueuedMessage | null => { + if (submissionId === undefined) return null; + const receipt = db + .select() + .from(threadSubmissionReceipts) + .where( + and( + eq(threadSubmissionReceipts.threadId, thread.id), + eq(threadSubmissionReceipts.submissionId, submissionId), + ), + ) + .get(); + if (!receipt) return null; + if (receipt.fingerprint !== fingerprint) + throw new ApiError( + 409, + "client_submission_conflict", + "Submission ID is already used for another message", + ); + return threadQueuedMessageSchema.parse(JSON.parse(receipt.queuedMessage)); + }; + const accepted = readReceipt(deps.db); + if (accepted) return accepted; ensureThreadQueueIsWritable(thread); await validatePromptAttachmentReferences({ db: deps.db, @@ -231,15 +271,26 @@ export async function createQueuedMessageForThread( senderThreadId: payload.senderThreadId, targetThread: thread, }); - const { currentThread, hasProviderSession, queuedMessage } = + const { currentThread, hasProviderSession, queuedMessage, replayed } = deps.db.transaction( (tx) => { + const accepted = readReceipt(tx); + if (accepted) + return { + currentThread: thread, + hasProviderSession: false, + queuedMessage: accepted, + replayed: true, + }; const currentThread = getThread(tx, thread.id); if (!currentThread) { throw new ApiError(404, "thread_not_found", "Thread not found"); } const { hasProviderSession } = admitQueuedMessage(tx, currentThread); const queuedMessage = createQueuedThreadMessageInTransaction(tx, { + ...(submissionId === undefined + ? {} + : { id: `qmsg_${thread.id}_${submissionId}` }), threadId: thread.id, content: payload.input, senderThreadId, @@ -259,15 +310,41 @@ export async function createQueuedMessageForThread( waitingOn: currentThread.status === "stopping" ? { kind: "stopping" } - : { kind: "thread-busy" }, + : args.steerWhenActive || + (args.startWhenIdle && + (currentThread.status === "idle" || + currentThread.status === "error")) + ? null + : { kind: "thread-busy" }, sendAt: null, payload: { kind: "inline" }, systemNotice: null, }); - return { currentThread, hasProviderSession, queuedMessage }; + const result = { + ...toThreadQueuedMessage(queuedMessage), + ...(submissionId === undefined + ? {} + : { clientSubmissionId: submissionId }), + }; + if (submissionId !== undefined) + tx.insert(threadSubmissionReceipts) + .values({ + threadId: thread.id, + submissionId, + fingerprint, + queuedMessage: JSON.stringify(result), + }) + .run(); + return { + currentThread, + hasProviderSession, + queuedMessage: result, + replayed: false, + }; }, { behavior: "immediate" }, ); + if (replayed) return queuedMessage; deps.hub.notifyThread(thread.id, ["queue-changed"]); if (senderThreadId === null && payload.input.length > 0) { captureUserMessageSentTelemetry(deps, { @@ -276,13 +353,45 @@ export async function createQueuedMessageForThread( providerId: thread.providerId, }); } - if (currentThread.status === "idle" && hasProviderSession) { + if (args.startWhenIdle) { + const claimed = claimQueuedThreadMessage( + deps.db, + deps.hub, + queuedMessage.id, + ); + if (claimed) { + try { + await withActiveQueuedMessageClaims([claimed], () => + sendClaimedQueuedMessage(deps, { + mode: args.steerWhenActive ? "steer-if-active" : "queue-if-active", + queuedMessages: [claimed], + sendNow: false, + threadId: thread.id, + }), + ); + } catch (error) { + releaseQueuedMessageClaims(deps, [claimed]); + if ( + !isQueuedMessageClaimLostError(error) && + !isQueuedMessageAutoSendPausedError(error) && + !(error instanceof ThreadContextClearInProgressError) && + !isCommandTimeoutError(error) + ) { + recordQueuedMessageDrainFailure(deps, { + error, + row: claimed, + thread: currentThread, + }); + } + } + } + } else if (currentThread.status === "idle" && hasProviderSession) { requestQueuedMessageDispatch(deps, { kind: "thread-ready", threadId: thread.id, }); } - return toThreadQueuedMessage(queuedMessage); + return queuedMessage; } function isQueuedMessageAutoSendCandidate( @@ -316,7 +425,7 @@ function respectsManualStopPause( function sendQueuedMessagePayload( queuedMessage: ThreadQueuedMessage, - mode: SendQueuedMessageMode, + mode: SendMessageRequest["mode"], senderThreadId: string | null, ): SendMessageRequest { return { diff --git a/apps/server/src/services/threads/thread-send-request.ts b/apps/server/src/services/threads/thread-send-request.ts index 132257c3c4f..f985097c05c 100644 --- a/apps/server/src/services/threads/thread-send-request.ts +++ b/apps/server/src/services/threads/thread-send-request.ts @@ -7,6 +7,8 @@ import type { LoggedPendingInteractionWorkSessionDeps } from "../../types.js"; import { attemptDispatch } from "./dispatch-attempt.js"; import { requireThreadCommandEnvironment } from "./thread-command-environment.js"; import { sendThreadMessage } from "./thread-send.js"; +import { createQueuedMessageForThread } from "./queued-messages.js"; +import { ApiError } from "../../errors.js"; interface AcceptThreadSendRequestArgs { payload: SendMessageRequest; @@ -17,6 +19,41 @@ export async function acceptThreadSendRequest( deps: LoggedPendingInteractionWorkSessionDeps, args: AcceptThreadSendRequestArgs, ): Promise { + if (args.payload.clientSubmissionId !== undefined) { + const { mode, pluginSubmission, sendAt, ...payload } = args.payload; + if ( + (mode !== "start" && + mode !== "queue-if-active" && + mode !== "steer-if-active") || + pluginSubmission !== undefined || + sendAt !== undefined || + payload.input.some( + (block) => + block.type === "text" && + block.mentions.some( + (mention) => + mention.resource.kind === "command" && + mention.resource.source === "command", + ), + ) + ) { + throw new ApiError( + 400, + "client_submission_unsupported", + "Submission keys require an ordinary, unscheduled message", + ); + } + return { + ok: true, + delivery: "queued", + queuedMessage: await createQueuedMessageForThread(deps, { + thread: args.thread, + payload, + startWhenIdle: true, + steerWhenActive: mode === "steer-if-active", + }), + }; + } if (isStandaloneBuiltinClearCommand(args.payload.input)) { const environment = await requireThreadCommandEnvironment(deps, { thread: args.thread, diff --git a/apps/server/test/threads/requested-queue-drain.test.ts b/apps/server/test/threads/requested-queue-drain.test.ts index 284c555e894..2c0c82c9832 100644 --- a/apps/server/test/threads/requested-queue-drain.test.ts +++ b/apps/server/test/threads/requested-queue-drain.test.ts @@ -1,10 +1,12 @@ import { createQueuedThreadMessage, + getQueuedThreadMessage, listEvents, listQueuedThreadMessages, setQueuedThreadMessageFailureReason, setQueuedThreadMessageGroupBoundary, } from "@bb/db"; +import { createDeferredPromise } from "@bb/test-helpers"; import type { PluginHookName } from "@get-bb/plugin-sdk"; import { afterEach, describe, expect, it, vi } from "vitest"; import { @@ -451,6 +453,138 @@ describe("the requested queue drain", () => { }, ); + it.each([false, true])( + "resumes a fresh keyed Send after Stop while respecting plugin hold=%s", + async (held) => { + await withTestHarness(async (harness) => { + const { thread } = seedRunnableThread(harness, { + hostId: `host-keyed-resume-${held}`, + status: "active", + }); + const older = seedQueuedMessage(harness.deps, { + threadId: thread.id, + content: textInput("Work queued before Stop"), + waitingOn: { kind: "thread-busy" }, + }); + await stopThread(harness, thread.id); + vi.useFakeTimers(); + let hold = held; + let hookCalls = 0; + installHooks({ + "message.dispatch": [ + { + pluginId: "limiter", + handler: () => { + hookCalls += 1; + return hold + ? ({ action: "wait", reason: "At capacity" } as const) + : ({ action: "proceed" } as const); + }, + }, + ], + }); + const args = { + thread, + payload: { + input: textInput("Resume this thread"), + mode: "queue-if-active" as const, + clientSubmissionId: "resume-after-stop", + }, + }; + const before = turnRequests(harness, thread.id).length; + const accepted = await acceptThreadSendRequest(harness.deps, args); + expect(hookCalls).toBe(1); + expect(turnRequests(harness, thread.id)).toHaveLength( + before + (held ? 0 : 1), + ); + expect(await acceptThreadSendRequest(harness.deps, args)).toEqual( + accepted, + ); + expect(hookCalls).toBe(1); + if (held) { + expect( + listQueuedThreadMessages(harness.db, thread.id)[1]?.waitingOn, + ).toBe( + JSON.stringify({ + kind: "plugin", + pluginId: "limiter", + reason: "At capacity", + }), + ); + hold = false; + vi.advanceTimersByTime(1_001); + await runPluginWake(harness); + } + expect(turnRequests(harness, thread.id)).toHaveLength(before + 1); + expect(listQueuedThreadMessages(harness.db, thread.id)).toMatchObject([ + { id: older.id, claimedAt: null }, + ]); + }); + }, + ); + + it("delivers overlapping distinct keyed Steers without another dispatch wake", async () => { + await withTestHarness(async (harness) => { + const { thread } = seedRunnableThread(harness, { + hostId: "host-overlapping-keyed-steers", + status: "active", + }); + const older = seedQueuedMessage(harness.deps, { + threadId: thread.id, + content: textInput("After the active turn"), + waitingOn: { kind: "thread-busy" }, + }); + const entered = createDeferredPromise(); + const release = createDeferredPromise(); + installHooks({ + "message.dispatch": [ + { + pluginId: "barrier", + handler: async () => { + entered.resolve(); + await release.promise; + return { action: "proceed" } as const; + }, + }, + ], + }); + const before = turnRequests(harness, thread.id).length; + const send = (id: string) => + acceptThreadSendRequest(harness.deps, { + thread, + payload: { + input: textInput(id), + mode: "steer-if-active", + clientSubmissionId: id, + }, + }); + const first = send("steer-a"); + await entered.promise; + const second = send("steer-b"); + const third = send("steer-c"); + try { + await vi.waitFor(() => { + for (const id of ["steer-a", "steer-b", "steer-c"]) { + expect( + getQueuedThreadMessage(harness.db, `qmsg_${thread.id}_${id}`), + ).not.toBeNull(); + } + }); + } finally { + release.resolve(); + } + await Promise.all([first, second, third]); + await vi.waitFor(() => + expect(turnRequests(harness, thread.id)).toHaveLength(before + 3), + ); + await send("steer-b"); + expect(turnRequests(harness, thread.id)).toHaveLength(before + 3); + expect(listQueuedThreadMessages(harness.db, thread.id)).toMatchObject([ + { id: older.id }, + ]); + }); + }); + it("resumes host-offline work without releasing ordinary work paused by Stop", async () => { await withTestHarness(async (harness) => { const { thread, environment } = seedRunnableThread(harness, { diff --git a/apps/server/test/threads/thread-send-dispatch.test.ts b/apps/server/test/threads/thread-send-dispatch.test.ts index 87246e4a001..fac5e6ebc97 100644 --- a/apps/server/test/threads/thread-send-dispatch.test.ts +++ b/apps/server/test/threads/thread-send-dispatch.test.ts @@ -7,6 +7,7 @@ import { markThreadDeleted, setQueuedThreadMessageFailureReason, setQueuedThreadMessageGroupBoundary, + deleteQueuedThreadMessage, } from "@bb/db"; import type { EnvironmentRow } from "@bb/db"; import { @@ -1355,6 +1356,103 @@ describe("service tier execution lifecycle", () => { }, ); + it("accepts concurrent retries once and remembers acceptance after queue consumption", async () => { + await withTestHarness(async (harness) => { + const { thread } = seedProviderThreadFixture({ + harness, + value: 181, + status: "active", + serviceTier: "default", + }); + const args = { + thread, + payload: { + input: textInput("keep this message"), + clientSubmissionId: "same-submission", + }, + }; + const [first, retry] = await Promise.all([ + createQueuedMessageForThread(harness.deps, args), + createQueuedMessageForThread(harness.deps, args), + ]); + expect(retry).toEqual(first); + expect(listQueuedThreadMessages(harness.db, thread.id)).toHaveLength(1); + await expect( + createQueuedMessageForThread(harness.deps, { + ...args, + payload: { ...args.payload, input: textInput("different message") }, + }), + ).rejects.toThrow("already used"); + deleteQueuedThreadMessage(harness.db, harness.deps.hub, first.id); + expect(await createQueuedMessageForThread(harness.deps, args)).toEqual( + first, + ); + expect(listQueuedThreadMessages(harness.db, thread.id)).toHaveLength(0); + }); + }); + + it("delivers a keyed steer into the active turn once while ordinary queued input waits", async () => { + await withTestHarness(async (harness) => { + const { environment, thread } = seedProviderThreadFixture({ + harness, + value: 182, + status: "active", + }); + seedTurnStarted(harness.deps, { + environmentId: environment.id, + providerThreadId: "provider-send-dispatch-182", + threadId: thread.id, + turnId: "turn-keyed-steer", + }); + const waiting = await createQueuedMessageForThread(harness.deps, { + thread, + payload: { input: textInput("after this turn") }, + }); + const args = { + thread, + payload: { + input: textInput("change direction now"), + mode: "steer-if-active" as const, + clientSubmissionId: "steer-submission", + }, + }; + const [first, retry] = await Promise.all([ + acceptThreadSendRequest(harness.deps, args), + acceptThreadSendRequest(harness.deps, args), + ]); + expect(retry).toEqual(first); + await runQueuedMessageDispatch(harness.deps, { + kind: "thread-ready", + threadId: thread.id, + }); + expect(listQueuedThreadMessages(harness.db, thread.id)).toMatchObject([ + { id: waiting.id }, + ]); + expect( + listQueuedThreadCommands(harness, "turn.submit", thread.id), + ).toMatchObject([ + { + input: textInput("change direction now"), + target: { mode: "steer", expectedTurnId: "turn-keyed-steer" }, + }, + ]); + expect(await acceptThreadSendRequest(harness.deps, args)).toEqual(first); + await runQueuedMessageDispatch(harness.deps, { + kind: "thread-ready", + threadId: thread.id, + }); + expect( + listQueuedThreadCommands(harness, "turn.submit", thread.id), + ).toHaveLength(1); + await expect( + acceptThreadSendRequest(harness.deps, { + ...args, + payload: { ...args.payload, mode: "queue-if-active" }, + }), + ).rejects.toThrow("already used"); + }); + }); + it("keeps queued choices separate until dispatch and preserves the next row", async () => { await withTestHarness(async (harness) => { const { thread } = seedProviderThreadFixture({ diff --git a/packages/db/drizzle/0127_parched_roxanne_simpson.sql b/packages/db/drizzle/0127_parched_roxanne_simpson.sql new file mode 100644 index 00000000000..82d83db4270 --- /dev/null +++ b/packages/db/drizzle/0127_parched_roxanne_simpson.sql @@ -0,0 +1,8 @@ +CREATE TABLE `thread_submission_receipts` ( + `thread_id` text NOT NULL, + `submission_id` text NOT NULL, + `fingerprint` text NOT NULL, + `queued_message` text NOT NULL, + PRIMARY KEY(`thread_id`, `submission_id`), + FOREIGN KEY (`thread_id`) REFERENCES `threads`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/packages/db/drizzle/meta/0127_snapshot.json b/packages/db/drizzle/meta/0127_snapshot.json new file mode 100644 index 00000000000..f8a43bceb1a --- /dev/null +++ b/packages/db/drizzle/meta/0127_snapshot.json @@ -0,0 +1,5061 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "85aefa10-9eaa-4dfb-8cba-1b97f3efa0ad", + "prevId": "48979c46-bc2b-411d-a6cc-18dfcb7cd457", + "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 + }, + "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_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_submission_receipts": { + "name": "thread_submission_receipts", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "submission_id": { + "name": "submission_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "queued_message": { + "name": "queued_message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_submission_receipts_thread_id_threads_id_fk": { + "name": "thread_submission_receipts_thread_id_threads_id_fk", + "tableFrom": "thread_submission_receipts", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "thread_submission_receipts_thread_id_submission_id_pk": { + "columns": [ + "thread_id", + "submission_id" + ], + "name": "thread_submission_receipts_thread_id_submission_id_pk" + } + }, + "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 6a30ce673b3..e8ba01e4586 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -890,6 +890,13 @@ "when": 1789607725218, "tag": "0126_overconfident_vin_gonzales", "breakpoints": true + }, + { + "idx": 127, + "version": "6", + "when": 1789843084867, + "tag": "0127_parched_roxanne_simpson", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/data/queued-thread-messages.ts b/packages/db/src/data/queued-thread-messages.ts index 79a029c6a60..590c0efc978 100644 --- a/packages/db/src/data/queued-thread-messages.ts +++ b/packages/db/src/data/queued-thread-messages.ts @@ -54,6 +54,7 @@ import { createOrderKeyAfter, createOrderKeyBetween } from "./order-keys.js"; import { queryInSqliteVariableBatches } from "./events.js"; export interface CreateQueuedThreadMessageInput { + id?: string; threadId: string; content: PromptInput[]; senderThreadId?: string | null; @@ -608,7 +609,7 @@ export function createQueuedThreadMessageInTransaction( input.threadId, projectAttachmentPaths(input.content), ); - const id = createQueuedThreadMessageId(); + const id = input.id ?? createQueuedThreadMessageId(); const lastQueuedMessage = getLastQueuedThreadMessage(tx, input.threadId); const sortKey = lastQueuedMessage ? createOrderKeyAfter({ previousKey: lastQueuedMessage.sortKey }) diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 68c9b152330..64a664bbf32 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -756,6 +756,19 @@ export const threadDynamicContextFileStates = sqliteTable( ], ); +export const threadSubmissionReceipts = sqliteTable( + "thread_submission_receipts", + { + threadId: text("thread_id") + .notNull() + .references(() => threads.id, { onDelete: "cascade" }), + submissionId: text("submission_id").notNull(), + fingerprint: text("fingerprint").notNull(), + queuedMessage: text("queued_message").notNull(), + }, + (table) => [primaryKey({ columns: [table.threadId, table.submissionId] })], +); + export const events = sqliteTable( "events", { diff --git a/packages/db/test/migrate.test.ts b/packages/db/test/migrate.test.ts index 4da166327e3..cd12b666636 100644 --- a/packages/db/test/migrate.test.ts +++ b/packages/db/test/migrate.test.ts @@ -781,6 +781,7 @@ function rewindEnvironmentProvisioningMigration(db: DbConnection): void { } function dropQueueReworkSchema(db: DbConnection): void { + db.$client.exec("DROP TABLE IF EXISTS thread_submission_receipts"); rewindEnvironmentProvisioningMigration(db); // Indexes first: SQLite refuses to drop a column an existing index names. for (const index of [ @@ -858,6 +859,7 @@ function rewindEnvironmentRowFactsMigration(db: DbConnection): void { } function rewindMachineProvidersMigration(db: DbConnection): void { + db.$client.exec("DROP TABLE IF EXISTS thread_submission_receipts"); const queuedDispatchOrigin = db.$client .prepare<[], TableInfoRow>("PRAGMA table_info(queued_thread_messages)") .all(); @@ -868,9 +870,7 @@ function rewindMachineProvidersMigration(db: DbConnection): void { "requested_by_thread_id", ]) { if (!queuedDispatchOrigin.some((column) => column.name === name)) continue; - db.$client.exec( - `ALTER TABLE queued_thread_messages DROP COLUMN ${name}`, - ); + db.$client.exec(`ALTER TABLE queued_thread_messages DROP COLUMN ${name}`); } db.$client.exec("DROP TABLE IF EXISTS thread_pruning_cursors"); db.$client.exec("DROP TABLE IF EXISTS project_attachment_threads"); diff --git a/packages/domain/src/plugin-sdk-version.ts b/packages/domain/src/plugin-sdk-version.ts index 6bae9ee9f76..e6dc0e7cdde 100644 --- a/packages/domain/src/plugin-sdk-version.ts +++ b/packages/domain/src/plugin-sdk-version.ts @@ -1,3 +1,3 @@ -export const PLUGIN_SDK_VERSION = "0.4.106"; +export const PLUGIN_SDK_VERSION = "0.4.107"; export const PLUGIN_SDK_MAJOR = Number(PLUGIN_SDK_VERSION.split(".", 1)[0]); diff --git a/packages/domain/src/thread.ts b/packages/domain/src/thread.ts index 405edfee51a..c93228c50b7 100644 --- a/packages/domain/src/thread.ts +++ b/packages/domain/src/thread.ts @@ -336,6 +336,7 @@ export const threadPullRequestSchema = z export type ThreadPullRequest = z.infer; export const threadQueuedMessageSchema = z.object({ + clientSubmissionId: z.string().optional(), id: z.string(), origin: threadCreateOriginSchema.nullable(), originPluginId: z.string().nullable(), diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index fbf0990e803..28fd6a30e58 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@get-bb/plugin-sdk", - "version": "0.4.106", + "version": "0.4.107", "homepage": "https://github.com/get-bb/bb#readme", "bugs": { "url": "https://github.com/get-bb/bb/issues" diff --git a/packages/sdk/src/areas/threads.ts b/packages/sdk/src/areas/threads.ts index 292420f4382..34dd9b9d4dc 100644 --- a/packages/sdk/src/areas/threads.ts +++ b/packages/sdk/src/areas/threads.ts @@ -264,6 +264,7 @@ export interface ThreadDeleteArgs extends DeleteThreadRequest { } export interface ThreadSendArgs extends SendMessageRequest { + signal?: AbortSignal; threadId: string; } @@ -312,6 +313,7 @@ export interface ThreadQueuedMessageArgs { } export interface ThreadQueuedMessageCreateArgs extends CreateQueuedMessageRequest { + signal?: AbortSignal; threadId: string; } @@ -673,6 +675,7 @@ function updateJson(args: ThreadUpdateArgs): UpdateThreadRequest { function sendJson(args: ThreadSendArgs): SendMessageRequest { return { + clientSubmissionId: args.clientSubmissionId, input: args.input, mode: args.mode, model: args.model, @@ -965,12 +968,15 @@ export function createThreadsArea(args: CreateSdkAreaArgs): ThreadsArea { }; const queuedMessages: ThreadQueuedMessagesArea = { async create(input) { - const { threadId, ...json } = input; + const { threadId, signal, ...json } = input; return transport.readJson( - transport.api.v1.threads[":id"]["queued-messages"].$post({ - param: { id: threadId }, - json, - }), + transport.api.v1.threads[":id"]["queued-messages"].$post( + { + param: { id: threadId }, + json, + }, + ...signalRequestArgs(signal), + ), ); }, async delete(input) { @@ -1288,10 +1294,13 @@ export function createThreadsArea(args: CreateSdkAreaArgs): ThreadsArea { }, async send(input) { return transport.readJson( - transport.api.v1.threads[":id"].send.$post({ - param: { id: input.threadId }, - json: sendJson(input), - }), + transport.api.v1.threads[":id"].send.$post( + { + param: { id: input.threadId }, + json: sendJson(input), + }, + ...signalRequestArgs(input.signal), + ), ); }, async retry(input) { diff --git a/packages/server-contract/src/api/system.ts b/packages/server-contract/src/api/system.ts index 5590e36ae96..24f54be99db 100644 --- a/packages/server-contract/src/api/system.ts +++ b/packages/server-contract/src/api/system.ts @@ -178,6 +178,7 @@ export const serverAccessStatusSchema = z.object({ export type ServerAccessStatus = z.infer; export const systemConfigResponseSchema = z.object({ + messageSubmissionKeys: z.boolean().optional(), serverAccess: serverAccessStatusSchema, generalSettings: appSettingsSchema.extend({ showUnhandledProviderEvents: z.boolean().optional(), diff --git a/packages/server-contract/src/api/threads.ts b/packages/server-contract/src/api/threads.ts index 6e3b33f2dd4..10755285091 100644 --- a/packages/server-contract/src/api/threads.ts +++ b/packages/server-contract/src/api/threads.ts @@ -231,6 +231,7 @@ export const forkThreadRequestSchema = z export type ForkThreadRequest = z.infer; const sendMessageRequestFieldsSchema = z.object({ + clientSubmissionId: z.string().min(1).max(100).optional(), input: z.array(promptInputSchema).min(1), model: z.string().optional(), serviceTier: serviceTierSchema.optional(), @@ -361,6 +362,7 @@ export const sendQueuedMessageModeSchema = z.enum(["auto", "steer"]); export type SendQueuedMessageMode = z.infer; export const createQueuedMessageRequestSchema = z.object({ + clientSubmissionId: z.string().min(1).max(100).optional(), input: z.array(promptInputSchema).min(1), model: z.string().optional(), serviceTier: serviceTierSchema.optional(), diff --git a/packages/server-contract/test/contract.test.ts b/packages/server-contract/test/contract.test.ts index 602d1dae150..3f3cb8dd5ea 100644 --- a/packages/server-contract/test/contract.test.ts +++ b/packages/server-contract/test/contract.test.ts @@ -395,6 +395,9 @@ const OPTIONAL_SERVER_FIELD_GROUPS: readonly OptionalServerFieldGroup[] = [ reason: "Execution input source metadata is omitted by legacy callers; when omitted, supplied execution values are treated as explicit.", fields: [ + "createQueuedMessageRequestSchema.clientSubmissionId", + "sendMessageRequestSchema.clientSubmissionId", + "sendQueuedMessageResponseSchema.queuedMessage.clientSubmissionId", "createQueuedMessageRequestSchema.executionInputSources", "createQueuedMessageRequestSchema.executionInputSources.model", "createQueuedMessageRequestSchema.executionInputSources.permissionMode", diff --git a/packages/templates/src/templates/bb-guide-threads.md b/packages/templates/src/templates/bb-guide-threads.md index 0329c745ac1..33a4d0658a2 100644 --- a/packages/templates/src/templates/bb-guide-threads.md +++ b/packages/templates/src/templates/bb-guide-threads.md @@ -349,7 +349,7 @@ Interactions: Queued messages: bb thread queue list [] [--wait-holder plugin:] - bb thread queue create + bb thread queue create [--submission-id ] bb thread queue update [--file ] [--image ] bb thread queue send [--mode auto|steer] bb thread queue reorder [--after ] [--before ] @@ -359,6 +359,12 @@ Queued messages: The `Sender` column identifies agent threads and system notices; user messages leave it blank. The SDK and `--json` include `initiator` and `senderThreadId`. + Reuse --submission-id with the same message when retrying queue create or + thread tell --mode queue or --mode steer after a lost response. The server + recognizes the submission even after dispatch. Keys are scoped to a thread and do not + support commands or scheduled sends. Steering joins the active turn when + possible; ordinary queued messages wait for that turn to finish. + A queued message is one that could not dispatch yet. Every one carries a typed reason in its `Waiting on` column: waiting for the current turn to finish, for the workspace, for a pending interaction, for a clock (`Send at`), 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..ee458a3cecc 100644 --- a/plugins/bb-guide/skills/bb-cli/references/thread-operation.md +++ b/plugins/bb-guide/skills/bb-cli/references/thread-operation.md @@ -1,5 +1,7 @@ # Thread coordination and inspection +For duplicate-safe retries of ordinary messages, use `bb thread tell ID MESSAGE --mode queue --submission-id KEY` or `bb thread queue create ID MESSAGE --submission-id KEY`. Use `--mode steer` to join the active turn when possible. Reuse the same key, mode, and payload for retries on the same thread, including after a response is lost. Keys remain recognized after the queued message has dispatched; different content or delivery intent with the same key is rejected. Submission keys do not apply to commands or scheduled sends. + ## Coordinating Work - Use one clear owner per task.