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.