From e4d21d30d5ad4351748a28b4e71b8d502b5bb342 Mon Sep 17 00:00:00 2001 From: Frank Hamand Date: Mon, 13 Jul 2026 14:59:18 +0100 Subject: [PATCH] fix(code): never lose a local task's initial prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial prompt for a local task was only held in memory, so if the agent hadn't produced its first response yet and the user looked away (app backgrounded, reloaded, crashed, or a transient connect failure exhausted its silent retries), the prompt was lost and the task never started. Persist the prompt durably in the workspace-server task_metadata table keyed by task id the moment the task run is created, re-send it on resume when the agent hasn't consumed it (detected via the session/prompt echo in the replayed log), and clear it once consumed. clearSessionError now falls back to the durable copy so Retry and auto-retry recover after a reload wiped the in-memory session. Cloud tasks are unaffected — they self-fetch their prompt server-side. Generated-By: PostHog Code Task-Id: a4d03ce4-bd03-46db-8893-8a110e54e865 --- .../sessions/initialPromptPersistence.test.ts | 208 ++++ .../src/sessions/sessionEventBatching.test.ts | 5 + packages/core/src/sessions/sessionService.ts | 142 ++- .../src/routers/workspace.router.ts | 26 + .../migrations/0021_melodic_george_stacy.sql | 1 + .../src/db/migrations/meta/0021_snapshot.json | 1079 +++++++++++++++++ .../src/db/migrations/meta/_journal.json | 9 +- .../task-metadata-repository.mock.ts | 6 + .../task-metadata-repository.test.ts | 56 + .../repositories/task-metadata-repository.ts | 6 + packages/workspace-server/src/db/schema.ts | 5 + .../workspace-metadata/workspace-metadata.ts | 15 + .../src/services/workspace/schemas.ts | 16 + 13 files changed, 1560 insertions(+), 14 deletions(-) create mode 100644 packages/core/src/sessions/initialPromptPersistence.test.ts create mode 100644 packages/workspace-server/src/db/migrations/0021_melodic_george_stacy.sql create mode 100644 packages/workspace-server/src/db/migrations/meta/0021_snapshot.json create mode 100644 packages/workspace-server/src/db/repositories/task-metadata-repository.test.ts diff --git a/packages/core/src/sessions/initialPromptPersistence.test.ts b/packages/core/src/sessions/initialPromptPersistence.test.ts new file mode 100644 index 0000000000..b354907814 --- /dev/null +++ b/packages/core/src/sessions/initialPromptPersistence.test.ts @@ -0,0 +1,208 @@ +import type { ContentBlock } from "@agentclientprotocol/sdk"; +import type { AcpMessage, AgentSession } from "@posthog/shared"; +import { describe, expect, it, vi } from "vitest"; +import { SessionService, type SessionServiceDeps } from "./sessionService"; + +const TASK_ID = "task-1"; +const RUN_ID = "run-1"; + +const PROMPT: ContentBlock[] = [{ type: "text", text: "do the thing" }]; + +function promptEcho(): AcpMessage { + return { + type: "acp_message", + ts: 0, + message: { + jsonrpc: "2.0", + id: 1, + method: "session/prompt", + params: {}, + }, + } as unknown as AcpMessage; +} + +function createHarness( + overrides: { + session?: AgentSession | null; + getPendingInitialPrompt?: string | null; + } = {}, +) { + const sessions: Record = {}; + if (overrides.session) + sessions[overrides.session.taskRunId] = overrides.session; + + const setPendingInitialPrompt = vi.fn().mockResolvedValue(undefined); + const getPendingInitialPrompt = vi + .fn() + .mockResolvedValue(overrides.getPendingInitialPrompt ?? null); + const clearPendingInitialPrompt = vi.fn().mockResolvedValue(undefined); + + const store = { + getSessions: () => sessions, + getSessionByTaskId: (taskId: string) => + Object.values(sessions).find((s) => s.taskId === taskId), + setSession: (session: AgentSession) => { + sessions[session.taskRunId] = session; + }, + updateSession: (taskRunId: string, updates: Partial) => { + const session = sessions[taskRunId]; + if (session) Object.assign(session, updates); + }, + replaceOptimisticWithEvent: vi.fn(), + appendEvents: vi.fn(), + }; + + const deps = { + store, + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, + trpc: { + agent: { + onSessionIdleKilled: { subscribe: () => ({ unsubscribe: vi.fn() }) }, + }, + workspace: { + setPendingInitialPrompt: { mutate: setPendingInitialPrompt }, + getPendingInitialPrompt: { query: getPendingInitialPrompt }, + clearPendingInitialPrompt: { mutate: clearPendingInitialPrompt }, + }, + }, + } as unknown as SessionServiceDeps; + + const service = new SessionService(deps); + + return { + service, + setPendingInitialPrompt, + getPendingInitialPrompt, + clearPendingInitialPrompt, + }; +} + +describe("initial prompt persistence", () => { + describe("resendPendingPromptIfNeeded", () => { + it("clears the durable prompt without resending when the log already has the echo", async () => { + const h = createHarness({ + getPendingInitialPrompt: JSON.stringify(PROMPT), + }); + const sendPrompt = vi + .spyOn(h.service, "sendPrompt") + .mockResolvedValue({ stopReason: "end_turn" }); + + await ( + h.service as unknown as { + resendPendingPromptIfNeeded: ( + taskId: string, + events: AcpMessage[], + ) => Promise; + } + ).resendPendingPromptIfNeeded(TASK_ID, [promptEcho()]); + + expect(h.clearPendingInitialPrompt).toHaveBeenCalledWith({ + taskId: TASK_ID, + }); + expect(h.getPendingInitialPrompt).not.toHaveBeenCalled(); + expect(sendPrompt).not.toHaveBeenCalled(); + }); + + it("resends the stored prompt exactly once when the log lacks the echo", async () => { + const h = createHarness({ + getPendingInitialPrompt: JSON.stringify(PROMPT), + }); + const sendPrompt = vi + .spyOn(h.service, "sendPrompt") + .mockResolvedValue({ stopReason: "end_turn" }); + + await ( + h.service as unknown as { + resendPendingPromptIfNeeded: ( + taskId: string, + events: AcpMessage[], + ) => Promise; + } + ).resendPendingPromptIfNeeded(TASK_ID, []); + + expect(sendPrompt).toHaveBeenCalledTimes(1); + expect(sendPrompt).toHaveBeenCalledWith(TASK_ID, PROMPT); + }); + + it("does nothing when there is no stored prompt", async () => { + const h = createHarness({ getPendingInitialPrompt: null }); + const sendPrompt = vi + .spyOn(h.service, "sendPrompt") + .mockResolvedValue({ stopReason: "end_turn" }); + + await ( + h.service as unknown as { + resendPendingPromptIfNeeded: ( + taskId: string, + events: AcpMessage[], + ) => Promise; + } + ).resendPendingPromptIfNeeded(TASK_ID, []); + + expect(sendPrompt).not.toHaveBeenCalled(); + }); + }); + + describe("handleSessionEvent", () => { + it("clears the durable prompt on the prompt echo", () => { + const session = { + taskRunId: RUN_ID, + taskId: TASK_ID, + events: [], + messageQueue: [], + optimisticItems: [], + } as unknown as AgentSession; + const h = createHarness({ session }); + + ( + h.service as unknown as { + handleSessionEvent: (runId: string, msg: AcpMessage) => void; + } + ).handleSessionEvent(RUN_ID, promptEcho()); + + expect(h.clearPendingInitialPrompt).toHaveBeenCalledWith({ + taskId: TASK_ID, + }); + }); + }); + + describe("clearSessionError", () => { + it("recovers the durable prompt when the in-memory session is gone", async () => { + const h = createHarness({ + session: null, + getPendingInitialPrompt: JSON.stringify(PROMPT), + }); + const createNewLocalSession = vi + .spyOn( + h.service as unknown as { + createNewLocalSession: (...args: unknown[]) => Promise; + }, + "createNewLocalSession", + ) + .mockResolvedValue(undefined); + vi.spyOn( + h.service as unknown as { + getAuthCredentialsStatus: () => Promise; + }, + "getAuthCredentialsStatus", + ).mockResolvedValue({ kind: "ready", auth: { client: {} } }); + + await h.service.clearSessionError(TASK_ID, "/repo"); + + expect(h.getPendingInitialPrompt).toHaveBeenCalledWith({ + taskId: TASK_ID, + }); + expect(createNewLocalSession).toHaveBeenCalledWith( + TASK_ID, + "Task", + "/repo", + { client: {} }, + PROMPT, + undefined, + undefined, + undefined, + undefined, + ); + }); + }); +}); diff --git a/packages/core/src/sessions/sessionEventBatching.test.ts b/packages/core/src/sessions/sessionEventBatching.test.ts index c1d47f2c64..75befb7f85 100644 --- a/packages/core/src/sessions/sessionEventBatching.test.ts +++ b/packages/core/src/sessions/sessionEventBatching.test.ts @@ -109,6 +109,11 @@ function createHarness() { subscribe: () => ({ unsubscribe: vi.fn() }), }, }, + workspace: { + clearPendingInitialPrompt: { + mutate: vi.fn().mockResolvedValue(undefined), + }, + }, }, } as unknown as SessionServiceDeps; diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index c883aed101..332be34d50 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -166,7 +166,12 @@ export interface SessionTrpc { onPermissionRequest: TrpcSubscription; onSessionIdleKilled: TrpcSubscription; }; - workspace: { verify: TrpcQuery }; + workspace: { + verify: TrpcQuery; + setPendingInitialPrompt: TrpcMutation; + getPendingInitialPrompt: TrpcQuery; + clearPendingInitialPrompt: TrpcMutation; + }; cloudTask: { watch: TrpcMutation; unwatch: TrpcMutation; @@ -856,7 +861,7 @@ export class SessionService { return; } - await this.reconnectToLocalSession( + const reconnected = await this.reconnectToLocalSession( taskId, existingRunId, taskTitle, @@ -865,6 +870,12 @@ export class SessionService { auth, logResult, ); + if (reconnected) { + await this.resendPendingPromptIfNeeded( + taskId, + convertStoredEntriesToEvents(logResult.rawEntries), + ); + } } else { if (!this.d.getIsOnline()) { this.d.log.info("Skipping connection attempt - offline", { taskId }); @@ -1160,6 +1171,61 @@ export class SessionService { } } + /** + * After resuming a local session, re-send a durably-stored initial prompt + * that the agent never got a chance to consume (e.g. the app reloaded or + * crashed between task creation and the agent's first response). + * + * The agent echoes every prompt it receives as a `session/prompt` request in + * the run log — the same signal `handleSessionEvent` uses to clear the + * in-memory `initialPrompt`. If the replayed log already contains that echo, + * the prompt was delivered, so we just clear the durable copy. Otherwise we + * re-send it exactly once; the live echo then clears the durable copy. + */ + private async resendPendingPromptIfNeeded( + taskId: string, + replayedEvents: AcpMessage[], + ): Promise { + const alreadyConsumed = replayedEvents.some( + (e) => + isJsonRpcRequest(e.message) && e.message.method === "session/prompt", + ); + if (alreadyConsumed) { + void this.d.trpc.workspace.clearPendingInitialPrompt + .mutate({ taskId }) + .catch(() => {}); + return; + } + + let promptJson: string | null = null; + try { + promptJson = await this.d.trpc.workspace.getPendingInitialPrompt.query({ + taskId, + }); + } catch (err) { + this.d.log.warn("Failed to read pending initial prompt", { taskId, err }); + return; + } + if (!promptJson) return; + + let prompt: ContentBlock[]; + try { + prompt = JSON.parse(promptJson) as ContentBlock[]; + } catch (err) { + this.d.log.warn("Failed to parse pending initial prompt", { + taskId, + err, + }); + return; + } + if (!prompt.length) return; + + this.d.log.info("Re-sending unconsumed initial prompt after resume", { + taskId, + }); + await this.sendPrompt(taskId, prompt); + } + private async teardownSession( taskRunId: string, opts?: { preserveResumeState?: boolean }, @@ -1393,6 +1459,23 @@ export class SessionService { throw new Error("Failed to create task run. Please try again."); } + // Durably persist the initial prompt before spawning the agent, so it + // survives reload/crash/transient failure before the agent's first + // response. Never let a persistence failure abort task creation. + if (initialPrompt?.length) { + try { + await this.d.trpc.workspace.setPendingInitialPrompt.mutate({ + taskId, + promptJson: JSON.stringify(initialPrompt), + }); + } catch (err) { + this.d.log.warn("Failed to persist pending initial prompt", { + taskId, + err, + }); + } + } + const { customInstructions: startCustomInstructions } = this.d.settings; const preferredModel = model ?? this.d.DEFAULT_GATEWAY_MODEL; const result = await this.d.trpc.agent.start.mutate({ @@ -2044,6 +2127,16 @@ export class SessionService { isJsonRpcRequest(acpMsg.message) && acpMsg.message.method === "session/prompt"; + // The prompt echo means the agent received the initial prompt, so the + // durable copy is no longer needed. Fire independently of in-memory state + // so it also clears after a resume-resend (where the rebuilt session has + // no in-memory initialPrompt). Clearing a null row is a harmless no-op. + if (isUserPromptEcho) { + void this.d.trpc.workspace.clearPendingInitialPrompt + .mutate({ taskId: session.taskId }) + .catch(() => {}); + } + // Once the agent starts responding, clear initialPrompt so that // retry reconnects to this session instead of creating a new one. if (!isUserPromptEcho && session.initialPrompt?.length) { @@ -4016,19 +4109,42 @@ export class SessionService { async clearSessionError(taskId: string, repoPath: string): Promise { this.localRepoPaths.set(taskId, repoPath); const session = this.d.store.getSessionByTaskId(taskId); + + // Prefer the in-memory prompt; fall back to the durable copy so retry + // still recovers after a reload wiped the in-memory session (a resumed + // session is rebuilt without its initialPrompt). + let initialPrompt = session?.initialPrompt; + if (!initialPrompt?.length) { + try { + const promptJson = + await this.d.trpc.workspace.getPendingInitialPrompt.query({ taskId }); + if (promptJson) { + initialPrompt = JSON.parse(promptJson) as ContentBlock[]; + } + } catch (err) { + this.d.log.warn("Failed to read pending initial prompt on retry", { + taskId, + err, + }); + } + } + + // Only recreate the run if it holds no conversation beyond prompt echoes — + // recreating over history orphans the populated run log. With no in-memory + // session (post-reload) a surviving durable prompt means it was never + // consumed, so there is nothing to orphan. if ( - session?.initialPrompt?.length && - !(await this.runHasConversationHistory(session)) + initialPrompt?.length && + (!session || !(await this.runHasConversationHistory(session))) ) { - const { - taskTitle, - initialPrompt, - executionMode, - adapter, - model, - reasoningLevel, - } = session; - await this.teardownSession(session.taskRunId); + const taskTitle = session?.taskTitle ?? "Task"; + const executionMode = session?.executionMode; + const adapter = session?.adapter; + const model = session?.model; + const reasoningLevel = session?.reasoningLevel; + if (session) { + await this.teardownSession(session.taskRunId); + } const authStatus = await this.getAuthCredentialsStatus(); if (authStatus.kind === "restoring") { throw new Error("Authentication is still restoring. Please wait."); diff --git a/packages/host-router/src/routers/workspace.router.ts b/packages/host-router/src/routers/workspace.router.ts index 6a1e65179d..3253183612 100644 --- a/packages/host-router/src/routers/workspace.router.ts +++ b/packages/host-router/src/routers/workspace.router.ts @@ -6,6 +6,7 @@ import { cachedPrUrlOutput, checkWorktreeBranchInput, checkWorktreeBranchOutput, + clearPendingInitialPromptInput, createWorkspaceInput, createWorkspaceOutput, deleteWorkspaceInput, @@ -16,6 +17,8 @@ import { getAllWorkspacesOutput, getLocalTasksInput, getLocalTasksOutput, + getPendingInitialPromptInput, + getPendingInitialPromptOutput, getPinnedTaskIdsOutput, getTaskTimestampsInput, getTaskTimestampsOutput, @@ -36,6 +39,7 @@ import { markViewedInput, reconcileCloudWorkspacesInput, reconcileCloudWorkspacesOutput, + setPendingInitialPromptInput, setPrimaryPrUrlInput, taskPrStatusInput, taskPrStatusOutput, @@ -217,6 +221,28 @@ export const workspaceRouter = router({ .output(getAllTaskTimestampsOutput) .query(({ ctx }) => getMetadata(ctx.container).getAllTaskTimestamps()), + setPendingInitialPrompt: publicProcedure + .input(setPendingInitialPromptInput) + .mutation(({ ctx, input }) => + getMetadata(ctx.container).setPendingInitialPrompt( + input.taskId, + input.promptJson, + ), + ), + + getPendingInitialPrompt: publicProcedure + .input(getPendingInitialPromptInput) + .output(getPendingInitialPromptOutput) + .query(({ ctx, input }) => + getMetadata(ctx.container).getPendingInitialPrompt(input.taskId), + ), + + clearPendingInitialPrompt: publicProcedure + .input(clearPendingInitialPromptInput) + .mutation(({ ctx, input }) => + getMetadata(ctx.container).clearPendingInitialPrompt(input.taskId), + ), + linkBranch: publicProcedure .input(linkBranchInput) .mutation(({ ctx, input }) => diff --git a/packages/workspace-server/src/db/migrations/0021_melodic_george_stacy.sql b/packages/workspace-server/src/db/migrations/0021_melodic_george_stacy.sql new file mode 100644 index 0000000000..9b9e26827a --- /dev/null +++ b/packages/workspace-server/src/db/migrations/0021_melodic_george_stacy.sql @@ -0,0 +1 @@ +ALTER TABLE `task_metadata` ADD `pending_initial_prompt` text; \ No newline at end of file diff --git a/packages/workspace-server/src/db/migrations/meta/0021_snapshot.json b/packages/workspace-server/src/db/migrations/meta/0021_snapshot.json new file mode 100644 index 0000000000..2c9a881cff --- /dev/null +++ b/packages/workspace-server/src/db/migrations/meta/0021_snapshot.json @@ -0,0 +1,1079 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3f59444d-2f92-46f5-991d-9d3f2c4515f5", + "prevId": "e99d8d2c-d74b-4dea-9261-b5923c869d7a", + "tables": { + "archives": { + "name": "archives", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "archives_workspaceId_unique": { + "name": "archives_workspaceId_unique", + "columns": [ + "workspace_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "archives_workspace_id_workspaces_id_fk": { + "name": "archives_workspace_id_workspaces_id_fk", + "tableFrom": "archives", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_org_project_preferences": { + "name": "auth_org_project_preferences", + "columns": { + "account_key": { + "name": "account_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_region": { + "name": "cloud_region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_selected_project_id": { + "name": "last_selected_project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "auth_org_project_account_region_org_idx": { + "name": "auth_org_project_account_region_org_idx", + "columns": [ + "account_key", + "cloud_region", + "org_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_preferences": { + "name": "auth_preferences", + "columns": { + "account_key": { + "name": "account_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_region": { + "name": "cloud_region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_selected_project_id": { + "name": "last_selected_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_selected_org_id": { + "name": "last_selected_org_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "auth_preferences_account_region_idx": { + "name": "auth_preferences_account_region_idx", + "columns": [ + "account_key", + "cloud_region" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_sessions": { + "name": "auth_sessions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_region": { + "name": "cloud_region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "selected_project_id": { + "name": "selected_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_version": { + "name": "scope_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "autoresearch_runs": { + "name": "autoresearch_runs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "autoresearch_runs_task_id_idx": { + "name": "autoresearch_runs_task_id_idx", + "columns": [ + "task_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "browser_tabs": { + "name": "browser_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "window_id": { + "name": "window_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel_section": { + "name": "channel_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "app_view": { + "name": "app_view", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scroll_state": { + "name": "scroll_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "browser_tabs_window_idx": { + "name": "browser_tabs_window_idx", + "columns": [ + "window_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "browser_tabs_window_id_browser_windows_id_fk": { + "name": "browser_tabs_window_id_browser_windows_id_fk", + "tableFrom": "browser_tabs", + "tableTo": "browser_windows", + "columnsFrom": [ + "window_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "browser_windows": { + "name": "browser_windows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "bounds": { + "name": "bounds", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_tab_id": { + "name": "active_tab_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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": {} + }, + "claude_session_imports": { + "name": "claude_session_imports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_session_id": { + "name": "source_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_session_id": { + "name": "imported_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repo_path": { + "name": "repo_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_mtime_ms": { + "name": "source_mtime_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_size_bytes": { + "name": "source_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_last_entry_uuid": { + "name": "source_last_entry_uuid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "claude_session_imports_importedSessionId_unique": { + "name": "claude_session_imports_importedSessionId_unique", + "columns": [ + "imported_session_id" + ], + "isUnique": true + }, + "claude_session_imports_source_idx": { + "name": "claude_session_imports_source_idx", + "columns": [ + "source_session_id" + ], + "isUnique": false + }, + "claude_session_imports_task_idx": { + "name": "claude_session_imports_task_idx", + "columns": [ + "task_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "default_additional_directories": { + "name": "default_additional_directories", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories": { + "name": "repositories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "repositories_path_unique": { + "name": "repositories_path_unique", + "columns": [ + "path" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "suspensions": { + "name": "suspensions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "suspensions_workspaceId_unique": { + "name": "suspensions_workspaceId_unique", + "columns": [ + "workspace_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "suspensions_workspace_id_workspaces_id_fk": { + "name": "suspensions_workspace_id_workspaces_id_fk", + "tableFrom": "suspensions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_metadata": { + "name": "task_metadata", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_viewed_at": { + "name": "last_viewed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_initial_prompt": { + "name": "pending_initial_prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspaces": { + "name": "workspaces", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_branch": { + "name": "linked_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_viewed_at": { + "name": "last_viewed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "additional_directories": { + "name": "additional_directories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_state": { + "name": "pr_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_urls": { + "name": "pr_urls", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "workspaces_taskId_unique": { + "name": "workspaces_taskId_unique", + "columns": [ + "task_id" + ], + "isUnique": true + }, + "workspaces_repository_id_idx": { + "name": "workspaces_repository_id_idx", + "columns": [ + "repository_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "workspaces_repository_id_repositories_id_fk": { + "name": "workspaces_repository_id_repositories_id_fk", + "tableFrom": "workspaces", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "worktrees": { + "name": "worktrees", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "worktrees_workspaceId_unique": { + "name": "worktrees_workspaceId_unique", + "columns": [ + "workspace_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "worktrees_workspace_id_workspaces_id_fk": { + "name": "worktrees_workspace_id_workspaces_id_fk", + "tableFrom": "worktrees", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/workspace-server/src/db/migrations/meta/_journal.json b/packages/workspace-server/src/db/migrations/meta/_journal.json index 25b5e27f0f..0187e149e9 100644 --- a/packages/workspace-server/src/db/migrations/meta/_journal.json +++ b/packages/workspace-server/src/db/migrations/meta/_journal.json @@ -148,6 +148,13 @@ "when": 1783685997328, "tag": "0020_repair_browser_tabs_schema", "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1784043796550, + "tag": "0021_melodic_george_stacy", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/workspace-server/src/db/repositories/task-metadata-repository.mock.ts b/packages/workspace-server/src/db/repositories/task-metadata-repository.mock.ts index 4bea1b96e7..1c1d131880 100644 --- a/packages/workspace-server/src/db/repositories/task-metadata-repository.mock.ts +++ b/packages/workspace-server/src/db/repositories/task-metadata-repository.mock.ts @@ -33,6 +33,10 @@ export function createMockTaskMetadataRepository(): MockTaskMetadataRepository { "archivedAt" in patch ? (patch.archivedAt ?? null) : (existing?.archivedAt ?? null), + pendingInitialPrompt: + "pendingInitialPrompt" in patch + ? (patch.pendingInitialPrompt ?? null) + : (existing?.pendingInitialPrompt ?? null), createdAt: existing?.createdAt ?? ts, updatedAt: ts, }); @@ -44,6 +48,8 @@ export function createMockTaskMetadataRepository(): MockTaskMetadataRepository { findAllPinned: () => [...rows.values()].filter((r) => r.pinnedAt != null), findAllArchived: () => [...rows.values()].filter((r) => r.archivedAt != null), + getPendingInitialPrompt: (taskId) => + rows.get(taskId)?.pendingInitialPrompt ?? null, upsert: apply, delete: (taskId) => { rows.delete(taskId); diff --git a/packages/workspace-server/src/db/repositories/task-metadata-repository.test.ts b/packages/workspace-server/src/db/repositories/task-metadata-repository.test.ts new file mode 100644 index 0000000000..5c6de1d174 --- /dev/null +++ b/packages/workspace-server/src/db/repositories/task-metadata-repository.test.ts @@ -0,0 +1,56 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { DatabaseService } from "../service"; +import { createTestDb, type TestDatabase } from "../test-helpers"; +import { TaskMetadataRepository } from "./task-metadata-repository"; + +let testDb: TestDatabase; +let repo: TaskMetadataRepository; + +beforeEach(() => { + testDb = createTestDb(); + const databaseService = { db: testDb.db } as unknown as DatabaseService; + repo = new TaskMetadataRepository(databaseService); +}); + +afterEach(() => { + testDb.close(); +}); + +describe("TaskMetadataRepository pending initial prompt", () => { + it("round-trips a pending initial prompt", () => { + repo.upsert("task-1", { pendingInitialPrompt: '[{"type":"text"}]' }); + + expect(repo.getPendingInitialPrompt("task-1")).toBe('[{"type":"text"}]'); + }); + + it("returns null when nothing is stored", () => { + expect(repo.getPendingInitialPrompt("missing")).toBeNull(); + }); + + it("clears the pending prompt when set to null", () => { + repo.upsert("task-1", { pendingInitialPrompt: '[{"type":"text"}]' }); + repo.upsert("task-1", { pendingInitialPrompt: null }); + + expect(repo.getPendingInitialPrompt("task-1")).toBeNull(); + }); + + it("does not wipe the pending prompt when other fields are upserted", () => { + repo.upsert("task-1", { pendingInitialPrompt: '[{"type":"text"}]' }); + repo.upsert("task-1", { pinnedAt: "2026-07-13T00:00:00.000Z" }); + + expect(repo.getPendingInitialPrompt("task-1")).toBe('[{"type":"text"}]'); + expect(repo.findByTaskId("task-1")?.pinnedAt).toBe( + "2026-07-13T00:00:00.000Z", + ); + }); + + it("does not wipe other fields when the pending prompt is upserted", () => { + repo.upsert("task-1", { pinnedAt: "2026-07-13T00:00:00.000Z" }); + repo.upsert("task-1", { pendingInitialPrompt: '[{"type":"text"}]' }); + + expect(repo.findByTaskId("task-1")?.pinnedAt).toBe( + "2026-07-13T00:00:00.000Z", + ); + expect(repo.getPendingInitialPrompt("task-1")).toBe('[{"type":"text"}]'); + }); +}); diff --git a/packages/workspace-server/src/db/repositories/task-metadata-repository.ts b/packages/workspace-server/src/db/repositories/task-metadata-repository.ts index 93fefcbc5d..68dac624d0 100644 --- a/packages/workspace-server/src/db/repositories/task-metadata-repository.ts +++ b/packages/workspace-server/src/db/repositories/task-metadata-repository.ts @@ -14,6 +14,7 @@ export interface TaskMetadataPatch { lastViewedAt?: string | null; lastActivityAt?: string | null; archivedAt?: string | null; + pendingInitialPrompt?: string | null; } /** @@ -27,6 +28,7 @@ export interface ITaskMetadataRepository { findAll(): TaskMetadataRow[]; findAllPinned(): TaskMetadataRow[]; findAllArchived(): TaskMetadataRow[]; + getPendingInitialPrompt(taskId: string): string | null; upsert(taskId: string, patch: TaskMetadataPatch): void; delete(taskId: string): void; } @@ -72,6 +74,10 @@ export class TaskMetadataRepository implements ITaskMetadataRepository { .all(); } + getPendingInitialPrompt(taskId: string): string | null { + return this.findByTaskId(taskId)?.pendingInitialPrompt ?? null; + } + upsert(taskId: string, patch: TaskMetadataPatch): void { const timestamp = now(); this.db diff --git a/packages/workspace-server/src/db/schema.ts b/packages/workspace-server/src/db/schema.ts index 4f2e47de9d..453c0be273 100644 --- a/packages/workspace-server/src/db/schema.ts +++ b/packages/workspace-server/src/db/schema.ts @@ -59,6 +59,11 @@ export const taskMetadata = sqliteTable("task_metadata", { // row, so this timestamp is their only home — without it, archiving them is a // silent no-op and they reappear on the next refetch. archivedAt: text(), + // JSON-encoded ContentBlock[] initial prompt for a LOCAL task, stored the + // moment the task run is created so it survives reload/crash before the + // agent's first response. Cleared once the agent echoes the prompt + // (session/prompt). Null = nothing pending. + pendingInitialPrompt: text(), createdAt: createdAt(), updatedAt: updatedAt(), }); diff --git a/packages/workspace-server/src/services/workspace-metadata/workspace-metadata.ts b/packages/workspace-server/src/services/workspace-metadata/workspace-metadata.ts index e7eb2b3f9d..af79017fc6 100644 --- a/packages/workspace-server/src/services/workspace-metadata/workspace-metadata.ts +++ b/packages/workspace-server/src/services/workspace-metadata/workspace-metadata.ts @@ -73,6 +73,21 @@ export class WorkspaceMetadataService { this.taskMetadataRepo.upsert(taskId, { lastActivityAt }); } + // The pending initial prompt is short-lived per-task creation state, always + // keyed purely by task id in `task_metadata` — it never lives on a + // `workspaces` row, so (unlike pin/view/activity) there is no fallback branch. + setPendingInitialPrompt(taskId: string, promptJson: string): void { + this.taskMetadataRepo.upsert(taskId, { pendingInitialPrompt: promptJson }); + } + + getPendingInitialPrompt(taskId: string): string | null { + return this.taskMetadataRepo.getPendingInitialPrompt(taskId); + } + + clearPendingInitialPrompt(taskId: string): void { + this.taskMetadataRepo.upsert(taskId, { pendingInitialPrompt: null }); + } + getPinnedTaskIds(): string[] { return [ ...this.workspaceRepo.findAllPinned().map((w) => w.taskId), diff --git a/packages/workspace-server/src/services/workspace/schemas.ts b/packages/workspace-server/src/services/workspace/schemas.ts index 4d08d5587d..171afc2c33 100644 --- a/packages/workspace-server/src/services/workspace/schemas.ts +++ b/packages/workspace-server/src/services/workspace/schemas.ts @@ -277,6 +277,22 @@ export const getAllTaskTimestampsOutput = z.record( }), ); +// Durable per-task pending initial prompt (JSON-encoded ContentBlock[]). +export const setPendingInitialPromptInput = z.object({ + taskId: z.string(), + promptJson: z.string(), +}); + +export const getPendingInitialPromptInput = z.object({ + taskId: z.string(), +}); + +export const getPendingInitialPromptOutput = z.string().nullable(); + +export const clearPendingInitialPromptInput = z.object({ + taskId: z.string(), +}); + // Task PR status export const taskPrStatusInput = z.object({ taskId: z.string(),