diff --git a/src/main/db.ts b/src/main/db.ts index 7e53dc48b..e9bcc419a 100644 --- a/src/main/db.ts +++ b/src/main/db.ts @@ -37,6 +37,7 @@ export { dbReadThreadRuntimeSummaries, dbGetThreadRuntimeSummaries, dbGetThreadRuntimeItem, + dbGetLatestThreadGoalItem, dbGetThreadRuntimeItems, dbGetThreadRuntimeItemsPage, dbTruncateThreadRuntimeAfter, diff --git a/src/main/db/runtimeItems.test.ts b/src/main/db/runtimeItems.test.ts index 44cb2a40a..97b99c087 100644 --- a/src/main/db/runtimeItems.test.ts +++ b/src/main/db/runtimeItems.test.ts @@ -8,6 +8,7 @@ import { closeDatabase, initDatabase } from "./connection"; import { dbUpsertProject, dbUpsertThread } from "./projectsThreads"; import { dbApplyThreadRuntimeEvents, + dbGetLatestThreadGoalItem, dbGetThreadContextUsage, dbGetLatestThreadRuntimeAnchorItemId, dbGetThreadRuntimeItems, @@ -476,4 +477,39 @@ describe.skipIf(!sqliteAvailable)("runtimeItems incremental persistence", () => ]); expect(page.nextCursor).toBe(1); }); + + it("returns the latest goal even when its position is before the tail window", () => { + dbReplaceThreadRuntimeItems("thread-1", [ + { + id: "goal-outside-tail", + type: "goal", + state: "updated", + payload: { action: "set", objective: "outside-tail goal" }, + streams: {}, + }, + ...Array.from({ length: 90 }, (_, index) => ({ + id: `assistant-${index}`, + type: "assistant_message" as const, + state: "completed" as const, + streams: {}, + })), + ]); + + const tail = dbGetThreadRuntimeItemsPage("thread-1", undefined, 500, 40); + expect(tail.items.some((item) => item.id === "goal-outside-tail")).toBe(false); + expect(tail.items.at(-1)?.id).toBe("assistant-89"); + + expect(dbGetLatestThreadGoalItem("thread-1")?.id).toBe("goal-outside-tail"); + + dbApplyThreadRuntimeEvents("thread-1", [ + { + type: "item.started", + threadId: "thread-1", + itemId: "goal-new", + itemType: "goal", + payload: { action: "updated", objective: "new goal" }, + }, + ]); + expect(dbGetLatestThreadGoalItem("thread-1")?.id).toBe("goal-new"); + }); }); diff --git a/src/main/db/runtimeItems.ts b/src/main/db/runtimeItems.ts index 4862348a6..dd1bf2b00 100644 --- a/src/main/db/runtimeItems.ts +++ b/src/main/db/runtimeItems.ts @@ -209,6 +209,21 @@ export function dbGetThreadRuntimeItem( return row ? mapRuntimeItemRow(row) : null; } +/** Reads the latest goal even when it precedes the paged transcript window. */ +export function dbGetLatestThreadGoalItem(threadId: string): PersistedRuntimeItem | null { + const sqlite = getSqlite(); + const row = sqlite + .prepare( + `SELECT item_id, type, state, payload, streams, parent_item_id + FROM thread_runtime_items + WHERE thread_id = ? AND type = 'goal' + ORDER BY position DESC + LIMIT 1`, + ) + .get(threadId) as PersistedRuntimeItemRow | undefined; + return row ? mapRuntimeItemRow(row) : null; +} + export function dbGetThreadRuntimeItemsPage( threadId: string, beforePosition: number | undefined, diff --git a/src/main/ipc/localHandlers.ts b/src/main/ipc/localHandlers.ts index e541eee05..5a23717a7 100644 --- a/src/main/ipc/localHandlers.ts +++ b/src/main/ipc/localHandlers.ts @@ -14,6 +14,7 @@ import { dbGetThreadContextUsage, dbGetThreadRuntimeItems, dbGetThreadRuntimeItemsPage, + dbGetLatestThreadGoalItem, dbTruncateThreadRuntimeAfter, dbGetThreads, dbPersistExperimentState, @@ -527,6 +528,7 @@ export function createLocalIpcHandlers( dbGetThreadRuntimeItems: ({ threadId }) => dbGetThreadRuntimeItems(threadId), dbGetThreadRuntimeItemsPage: ({ threadId, beforePosition, limit, targetTimelineEntryCount }) => dbGetThreadRuntimeItemsPage(threadId, beforePosition, limit, targetTimelineEntryCount), + dbGetLatestThreadGoalItem: ({ threadId }) => dbGetLatestThreadGoalItem(threadId), dbTruncateThreadRuntimeAfter: ({ threadId, itemId }) => dbTruncateThreadRuntimeAfter(threadId, itemId), dbReplaceThreadRuntimeItems: ({ threadId, items }) => diff --git a/src/main/remote/RemoteAccessServer.test.ts b/src/main/remote/RemoteAccessServer.test.ts index af308bc0a..0aaee9154 100644 --- a/src/main/remote/RemoteAccessServer.test.ts +++ b/src/main/remote/RemoteAccessServer.test.ts @@ -47,6 +47,7 @@ import { dbGetProjects, dbGetThread, dbGetThreadContextUsage, + dbGetLatestThreadGoalItem, dbGetLatestThreadRuntimeAnchorItemId, dbGetThreadRuntimeItems, dbGetThreadRuntimeItemsPage, @@ -90,6 +91,7 @@ vi.mock("../db", () => { dbGetProjects: vi.fn<() => unknown[]>(() => []), dbGetThreadCompletedTurns: vi.fn<() => unknown[]>(() => []), dbGetThreadContextUsage: vi.fn<() => null>(() => null), + dbGetLatestThreadGoalItem: vi.fn<() => unknown>(() => null), dbGetLatestThreadRuntimeAnchorItemId: vi.fn<() => null>(() => null), dbGetThreadRuntimeItems: vi.fn<() => unknown[]>(() => []), dbGetThreadRuntimeItemsPage: vi.fn<() => { items: unknown[]; nextCursor: number | null }>( @@ -158,6 +160,7 @@ afterEach(async () => { vi.mocked(dbGetProjectNotes).mockReset().mockReturnValue(null); vi.mocked(dbGetProjects).mockReset().mockReturnValue([]); vi.mocked(dbGetThreadContextUsage).mockReset().mockReturnValue(null); + vi.mocked(dbGetLatestThreadGoalItem).mockReset().mockReturnValue(null); vi.mocked(dbGetLatestThreadRuntimeAnchorItemId).mockReset().mockReturnValue(null); vi.mocked(dbGetThreadRuntimeItems).mockReset().mockReturnValue([]); vi.mocked(dbGetThreadRuntimeItemsPage) @@ -919,8 +922,16 @@ describe("RemoteAccessServer", () => { streams: {}, }, ]; + const goalItem = { + id: "goal-outside-tail", + type: "goal", + state: "updated" as const, + payload: { action: "set", objective: "Keep the remote dock visible" }, + streams: {}, + }; const tailPage = { items: [fullItems[1]!], nextCursor: 41 }; vi.mocked(dbGetThread).mockReturnValue(thread); + vi.mocked(dbGetLatestThreadGoalItem).mockReturnValue(goalItem); vi.mocked(dbGetThreadRuntimeItems).mockReturnValue(fullItems); vi.mocked(dbGetThreadRuntimeItemsPage).mockReturnValue(tailPage); @@ -949,7 +960,7 @@ describe("RemoteAccessServer", () => { ); expect(tailResponse.status).toBe(200); await expect(tailResponse.json()).resolves.toMatchObject({ - runtimeItems: tailPage.items, + runtimeItems: [goalItem, ...tailPage.items], runtimeNextCursor: 41, }); expect(dbGetThreadRuntimeItemsPage).toHaveBeenLastCalledWith( @@ -959,6 +970,8 @@ describe("RemoteAccessServer", () => { 40, ); + const tailWithGoal = { items: [goalItem, ...tailPage.items], nextCursor: 41 }; + vi.mocked(dbGetThreadRuntimeItemsPage).mockReturnValue(tailWithGoal); const narrowTailResponse = await fetch( new URL( "/api/threads/thread-paged/history?runtimePage=1&targetTimelineEntryCount=20", @@ -968,7 +981,7 @@ describe("RemoteAccessServer", () => { ); expect(narrowTailResponse.status).toBe(200); await expect(narrowTailResponse.json()).resolves.toMatchObject({ - runtimeItems: tailPage.items, + runtimeItems: tailWithGoal.items, runtimeNextCursor: 41, }); expect(dbGetThreadRuntimeItemsPage).toHaveBeenLastCalledWith( @@ -978,6 +991,7 @@ describe("RemoteAccessServer", () => { 20, ); + vi.mocked(dbGetThreadRuntimeItemsPage).mockReturnValue(tailPage); const olderResponse = await fetch( new URL( "/api/threads/thread-paged/history/items?beforePosition=41&limit=500&targetTimelineEntryCount=40", diff --git a/src/main/remote/server/snapshots.ts b/src/main/remote/server/snapshots.ts index 41c7aaec1..baa4b5f06 100644 --- a/src/main/remote/server/snapshots.ts +++ b/src/main/remote/server/snapshots.ts @@ -19,6 +19,7 @@ import { dbGetThread, dbGetThreadCompletedTurns, dbGetThreadContextUsage, + dbGetLatestThreadGoalItem, dbGetThreadRuntimeItems, dbGetThreadRuntimeItemsPage, dbGetThreadRuntimeSummaries, @@ -148,16 +149,19 @@ export async function buildThreadSnapshot( const runtimePage = options.runtimePage ? dbGetThreadRuntimeItemsPage(threadId, undefined, 500, options.targetTimelineEntryCount ?? 40) : null; + const runtimeItems = runtimePage?.items ?? dbGetThreadRuntimeItems(threadId); + const latestGoal = runtimePage ? dbGetLatestThreadGoalItem(threadId) : null; + const runtimeItemsWithGoal = + latestGoal && !runtimeItems.some((item) => item.id === latestGoal.id) + ? [latestGoal, ...runtimeItems] + : runtimeItems; return remoteThreadSnapshotSchema.parse( withStableUpdatedAt(`thread:${threadId}`, { snapshotSeq: ctx.seq, thread, // Inline image bytes are replaced by host-minted references: they are ~89% // of runtime payload bytes and the client fetches each one on demand. - runtimeItems: projectRuntimeItemsImageRefs( - threadId, - runtimePage?.items ?? dbGetThreadRuntimeItems(threadId), - ), + runtimeItems: projectRuntimeItemsImageRefs(threadId, runtimeItemsWithGoal), ...(runtimePage ? { runtimeNextCursor: runtimePage.nextCursor } : {}), completedTurns: dbGetThreadCompletedTurns(threadId), contextUsage: dbGetThreadContextUsage(threadId), diff --git a/src/mobile/bridge.ts b/src/mobile/bridge.ts index e8dfb68bc..3b6a2bac7 100644 --- a/src/mobile/bridge.ts +++ b/src/mobile/bridge.ts @@ -289,6 +289,7 @@ const remoteBridgeOverrides = { payload.beforePosition === undefined ? Promise.resolve({ items: [], nextCursor: null }) : invokeRemoteIpcProcedure(requireClient(), "dbGetThreadRuntimeItemsPage", payload), + dbGetLatestThreadGoalItem: () => Promise.resolve(null), dbGetThreadCompletedTurns: () => Promise.resolve([]), dbGetThreadContextUsage: () => Promise.resolve(null), diff --git a/src/mobile/storeSync.applyThreadSnapshot.test.tsx b/src/mobile/storeSync.applyThreadSnapshot.test.tsx index 612749fac..a5053cf5e 100644 --- a/src/mobile/storeSync.applyThreadSnapshot.test.tsx +++ b/src/mobile/storeSync.applyThreadSnapshot.test.tsx @@ -374,6 +374,58 @@ describe("applyThreadSnapshot", () => { expect(assistantStreamText("tail-a")).toBe("updated"); }); + it("catches up an active paged snapshot while preserving a pinned goal's order", () => { + const goal: PersistedRuntimeItem = { + id: "goal-1", + type: "goal", + state: "updated", + payload: { action: "set", objective: "Finish the task" }, + streams: {}, + }; + applyThreadSnapshot( + makeSnapshot({ + status: "working", + items: [goal, makeItem({ id: "tail-a" })], + runtimeNextCursor: 10, + }), + ); + + useAppStore.getState().applyRuntimeEvents(THREAD_ID, [ + { + type: "item.started", + threadId: THREAD_ID, + itemId: "live-message", + itemType: "assistant_message", + }, + { + type: "content.delta", + threadId: THREAD_ID, + itemId: "live-message", + stream: "assistant_text", + delta: "partial", + }, + ]); + + applyThreadSnapshot( + makeSnapshot({ + status: "working", + items: [ + goal, + makeItem({ id: "tail-a" }), + makeItem({ id: "live-message", assistantText: "partial complete" }), + ], + runtimeNextCursor: 10, + }), + ); + + expect(useAppStore.getState().runtimeItemIdsByThread[THREAD_ID]).toEqual([ + "goal-1", + "tail-a", + "live-message", + ]); + expect(assistantStreamText("live-message")).toBe("partial complete"); + }); + it("splices a missed initial user_message ahead of a fresher live transcript", () => { // Launch race: the thread's first events broadcast before this client's // mirrored thread list contained the id, so the live filter dropped the diff --git a/src/mobile/useRemoteDesktop.test.tsx b/src/mobile/useRemoteDesktop.test.tsx index 3a555ae80..1b5905a9e 100644 --- a/src/mobile/useRemoteDesktop.test.tsx +++ b/src/mobile/useRemoteDesktop.test.tsx @@ -255,7 +255,8 @@ vi.mock("./storeSync", () => ({ resetRemoteStores: (...a: unknown[]) => h.resetRemoteStores(...a), })); -vi.mock("@/renderer/state/chatRuntimePersister", () => ({ +vi.mock("@/renderer/state/chatRuntimePersister", async (importOriginal) => ({ + ...(await importOriginal()), seedOlderThreadRuntimeItemsCursor: (...a: unknown[]) => h.seedOlderThreadRuntimeItemsCursor(...a), })); vi.mock("@/renderer/state/fileCheckpointActions", () => ({ @@ -1279,11 +1280,16 @@ describe("useRemoteDesktop", () => { it("preserves an advanced page cursor when a fresh tail overlaps local history", async () => { const desktop = makeDesktop("d1"); const client = clientFor("d1"); - useAppStore.setState({ runtimeItemIdsByThread: { t1: ["shared-tail-start"] } }); + useAppStore.setState({ + runtimeItemIdsByThread: { t1: ["pinned-goal", "shared-tail-start"] }, + }); client.threadHistory.mockResolvedValueOnce({ snapshotSeq: 2, thread: { id: "t1", status: "idle", presentationMode: "gui" }, - runtimeItems: [{ id: "shared-tail-start" }], + runtimeItems: [ + { id: "pinned-goal", type: "goal" }, + { id: "shared-tail-start", type: "assistant_message" }, + ], runtimeNextCursor: 80, completedTurns: [], contextUsage: null, @@ -1303,11 +1309,16 @@ describe("useRemoteDesktop", () => { it("replaces the page cursor when a fresh tail is disjoint from local history", async () => { const desktop = makeDesktop("d1"); const client = clientFor("d1"); - useAppStore.setState({ runtimeItemIdsByThread: { t1: ["cached-tail-start"] } }); + useAppStore.setState({ + runtimeItemIdsByThread: { t1: ["pinned-goal", "cached-tail-start"] }, + }); client.threadHistory.mockResolvedValueOnce({ snapshotSeq: 2, thread: { id: "t1", status: "idle", presentationMode: "gui" }, - runtimeItems: [{ id: "fresh-tail-start" }], + runtimeItems: [ + { id: "pinned-goal", type: "goal" }, + { id: "fresh-tail-start", type: "assistant_message" }, + ], runtimeNextCursor: 120, completedTurns: [], contextUsage: null, diff --git a/src/mobile/useRemoteDesktop.ts b/src/mobile/useRemoteDesktop.ts index 712ffbf9c..94a2a6e85 100644 --- a/src/mobile/useRemoteDesktop.ts +++ b/src/mobile/useRemoteDesktop.ts @@ -28,7 +28,10 @@ import { performThreadInputSubmit } from "@/renderer/actions/threadRuntimeAction import { worktreePlacementPayload } from "@/renderer/actions/worktreePlacement"; import { captureFileCheckpoint } from "@/renderer/state/fileCheckpointActions"; import { useAppStore } from "@/renderer/state/appStore"; -import { seedOlderThreadRuntimeItemsCursor } from "@/renderer/state/chatRuntimePersister"; +import { + runtimePageOverlapsExistingTranscript, + seedOlderThreadRuntimeItemsCursor, +} from "@/renderer/state/chatRuntimePersister"; import { readBridge } from "@/renderer/bridge"; import type { DraftStartInput } from "@/renderer/components/thread/ThreadDraftComposerArea"; import { i18n } from "@/renderer/i18n/i18n"; @@ -679,10 +682,11 @@ export function useRemoteDesktop() { // Bail if the user opened another thread while the fetch was in flight. if (isStaleSelection()) return latest; latest = { snapshot: next, fromServer: true }; - const firstSnapshotItemId = next.runtimeItems[0]?.id; const existingRuntimeItemIds = useAppStore.getState().runtimeItemIdsByThread[threadId] ?? []; - const tailOverlapsExistingTranscript = - firstSnapshotItemId !== undefined && existingRuntimeItemIds.includes(firstSnapshotItemId); + const tailOverlapsExistingTranscript = runtimePageOverlapsExistingTranscript( + next.runtimeItems, + existingRuntimeItemIds, + ); seedOlderThreadRuntimeItemsCursor(threadId, next.runtimeNextCursor ?? null, { preserveExistingCursor: tailOverlapsExistingTranscript, }); diff --git a/src/renderer/remoteProcedureRoutes.ts b/src/renderer/remoteProcedureRoutes.ts index 24a8c18f8..fdce9cb21 100644 --- a/src/renderer/remoteProcedureRoutes.ts +++ b/src/renderer/remoteProcedureRoutes.ts @@ -82,6 +82,7 @@ export const NON_ROUTER_PROJECT_PROCEDURES = { dbDeleteThread: "remote-mirrors-not-persisted", dbSyncAll: "remote-mirrors-not-persisted", dbGetThreadRuntimeItems: "remote-runtime-mirror-local", + dbGetLatestThreadGoalItem: "remote-runtime-snapshot-provided", dbReplaceThreadRuntimeItems: "remote-runtime-mirror-local", dbGetThreadCompletedTurns: "remote-runtime-mirror-local", dbReplaceThreadCompletedTurns: "remote-runtime-mirror-local", diff --git a/src/renderer/state/chatRuntimePersister.test.ts b/src/renderer/state/chatRuntimePersister.test.ts index 548ca270d..53ad8262c 100644 --- a/src/renderer/state/chatRuntimePersister.test.ts +++ b/src/renderer/state/chatRuntimePersister.test.ts @@ -29,6 +29,18 @@ const { bridge } = vi.hoisted(() => ({ .fn<(threadId: string) => Promise>() .mockResolvedValue([]), dbGetThreadContextUsage: vi.fn<(threadId: string) => Promise>().mockResolvedValue(null), + dbGetLatestThreadGoalItem: vi + .fn< + (input: { threadId: string }) => Promise<{ + id: string; + type: string; + state: "started" | "updated" | "completed"; + payload?: unknown; + streams: Record; + parentItemId?: string; + } | null> + >() + .mockResolvedValue(null), }, })); @@ -160,6 +172,7 @@ describe("paged runtime hydration", () => { vi.clearAllMocks(); bridge.dbGetThreadCompletedTurns.mockResolvedValue([]); bridge.dbGetThreadContextUsage.mockResolvedValue(null); + bridge.dbGetLatestThreadGoalItem.mockResolvedValue(null); useAppStore.setState((state) => ({ ...state, runtimeItemIdsByThread: {}, @@ -536,4 +549,92 @@ describe("paged runtime hydration", () => { evictOversizedInactiveThreadRuntimeItems([threadId]); expect(useAppStore.getState().runtimeItemIdsByThread[threadId]).toBeUndefined(); }); + + it("re-pins an out-of-window goal item when the paged tail has none", async () => { + const threadId = "long-goal-thread"; + bridge.dbGetThreadRuntimeItemsPage.mockResolvedValueOnce({ + items: [makeItem({ id: "assistant-recent", type: "assistant_message" })], + nextCursor: 50, + }); + bridge.dbGetLatestThreadGoalItem.mockResolvedValueOnce({ + id: "goal-old", + type: "goal", + state: "updated", + payload: { action: "set", objective: "ship coverage", status: "active" }, + streams: {}, + }); + + await hydrateThreadRuntimeItems(threadId); + + expect(useAppStore.getState().runtimeItemIdsByThread[threadId]).toEqual([ + "goal-old", + "assistant-recent", + ]); + const goal = useAppStore.getState().runtimeItemsByIdByThread[threadId]?.["goal-old"]; + expect(goal?.type).toBe("goal"); + }); + + it("retries hydration after the latest goal lookup fails", async () => { + const threadId = "goal-retry-thread"; + bridge.dbGetThreadRuntimeItemsPage.mockResolvedValue({ + items: [makeItem({ id: "assistant-recent", type: "assistant_message" })], + nextCursor: 50, + }); + bridge.dbGetLatestThreadGoalItem + .mockRejectedValueOnce(new Error("database unavailable")) + .mockResolvedValueOnce({ + id: "goal-old", + type: "goal", + state: "updated", + payload: { action: "set", objective: "retry coverage", status: "active" }, + streams: {}, + }); + + await hydrateThreadRuntimeItems(threadId); + await hydrateThreadRuntimeItems(threadId); + + expect(bridge.dbGetLatestThreadGoalItem).toHaveBeenCalledTimes(2); + expect(useAppStore.getState().runtimeItemIdsByThread[threadId]).toEqual([ + "goal-old", + "assistant-recent", + ]); + }); + + it("does not duplicate a goal item already present in the tail", async () => { + const threadId = "goal-in-tail-thread"; + bridge.dbGetThreadRuntimeItemsPage.mockResolvedValueOnce({ + items: [ + makeItem({ id: "goal-in-tail", type: "goal", payload: { objective: "keep me" } }), + makeItem({ id: "assistant-recent", type: "assistant_message" }), + ], + nextCursor: 50, + }); + bridge.dbGetLatestThreadGoalItem.mockResolvedValueOnce({ + id: "goal-in-tail", + type: "goal", + state: "updated", + payload: { objective: "keep me" }, + streams: {}, + }); + + await hydrateThreadRuntimeItems(threadId); + + expect(useAppStore.getState().runtimeItemIdsByThread[threadId]).toEqual([ + "goal-in-tail", + "assistant-recent", + ]); + }); + + it("does not pin when no persisted goal exists", async () => { + const threadId = "no-goal-thread"; + bridge.dbGetThreadRuntimeItemsPage.mockResolvedValueOnce({ + items: [makeItem({ id: "assistant-recent", type: "assistant_message" })], + nextCursor: null, + }); + bridge.dbGetLatestThreadGoalItem.mockResolvedValueOnce(null); + + await hydrateThreadRuntimeItems(threadId); + + expect(useAppStore.getState().runtimeItemIdsByThread[threadId]).toEqual(["assistant-recent"]); + }); }); diff --git a/src/renderer/state/chatRuntimePersister.ts b/src/renderer/state/chatRuntimePersister.ts index 99e1b2cc4..98fc763b0 100644 --- a/src/renderer/state/chatRuntimePersister.ts +++ b/src/renderer/state/chatRuntimePersister.ts @@ -1,4 +1,5 @@ import type { ToolCallPayload } from "@/shared/contracts"; +import type { PersistedRuntimeItem } from "@/shared/ipc"; import { isDelegatedAgentTool } from "@/shared/toolCallClassification"; import { captureRendererException } from "../diagnostics/sentry"; import { imageViewRendersInline } from "../components/thread/ChatPane/parts/items/imageViewSource"; @@ -56,6 +57,14 @@ export function seedOlderThreadRuntimeItemsCursor( olderRuntimePageCursorByThread.set(threadId, Math.min(currentCursor, cursor)); } +export function runtimePageOverlapsExistingTranscript( + runtimeItems: readonly Pick[], + existingItemIds: readonly string[], +): boolean { + const existingIds = new Set(existingItemIds); + return runtimeItems.some((item) => item.type !== "goal" && existingIds.has(item.id)); +} + export function hasHydratedThreadRuntimeItems(threadId: string): boolean { return hydratedThreadRuntimeIds.has(threadId); } @@ -148,7 +157,7 @@ export async function hydrateThreadRuntimeItems(threadId: string): Promise async function hydrateThreadRuntimeItemsFromDb(threadId: string): Promise { const bridge = readBridge(); - const [itemsResult, turnsResult, contextResult] = await Promise.allSettled([ + const [itemsResult, turnsResult, contextResult, latestGoalResult] = await Promise.allSettled([ Promise.resolve().then(() => bridge.dbGetThreadRuntimeItemsPage({ threadId, @@ -158,6 +167,7 @@ async function hydrateThreadRuntimeItemsFromDb(threadId: string): Promise bridge.dbGetThreadCompletedTurns(threadId)), Promise.resolve().then(() => bridge.dbGetThreadContextUsage(threadId)), + Promise.resolve().then(() => bridge.dbGetLatestThreadGoalItem({ threadId })), ]); if (itemsResult.status === "fulfilled") { @@ -193,6 +203,19 @@ async function hydrateThreadRuntimeItemsFromDb(threadId: string): Promise