fix(dashboard): plumb TaskStore through submit/retry/rewind planning APIs
createSession captured a TaskStore but submitResponse/retrySession/ rewindSession had no way to receive one — so rehydrated sessions (loaded from SQLite, no prior createSession call) failed with "Planning session has no task store and cannot be resumed without project context" when they needed to rebuild an agent. - Add optional `store` to Session and capture it on createSession. - Extend submitResponse, retrySession, rewindSession to accept and propagate a store argument; ensureSessionAgent falls back to the session-captured value when the caller doesn't plumb one. - Update planning.test.ts rehydration paths to pass MOCK_TASK_STORE where they previously relied on the implicit error to test against. Fixes 7 failing tests in planning.test.ts (rehydration + retry/rewind flows). Same pattern applies to mission-interview.ts and the related session-* test files but is deferred to a separate commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -959,7 +959,7 @@ describe("planning module", () => {
|
||||
const createFnAgentSpy = vi.fn(async () => resumedAgent);
|
||||
__setCreateFnAgent(createFnAgentSpy as any);
|
||||
|
||||
const response = await submitResponse(row.id, { refine: true }, TEST_ROOT_DIR);
|
||||
const response = await submitResponse(row.id, { refine: true }, TEST_ROOT_DIR, undefined, MOCK_TASK_STORE);
|
||||
expect(response.type).toBe("question");
|
||||
if (response.type === "question") {
|
||||
expect(response.data.id).toBe("q-refine-rehydrated");
|
||||
@@ -1016,6 +1016,8 @@ describe("planning module", () => {
|
||||
row.id,
|
||||
{ "q-2": "Must run on mobile" },
|
||||
TEST_ROOT_DIR,
|
||||
undefined,
|
||||
MOCK_TASK_STORE,
|
||||
);
|
||||
|
||||
expect(response.type).toBe("question");
|
||||
@@ -1177,7 +1179,7 @@ describe("planning module", () => {
|
||||
]);
|
||||
__setCreateFnAgent(async () => resumedAgent);
|
||||
|
||||
await retrySession(row.id, TEST_ROOT_DIR);
|
||||
await retrySession(row.id, TEST_ROOT_DIR, undefined, MOCK_TASK_STORE);
|
||||
|
||||
expect(resumedAgent.session.prompt).toHaveBeenCalledTimes(1);
|
||||
expect(resumedAgent.session.prompt.mock.calls[0]?.[0]).toContain("What should we build?");
|
||||
@@ -1216,7 +1218,7 @@ describe("planning module", () => {
|
||||
]);
|
||||
__setCreateFnAgent(async () => resumedAgent);
|
||||
|
||||
await retrySession(row.id, TEST_ROOT_DIR);
|
||||
await retrySession(row.id, TEST_ROOT_DIR, undefined, MOCK_TASK_STORE);
|
||||
|
||||
expect(resumedAgent.session.prompt).toHaveBeenCalledTimes(1);
|
||||
expect(resumedAgent.session.prompt.mock.calls[0]?.[0]).toBe("Ship notifications");
|
||||
@@ -1271,7 +1273,7 @@ describe("planning module", () => {
|
||||
const createFnAgentSpy = vi.fn(async () => resumedAgent);
|
||||
__setCreateFnAgent(createFnAgentSpy as any);
|
||||
|
||||
await retrySession(row.id, TEST_ROOT_DIR, promptOverrides);
|
||||
await retrySession(row.id, TEST_ROOT_DIR, promptOverrides, MOCK_TASK_STORE);
|
||||
|
||||
expect(createFnAgentSpy).toHaveBeenCalledTimes(1);
|
||||
const callArg = createFnAgentSpy.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
@@ -1314,7 +1316,7 @@ describe("planning module", () => {
|
||||
const createFnAgentSpy = vi.fn(async () => resumedAgent);
|
||||
__setCreateFnAgent(createFnAgentSpy as any);
|
||||
|
||||
await retrySession(row.id, TEST_ROOT_DIR);
|
||||
await retrySession(row.id, TEST_ROOT_DIR, undefined, MOCK_TASK_STORE);
|
||||
|
||||
expect(createFnAgentSpy).toHaveBeenCalledTimes(1);
|
||||
const callArg = createFnAgentSpy.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
|
||||
@@ -318,6 +318,15 @@ interface Session {
|
||||
error?: string;
|
||||
/** AI agent session for real-time interaction */
|
||||
agent?: AgentResult;
|
||||
/**
|
||||
* TaskStore reference captured at session creation. Used by
|
||||
* ensureSessionAgent to rebuild the agent after rehydration (when no
|
||||
* store is plumbed through the submitResponse/retry/rewind call sites).
|
||||
* Not persisted — restored only for the lifetime of the in-memory session.
|
||||
*/
|
||||
store?: TaskStore;
|
||||
/** Project root captured at session creation; mirrors `store` for agent rebuild. */
|
||||
rootDir?: string;
|
||||
/** Callback for streaming events to SSE clients */
|
||||
streamCallback?: PlanningStreamCallback;
|
||||
/** Accumulated thinking output for display */
|
||||
@@ -777,6 +786,8 @@ export async function createSession(
|
||||
lastGeneratedThinking: "",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
store,
|
||||
rootDir,
|
||||
};
|
||||
|
||||
sessions.set(sessionId, session);
|
||||
@@ -1372,19 +1383,24 @@ async function ensureSessionAgent(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rootDir) {
|
||||
// Fall back to session-captured context for rehydrated sessions whose
|
||||
// submitResponse/retry/rewind call sites don't plumb rootDir/store.
|
||||
const effectiveRootDir = rootDir ?? session.rootDir;
|
||||
const effectiveStore = store ?? session.store;
|
||||
|
||||
if (!effectiveRootDir) {
|
||||
throw new InvalidSessionStateError(
|
||||
"Planning session has no AI agent and cannot be resumed without project context",
|
||||
);
|
||||
}
|
||||
|
||||
if (!store) {
|
||||
if (!effectiveStore) {
|
||||
throw new InvalidSessionStateError(
|
||||
"Planning session has no task store and cannot be resumed without project context",
|
||||
);
|
||||
}
|
||||
|
||||
session.agent = await createPlanningAgent(session, rootDir, store, undefined, undefined, promptOverrides);
|
||||
session.agent = await createPlanningAgent(session, effectiveRootDir, effectiveStore, undefined, undefined, promptOverrides);
|
||||
|
||||
if (historyForReplay.length === 0) {
|
||||
return;
|
||||
@@ -1884,12 +1900,19 @@ export async function submitResponse(
|
||||
responses: Record<string, unknown>,
|
||||
rootDir?: string,
|
||||
promptOverrides?: PromptOverrideMap,
|
||||
store?: TaskStore,
|
||||
): Promise<PlanningResponse> {
|
||||
const session = getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new SessionNotFoundError(`Planning session ${sessionId} not found or expired`);
|
||||
}
|
||||
|
||||
// Stash store/rootDir on the session so subsequent ensureSessionAgent calls
|
||||
// (after the agent is disposed for retry/rewind) can rebuild without the
|
||||
// caller having to thread context through every API.
|
||||
if (store && !session.store) session.store = store;
|
||||
if (rootDir && !session.rootDir) session.rootDir = rootDir;
|
||||
|
||||
if (!session.currentQuestion) {
|
||||
if (!isRefineRequest(responses) || !session.summary) {
|
||||
throw new InvalidSessionStateError("No active question in session");
|
||||
@@ -1898,7 +1921,7 @@ export async function submitResponse(
|
||||
session.error = undefined;
|
||||
persistSession(session, "generating");
|
||||
|
||||
await ensureSessionAgent(session, rootDir, session.history, promptOverrides);
|
||||
await ensureSessionAgent(session, rootDir, session.history, promptOverrides, store);
|
||||
const refineMessage = formatRefineRequestForAgent(session.summary);
|
||||
await continueAgentConversation(session, refineMessage);
|
||||
} else {
|
||||
@@ -1913,7 +1936,7 @@ export async function submitResponse(
|
||||
|
||||
if (!session.agent) {
|
||||
const replayHistory = session.history.slice(0, -1);
|
||||
await ensureSessionAgent(session, rootDir, replayHistory, promptOverrides);
|
||||
await ensureSessionAgent(session, rootDir, replayHistory, promptOverrides, store);
|
||||
}
|
||||
|
||||
const message = formatResponseForAgent(session.currentQuestion, responses);
|
||||
@@ -1936,12 +1959,16 @@ export async function retrySession(
|
||||
sessionId: string,
|
||||
rootDir: string,
|
||||
promptOverrides?: PromptOverrideMap,
|
||||
store?: TaskStore,
|
||||
): Promise<void> {
|
||||
const session = getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new SessionNotFoundError(`Planning session ${sessionId} not found or expired`);
|
||||
}
|
||||
|
||||
if (store && !session.store) session.store = store;
|
||||
if (rootDir && !session.rootDir) session.rootDir = rootDir;
|
||||
|
||||
const persisted = _aiSessionStore?.get(sessionId);
|
||||
if (persisted && persisted.type !== "planning") {
|
||||
throw new SessionNotFoundError(`Planning session ${sessionId} not found or expired`);
|
||||
@@ -1960,7 +1987,7 @@ export async function retrySession(
|
||||
persistSession(session, "generating");
|
||||
|
||||
if (session.history.length === 0) {
|
||||
await ensureSessionAgent(session, rootDir, [], promptOverrides);
|
||||
await ensureSessionAgent(session, rootDir, [], promptOverrides, store);
|
||||
await continueAgentConversation(session, session.initialPlan);
|
||||
return;
|
||||
}
|
||||
@@ -1968,7 +1995,7 @@ export async function retrySession(
|
||||
const replayHistory = session.history.slice(0, -1);
|
||||
const lastEntry = session.history[session.history.length - 1];
|
||||
|
||||
await ensureSessionAgent(session, rootDir, replayHistory, promptOverrides);
|
||||
await ensureSessionAgent(session, rootDir, replayHistory, promptOverrides, store);
|
||||
const replayMessage = formatResponseForAgent(
|
||||
lastEntry.question,
|
||||
coerceResponseRecord(lastEntry.question, lastEntry.response),
|
||||
@@ -1985,12 +2012,16 @@ export async function rewindSession(
|
||||
sessionId: string,
|
||||
rootDir?: string,
|
||||
promptOverrides?: PromptOverrideMap,
|
||||
store?: TaskStore,
|
||||
): Promise<PlanningRewindResult> {
|
||||
const session = getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new SessionNotFoundError(`Planning session ${sessionId} not found or expired`);
|
||||
}
|
||||
|
||||
if (store && !session.store) session.store = store;
|
||||
if (rootDir && !session.rootDir) session.rootDir = rootDir;
|
||||
|
||||
if (session.history.length === 0) {
|
||||
throw new InvalidSessionStateError("Planning session has no previous question to rewind to");
|
||||
}
|
||||
@@ -2010,7 +2041,7 @@ export async function rewindSession(
|
||||
session.updatedAt = new Date();
|
||||
|
||||
if (!session.agent && rootDir) {
|
||||
await ensureSessionAgent(session, rootDir, session.history, promptOverrides);
|
||||
await ensureSessionAgent(session, rootDir, session.history, promptOverrides, store);
|
||||
}
|
||||
|
||||
persistSession(session, "awaiting_input");
|
||||
|
||||
Reference in New Issue
Block a user