gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27
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/README.md +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +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.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 +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- 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-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-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/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Auto-mode coordination is single-host
|
|
2
|
+
|
|
3
|
+
The DB-backed coordination tables introduced by Phase B (`workers`,
|
|
4
|
+
`milestone_leases`, `unit_dispatches`, `cancellation_requests`,
|
|
5
|
+
`command_queue`) and the supporting `runtime_kv` table from Phase C all
|
|
6
|
+
rely on **shared SQLite WAL on local disk**. They do not work across
|
|
7
|
+
machines.
|
|
8
|
+
|
|
9
|
+
## Why single-host only
|
|
10
|
+
|
|
11
|
+
- SQLite WAL coordination — the locking primitives that make
|
|
12
|
+
`claimMilestoneLease`, `recordDispatchClaim`, and `claimNextCommand`
|
|
13
|
+
atomic — is local-disk only. Network filesystems (NFS, SMB, S3FS) and
|
|
14
|
+
fuse mounts break the lock semantics that the WAL relies on.
|
|
15
|
+
- Heartbeat TTL (`workers.last_heartbeat_at`) compares timestamps written
|
|
16
|
+
with SQLite wall-clock time (`datetime('now')`). Across machines without
|
|
17
|
+
wall-clock synchronization (for example NTP/chrony), TTL filtering can
|
|
18
|
+
produce phantom-active or premature-crashed verdicts. Monotonic clocks
|
|
19
|
+
are not used for these comparisons.
|
|
20
|
+
- Fencing tokens (`milestone_leases.fencing_token`) are monotonically
|
|
21
|
+
ordered by SQL within a single transaction. Cross-host races could
|
|
22
|
+
produce duplicate tokens if two SQLite processes opened the same DB
|
|
23
|
+
on a network mount.
|
|
24
|
+
|
|
25
|
+
## What does work
|
|
26
|
+
|
|
27
|
+
- Multiple `gsd auto` worker processes on the **same machine**, sharing
|
|
28
|
+
the project's SQLite DB via WAL. The lease check refuses concurrent
|
|
29
|
+
claims on the same milestone; the dispatch ledger's partial unique
|
|
30
|
+
index refuses double-claims of the same unit.
|
|
31
|
+
- A single `gsd auto` worker plus arbitrary read-only consumers
|
|
32
|
+
(dashboards, doctors) on the same machine.
|
|
33
|
+
- Worktree-based parallelism on the same machine, where each worker
|
|
34
|
+
holds a different milestone lease.
|
|
35
|
+
|
|
36
|
+
## Multi-host alternatives
|
|
37
|
+
|
|
38
|
+
If you need to coordinate `gsd auto` workers across machines, you need
|
|
39
|
+
a real coordinator: Postgres for the ledger + a leader-election service
|
|
40
|
+
(etcd, Consul) for the leases. That's out of scope for these phases.
|
|
41
|
+
The schema and module shapes here would need a non-trivial backend
|
|
42
|
+
swap before they could ride on top of either.
|
|
@@ -24,6 +24,7 @@ import { resolveMilestoneIntegrationBranch } from "./git-service.js";
|
|
|
24
24
|
import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
|
|
25
25
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
26
26
|
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
27
|
+
import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
|
|
27
28
|
/** In-memory health history for the current auto-mode session. */
|
|
28
29
|
let healthHistory = [];
|
|
29
30
|
/** Count of consecutive units with unresolved errors. */
|
|
@@ -159,6 +160,9 @@ export async function preDispatchHealthGate(basePath) {
|
|
|
159
160
|
// If a stale lock exists, the crash recovery path should handle it,
|
|
160
161
|
// not a new dispatch. This prevents double-dispatch after crashes.
|
|
161
162
|
try {
|
|
163
|
+
if (existsSync(join(gsdRoot(basePath), "gsd.db"))) {
|
|
164
|
+
await ensureDbOpen(basePath);
|
|
165
|
+
}
|
|
162
166
|
const lock = readCrashLock(basePath);
|
|
163
167
|
if (lock && !isLockProcessAlive(lock)) {
|
|
164
168
|
// Auto-clear it since we're about to dispatch anyway
|
|
@@ -6,6 +6,8 @@ import { deriveState, isGhostMilestone, isReusableGhostMilestone } from "./state
|
|
|
6
6
|
import { saveFile } from "./files.js";
|
|
7
7
|
import { nativeIsRepo, nativeForEachRef, nativeUpdateRef } from "./native-git-bridge.js";
|
|
8
8
|
import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
|
|
9
|
+
import { getActiveAutoWorkers } from "./db/auto-workers.js";
|
|
10
|
+
import { normalizeRealPath } from "./paths.js";
|
|
9
11
|
import { ensureGitignore, isGsdGitignored } from "./gitignore.js";
|
|
10
12
|
import { readAllSessionStatuses, isSessionStale, removeSessionStatus } from "./session-status-io.js";
|
|
11
13
|
import { recoverFailedMigration } from "./migrate-external.js";
|
|
@@ -26,6 +28,9 @@ function hasAssessmentVerdict(basePath, mid, sid) {
|
|
|
26
28
|
export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix) {
|
|
27
29
|
const root = gsdRoot(basePath);
|
|
28
30
|
// ── Stale crash lock ──────────────────────────────────────────────────
|
|
31
|
+
// Phase C pt 2: the lock state lives in the workers + unit_dispatches
|
|
32
|
+
// tables now, not auto.lock. readCrashLock synthesizes a LockData from
|
|
33
|
+
// the DB; isLockProcessAlive is a pure OS PID check.
|
|
29
34
|
try {
|
|
30
35
|
const lock = readCrashLock(basePath);
|
|
31
36
|
if (lock) {
|
|
@@ -36,13 +41,13 @@ export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldF
|
|
|
36
41
|
code: "stale_crash_lock",
|
|
37
42
|
scope: "project",
|
|
38
43
|
unitId: "project",
|
|
39
|
-
message: `Stale auto
|
|
40
|
-
file: "
|
|
44
|
+
message: `Stale auto-mode worker (PID ${lock.pid}, started ${lock.startedAt}, was executing ${lock.unitType} ${lock.unitId}) — process is no longer running`,
|
|
45
|
+
file: "<workers table>",
|
|
41
46
|
fixable: true,
|
|
42
47
|
});
|
|
43
48
|
if (shouldFix("stale_crash_lock")) {
|
|
44
49
|
clearLock(basePath);
|
|
45
|
-
fixesApplied.push("cleared stale auto
|
|
50
|
+
fixesApplied.push("cleared stale auto-mode worker state");
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
}
|
|
@@ -60,9 +65,20 @@ export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldF
|
|
|
60
65
|
if (existsSync(lockDir)) {
|
|
61
66
|
const statRes = statSync(lockDir);
|
|
62
67
|
if (statRes.isDirectory()) {
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
// Phase C pt 2: "any live process holds the lock?" check now means
|
|
69
|
+
// "is any worker registered with status='active' AND a fresh
|
|
70
|
+
// heartbeat for this project?" — readCrashLock returns null for
|
|
71
|
+
// healthy live workers (it surfaces stale ones only), so we must
|
|
72
|
+
// consult getActiveAutoWorkers directly.
|
|
73
|
+
const projectRoot = normalizeRealPath(basePath);
|
|
74
|
+
const activeWorkers = getActiveAutoWorkers().filter((w) => w.project_root_realpath === projectRoot && isLockProcessAlive({
|
|
75
|
+
pid: w.pid,
|
|
76
|
+
startedAt: w.started_at,
|
|
77
|
+
unitType: "starting",
|
|
78
|
+
unitId: "bootstrap",
|
|
79
|
+
unitStartedAt: w.started_at,
|
|
80
|
+
}));
|
|
81
|
+
const lockHolderAlive = activeWorkers.length > 0;
|
|
66
82
|
if (!lockHolderAlive) {
|
|
67
83
|
issues.push({
|
|
68
84
|
severity: "error",
|
|
@@ -2,8 +2,8 @@ import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync } from "nod
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadFile, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
4
4
|
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
|
|
5
|
-
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
|
|
6
|
-
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
|
|
5
|
+
import { isDbAvailable, openDatabase, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
|
|
6
|
+
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath, resolveGsdPathContract } from "./paths.js";
|
|
7
7
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
8
8
|
import { invalidateAllCaches } from "./cache.js";
|
|
9
9
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
@@ -309,6 +309,16 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
309
309
|
const fix = options?.fix === true;
|
|
310
310
|
const dryRun = options?.dryRun === true;
|
|
311
311
|
const fixLevel = options?.fixLevel ?? "all";
|
|
312
|
+
// CLI doctor can run before any tool handler has opened the DB. Runtime
|
|
313
|
+
// health checks need the existing project DB to surface DB-backed crash
|
|
314
|
+
// locks, paused sessions, and coordination rows.
|
|
315
|
+
const dbPath = resolveGsdPathContract(basePath).projectDb;
|
|
316
|
+
if (existsSync(dbPath)) {
|
|
317
|
+
try {
|
|
318
|
+
openDatabase(dbPath);
|
|
319
|
+
}
|
|
320
|
+
catch { /* surfaced later as db_unavailable */ }
|
|
321
|
+
}
|
|
312
322
|
// Issue codes that represent completion state transitions — creating summary
|
|
313
323
|
// stubs, marking slices/milestones done in the roadmap. These belong to the
|
|
314
324
|
// dispatch lifecycle (complete-slice, complete-milestone units), not to
|
|
@@ -136,10 +136,137 @@ function openRawDb(path) {
|
|
|
136
136
|
const Database = providerModule;
|
|
137
137
|
return new Database(path);
|
|
138
138
|
}
|
|
139
|
-
export const SCHEMA_VERSION =
|
|
139
|
+
export const SCHEMA_VERSION = 25;
|
|
140
140
|
function indexExists(db, name) {
|
|
141
141
|
return !!db.prepare("SELECT 1 as present FROM sqlite_master WHERE type = 'index' AND name = ?").get(name);
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Create the v24 coordination tables (workers, milestone_leases,
|
|
145
|
+
* unit_dispatches, cancellation_requests, command_queue) and their indexes.
|
|
146
|
+
*
|
|
147
|
+
* Idempotent — uses IF NOT EXISTS throughout. Called from both the
|
|
148
|
+
* fresh-install path and the v24 migration block in migrateSchema().
|
|
149
|
+
*
|
|
150
|
+
* Single-host invariant: these tables coordinate concurrent auto-mode
|
|
151
|
+
* workers via shared SQLite WAL on local disk only. NFS / network
|
|
152
|
+
* filesystems break the coordination semantics — multi-host execution
|
|
153
|
+
* needs a real coordinator (etcd, Postgres) and is out of scope.
|
|
154
|
+
*/
|
|
155
|
+
function createCoordinationTablesV24(db) {
|
|
156
|
+
const ddl = [
|
|
157
|
+
`CREATE TABLE IF NOT EXISTS workers (
|
|
158
|
+
worker_id TEXT PRIMARY KEY,
|
|
159
|
+
host TEXT NOT NULL,
|
|
160
|
+
pid INTEGER NOT NULL,
|
|
161
|
+
started_at TEXT NOT NULL,
|
|
162
|
+
version TEXT NOT NULL,
|
|
163
|
+
last_heartbeat_at TEXT NOT NULL,
|
|
164
|
+
status TEXT NOT NULL,
|
|
165
|
+
project_root_realpath TEXT NOT NULL
|
|
166
|
+
)`,
|
|
167
|
+
`CREATE TABLE IF NOT EXISTS milestone_leases (
|
|
168
|
+
milestone_id TEXT PRIMARY KEY,
|
|
169
|
+
worker_id TEXT NOT NULL,
|
|
170
|
+
fencing_token INTEGER NOT NULL,
|
|
171
|
+
acquired_at TEXT NOT NULL,
|
|
172
|
+
expires_at TEXT NOT NULL,
|
|
173
|
+
status TEXT NOT NULL,
|
|
174
|
+
FOREIGN KEY (worker_id) REFERENCES workers(worker_id),
|
|
175
|
+
FOREIGN KEY (milestone_id) REFERENCES milestones(id)
|
|
176
|
+
)`,
|
|
177
|
+
`CREATE TABLE IF NOT EXISTS unit_dispatches (
|
|
178
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
179
|
+
trace_id TEXT NOT NULL,
|
|
180
|
+
turn_id TEXT,
|
|
181
|
+
worker_id TEXT NOT NULL,
|
|
182
|
+
milestone_lease_token INTEGER NOT NULL,
|
|
183
|
+
milestone_id TEXT NOT NULL,
|
|
184
|
+
slice_id TEXT,
|
|
185
|
+
task_id TEXT,
|
|
186
|
+
unit_type TEXT NOT NULL,
|
|
187
|
+
unit_id TEXT NOT NULL,
|
|
188
|
+
status TEXT NOT NULL,
|
|
189
|
+
attempt_n INTEGER NOT NULL DEFAULT 1,
|
|
190
|
+
started_at TEXT NOT NULL,
|
|
191
|
+
ended_at TEXT,
|
|
192
|
+
exit_reason TEXT,
|
|
193
|
+
error_summary TEXT,
|
|
194
|
+
verification_evidence_id INTEGER,
|
|
195
|
+
next_run_at TEXT,
|
|
196
|
+
retry_after_ms INTEGER,
|
|
197
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
198
|
+
last_error_code TEXT,
|
|
199
|
+
last_error_at TEXT,
|
|
200
|
+
FOREIGN KEY (worker_id) REFERENCES workers(worker_id),
|
|
201
|
+
FOREIGN KEY (verification_evidence_id) REFERENCES verification_evidence(id)
|
|
202
|
+
)`,
|
|
203
|
+
`CREATE TABLE IF NOT EXISTS cancellation_requests (
|
|
204
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
205
|
+
requested_at TEXT NOT NULL,
|
|
206
|
+
requested_by TEXT NOT NULL,
|
|
207
|
+
scope TEXT NOT NULL,
|
|
208
|
+
scope_id TEXT NOT NULL,
|
|
209
|
+
dispatch_id INTEGER,
|
|
210
|
+
reason TEXT NOT NULL,
|
|
211
|
+
status TEXT NOT NULL,
|
|
212
|
+
acked_at TEXT,
|
|
213
|
+
acked_worker_id TEXT,
|
|
214
|
+
FOREIGN KEY (dispatch_id) REFERENCES unit_dispatches(id),
|
|
215
|
+
FOREIGN KEY (acked_worker_id) REFERENCES workers(worker_id)
|
|
216
|
+
)`,
|
|
217
|
+
`CREATE TABLE IF NOT EXISTS command_queue (
|
|
218
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
219
|
+
target_worker TEXT,
|
|
220
|
+
command TEXT NOT NULL,
|
|
221
|
+
args_json TEXT NOT NULL DEFAULT '{}',
|
|
222
|
+
enqueued_at TEXT NOT NULL,
|
|
223
|
+
claimed_at TEXT,
|
|
224
|
+
claimed_by TEXT,
|
|
225
|
+
completed_at TEXT,
|
|
226
|
+
result_json TEXT
|
|
227
|
+
)`,
|
|
228
|
+
];
|
|
229
|
+
for (const stmt of ddl)
|
|
230
|
+
db.exec(stmt);
|
|
231
|
+
// Indexes — created here so both fresh-install and v24-migration paths
|
|
232
|
+
// produce identical structure.
|
|
233
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_unit_dispatches_active ON unit_dispatches(milestone_id, status)");
|
|
234
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_unit_dispatches_trace ON unit_dispatches(trace_id, turn_id)");
|
|
235
|
+
// Partial unique index — prevents two workers from claiming the same
|
|
236
|
+
// unit concurrently. Codex review MEDIUM B2: enforces double-claim guard
|
|
237
|
+
// at the DB level.
|
|
238
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_unit_dispatches_active_per_unit "
|
|
239
|
+
+ "ON unit_dispatches(unit_id) WHERE status IN ('claimed','running')");
|
|
240
|
+
// command_queue index — SQLite indexes NULLs in B-trees, so this single
|
|
241
|
+
// index serves both targeted (target_worker = ?) and broadcast
|
|
242
|
+
// (target_worker IS NULL) queries. Codex review LOW B4 documented.
|
|
243
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_command_queue_pending ON command_queue(target_worker, claimed_at)");
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Create the v25 runtime_kv table. Idempotent — uses IF NOT EXISTS.
|
|
247
|
+
*
|
|
248
|
+
* STRICT INVARIANT: runtime_kv is NON-CORRECTNESS-CRITICAL. UI cursors,
|
|
249
|
+
* dashboard caches, last-seen-version markers, resume cursors, and other
|
|
250
|
+
* "soft" state are OK. Anything that drives auto-mode control flow gets
|
|
251
|
+
* typed columns in unit_dispatches / workers / milestone_leases — never
|
|
252
|
+
* a bag of JSON in runtime_kv.
|
|
253
|
+
*
|
|
254
|
+
* Scope partitioning: ('global', '', key) for project-wide values;
|
|
255
|
+
* ('worker', worker_id, key) for per-worker state (resume cursors);
|
|
256
|
+
* ('milestone', milestone_id, key) for per-milestone soft state.
|
|
257
|
+
*/
|
|
258
|
+
function createRuntimeKvTableV25(db) {
|
|
259
|
+
db.exec(`
|
|
260
|
+
CREATE TABLE IF NOT EXISTS runtime_kv (
|
|
261
|
+
scope TEXT NOT NULL,
|
|
262
|
+
scope_id TEXT NOT NULL DEFAULT '',
|
|
263
|
+
key TEXT NOT NULL,
|
|
264
|
+
value_json TEXT NOT NULL,
|
|
265
|
+
updated_at TEXT NOT NULL,
|
|
266
|
+
PRIMARY KEY (scope, scope_id, key)
|
|
267
|
+
)
|
|
268
|
+
`);
|
|
269
|
+
}
|
|
143
270
|
function dedupeVerificationEvidenceRows(db) {
|
|
144
271
|
db.exec(`
|
|
145
272
|
DELETE FROM verification_evidence
|
|
@@ -513,6 +640,8 @@ function initSchema(db, fileBacked) {
|
|
|
513
640
|
db.exec(`CREATE VIEW IF NOT EXISTS active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL`);
|
|
514
641
|
const existing = db.prepare("SELECT count(*) as cnt FROM schema_version").get();
|
|
515
642
|
if (existing && existing["cnt"] === 0) {
|
|
643
|
+
createCoordinationTablesV24(db);
|
|
644
|
+
createRuntimeKvTableV25(db);
|
|
516
645
|
// Fresh install — all tables are created above with the full current schema,
|
|
517
646
|
// so it is safe to create all migration-specific indexes here. For existing
|
|
518
647
|
// databases these indexes are created inside the individual migration guards
|
|
@@ -1142,6 +1271,26 @@ function migrateSchema(db) {
|
|
|
1142
1271
|
":applied_at": new Date().toISOString(),
|
|
1143
1272
|
});
|
|
1144
1273
|
}
|
|
1274
|
+
if (currentVersion < 24) {
|
|
1275
|
+
// v24: auto-mode coordination tables. See createCoordinationTablesV24
|
|
1276
|
+
// for full schema + invariants. No-op for fresh installs (the same
|
|
1277
|
+
// helper runs in the fresh-install path); for upgraded DBs this is
|
|
1278
|
+
// the only place these tables get created.
|
|
1279
|
+
createCoordinationTablesV24(db);
|
|
1280
|
+
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
|
|
1281
|
+
":version": 24,
|
|
1282
|
+
":applied_at": new Date().toISOString(),
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (currentVersion < 25) {
|
|
1286
|
+
// v25: runtime_kv non-correctness-critical key-value storage. See
|
|
1287
|
+
// createRuntimeKvTableV25 for the full schema + invariants.
|
|
1288
|
+
createRuntimeKvTableV25(db);
|
|
1289
|
+
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
|
|
1290
|
+
":version": 25,
|
|
1291
|
+
":applied_at": new Date().toISOString(),
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1145
1294
|
db.exec("COMMIT");
|
|
1146
1295
|
}
|
|
1147
1296
|
catch (err) {
|
|
@@ -1156,6 +1305,193 @@ let _exitHandlerRegistered = false;
|
|
|
1156
1305
|
let _dbOpenAttempted = false;
|
|
1157
1306
|
let _lastDbError = null;
|
|
1158
1307
|
let _lastDbPhase = null;
|
|
1308
|
+
/**
|
|
1309
|
+
* Identity key of the workspace whose connection is currently active
|
|
1310
|
+
* (currentDb). Set by openDatabaseByWorkspace(); null when the active
|
|
1311
|
+
* connection was opened via the legacy openDatabase(path) path.
|
|
1312
|
+
*/
|
|
1313
|
+
let _currentIdentityKey = null;
|
|
1314
|
+
/**
|
|
1315
|
+
* Workspace-scoped connection cache.
|
|
1316
|
+
* Key: GsdWorkspace.identityKey (realpath-normalized project root).
|
|
1317
|
+
* Value: the DB path and open adapter for that workspace.
|
|
1318
|
+
*
|
|
1319
|
+
* Sibling worktrees of the same project share the same identityKey (set by
|
|
1320
|
+
* createWorkspace) and therefore reuse the same cached connection, preserving
|
|
1321
|
+
* shared-WAL semantics. Different projects get distinct cache entries.
|
|
1322
|
+
*
|
|
1323
|
+
* NOTE: Only one connection is "active" at a time (currentDb/currentPath).
|
|
1324
|
+
* The cache allows fast re-activation of a previously opened connection when
|
|
1325
|
+
* callers switch between known workspaces via openDatabaseByWorkspace().
|
|
1326
|
+
*/
|
|
1327
|
+
const _dbCache = new Map();
|
|
1328
|
+
/** Test helper: expose the internal cache for inspection. Not for production use. */
|
|
1329
|
+
export function _getDbCache() {
|
|
1330
|
+
return _dbCache;
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Close and evict every entry in the workspace connection cache, then call
|
|
1334
|
+
* closeDatabase() to close the active connection.
|
|
1335
|
+
*
|
|
1336
|
+
* Use this for test teardown or process-shutdown paths where every open
|
|
1337
|
+
* connection must be flushed. Normal callers should use closeDatabase() or
|
|
1338
|
+
* closeDatabaseByWorkspace() instead.
|
|
1339
|
+
*/
|
|
1340
|
+
export function closeAllDatabases() {
|
|
1341
|
+
// Close all non-active cached connections first.
|
|
1342
|
+
for (const [key, entry] of _dbCache) {
|
|
1343
|
+
if (entry.db === currentDb)
|
|
1344
|
+
continue; // handled by closeDatabase() below
|
|
1345
|
+
_dbCache.delete(key);
|
|
1346
|
+
try {
|
|
1347
|
+
entry.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1348
|
+
}
|
|
1349
|
+
catch { /* best-effort */ }
|
|
1350
|
+
try {
|
|
1351
|
+
entry.db.exec("PRAGMA incremental_vacuum(64)");
|
|
1352
|
+
}
|
|
1353
|
+
catch { /* best-effort */ }
|
|
1354
|
+
try {
|
|
1355
|
+
entry.db.close();
|
|
1356
|
+
}
|
|
1357
|
+
catch { /* best-effort */ }
|
|
1358
|
+
}
|
|
1359
|
+
closeDatabase();
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Open (or reuse) the database connection scoped to the given workspace.
|
|
1363
|
+
*
|
|
1364
|
+
* Uses workspace.identityKey as the cache key, so sibling worktrees of the
|
|
1365
|
+
* same project resolve to the same connection. On a cache hit the existing
|
|
1366
|
+
* adapter is reactivated as the current connection without re-opening the
|
|
1367
|
+
* file. On a cache miss, delegates to openDatabase() for the full
|
|
1368
|
+
* open + schema-init + migration flow, then caches the result.
|
|
1369
|
+
*
|
|
1370
|
+
* When switching to a different workspace, the previously active connection
|
|
1371
|
+
* is preserved in the cache (not closed), so callers can switch back to it
|
|
1372
|
+
* cheaply via a subsequent openDatabaseByWorkspace() call.
|
|
1373
|
+
*
|
|
1374
|
+
* @param workspace A GsdWorkspace created by createWorkspace().
|
|
1375
|
+
* @returns true if the connection is open and ready, false otherwise.
|
|
1376
|
+
*/
|
|
1377
|
+
export function openDatabaseByWorkspace(workspace) {
|
|
1378
|
+
const key = workspace.identityKey;
|
|
1379
|
+
const dbPath = workspace.contract.projectDb;
|
|
1380
|
+
const cached = _dbCache.get(key);
|
|
1381
|
+
if (cached) {
|
|
1382
|
+
// Reactivate the cached connection as the current singleton.
|
|
1383
|
+
currentDb = cached.db;
|
|
1384
|
+
currentPath = cached.dbPath;
|
|
1385
|
+
currentPid = process.pid;
|
|
1386
|
+
_dbOpenAttempted = true;
|
|
1387
|
+
_currentIdentityKey = key;
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
// Cache miss — need to open a new connection.
|
|
1391
|
+
//
|
|
1392
|
+
// If there is a currently active workspace connection, stash it in the
|
|
1393
|
+
// cache under its identity key before calling openDatabase(), because
|
|
1394
|
+
// openDatabase() will call closeDatabase() when the path changes (which
|
|
1395
|
+
// would destroy the existing adapter). By nulling out currentDb first,
|
|
1396
|
+
// we prevent openDatabase() from closing the live adapter.
|
|
1397
|
+
let oldDb = null;
|
|
1398
|
+
let oldPath = null;
|
|
1399
|
+
let oldPid = 0;
|
|
1400
|
+
let oldKey = null;
|
|
1401
|
+
if (currentDb !== null && _currentIdentityKey !== null) {
|
|
1402
|
+
// Snapshot the old globals so we can restore them on failure.
|
|
1403
|
+
oldDb = currentDb;
|
|
1404
|
+
oldPath = currentPath;
|
|
1405
|
+
oldPid = currentPid;
|
|
1406
|
+
oldKey = _currentIdentityKey;
|
|
1407
|
+
// Save the current connection so it stays alive in the cache.
|
|
1408
|
+
_dbCache.set(_currentIdentityKey, {
|
|
1409
|
+
dbPath: currentPath,
|
|
1410
|
+
db: currentDb,
|
|
1411
|
+
});
|
|
1412
|
+
// Detach from globals so openDatabase() opens fresh without closing it.
|
|
1413
|
+
currentDb = null;
|
|
1414
|
+
currentPath = null;
|
|
1415
|
+
currentPid = 0;
|
|
1416
|
+
_currentIdentityKey = null;
|
|
1417
|
+
}
|
|
1418
|
+
// Run the full open/schema/migration flow for the new workspace.
|
|
1419
|
+
// openDatabase() can throw on corrupt DB or permission error — catch so we
|
|
1420
|
+
// can restore the previous connection rather than leaving globals null.
|
|
1421
|
+
let opened;
|
|
1422
|
+
try {
|
|
1423
|
+
opened = openDatabase(dbPath);
|
|
1424
|
+
}
|
|
1425
|
+
catch (err) {
|
|
1426
|
+
// Failed to open the new DB. Restore the previous workspace connection so
|
|
1427
|
+
// the caller's workspace remains active (it is still safe in _dbCache).
|
|
1428
|
+
if (oldDb !== null) {
|
|
1429
|
+
currentDb = oldDb;
|
|
1430
|
+
currentPath = oldPath;
|
|
1431
|
+
currentPid = oldPid;
|
|
1432
|
+
_currentIdentityKey = oldKey;
|
|
1433
|
+
}
|
|
1434
|
+
throw err;
|
|
1435
|
+
}
|
|
1436
|
+
if (opened && currentDb) {
|
|
1437
|
+
_dbCache.set(key, { dbPath, db: currentDb });
|
|
1438
|
+
_currentIdentityKey = key;
|
|
1439
|
+
}
|
|
1440
|
+
else if (!opened && oldDb !== null) {
|
|
1441
|
+
// Restore the previous connection so the caller's workspace remains active.
|
|
1442
|
+
// The failed attempt left no live adapter, so the globals stayed null.
|
|
1443
|
+
currentDb = oldDb;
|
|
1444
|
+
currentPath = oldPath;
|
|
1445
|
+
currentPid = oldPid;
|
|
1446
|
+
_currentIdentityKey = oldKey;
|
|
1447
|
+
}
|
|
1448
|
+
return opened;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Open (or reuse) the database connection scoped to the workspace in a
|
|
1452
|
+
* MilestoneScope. Thin delegation to openDatabaseByWorkspace().
|
|
1453
|
+
*/
|
|
1454
|
+
export function openDatabaseByScope(scope) {
|
|
1455
|
+
return openDatabaseByWorkspace(scope.workspace);
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Close the database connection for the given workspace and remove it from
|
|
1459
|
+
* the cache. If the workspace's connection is currently active (currentDb),
|
|
1460
|
+
* performs a full closeDatabase() including WAL checkpoint. Otherwise only
|
|
1461
|
+
* removes the cache entry (the adapter was already replaced by a later open).
|
|
1462
|
+
*/
|
|
1463
|
+
export function closeDatabaseByWorkspace(workspace) {
|
|
1464
|
+
const key = workspace.identityKey;
|
|
1465
|
+
const cached = _dbCache.get(key);
|
|
1466
|
+
if (!cached)
|
|
1467
|
+
return;
|
|
1468
|
+
_dbCache.delete(key);
|
|
1469
|
+
if (currentDb === cached.db) {
|
|
1470
|
+
// This workspace's connection is the active one — full close.
|
|
1471
|
+
closeDatabase();
|
|
1472
|
+
}
|
|
1473
|
+
else {
|
|
1474
|
+
// Connection was displaced by a later open; close the adapter directly.
|
|
1475
|
+
try {
|
|
1476
|
+
cached.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1477
|
+
}
|
|
1478
|
+
catch (e) {
|
|
1479
|
+
logWarning("db", `WAL checkpoint (byWorkspace) failed: ${e.message}`);
|
|
1480
|
+
}
|
|
1481
|
+
try {
|
|
1482
|
+
cached.db.exec("PRAGMA incremental_vacuum(64)");
|
|
1483
|
+
}
|
|
1484
|
+
catch (e) {
|
|
1485
|
+
logWarning("db", `incremental vacuum (byWorkspace) failed: ${e.message}`);
|
|
1486
|
+
}
|
|
1487
|
+
try {
|
|
1488
|
+
cached.db.close();
|
|
1489
|
+
}
|
|
1490
|
+
catch (e) {
|
|
1491
|
+
logWarning("db", `database close (byWorkspace) failed: ${e.message}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1159
1495
|
export function getDbProvider() {
|
|
1160
1496
|
loadProvider();
|
|
1161
1497
|
return providerName;
|
|
@@ -1300,6 +1636,13 @@ export function closeDatabase() {
|
|
|
1300
1636
|
catch (e) {
|
|
1301
1637
|
logWarning("db", `database close failed: ${e.message}`);
|
|
1302
1638
|
}
|
|
1639
|
+
// If this connection was workspace-tracked, evict it from the cache so
|
|
1640
|
+
// subsequent openDatabaseByWorkspace() calls re-open rather than reactivate
|
|
1641
|
+
// a closed adapter.
|
|
1642
|
+
if (_currentIdentityKey !== null) {
|
|
1643
|
+
_dbCache.delete(_currentIdentityKey);
|
|
1644
|
+
_currentIdentityKey = null;
|
|
1645
|
+
}
|
|
1303
1646
|
currentDb = null;
|
|
1304
1647
|
currentPath = null;
|
|
1305
1648
|
currentPid = 0;
|
|
@@ -2682,6 +3025,7 @@ export function deleteMilestone(milestoneId) {
|
|
|
2682
3025
|
currentDb.prepare(`DELETE FROM replan_history WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
2683
3026
|
currentDb.prepare(`DELETE FROM assessments WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
2684
3027
|
currentDb.prepare(`DELETE FROM artifacts WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
3028
|
+
currentDb.prepare(`DELETE FROM milestone_leases WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
2685
3029
|
currentDb.prepare(`DELETE FROM milestones WHERE id = :mid`).run({ ":mid": milestoneId });
|
|
2686
3030
|
});
|
|
2687
3031
|
}
|
|
@@ -2996,15 +3340,21 @@ export function deleteArtifactByPath(path) {
|
|
|
2996
3340
|
currentDb.prepare("DELETE FROM artifacts WHERE path = :path").run({ ":path": path });
|
|
2997
3341
|
}
|
|
2998
3342
|
/**
|
|
2999
|
-
* Drop
|
|
3000
|
-
*
|
|
3343
|
+
* Drop hierarchy rows in dependency order inside a transaction. Used by
|
|
3344
|
+
* `gsd recover` to rebuild engine state from markdown.
|
|
3001
3345
|
*/
|
|
3002
3346
|
export function clearEngineHierarchy() {
|
|
3003
3347
|
if (!currentDb)
|
|
3004
3348
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
3005
3349
|
transaction(() => {
|
|
3350
|
+
currentDb.exec("DELETE FROM verification_evidence");
|
|
3351
|
+
currentDb.exec("DELETE FROM quality_gates");
|
|
3352
|
+
currentDb.exec("DELETE FROM slice_dependencies");
|
|
3353
|
+
currentDb.exec("DELETE FROM assessments");
|
|
3354
|
+
currentDb.exec("DELETE FROM replan_history");
|
|
3006
3355
|
currentDb.exec("DELETE FROM tasks");
|
|
3007
3356
|
currentDb.exec("DELETE FROM slices");
|
|
3357
|
+
currentDb.exec("DELETE FROM milestone_leases");
|
|
3008
3358
|
currentDb.exec("DELETE FROM milestones");
|
|
3009
3359
|
});
|
|
3010
3360
|
}
|
|
@@ -3087,6 +3437,7 @@ export function restoreManifest(manifest) {
|
|
|
3087
3437
|
db.exec("DELETE FROM verification_evidence");
|
|
3088
3438
|
db.exec("DELETE FROM tasks");
|
|
3089
3439
|
db.exec("DELETE FROM slices");
|
|
3440
|
+
db.exec("DELETE FROM milestone_leases");
|
|
3090
3441
|
db.exec("DELETE FROM milestones");
|
|
3091
3442
|
db.exec("DELETE FROM decisions WHERE 1=1");
|
|
3092
3443
|
// Restore milestones
|
|
@@ -3149,6 +3500,7 @@ export function bulkInsertLegacyHierarchy(payload) {
|
|
|
3149
3500
|
transaction(() => {
|
|
3150
3501
|
db.prepare(`DELETE FROM tasks WHERE milestone_id IN (${placeholders})`).run(...clearMilestoneIds);
|
|
3151
3502
|
db.prepare(`DELETE FROM slices WHERE milestone_id IN (${placeholders})`).run(...clearMilestoneIds);
|
|
3503
|
+
db.prepare(`DELETE FROM milestone_leases WHERE milestone_id IN (${placeholders})`).run(...clearMilestoneIds);
|
|
3152
3504
|
db.prepare(`DELETE FROM milestones WHERE id IN (${placeholders})`).run(...clearMilestoneIds);
|
|
3153
3505
|
const insertMilestone = db.prepare("INSERT INTO milestones (id, title, status, created_at) VALUES (?, ?, ?, ?)");
|
|
3154
3506
|
for (const m of milestones) {
|
|
@@ -150,7 +150,7 @@ export async function showQueueAdd(ctx, pi, basePath, state) {
|
|
|
150
150
|
].join(" ");
|
|
151
151
|
// ── Dispatch the queue prompt ───────────────────────────────────────
|
|
152
152
|
// Activate the queue phase so the write-gate applies to CONTEXT.md writes
|
|
153
|
-
setQueuePhaseActive(true);
|
|
153
|
+
setQueuePhaseActive(true, basePath);
|
|
154
154
|
const queueInlinedTemplates = inlineTemplate("context", "Context");
|
|
155
155
|
const prompt = loadPrompt("queue", {
|
|
156
156
|
preamble,
|