diff --git a/src/resources/extensions/gsd/bootstrap/register-hooks.ts b/src/resources/extensions/gsd/bootstrap/register-hooks.ts index 7b131d86e..438d4d9b0 100644 --- a/src/resources/extensions/gsd/bootstrap/register-hooks.ts +++ b/src/resources/extensions/gsd/bootstrap/register-hooks.ts @@ -121,6 +121,8 @@ export function registerHooks(pi: ExtensionAPI): void { return { cancel: true }; } const basePath = process.cwd(); + const { ensureDbOpen } = await import("./dynamic-tools.js"); + await ensureDbOpen(); const state = await deriveState(basePath); if (!state.activeMilestone || !state.activeSlice || !state.activeTask) return; if (state.phase !== "executing") return; diff --git a/src/resources/extensions/gsd/bootstrap/system-context.ts b/src/resources/extensions/gsd/bootstrap/system-context.ts index bad24512d..3a336f9ee 100644 --- a/src/resources/extensions/gsd/bootstrap/system-context.ts +++ b/src/resources/extensions/gsd/bootstrap/system-context.ts @@ -293,6 +293,11 @@ function buildWorktreeContextBlock(): string { const RESUME_INTENT_PATTERNS = /^(continue|resume|ok|go|go ahead|proceed|keep going|carry on|next|yes|yeah|yep|sure|do it|let's go|pick up where you left off)$/; async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise { + const ensureStateDbOpen = async () => { + const { ensureDbOpen } = await import("./dynamic-tools.js"); + await ensureDbOpen(); + }; + const executeMatch = prompt.match(/Execute the next task:\s+(T\d+)\s+\("([^"]+)"\)\s+in slice\s+(S\d+)\s+of milestone\s+(M\d+(?:-[a-z0-9]{6})?)/i); if (executeMatch) { const [, taskId, taskTitle, sliceId, milestoneId] = executeMatch; @@ -302,6 +307,7 @@ async function buildGuidedExecuteContextInjection(prompt: string, basePath: stri const resumeMatch = prompt.match(/Resume interrupted work\.[\s\S]*?slice\s+(S\d+)\s+of milestone\s+(M\d+(?:-[a-z0-9]{6})?)/i); if (resumeMatch) { const [, sliceId, milestoneId] = resumeMatch; + await ensureStateDbOpen(); const state = await deriveState(basePath); if (state.activeMilestone?.id === milestoneId && state.activeSlice?.id === sliceId && state.activeTask) { return buildTaskExecutionContextInjection(basePath, milestoneId, sliceId, state.activeTask.id, state.activeTask.title); @@ -317,6 +323,7 @@ async function buildGuidedExecuteContextInjection(prompt: string, basePath: stri // replanning, gate evaluation, or other non-execution phases. const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, ""); if (RESUME_INTENT_PATTERNS.test(trimmed)) { + await ensureStateDbOpen(); const state = await deriveState(basePath); if (state.phase === "executing" && state.activeTask && state.activeMilestone && state.activeSlice) { return buildTaskExecutionContextInjection( diff --git a/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts b/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts new file mode 100644 index 000000000..5c2d18cfc --- /dev/null +++ b/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts @@ -0,0 +1,39 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const systemContextSrc = readFileSync( + join(import.meta.dirname, "..", "bootstrap", "system-context.ts"), + "utf-8", +); +const registerHooksSrc = readFileSync( + join(import.meta.dirname, "..", "bootstrap", "register-hooks.ts"), + "utf-8", +); + +describe("bootstrap deriveState DB guards (#3844)", () => { + test("system-context opens DB before deriveState in resume flows", () => { + const helperIdx = systemContextSrc.indexOf("const ensureStateDbOpen = async () => {"); + const firstDeriveIdx = systemContextSrc.indexOf("const state = await deriveState(basePath);"); + assert.ok(helperIdx > -1, "system-context should define a DB-open helper for deriveState callers"); + assert.ok(firstDeriveIdx > -1, "system-context should still derive state for resume flows"); + assert.ok(helperIdx < firstDeriveIdx, "system-context should prepare DB opening before deriveState resume calls"); + assert.match( + systemContextSrc, + /await ensureStateDbOpen\(\);\s*\n\s*const state = await deriveState\(basePath\);/g, + "system-context resume flows should open DB before deriveState", + ); + }); + + test("register-hooks opens DB before deriveState in session_before_compact", () => { + const compactIdx = registerHooksSrc.indexOf('pi.on("session_before_compact"'); + assert.ok(compactIdx > -1, "register-hooks should define session_before_compact"); + const compactSection = registerHooksSrc.slice(compactIdx, compactIdx + 1600); + const ensureIdx = compactSection.indexOf("ensureDbOpen()"); + const deriveIdx = compactSection.indexOf("deriveState(basePath)"); + assert.ok(ensureIdx > -1, "session_before_compact should call ensureDbOpen()"); + assert.ok(deriveIdx > -1, "session_before_compact should derive state"); + assert.ok(ensureIdx < deriveIdx, "session_before_compact should open DB before deriveState"); + }); +});