gsd-pi 2.67.0-dev.fe39184 → 2.68.0-dev.4cf2433
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-start.js +5 -31
- package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
- package/dist/resources/extensions/gsd/auto.js +94 -59
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
- package/dist/resources/extensions/gsd/doctor.js +8 -4
- package/dist/resources/extensions/gsd/gsd-db.js +11 -0
- package/dist/resources/extensions/gsd/guided-flow.js +40 -31
- package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
- package/dist/resources/extensions/gsd/state.js +7 -2
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/{webpack-42a66876b763aa26.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
- package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/index.js +1 -0
- package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
- package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
- package/packages/pi-coding-agent/src/core/index.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +17 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-start.ts +8 -54
- package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
- package/src/resources/extensions/gsd/auto.ts +104 -63
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
- package/src/resources/extensions/gsd/doctor.ts +9 -5
- package/src/resources/extensions/gsd/gsd-db.ts +12 -0
- package/src/resources/extensions/gsd/guided-flow.ts +42 -36
- package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
- package/src/resources/extensions/gsd/state.ts +7 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
- package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
- package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
- package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
- package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
- package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
- package/dist/web/standalone/.next/static/chunks/6502.5dcdcf1e1432e20d.js +0 -9
- /package/dist/web/standalone/.next/static/{gbSATDX4Jt2ufxzUr5nYm → gd7sngpqfUCltp8w_pCwF}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{gbSATDX4Jt2ufxzUr5nYm → gd7sngpqfUCltp8w_pCwF}/_ssgManifest.js +0 -0
|
@@ -63,6 +63,8 @@ export class AutoSession {
|
|
|
63
63
|
pendingVerificationRetry = null;
|
|
64
64
|
verificationRetryCount = new Map();
|
|
65
65
|
pausedSessionFile = null;
|
|
66
|
+
pausedUnitType = null;
|
|
67
|
+
pausedUnitId = null;
|
|
66
68
|
resourceVersionOnStart = null;
|
|
67
69
|
lastStateRebuildAt = 0;
|
|
68
70
|
// ── Sidecar queue ─────────────────────────────────────────────────────
|
|
@@ -159,6 +161,8 @@ export class AutoSession {
|
|
|
159
161
|
this.pendingVerificationRetry = null;
|
|
160
162
|
this.verificationRetryCount.clear();
|
|
161
163
|
this.pausedSessionFile = null;
|
|
164
|
+
this.pausedUnitType = null;
|
|
165
|
+
this.pausedUnitId = null;
|
|
162
166
|
this.resourceVersionOnStart = null;
|
|
163
167
|
this.lastStateRebuildAt = 0;
|
|
164
168
|
// Metrics
|
|
@@ -104,7 +104,7 @@ export function isVerificationNotApplicable(value) {
|
|
|
104
104
|
const v = (value ?? "").toLowerCase().trim().replace(/[.\s]+$/, "");
|
|
105
105
|
if (!v || v === "none")
|
|
106
106
|
return true;
|
|
107
|
-
return /^(?:none[\s._-]*
|
|
107
|
+
return /^(?:none(?:[\s._\u2014-]+[\s\S]*)?|n\/?a|not[\s._-]+(?:applicable|required|needed|provided)|no[\s._-]+operational[\s\S]*)$/i.test(v);
|
|
108
108
|
}
|
|
109
109
|
// ─── Rules ────────────────────────────────────────────────────────────────
|
|
110
110
|
export const DISPATCH_RULES = [
|
|
@@ -16,8 +16,7 @@ import { migrateToExternalState, recoverFailedMigration } from "./migrate-extern
|
|
|
16
16
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
17
17
|
import { gsdRoot, resolveMilestoneFile } from "./paths.js";
|
|
18
18
|
import { invalidateAllCaches } from "./cache.js";
|
|
19
|
-
import {
|
|
20
|
-
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive, } from "./crash-recovery.js";
|
|
19
|
+
import { writeLock, clearLock } from "./crash-recovery.js";
|
|
21
20
|
import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
22
21
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
23
22
|
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, nativeBranchList, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, } from "./native-git-bridge.js";
|
|
@@ -35,7 +34,6 @@ import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js";
|
|
|
35
34
|
import { hideFooter } from "./auto-dashboard.js";
|
|
36
35
|
import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath, } from "./debug-logger.js";
|
|
37
36
|
import { logWarning, logError } from "./workflow-logger.js";
|
|
38
|
-
import { parseUnitId } from "./unit-id.js";
|
|
39
37
|
import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } from "node:fs";
|
|
40
38
|
import { join } from "node:path";
|
|
41
39
|
import { sep as pathSep } from "node:path";
|
|
@@ -174,7 +172,7 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
|
|
|
174
172
|
}
|
|
175
173
|
return { recovered, warnings };
|
|
176
174
|
}
|
|
177
|
-
export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps) {
|
|
175
|
+
export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps, interrupted) {
|
|
178
176
|
const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildResolver, } = deps;
|
|
179
177
|
const lockResult = acquireSessionLock(base);
|
|
180
178
|
if (!lockResult.acquired) {
|
|
@@ -264,33 +262,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
264
262
|
}
|
|
265
263
|
// Initialize GitServiceImpl
|
|
266
264
|
s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
267
|
-
// Check for crash from previous session. Skip our own fresh bootstrap lock.
|
|
268
|
-
const crashLock = readCrashLock(base);
|
|
269
|
-
if (crashLock && crashLock.pid !== process.pid) {
|
|
270
|
-
if (isLockProcessAlive(crashLock)) {
|
|
271
|
-
ctx.ui.notify(`Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`, "error");
|
|
272
|
-
return releaseLockAndReturn();
|
|
273
|
-
}
|
|
274
|
-
const recoveredMid = parseUnitId(crashLock.unitId).milestone;
|
|
275
|
-
const milestoneAlreadyComplete = recoveredMid
|
|
276
|
-
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
277
|
-
: false;
|
|
278
|
-
if (milestoneAlreadyComplete) {
|
|
279
|
-
ctx.ui.notify(`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`, "info");
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
const activityDir = join(gsdRoot(base), "activity");
|
|
283
|
-
const recovery = synthesizeCrashRecovery(base, crashLock.unitType, crashLock.unitId, crashLock.sessionFile, activityDir);
|
|
284
|
-
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
285
|
-
s.pendingCrashRecovery = recovery.prompt;
|
|
286
|
-
ctx.ui.notify(`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`, "warning");
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
ctx.ui.notify(`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`, "warning");
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
clearLock(base);
|
|
293
|
-
}
|
|
294
265
|
// ── Debug mode ──
|
|
295
266
|
if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
|
|
296
267
|
enableDebug(base);
|
|
@@ -308,6 +279,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
308
279
|
});
|
|
309
280
|
ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
|
|
310
281
|
}
|
|
282
|
+
if (interrupted.classification !== "recoverable") {
|
|
283
|
+
s.pendingCrashRecovery = null;
|
|
284
|
+
}
|
|
311
285
|
// Invalidate caches before initial state derivation
|
|
312
286
|
invalidateAllCaches();
|
|
313
287
|
// Clean stale runtime unit files for completed milestones (#887)
|
|
@@ -983,6 +983,8 @@ function copyPlanningArtifacts(srcBase, wtPath) {
|
|
|
983
983
|
const dstGsd = join(wtPath, ".gsd");
|
|
984
984
|
if (!existsSync(srcGsd))
|
|
985
985
|
return;
|
|
986
|
+
if (isSamePath(srcGsd, dstGsd))
|
|
987
|
+
return;
|
|
986
988
|
// Copy milestones/ directory (planning files, roadmaps, plans, research)
|
|
987
989
|
safeCopyRecursive(join(srcGsd, "milestones"), join(dstGsd, "milestones"), {
|
|
988
990
|
force: true,
|
|
@@ -1209,8 +1211,32 @@ function autoCommitDirtyState(cwd) {
|
|
|
1209
1211
|
export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapContent) {
|
|
1210
1212
|
const worktreeCwd = process.cwd();
|
|
1211
1213
|
const milestoneBranch = autoWorktreeBranch(milestoneId);
|
|
1212
|
-
// 1. Auto-commit dirty state
|
|
1213
|
-
|
|
1214
|
+
// 1. Auto-commit dirty state before leaving.
|
|
1215
|
+
// Guard: when we entered through an auto-worktree (originalBase is set),
|
|
1216
|
+
// only auto-commit when cwd is on the milestone branch. In parallel mode,
|
|
1217
|
+
// cwd may be on the integration branch after a prior merge's
|
|
1218
|
+
// MergeConflictError left cwd unrestored. Auto-committing on the
|
|
1219
|
+
// integration branch captures dirty files from OTHER milestones under a
|
|
1220
|
+
// misleading commit message, contaminating the main branch (#2929).
|
|
1221
|
+
//
|
|
1222
|
+
// When originalBase is null (branch mode, no worktree), autoCommitDirtyState
|
|
1223
|
+
// runs unconditionally — the caller is responsible for cwd placement.
|
|
1224
|
+
{
|
|
1225
|
+
let shouldAutoCommit = true;
|
|
1226
|
+
if (originalBase !== null) {
|
|
1227
|
+
try {
|
|
1228
|
+
const currentBranch = nativeGetCurrentBranch(worktreeCwd);
|
|
1229
|
+
shouldAutoCommit = currentBranch === milestoneBranch;
|
|
1230
|
+
}
|
|
1231
|
+
catch {
|
|
1232
|
+
// If we can't determine the branch, skip the auto-commit to be safe
|
|
1233
|
+
shouldAutoCommit = false;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (shouldAutoCommit) {
|
|
1237
|
+
autoCommitDirtyState(worktreeCwd);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1214
1240
|
// Reconcile worktree DB into main DB before leaving worktree context.
|
|
1215
1241
|
// Skip when both paths resolve to the same physical file (shared WAL /
|
|
1216
1242
|
// symlink layout) — ATTACHing a WAL-mode file to itself corrupts the
|
|
@@ -1551,6 +1577,12 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1551
1577
|
}
|
|
1552
1578
|
}
|
|
1553
1579
|
restoreShelter();
|
|
1580
|
+
// Restore cwd so the caller is not stranded on the integration branch.
|
|
1581
|
+
// Without this, the next mergeMilestoneToMain call in a parallel merge
|
|
1582
|
+
// sequence uses process.cwd() (now the project root) as worktreeCwd,
|
|
1583
|
+
// causing autoCommitDirtyState to commit unrelated milestone files to
|
|
1584
|
+
// the integration branch (#2929).
|
|
1585
|
+
process.chdir(previousCwd);
|
|
1554
1586
|
throw new MergeConflictError(codeConflicts, "squash", milestoneBranch, mainBranch);
|
|
1555
1587
|
}
|
|
1556
1588
|
}
|
|
@@ -1725,25 +1757,40 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1725
1757
|
// changes (e.g. nativeHasChanges cache returned stale false, or auto-commit
|
|
1726
1758
|
// silently failed), force one final commit so code is not destroyed by
|
|
1727
1759
|
// `git worktree remove --force`.
|
|
1760
|
+
//
|
|
1761
|
+
// Guard: only run when worktreeCwd is on the milestone branch (#2929).
|
|
1762
|
+
// In parallel mode or branch-mode merges, worktreeCwd may be the project
|
|
1763
|
+
// root on the integration branch. Committing dirty state there would
|
|
1764
|
+
// capture unrelated files from other milestones.
|
|
1728
1765
|
if (existsSync(worktreeCwd)) {
|
|
1766
|
+
let preTeardownBranch = null;
|
|
1729
1767
|
try {
|
|
1730
|
-
|
|
1731
|
-
|
|
1768
|
+
preTeardownBranch = nativeGetCurrentBranch(worktreeCwd);
|
|
1769
|
+
}
|
|
1770
|
+
catch (err) {
|
|
1771
|
+
debugLog("mergeMilestoneToMain", { phase: "pre-teardown-branch-detect-failed", error: String(err) });
|
|
1772
|
+
}
|
|
1773
|
+
const isOnMilestoneBranch = preTeardownBranch === milestoneBranch;
|
|
1774
|
+
if (isOnMilestoneBranch) {
|
|
1775
|
+
try {
|
|
1776
|
+
const dirtyCheck = nativeWorkingTreeStatus(worktreeCwd);
|
|
1777
|
+
if (dirtyCheck) {
|
|
1778
|
+
debugLog("mergeMilestoneToMain", {
|
|
1779
|
+
phase: "pre-teardown-dirty",
|
|
1780
|
+
worktreeCwd,
|
|
1781
|
+
status: dirtyCheck.slice(0, 200),
|
|
1782
|
+
});
|
|
1783
|
+
nativeAddAllWithExclusions(worktreeCwd, RUNTIME_EXCLUSION_PATHS);
|
|
1784
|
+
nativeCommit(worktreeCwd, "chore: pre-teardown auto-commit of uncommitted worktree changes");
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
catch (e) {
|
|
1732
1788
|
debugLog("mergeMilestoneToMain", {
|
|
1733
|
-
phase: "pre-teardown-
|
|
1734
|
-
|
|
1735
|
-
status: dirtyCheck.slice(0, 200),
|
|
1789
|
+
phase: "pre-teardown-commit-error",
|
|
1790
|
+
error: String(e),
|
|
1736
1791
|
});
|
|
1737
|
-
nativeAddAllWithExclusions(worktreeCwd, RUNTIME_EXCLUSION_PATHS);
|
|
1738
|
-
nativeCommit(worktreeCwd, "chore: pre-teardown auto-commit of uncommitted worktree changes");
|
|
1739
1792
|
}
|
|
1740
1793
|
}
|
|
1741
|
-
catch (e) {
|
|
1742
|
-
debugLog("mergeMilestoneToMain", {
|
|
1743
|
-
phase: "pre-teardown-commit-error",
|
|
1744
|
-
error: String(e),
|
|
1745
|
-
});
|
|
1746
|
-
}
|
|
1747
1794
|
}
|
|
1748
1795
|
// 12. Remove worktree directory first (must happen before branch deletion)
|
|
1749
1796
|
try {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { deriveState } from "./state.js";
|
|
13
13
|
import { parseUnitId } from "./unit-id.js";
|
|
14
|
+
import { assessInterruptedSession, readPausedSessionMetadata, } from "./interrupted-session.js";
|
|
14
15
|
import { getManifestStatus } from "./files.js";
|
|
15
16
|
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
16
17
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
@@ -18,7 +19,7 @@ import { gsdRoot, resolveMilestoneFile, resolveMilestonePath, resolveDir, milest
|
|
|
18
19
|
import { invalidateAllCaches } from "./cache.js";
|
|
19
20
|
import { clearActivityLogState } from "./activity-log.js";
|
|
20
21
|
import { synthesizeCrashRecovery, getDeepDiagnostic, readActiveMilestoneId, } from "./session-forensics.js";
|
|
21
|
-
import { writeLock, clearLock, readCrashLock, isLockProcessAlive, } from "./crash-recovery.js";
|
|
22
|
+
import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, } from "./crash-recovery.js";
|
|
22
23
|
import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
23
24
|
import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
|
|
24
25
|
import { sendDesktopNotification } from "./notifications.js";
|
|
@@ -34,7 +35,8 @@ import { clearSkillSnapshot } from "./skill-discovery.js";
|
|
|
34
35
|
import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
|
|
35
36
|
import { getRtkSessionSavings } from "../shared/rtk-session-stats.js";
|
|
36
37
|
import { initMetrics, resetMetrics, getLedger, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
|
|
37
|
-
import {
|
|
38
|
+
import { logWarning } from "./workflow-logger.js";
|
|
39
|
+
import { homedir } from "node:os";
|
|
38
40
|
import { join } from "node:path";
|
|
39
41
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
40
42
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
@@ -665,6 +667,8 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
665
667
|
stepMode: s.stepMode,
|
|
666
668
|
pausedAt: new Date().toISOString(),
|
|
667
669
|
sessionFile: s.pausedSessionFile,
|
|
670
|
+
unitType: s.currentUnit?.type ?? undefined,
|
|
671
|
+
unitId: s.currentUnit?.id ?? undefined,
|
|
668
672
|
activeEngineId: s.activeEngineId,
|
|
669
673
|
activeRunDir: s.activeRunDir,
|
|
670
674
|
autoStartTime: s.autoStartTime,
|
|
@@ -853,38 +857,54 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
853
857
|
return;
|
|
854
858
|
}
|
|
855
859
|
const requestedStepMode = options?.step ?? false;
|
|
860
|
+
const interruptedAssessment = options?.interrupted ?? null;
|
|
856
861
|
// Escape stale worktree cwd from a previous milestone (#608).
|
|
857
862
|
base = escapeStaleWorktree(base);
|
|
863
|
+
const freshStartAssessment = interruptedAssessment
|
|
864
|
+
?? await assessInterruptedSession(base);
|
|
865
|
+
if (freshStartAssessment.classification === "running") {
|
|
866
|
+
const pid = freshStartAssessment.lock?.pid;
|
|
867
|
+
ctx.ui.notify(pid
|
|
868
|
+
? `Another auto-mode session (PID ${pid}) appears to be running.\nStop it with \`kill ${pid}\` before starting a new session.`
|
|
869
|
+
: "Another auto-mode session appears to be running.", "error");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
858
872
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
859
873
|
// Check persisted paused-session first (#1383) — survives /exit.
|
|
860
874
|
if (!s.paused) {
|
|
861
875
|
try {
|
|
876
|
+
const meta = freshStartAssessment.pausedSession ?? readPausedSessionMetadata(base);
|
|
862
877
|
const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
|
|
863
|
-
if (
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
// Don't delete pause file yet — defer until lock is acquired.
|
|
874
|
-
// If lock fails, the file must survive for retry.
|
|
875
|
-
s.pausedSessionFile = pausedPath;
|
|
876
|
-
ctx.ui.notify(`Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`, "info");
|
|
878
|
+
if (meta?.activeEngineId && meta.activeEngineId !== "dev") {
|
|
879
|
+
// Custom workflow resume — restore engine state
|
|
880
|
+
s.activeEngineId = meta.activeEngineId;
|
|
881
|
+
s.activeRunDir = meta.activeRunDir ?? null;
|
|
882
|
+
s.originalBasePath = meta.originalBasePath || base;
|
|
883
|
+
s.stepMode = meta.stepMode ?? requestedStepMode;
|
|
884
|
+
s.autoStartTime = meta.autoStartTime || Date.now();
|
|
885
|
+
s.paused = true;
|
|
886
|
+
try {
|
|
887
|
+
unlinkSync(pausedPath);
|
|
877
888
|
}
|
|
878
|
-
|
|
889
|
+
catch (e) {
|
|
890
|
+
logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
|
|
891
|
+
}
|
|
892
|
+
ctx.ui.notify(`Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`, "info");
|
|
893
|
+
}
|
|
894
|
+
else if (meta?.milestoneId) {
|
|
895
|
+
const shouldResumePausedSession = freshStartAssessment.classification === "recoverable"
|
|
896
|
+
&& (freshStartAssessment.hasResumableDiskState
|
|
897
|
+
|| !!freshStartAssessment.recoveryPrompt
|
|
898
|
+
|| !!freshStartAssessment.lock);
|
|
899
|
+
if (shouldResumePausedSession) {
|
|
879
900
|
// Validate the milestone still exists and isn't already complete (#1664).
|
|
880
901
|
const mDir = resolveMilestonePath(base, meta.milestoneId);
|
|
881
902
|
const summaryFile = resolveMilestoneFile(base, meta.milestoneId, "SUMMARY");
|
|
882
903
|
if (!mDir || summaryFile) {
|
|
883
|
-
// Stale milestone — clean up and fall through to fresh bootstrap
|
|
884
904
|
try {
|
|
885
905
|
unlinkSync(pausedPath);
|
|
886
906
|
}
|
|
887
|
-
catch (err) {
|
|
907
|
+
catch (err) {
|
|
888
908
|
logWarning("session", `pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
889
909
|
}
|
|
890
910
|
ctx.ui.notify(`Paused milestone ${meta.milestoneId} is ${!mDir ? "missing" : "already complete"}. Starting fresh.`, "info");
|
|
@@ -893,12 +913,26 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
893
913
|
s.currentMilestoneId = meta.milestoneId;
|
|
894
914
|
s.originalBasePath = meta.originalBasePath || base;
|
|
895
915
|
s.stepMode = meta.stepMode ?? requestedStepMode;
|
|
916
|
+
s.pausedSessionFile = meta.sessionFile ?? null;
|
|
917
|
+
s.pausedUnitType = meta.unitType ?? null;
|
|
918
|
+
s.pausedUnitId = meta.unitId ?? null;
|
|
896
919
|
s.autoStartTime = meta.autoStartTime || Date.now();
|
|
897
920
|
s.paused = true;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
921
|
+
try {
|
|
922
|
+
unlinkSync(pausedPath);
|
|
923
|
+
}
|
|
924
|
+
catch (e) {
|
|
925
|
+
logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
|
|
926
|
+
}
|
|
927
|
+
ctx.ui.notify(`Resuming paused session for ${meta.milestoneId}${meta.worktreePath && existsSync(meta.worktreePath) ? ` (worktree)` : ""}.`, "info");
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
else if (existsSync(pausedPath)) {
|
|
931
|
+
try {
|
|
932
|
+
unlinkSync(pausedPath);
|
|
933
|
+
}
|
|
934
|
+
catch (e) {
|
|
935
|
+
logWarning("session", `stale pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
|
|
902
936
|
}
|
|
903
937
|
}
|
|
904
938
|
}
|
|
@@ -907,6 +941,30 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
907
941
|
// Malformed or missing — proceed with fresh bootstrap
|
|
908
942
|
logWarning("session", `paused-session restore failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
909
943
|
}
|
|
944
|
+
// Guard against zero/missing autoStartTime after resume (#3585)
|
|
945
|
+
if (!s.autoStartTime || s.autoStartTime <= 0)
|
|
946
|
+
s.autoStartTime = Date.now();
|
|
947
|
+
}
|
|
948
|
+
if (!s.paused) {
|
|
949
|
+
s.stepMode = requestedStepMode;
|
|
950
|
+
}
|
|
951
|
+
if (freshStartAssessment.lock) {
|
|
952
|
+
clearLock(base);
|
|
953
|
+
}
|
|
954
|
+
if (!s.paused) {
|
|
955
|
+
s.pendingCrashRecovery =
|
|
956
|
+
freshStartAssessment.classification === "recoverable"
|
|
957
|
+
? freshStartAssessment.recoveryPrompt
|
|
958
|
+
: null;
|
|
959
|
+
if (freshStartAssessment.classification === "recoverable" && freshStartAssessment.lock) {
|
|
960
|
+
const info = formatCrashInfo(freshStartAssessment.lock);
|
|
961
|
+
if (freshStartAssessment.recoveryToolCallCount > 0) {
|
|
962
|
+
ctx.ui.notify(`${info}\nRecovered ${freshStartAssessment.recoveryToolCallCount} tool calls from crashed session. Resuming with full context.`, "warning");
|
|
963
|
+
}
|
|
964
|
+
else if (freshStartAssessment.hasResumableDiskState) {
|
|
965
|
+
ctx.ui.notify(`${info}\nResuming from disk state.`, "warning");
|
|
966
|
+
}
|
|
967
|
+
}
|
|
910
968
|
}
|
|
911
969
|
if (s.paused) {
|
|
912
970
|
const resumeLock = acquireSessionLock(base);
|
|
@@ -931,29 +989,19 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
931
989
|
s.active = true;
|
|
932
990
|
s.verbose = verboseMode;
|
|
933
991
|
s.stepMode = requestedStepMode;
|
|
934
|
-
|
|
935
|
-
// when resuming from a provider-error pause. The resume callback receives
|
|
936
|
-
// an ExtensionContext (from the agent_end hook) which lacks newSession —
|
|
937
|
-
// using it would crash runUnit with "newSession is not a function".
|
|
938
|
-
// Only override if the new ctx actually has newSession (user-initiated resume).
|
|
939
|
-
if ("newSession" in ctx && typeof ctx.newSession === "function") {
|
|
940
|
-
s.cmdCtx = ctx;
|
|
941
|
-
}
|
|
942
|
-
else if (!s.cmdCtx) {
|
|
943
|
-
// No saved cmdCtx — this shouldn't happen, but handle gracefully
|
|
944
|
-
s.cmdCtx = ctx;
|
|
945
|
-
}
|
|
946
|
-
// else: keep existing s.cmdCtx which has the real newSession
|
|
992
|
+
s.cmdCtx = ctx;
|
|
947
993
|
s.basePath = base;
|
|
948
|
-
setLogBasePath(base);
|
|
949
|
-
if (!s.autoStartTime || s.autoStartTime <= 0)
|
|
950
|
-
s.autoStartTime = Date.now();
|
|
951
994
|
s.unitDispatchCount.clear();
|
|
952
995
|
s.unitLifetimeDispatches.clear();
|
|
953
996
|
if (!getLedger())
|
|
954
997
|
initMetrics(base);
|
|
955
998
|
if (s.currentMilestoneId)
|
|
956
999
|
setActiveMilestoneId(base, s.currentMilestoneId);
|
|
1000
|
+
// Re-register health level notification callback lost across process restart
|
|
1001
|
+
setLevelChangeCallback((_from, to, summary) => {
|
|
1002
|
+
const level = to === "red" ? "error" : to === "yellow" ? "warning" : "info";
|
|
1003
|
+
ctx.ui.notify(summary, level);
|
|
1004
|
+
});
|
|
957
1005
|
// ── Auto-worktree: re-enter worktree on resume ──
|
|
958
1006
|
if (s.currentMilestoneId &&
|
|
959
1007
|
shouldUseWorktreeIsolation() &&
|
|
@@ -970,6 +1018,11 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
970
1018
|
ctx.ui.setFooter(hideFooter);
|
|
971
1019
|
ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
972
1020
|
restoreHookState(s.basePath);
|
|
1021
|
+
// Re-sync managed resources on resume so long-lived auto sessions pick up
|
|
1022
|
+
// bundled extension updates before resume-time verification/state logic runs.
|
|
1023
|
+
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(process.env.GSD_HOME || homedir(), ".gsd", "agent");
|
|
1024
|
+
const { initResources } = await import("../../../" + "resource-loader.js");
|
|
1025
|
+
initResources(agentDir);
|
|
973
1026
|
// Open the project DB before rebuild/derive so resume uses DB-backed
|
|
974
1027
|
// state instead of falling back to stale markdown parsing (#2940).
|
|
975
1028
|
await openProjectDbIfPresent(s.basePath);
|
|
@@ -996,7 +1049,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
996
1049
|
invalidateAllCaches();
|
|
997
1050
|
if (s.pausedSessionFile) {
|
|
998
1051
|
const activityDir = join(gsdRoot(s.basePath), "activity");
|
|
999
|
-
const recovery = synthesizeCrashRecovery(s.basePath, s.currentUnit?.type ?? "unknown", s.currentUnit?.id ?? "unknown", s.pausedSessionFile ?? undefined, activityDir);
|
|
1052
|
+
const recovery = synthesizeCrashRecovery(s.basePath, s.currentUnit?.type ?? s.pausedUnitType ?? "unknown", s.currentUnit?.id ?? s.pausedUnitId ?? "unknown", s.pausedSessionFile ?? undefined, activityDir);
|
|
1000
1053
|
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
1001
1054
|
s.pendingCrashRecovery = recovery.prompt;
|
|
1002
1055
|
ctx.ui.notify(`Recovered ${recovery.trace.toolCallCount} tool calls from paused session. Resuming with context.`, "info");
|
|
@@ -1018,7 +1071,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
1018
1071
|
lockBase,
|
|
1019
1072
|
buildResolver,
|
|
1020
1073
|
};
|
|
1021
|
-
const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
|
|
1074
|
+
const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps, freshStartAssessment);
|
|
1022
1075
|
if (!ready)
|
|
1023
1076
|
return;
|
|
1024
1077
|
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
|
@@ -1101,24 +1154,6 @@ function ensurePreconditions(unitType, unitId, base, state) {
|
|
|
1101
1154
|
}
|
|
1102
1155
|
}
|
|
1103
1156
|
}
|
|
1104
|
-
// ─── Diagnostics ──────────────────────────────────────────────────────────────
|
|
1105
|
-
/** Build recovery context from module state for recoverTimedOutUnit */
|
|
1106
|
-
function buildRecoveryContext() {
|
|
1107
|
-
return {
|
|
1108
|
-
basePath: s.basePath,
|
|
1109
|
-
verbose: s.verbose,
|
|
1110
|
-
currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(),
|
|
1111
|
-
unitRecoveryCount: s.unitRecoveryCount,
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Test-only: expose skip-loop state for unit tests.
|
|
1116
|
-
* Not part of the public API.
|
|
1117
|
-
*/
|
|
1118
|
-
/**
|
|
1119
|
-
* Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
|
|
1120
|
-
* Used for manual hook triggers via /gsd run-hook.
|
|
1121
|
-
*/
|
|
1122
1157
|
export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, triggerUnitId, hookPrompt, hookModel, targetBasePath) {
|
|
1123
1158
|
if (!s.active) {
|
|
1124
1159
|
s.active = true;
|
|
@@ -141,7 +141,7 @@ export async function buildBeforeAgentStartResult(event, ctx) {
|
|
|
141
141
|
warnDeprecatedAgentInstructions();
|
|
142
142
|
const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
|
|
143
143
|
// Re-inject forensics context on follow-up turns (#2941)
|
|
144
|
-
const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd()) : null;
|
|
144
|
+
const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd(), event.prompt) : null;
|
|
145
145
|
const worktreeBlock = buildWorktreeContextBlock();
|
|
146
146
|
const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
|
|
147
147
|
stopContextTimer({
|
|
@@ -425,7 +425,7 @@ function oneLine(text) {
|
|
|
425
425
|
* Check for an active forensics session and return the prompt content
|
|
426
426
|
* so it can be re-injected on follow-up turns.
|
|
427
427
|
*/
|
|
428
|
-
function buildForensicsContextInjection(basePath) {
|
|
428
|
+
export function buildForensicsContextInjection(basePath, prompt) {
|
|
429
429
|
const marker = readForensicsMarker(basePath);
|
|
430
430
|
if (!marker)
|
|
431
431
|
return null;
|
|
@@ -435,6 +435,11 @@ function buildForensicsContextInjection(basePath) {
|
|
|
435
435
|
clearForensicsMarker(basePath);
|
|
436
436
|
return null;
|
|
437
437
|
}
|
|
438
|
+
const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
|
|
439
|
+
if (trimmed && !RESUME_INTENT_PATTERNS.test(trimmed)) {
|
|
440
|
+
clearForensicsMarker(basePath);
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
438
443
|
return marker.promptContent;
|
|
439
444
|
}
|
|
440
445
|
/**
|
|
@@ -7,6 +7,7 @@ import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSl
|
|
|
7
7
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
8
8
|
import { invalidateAllCaches } from "./cache.js";
|
|
9
9
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
10
|
+
import { isClosedStatus } from "./status-guards.js";
|
|
10
11
|
import { GLOBAL_STATE_CODES } from "./doctor-types.js";
|
|
11
12
|
import { checkGitHealth, checkRuntimeHealth, checkGlobalHealth, checkEngineHealth } from "./doctor-checks.js";
|
|
12
13
|
import { checkEnvironmentHealth } from "./doctor-environment.js";
|
|
@@ -443,8 +444,9 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
443
444
|
slices = dbSlices.map(s => ({
|
|
444
445
|
id: s.id,
|
|
445
446
|
title: s.title,
|
|
446
|
-
done: s.status
|
|
447
|
+
done: isClosedStatus(s.status),
|
|
447
448
|
pending: s.status === "pending",
|
|
449
|
+
skipped: s.status === "skipped",
|
|
448
450
|
risk: (s.risk || "medium"),
|
|
449
451
|
depends: s.depends,
|
|
450
452
|
demo: s.demo,
|
|
@@ -541,8 +543,9 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
541
543
|
const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
|
|
542
544
|
if (!slicePath) {
|
|
543
545
|
// Pending slices haven't been planned yet — directories are created
|
|
544
|
-
// lazily by ensurePreconditions() at dispatch time.
|
|
545
|
-
|
|
546
|
+
// lazily by ensurePreconditions() at dispatch time. Skipped slices are
|
|
547
|
+
// intentionally allowed to remain summary-less and directory-less.
|
|
548
|
+
if (slice.pending || slice.skipped)
|
|
546
549
|
continue;
|
|
547
550
|
const expectedPath = relSlicePath(basePath, milestoneId, slice.id);
|
|
548
551
|
issues.push({
|
|
@@ -566,7 +569,8 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
566
569
|
const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
|
|
567
570
|
if (!tasksDir) {
|
|
568
571
|
// Pending slices haven't been planned yet — tasks/ is created on demand.
|
|
569
|
-
|
|
572
|
+
// Skipped slices may legitimately never create tasks/.
|
|
573
|
+
if (slice.pending || slice.skipped)
|
|
570
574
|
continue;
|
|
571
575
|
issues.push({
|
|
572
576
|
severity: slice.done ? "warning" : "error",
|
|
@@ -701,6 +701,7 @@ let currentDb = null;
|
|
|
701
701
|
let currentPath = null;
|
|
702
702
|
let currentPid = 0;
|
|
703
703
|
let _exitHandlerRegistered = false;
|
|
704
|
+
let _dbOpenAttempted = false;
|
|
704
705
|
export function getDbProvider() {
|
|
705
706
|
loadProvider();
|
|
706
707
|
return providerName;
|
|
@@ -708,7 +709,17 @@ export function getDbProvider() {
|
|
|
708
709
|
export function isDbAvailable() {
|
|
709
710
|
return currentDb !== null;
|
|
710
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Returns true if openDatabase() has been called at least once this session.
|
|
714
|
+
* Used to distinguish "DB not yet initialized" from "DB genuinely unavailable"
|
|
715
|
+
* so that early callers (e.g. before_agent_start context injection) don't
|
|
716
|
+
* trigger a false degraded-mode warning.
|
|
717
|
+
*/
|
|
718
|
+
export function wasDbOpenAttempted() {
|
|
719
|
+
return _dbOpenAttempted;
|
|
720
|
+
}
|
|
711
721
|
export function openDatabase(path) {
|
|
722
|
+
_dbOpenAttempted = true;
|
|
712
723
|
if (currentDb && currentPath !== path)
|
|
713
724
|
closeDatabase();
|
|
714
725
|
if (currentDb && currentPath === path)
|