gsd-pi 2.78.1-dev.d8826a445 → 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 +7 -5
- package/dist/resources/extensions/gsd/auto/session.js +33 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +46 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +19 -11
- package/dist/resources/extensions/gsd/auto-worktree.js +26 -187
- package/dist/resources/extensions/gsd/auto.js +79 -50
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -4
- 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/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 +161 -3
- package/dist/resources/extensions/gsd/guided-flow.js +6 -2
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/worktree-resolver.js +64 -0
- 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 +12 -12
- 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/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 +12 -12
- 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/package.json +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 +7 -5
- package/src/resources/extensions/gsd/auto/session.ts +36 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +53 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +19 -11
- package/src/resources/extensions/gsd/auto-worktree.ts +26 -211
- package/src/resources/extensions/gsd/auto.ts +89 -44
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -4
- 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/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 +170 -3
- package/src/resources/extensions/gsd/guided-flow.ts +6 -2
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/state.ts +44 -6
- 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-workers.test.ts +105 -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/detect-stuck-respects-retry.test.ts +173 -0
- 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 +3 -5
- 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/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -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/register-hooks-depth-verification.test.ts +110 -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 +7 -26
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +4 -8
- 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/workspace.test.ts +15 -9
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +31 -23
- package/src/resources/extensions/gsd/worktree-resolver.ts +62 -0
- 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/{AT5qi39nKXkdmQIOIoh0f → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{AT5qi39nKXkdmQIOIoh0f → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -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) {
|
|
@@ -2876,6 +3025,7 @@ export function deleteMilestone(milestoneId) {
|
|
|
2876
3025
|
currentDb.prepare(`DELETE FROM replan_history WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
2877
3026
|
currentDb.prepare(`DELETE FROM assessments WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
2878
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 });
|
|
2879
3029
|
currentDb.prepare(`DELETE FROM milestones WHERE id = :mid`).run({ ":mid": milestoneId });
|
|
2880
3030
|
});
|
|
2881
3031
|
}
|
|
@@ -3190,15 +3340,21 @@ export function deleteArtifactByPath(path) {
|
|
|
3190
3340
|
currentDb.prepare("DELETE FROM artifacts WHERE path = :path").run({ ":path": path });
|
|
3191
3341
|
}
|
|
3192
3342
|
/**
|
|
3193
|
-
* Drop
|
|
3194
|
-
*
|
|
3343
|
+
* Drop hierarchy rows in dependency order inside a transaction. Used by
|
|
3344
|
+
* `gsd recover` to rebuild engine state from markdown.
|
|
3195
3345
|
*/
|
|
3196
3346
|
export function clearEngineHierarchy() {
|
|
3197
3347
|
if (!currentDb)
|
|
3198
3348
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
3199
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");
|
|
3200
3355
|
currentDb.exec("DELETE FROM tasks");
|
|
3201
3356
|
currentDb.exec("DELETE FROM slices");
|
|
3357
|
+
currentDb.exec("DELETE FROM milestone_leases");
|
|
3202
3358
|
currentDb.exec("DELETE FROM milestones");
|
|
3203
3359
|
});
|
|
3204
3360
|
}
|
|
@@ -3281,6 +3437,7 @@ export function restoreManifest(manifest) {
|
|
|
3281
3437
|
db.exec("DELETE FROM verification_evidence");
|
|
3282
3438
|
db.exec("DELETE FROM tasks");
|
|
3283
3439
|
db.exec("DELETE FROM slices");
|
|
3440
|
+
db.exec("DELETE FROM milestone_leases");
|
|
3284
3441
|
db.exec("DELETE FROM milestones");
|
|
3285
3442
|
db.exec("DELETE FROM decisions WHERE 1=1");
|
|
3286
3443
|
// Restore milestones
|
|
@@ -3343,6 +3500,7 @@ export function bulkInsertLegacyHierarchy(payload) {
|
|
|
3343
3500
|
transaction(() => {
|
|
3344
3501
|
db.prepare(`DELETE FROM tasks WHERE milestone_id IN (${placeholders})`).run(...clearMilestoneIds);
|
|
3345
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);
|
|
3346
3504
|
db.prepare(`DELETE FROM milestones WHERE id IN (${placeholders})`).run(...clearMilestoneIds);
|
|
3347
3505
|
const insertMilestone = db.prepare("INSERT INTO milestones (id, title, status, created_at) VALUES (?, ?, ?, ?)");
|
|
3348
3506
|
for (const m of milestones) {
|
|
@@ -48,6 +48,8 @@ import { createWorkspace, scopeMilestone } from "./workspace.js";
|
|
|
48
48
|
export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, reserveMilestoneId, claimReservedId, getReservedMilestoneIds, clearReservedMilestoneIds, } from "./milestone-ids.js";
|
|
49
49
|
export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
|
|
50
50
|
import { logWarning } from "./workflow-logger.js";
|
|
51
|
+
import { deleteRuntimeKv } from "./db/runtime-kv.js";
|
|
52
|
+
import { PAUSED_SESSION_KV_KEY } from "./interrupted-session.js";
|
|
51
53
|
// ─── Scope-based validator wrappers ──────────────────────────────────────────
|
|
52
54
|
// These thin wrappers accept a MilestoneScope so callers that already hold a
|
|
53
55
|
// pinned scope never have to re-derive (basePath, milestoneId) separately.
|
|
@@ -1621,11 +1623,13 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1621
1623
|
if (interrupted.classification === "stale") {
|
|
1622
1624
|
clearLock(basePath);
|
|
1623
1625
|
if (interrupted.pausedSession) {
|
|
1626
|
+
// Phase C pt 2: paused-session.json migrated to runtime_kv
|
|
1627
|
+
// (global scope, key PAUSED_SESSION_KV_KEY).
|
|
1624
1628
|
try {
|
|
1625
|
-
|
|
1629
|
+
deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
|
|
1626
1630
|
}
|
|
1627
1631
|
catch (e) {
|
|
1628
|
-
logWarning("guided", `stale
|
|
1632
|
+
logWarning("guided", `stale paused-session DB cleanup failed: ${e.message}`, { file: "guided-flow.ts" });
|
|
1629
1633
|
}
|
|
1630
1634
|
}
|
|
1631
1635
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { verifyExpectedArtifact } from "./auto-recovery.js";
|
|
4
4
|
import { formatCrashInfo, isLockProcessAlive, readCrashLock, } from "./crash-recovery.js";
|
|
@@ -6,6 +6,7 @@ import { gsdRoot } from "./paths.js";
|
|
|
6
6
|
import { MILESTONE_ID_RE } from "./milestone-ids.js";
|
|
7
7
|
import { synthesizeCrashRecovery, } from "./session-forensics.js";
|
|
8
8
|
import { deriveState } from "./state.js";
|
|
9
|
+
import { getRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
|
|
9
10
|
const LEGACY_DEEP_SETUP_UNITS = new Set([
|
|
10
11
|
"workflow-preferences:WORKFLOW-PREFS",
|
|
11
12
|
"discuss-project:PROJECT",
|
|
@@ -32,24 +33,26 @@ function isStalePseudoMilestonePause(meta) {
|
|
|
32
33
|
&& typeof meta.unitId === "string"
|
|
33
34
|
&& LEGACY_DEEP_SETUP_UNITS.has(`${meta.unitType}:${meta.unitId}`);
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* runtime_kv key (global scope) that stores the most recent paused-session
|
|
38
|
+
* metadata. Phase C pt 2: replaces runtime/paused-session.json. The key is
|
|
39
|
+
* project-wide (not worker-scoped) because the paused state represents the
|
|
40
|
+
* last time auto-mode paused on this project — there is at most one paused
|
|
41
|
+
* session per project at a time.
|
|
42
|
+
*/
|
|
43
|
+
export const PAUSED_SESSION_KV_KEY = "paused_session";
|
|
35
44
|
export function readPausedSessionMetadata(basePath) {
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
// basePath is unused now (the DB is workspace-scoped via the connection
|
|
46
|
+
// openDatabase opened on it) but kept in the signature for callers.
|
|
47
|
+
void basePath;
|
|
48
|
+
const meta = getRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
|
|
49
|
+
if (!meta)
|
|
38
50
|
return null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (isStalePseudoMilestonePause(meta)) {
|
|
42
|
-
try {
|
|
43
|
-
unlinkSync(pausedPath);
|
|
44
|
-
}
|
|
45
|
-
catch { /* non-fatal */ }
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
return meta;
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
+
if (isStalePseudoMilestonePause(meta)) {
|
|
52
|
+
deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
|
|
51
53
|
return null;
|
|
52
54
|
}
|
|
55
|
+
return meta;
|
|
53
56
|
}
|
|
54
57
|
export function isBootstrapCrashLock(lock) {
|
|
55
58
|
return !!(lock &&
|
|
@@ -208,10 +208,17 @@ export async function getActiveMilestoneId(basePath) {
|
|
|
208
208
|
* Legacy filesystem parsing is available only through an explicit opt-in for
|
|
209
209
|
* tests/recovery flows; runtime must not silently infer state from markdown.
|
|
210
210
|
*/
|
|
211
|
-
export async function deriveState(basePath) {
|
|
212
|
-
//
|
|
211
|
+
export async function deriveState(basePath, opts) {
|
|
212
|
+
// Use the canonical project root (when provided) as the cache key so that
|
|
213
|
+
// two calls with different basePath strings (e.g. worktree path vs project
|
|
214
|
+
// root) but the same canonical .gsd/ share a single cache entry. The same
|
|
215
|
+
// key is used for both the lookup AND the write below — keying lookup on
|
|
216
|
+
// canonical-root while writing on basePath would silently return stale
|
|
217
|
+
// results across path-form alternation.
|
|
218
|
+
const cacheKey = opts?.projectRootForReads ?? basePath;
|
|
219
|
+
// Return cached result if within the TTL window for the same cacheKey
|
|
213
220
|
if (_stateCache &&
|
|
214
|
-
_stateCache.basePath ===
|
|
221
|
+
_stateCache.basePath === cacheKey &&
|
|
215
222
|
Date.now() - _stateCache.timestamp < CACHE_TTL_MS) {
|
|
216
223
|
return _stateCache.result;
|
|
217
224
|
}
|
|
@@ -230,7 +237,7 @@ export async function deriveState(basePath) {
|
|
|
230
237
|
if (wasDbOpenAttempted()) {
|
|
231
238
|
logWarning("state", "DB unavailable — using explicit legacy filesystem state derivation");
|
|
232
239
|
}
|
|
233
|
-
result = await _deriveStateImpl(basePath);
|
|
240
|
+
result = await _deriveStateImpl(basePath, opts);
|
|
234
241
|
_telemetry.markdownDeriveCount++;
|
|
235
242
|
}
|
|
236
243
|
else {
|
|
@@ -252,7 +259,7 @@ export async function deriveState(basePath) {
|
|
|
252
259
|
}
|
|
253
260
|
stopTimer({ phase: result.phase, milestone: result.activeMilestone?.id });
|
|
254
261
|
debugCount("deriveStateCalls");
|
|
255
|
-
_stateCache = { basePath, result, timestamp: Date.now() };
|
|
262
|
+
_stateCache = { basePath: cacheKey, result, timestamp: Date.now() };
|
|
256
263
|
return result;
|
|
257
264
|
}
|
|
258
265
|
/**
|
|
@@ -689,7 +696,15 @@ export async function deriveStateFromDb(basePath) {
|
|
|
689
696
|
// LEGACY: Filesystem-based state derivation for unmigrated projects.
|
|
690
697
|
// DB-backed projects use deriveStateFromDb() above. Target: extract to
|
|
691
698
|
// state-legacy.ts when all projects are DB-backed.
|
|
692
|
-
export async function _deriveStateImpl(basePath) {
|
|
699
|
+
export async function _deriveStateImpl(basePath, opts) {
|
|
700
|
+
// When the caller supplies a canonical project root for reads (e.g.
|
|
701
|
+
// s.canonicalProjectRoot from auto-mode), route all artifact reads through
|
|
702
|
+
// it. This prevents the worktree-local empty `.gsd/` from being consulted
|
|
703
|
+
// when the canonical state lives at the project root (or via a `.gsd`
|
|
704
|
+
// symlink into the external state dir).
|
|
705
|
+
if (opts?.projectRootForReads) {
|
|
706
|
+
basePath = opts.projectRootForReads;
|
|
707
|
+
}
|
|
693
708
|
const diskIds = findMilestoneIds(basePath);
|
|
694
709
|
const customOrder = loadQueueOrder(basePath);
|
|
695
710
|
const milestoneIds = sortByQueueOrder(diskIds, customOrder);
|
|
@@ -22,6 +22,7 @@ import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js
|
|
|
22
22
|
import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
|
|
23
23
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
24
24
|
import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
|
|
25
|
+
import { claimMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
|
|
25
26
|
// ─── Path Comparison Helper ────────────────────────────────────────────────
|
|
26
27
|
/**
|
|
27
28
|
* Compare two paths for physical identity, tolerating trailing slashes,
|
|
@@ -111,6 +112,69 @@ export class WorktreeResolver {
|
|
|
111
112
|
});
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
115
|
+
// Phase B: claim a milestone lease before any worktree mutation. Two
|
|
116
|
+
// workers cannot enter the same milestone concurrently. Best-effort:
|
|
117
|
+
// skip if no worker registered (single-worker fallback) or DB
|
|
118
|
+
// unavailable; reuse existing lease if we already hold it on this
|
|
119
|
+
// milestone (re-entry within the same session).
|
|
120
|
+
if (this.s.workerId) {
|
|
121
|
+
if (this.s.currentMilestoneId === milestoneId && this.s.milestoneLeaseToken !== null) {
|
|
122
|
+
// Already held — no-op, the heartbeat in loop.ts refreshes TTL.
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// If we held a different milestone, release it first so other
|
|
126
|
+
// workers don't have to wait for TTL.
|
|
127
|
+
if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
|
|
128
|
+
try {
|
|
129
|
+
releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
debugLog("WorktreeResolver", {
|
|
133
|
+
action: "enterMilestone",
|
|
134
|
+
milestoneId,
|
|
135
|
+
releasePriorLeaseError: err instanceof Error ? err.message : String(err),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
this.s.milestoneLeaseToken = null;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const claim = claimMilestoneLease(this.s.workerId, milestoneId);
|
|
142
|
+
if (claim.ok) {
|
|
143
|
+
this.s.milestoneLeaseToken = claim.token;
|
|
144
|
+
debugLog("WorktreeResolver", {
|
|
145
|
+
action: "enterMilestone",
|
|
146
|
+
milestoneId,
|
|
147
|
+
leaseAcquired: true,
|
|
148
|
+
fencingToken: claim.token,
|
|
149
|
+
expiresAt: claim.expiresAt,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Lease held by another worker — fail loud so the user can
|
|
154
|
+
// see the conflict instead of silently double-running.
|
|
155
|
+
const msg = `Milestone ${milestoneId} is held by worker ${claim.byWorker} until ${claim.expiresAt}.`;
|
|
156
|
+
debugLog("WorktreeResolver", {
|
|
157
|
+
action: "enterMilestone",
|
|
158
|
+
milestoneId,
|
|
159
|
+
leaseHeldByOther: claim.byWorker,
|
|
160
|
+
expiresAt: claim.expiresAt,
|
|
161
|
+
});
|
|
162
|
+
ctx.notify(`${msg} Another auto-mode worker is active. Stop it before entering ${milestoneId}.`, "error");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
// DB unavailable or other error — log and fall through to the
|
|
168
|
+
// pre-Phase-B single-worker behavior so a fresh project before
|
|
169
|
+
// DB init still works.
|
|
170
|
+
debugLog("WorktreeResolver", {
|
|
171
|
+
action: "enterMilestone",
|
|
172
|
+
milestoneId,
|
|
173
|
+
leaseError: err instanceof Error ? err.message : String(err),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
114
178
|
// Resolve the project root for worktree operations via shared helper.
|
|
115
179
|
// Handles the case where originalBasePath is falsy and basePath is itself
|
|
116
180
|
// a worktree path — prevents double-nested worktree paths (#3729).
|