Merge pull request #2904 from mastertyko/fix/auto-resume-cold-db-bootstrap
fix(gsd): resume cold auto bootstrap from db
This commit is contained in:
commit
f9dd134933
5 changed files with 94 additions and 20 deletions
|
|
@ -58,8 +58,9 @@ import { initRoutingHistory } from "./routing-history.js";
|
|||
import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
|
||||
import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
|
||||
import { snapshotSkills } from "./skill-discovery.js";
|
||||
import { isDbAvailable, getMilestone } from "./gsd-db.js";
|
||||
import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js";
|
||||
import { hideFooter } from "./auto-dashboard.js";
|
||||
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
||||
import {
|
||||
debugLog,
|
||||
enableDebug,
|
||||
|
|
@ -103,6 +104,20 @@ export interface BootstrapDeps {
|
|||
* cycles indefinitely when the discuss workflow doesn't produce a milestone. */
|
||||
let _consecutiveCompleteBootstraps = 0;
|
||||
const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
|
||||
|
||||
async function openProjectDbIfPresent(basePath: string): Promise<void> {
|
||||
const gsdDbPath = resolveProjectRootDbPath(basePath);
|
||||
if (!existsSync(gsdDbPath) || isDbAvailable()) return;
|
||||
|
||||
try {
|
||||
openDatabase(gsdDbPath);
|
||||
} catch (err) {
|
||||
process.stderr.write(
|
||||
`gsd-db: failed to open existing database: ${(err as Error).message}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function bootstrapAutoSession(
|
||||
s: AutoSession,
|
||||
ctx: ExtensionCommandContext,
|
||||
|
|
@ -265,6 +280,10 @@ export async function bootstrapAutoSession(
|
|||
ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
|
||||
}
|
||||
|
||||
// Open the project DB before the first derive so resume uses DB truth
|
||||
// immediately on cold starts instead of falling back to markdown (#2841).
|
||||
await openProjectDbIfPresent(base);
|
||||
|
||||
// Invalidate caches before initial state derivation
|
||||
invalidateAllCaches();
|
||||
|
||||
|
|
@ -535,15 +554,14 @@ export async function bootstrapAutoSession(
|
|||
}
|
||||
|
||||
// ── DB lifecycle ──
|
||||
const gsdDbPath = join(s.basePath, ".gsd", "gsd.db");
|
||||
const gsdDbPath = resolveProjectRootDbPath(s.basePath);
|
||||
const gsdDirPath = join(s.basePath, ".gsd");
|
||||
if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
|
||||
const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
|
||||
const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
|
||||
const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
|
||||
try {
|
||||
const { openDatabase: openDb } = await import("./gsd-db.js");
|
||||
openDb(gsdDbPath);
|
||||
openDatabase(gsdDbPath);
|
||||
if (hasDecisions || hasRequirements || hasMilestones) {
|
||||
const { migrateFromMarkdown } = await import("./md-importer.js");
|
||||
migrateFromMarkdown(s.basePath);
|
||||
|
|
@ -556,8 +574,7 @@ export async function bootstrapAutoSession(
|
|||
}
|
||||
if (existsSync(gsdDbPath) && !isDbAvailable()) {
|
||||
try {
|
||||
const { openDatabase: openDb } = await import("./gsd-db.js");
|
||||
openDb(gsdDbPath);
|
||||
openDatabase(gsdDbPath);
|
||||
} catch (err) {
|
||||
process.stderr.write(
|
||||
`gsd-db: failed to open existing database: ${(err as Error).message}\n`,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ function parseTableSlices(section: string): RoadmapSliceEntry[] {
|
|||
const fullRow = line.toLowerCase();
|
||||
const done =
|
||||
/\[x\]/i.test(line) ||
|
||||
/[✅☑✓]/.test(line) ||
|
||||
/\bdone\b/.test(fullRow) ||
|
||||
/\bcomplete(?:d)?\b/.test(fullRow);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createTestContext } from "./test-helpers.ts";
|
||||
|
||||
const { assertTrue, report } = createTestContext();
|
||||
|
||||
const srcPath = join(import.meta.dirname, "..", "auto-start.ts");
|
||||
const src = readFileSync(srcPath, "utf-8");
|
||||
|
||||
console.log("\n=== #2841: cold DB opened before initial deriveState ===");
|
||||
|
||||
const helperIdx = src.indexOf("async function openProjectDbIfPresent");
|
||||
assertTrue(helperIdx >= 0, "auto-start.ts defines a helper for pre-derive DB open (#2841)");
|
||||
|
||||
const helperRegion = helperIdx >= 0 ? src.slice(helperIdx, helperIdx + 500) : "";
|
||||
assertTrue(
|
||||
helperRegion.includes("resolveProjectRootDbPath(basePath)"),
|
||||
"pre-derive DB helper resolves the project-root DB path (#2841)",
|
||||
);
|
||||
assertTrue(
|
||||
helperRegion.includes("openDatabase(gsdDbPath)"),
|
||||
"pre-derive DB helper opens the resolved DB path (#2841)",
|
||||
);
|
||||
|
||||
const firstDeriveIdx = src.indexOf("let state = await deriveState(base);");
|
||||
assertTrue(firstDeriveIdx > 0, "auto-start.ts has the initial deriveState(base) call");
|
||||
|
||||
const preDeriveRegion = firstDeriveIdx > 0 ? src.slice(0, firstDeriveIdx) : "";
|
||||
const preDeriveOpenIdx = preDeriveRegion.lastIndexOf("await openProjectDbIfPresent(base);");
|
||||
|
||||
assertTrue(
|
||||
preDeriveOpenIdx > 0,
|
||||
"bootstrapAutoSession opens the DB before the first deriveState(base) call (#2841)",
|
||||
);
|
||||
|
||||
report();
|
||||
|
|
@ -116,6 +116,23 @@ test("parseRoadmapSlices: table with Status Done/Complete text (#1736)", () => {
|
|||
assert.equal(slices[2]?.done, true);
|
||||
});
|
||||
|
||||
test("parseRoadmapSlices: table with glyph completion markers (#2841)", () => {
|
||||
const tableContent = [
|
||||
"# M003: Glyph Status", "", "## Slices", "",
|
||||
"| Slice | Title | Risk | Status |", "|---|---|---|---|",
|
||||
"| S01 | First | Low | ✅ |",
|
||||
"| S02 | Second | High | Pending |",
|
||||
"| S03 | Third | Medium | ☑ |",
|
||||
"| S04 | Fourth | Medium | ✓ |", "",
|
||||
].join("\n");
|
||||
const slices = parseRoadmapSlices(tableContent);
|
||||
assert.equal(slices.length, 4);
|
||||
assert.equal(slices[0]?.done, true);
|
||||
assert.equal(slices[1]?.done, false);
|
||||
assert.equal(slices[2]?.done, true);
|
||||
assert.equal(slices[3]?.done, true);
|
||||
});
|
||||
|
||||
test("parseRoadmapSlices: table with dependencies column (#1736)", () => {
|
||||
const tableContent = [
|
||||
"# M004: Deps", "", "## Slices", "",
|
||||
|
|
|
|||
|
|
@ -33,23 +33,25 @@ assertTrue(dbLifecycleIdx > 0, "auto-start.ts has a DB lifecycle section");
|
|||
|
||||
const afterDbLifecycle = src.slice(dbLifecycleIdx);
|
||||
|
||||
// Find the second isDbAvailable check — the one AFTER the open attempts.
|
||||
// The first check at line ~543 tries to open the DB.
|
||||
// There must be a SECOND check that gates bootstrap if it's still unavailable.
|
||||
const firstCheck = afterDbLifecycle.indexOf("isDbAvailable()");
|
||||
assertTrue(firstCheck > 0, "DB lifecycle section has isDbAvailable() check");
|
||||
|
||||
const afterFirstCheck = afterDbLifecycle.slice(firstCheck + "isDbAvailable()".length);
|
||||
const secondCheck = afterFirstCheck.indexOf("isDbAvailable()");
|
||||
|
||||
// The DB lifecycle section may contain multiple isDbAvailable() checks now that
|
||||
// cold-start bootstrap can pre-open the DB earlier in the file. What matters
|
||||
// for #2419 is the explicit abort gate after the DB open attempts.
|
||||
assertTrue(
|
||||
secondCheck > 0,
|
||||
"auto-start.ts has a SECOND isDbAvailable() check after the open attempt — this is the gate (#2419)",
|
||||
afterDbLifecycle.includes("!isDbAvailable()"),
|
||||
"DB lifecycle section still checks for unavailable DB state (#2419)",
|
||||
);
|
||||
|
||||
// The second check must lead to releaseLockAndReturn (abort bootstrap)
|
||||
if (secondCheck > 0) {
|
||||
const gateRegion = afterFirstCheck.slice(secondCheck, secondCheck + 500);
|
||||
const gateMatch = afterDbLifecycle.match(
|
||||
/if\s*\(existsSync\(gsdDbPath\)\s*&&\s*!isDbAvailable\(\)\)\s*\{[\s\S]*?releaseLockAndReturn\(\);[\s\S]*?\}/,
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
!!gateMatch,
|
||||
"auto-start.ts has a hard abort gate when gsd.db exists but SQLite is still unavailable (#2419)",
|
||||
);
|
||||
|
||||
if (gateMatch) {
|
||||
const gateRegion = gateMatch[0];
|
||||
assertTrue(
|
||||
gateRegion.includes("releaseLockAndReturn"),
|
||||
"The DB availability gate calls releaseLockAndReturn() to abort bootstrap (#2419)",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue