gsd-pi 2.79.0 → 2.80.0
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 +94 -47
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
- package/dist/resources/extensions/gsd/auto/phases.js +61 -7
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
- package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
- package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/auto.js +159 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +41 -45
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
- package/dist/resources/extensions/gsd/commands/context.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +34 -1
- package/dist/resources/extensions/gsd/guided-flow.js +40 -0
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/post-execution-checks.js +25 -6
- package/dist/resources/extensions/gsd/preferences-types.js +20 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +82 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- package/dist/resources/extensions/shared/interview-ui.js +15 -4
- 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 +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- 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 +9 -9
- 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/package.json +1 -1
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +53 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +129 -2
- package/packages/mcp-server/src/workflow-tools.ts +81 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
- package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
- package/src/resources/extensions/gsd/auto/phases.ts +88 -9
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
- package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
- package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/auto.ts +167 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +49 -46
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
- package/src/resources/extensions/gsd/commands/context.ts +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +35 -1
- package/src/resources/extensions/gsd/guided-flow.ts +47 -0
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/post-execution-checks.ts +31 -6
- package/src/resources/extensions/gsd/preferences-types.ts +23 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
- package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +132 -3
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +84 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/interview-ui.ts +18 -5
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
- /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_ssgManifest.js +0 -0
|
@@ -147,6 +147,7 @@ import {
|
|
|
147
147
|
MergeConflictError,
|
|
148
148
|
parseSliceBranch,
|
|
149
149
|
setActiveMilestoneId,
|
|
150
|
+
resolveProjectRoot,
|
|
150
151
|
} from "./worktree.js";
|
|
151
152
|
import { GitServiceImpl } from "./git-service.js";
|
|
152
153
|
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
|
@@ -231,6 +232,8 @@ import type { ErrorContext } from "./auto/types.js";
|
|
|
231
232
|
import { runAutoLoopWithUok } from "./uok/kernel.js";
|
|
232
233
|
import { resolveUokFlags } from "./uok/flags.js";
|
|
233
234
|
import { validateDirectory } from "./validate-directory.js";
|
|
235
|
+
import { createAutoOrchestrator } from "./auto/orchestrator.js";
|
|
236
|
+
import type { AutoOrchestrationModule, AutoOrchestratorDeps } from "./auto/contracts.js";
|
|
234
237
|
// Slice-level parallelism (#2340)
|
|
235
238
|
import { getEligibleSlices } from "./slice-parallel-eligibility.js";
|
|
236
239
|
import { startSliceParallel } from "./slice-parallel-orchestrator.js";
|
|
@@ -1208,6 +1211,12 @@ export async function stopAuto(
|
|
|
1208
1211
|
// changes the user made between sessions (#4959 / CodeRabbit).
|
|
1209
1212
|
if (pi) clearToolBaseline(pi);
|
|
1210
1213
|
|
|
1214
|
+
try {
|
|
1215
|
+
await s.orchestration?.stop(reason ?? "stop");
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
debugLog("stop-orchestration-stop", { error: err instanceof Error ? err.message : String(err) });
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1211
1220
|
// Reset all session state in one call
|
|
1212
1221
|
s.reset();
|
|
1213
1222
|
}
|
|
@@ -1264,6 +1273,7 @@ export async function pauseAuto(
|
|
|
1264
1273
|
activeRunDir: s.activeRunDir,
|
|
1265
1274
|
autoStartTime: s.autoStartTime,
|
|
1266
1275
|
milestoneLock: s.sessionMilestoneLock ?? undefined,
|
|
1276
|
+
pauseReason: _errorContext?.message,
|
|
1267
1277
|
};
|
|
1268
1278
|
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, pausedMeta);
|
|
1269
1279
|
} catch (err) {
|
|
@@ -1306,6 +1316,12 @@ export async function pauseAuto(
|
|
|
1306
1316
|
resolveAgentEnd({ messages: [] });
|
|
1307
1317
|
_resetPendingResolve();
|
|
1308
1318
|
|
|
1319
|
+
try {
|
|
1320
|
+
await s.orchestration?.stop("pause");
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
debugLog("pause-orchestration-stop", { error: err instanceof Error ? err.message : String(err) });
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1309
1325
|
s.active = false;
|
|
1310
1326
|
s.paused = true;
|
|
1311
1327
|
deactivateGSD();
|
|
@@ -1363,6 +1379,141 @@ function buildResolver(): WorktreeResolver {
|
|
|
1363
1379
|
return new WorktreeResolver(s, buildResolverDeps());
|
|
1364
1380
|
}
|
|
1365
1381
|
|
|
1382
|
+
/**
|
|
1383
|
+
* Thin entry glue for the new Auto Orchestration module.
|
|
1384
|
+
*
|
|
1385
|
+
* This intentionally wires only dispatch + error notification today, with
|
|
1386
|
+
* no behavior changes to the existing auto loop. It provides a concrete seam
|
|
1387
|
+
* the next refactor steps can adopt incrementally.
|
|
1388
|
+
*/
|
|
1389
|
+
export function createWiredAutoOrchestrationModule(
|
|
1390
|
+
ctx: ExtensionContext,
|
|
1391
|
+
_pi: ExtensionAPI,
|
|
1392
|
+
dispatchBasePath: string,
|
|
1393
|
+
runtimeBasePath = resolveProjectRoot(dispatchBasePath),
|
|
1394
|
+
): AutoOrchestrationModule {
|
|
1395
|
+
const flowId = `auto-orchestrator-${Date.now()}`;
|
|
1396
|
+
let seq = 0;
|
|
1397
|
+
|
|
1398
|
+
const deps: AutoOrchestratorDeps = {
|
|
1399
|
+
dispatch: {
|
|
1400
|
+
async decideNextUnit() {
|
|
1401
|
+
const state = await deriveState(dispatchBasePath);
|
|
1402
|
+
const active = state.activeMilestone;
|
|
1403
|
+
if (!active) return null;
|
|
1404
|
+
|
|
1405
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1406
|
+
const action = await resolveDispatch({
|
|
1407
|
+
basePath: dispatchBasePath,
|
|
1408
|
+
mid: active.id,
|
|
1409
|
+
midTitle: active.title,
|
|
1410
|
+
state,
|
|
1411
|
+
prefs,
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
if (action.action !== "dispatch") return null;
|
|
1415
|
+
return {
|
|
1416
|
+
unitType: action.unitType,
|
|
1417
|
+
unitId: action.unitId,
|
|
1418
|
+
reason: action.matchedRule ?? "dispatch",
|
|
1419
|
+
preconditions: [],
|
|
1420
|
+
};
|
|
1421
|
+
},
|
|
1422
|
+
},
|
|
1423
|
+
recovery: {
|
|
1424
|
+
async classifyAndRecover(input) {
|
|
1425
|
+
const reason = input.error instanceof Error ? input.error.message : String(input.error ?? "unknown auto error");
|
|
1426
|
+
return { action: "escalate" as const, reason };
|
|
1427
|
+
},
|
|
1428
|
+
},
|
|
1429
|
+
worktree: {
|
|
1430
|
+
async prepareForUnit() {},
|
|
1431
|
+
async syncAfterUnit() {},
|
|
1432
|
+
async cleanupOnStop() {},
|
|
1433
|
+
},
|
|
1434
|
+
health: {
|
|
1435
|
+
async preAdvanceGate() {
|
|
1436
|
+
const gate = await preDispatchHealthGate(dispatchBasePath);
|
|
1437
|
+
return {
|
|
1438
|
+
allow: gate.proceed,
|
|
1439
|
+
reason: gate.reason,
|
|
1440
|
+
};
|
|
1441
|
+
},
|
|
1442
|
+
async postAdvanceRecord(result) {
|
|
1443
|
+
if (result.kind === "error") {
|
|
1444
|
+
recordHealthSnapshot(1, 0, 0, [{
|
|
1445
|
+
code: "orchestration-error",
|
|
1446
|
+
message: result.reason ?? "orchestration error",
|
|
1447
|
+
severity: "error",
|
|
1448
|
+
unitId: "orchestration",
|
|
1449
|
+
}], [], "orchestration");
|
|
1450
|
+
} else if (result.kind === "blocked") {
|
|
1451
|
+
recordHealthSnapshot(0, 1, 0, [{
|
|
1452
|
+
code: "orchestration-blocked",
|
|
1453
|
+
message: result.reason ?? "orchestration blocked",
|
|
1454
|
+
severity: "warning",
|
|
1455
|
+
unitId: "orchestration",
|
|
1456
|
+
}], [], "orchestration");
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
},
|
|
1460
|
+
runtime: {
|
|
1461
|
+
async ensureLockOwnership() {
|
|
1462
|
+
const status = getSessionLockStatus(runtimeBasePath);
|
|
1463
|
+
if (!status.valid || status.failureReason === "pid-mismatch") {
|
|
1464
|
+
throw new Error("session lock held by another process");
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
async journalTransition(event) {
|
|
1468
|
+
const eventType = event.name === "start"
|
|
1469
|
+
? "iteration-start"
|
|
1470
|
+
: event.name === "resume"
|
|
1471
|
+
? "iteration-start"
|
|
1472
|
+
: event.name === "advance"
|
|
1473
|
+
? "dispatch-match"
|
|
1474
|
+
: event.name === "advance-blocked"
|
|
1475
|
+
? "guard-block"
|
|
1476
|
+
: event.name === "advance-stopped"
|
|
1477
|
+
? "dispatch-stop"
|
|
1478
|
+
: event.name === "advance-error"
|
|
1479
|
+
? "iteration-end"
|
|
1480
|
+
: event.name === "advance-paused" || event.name === "advance-retry"
|
|
1481
|
+
? "guard-block"
|
|
1482
|
+
: event.name === "stop"
|
|
1483
|
+
? "terminal"
|
|
1484
|
+
: "iteration-end";
|
|
1485
|
+
|
|
1486
|
+
_emitJournalEvent(runtimeBasePath, {
|
|
1487
|
+
ts: new Date().toISOString(),
|
|
1488
|
+
flowId,
|
|
1489
|
+
seq: ++seq,
|
|
1490
|
+
eventType,
|
|
1491
|
+
data: {
|
|
1492
|
+
source: "auto-orchestrator",
|
|
1493
|
+
name: event.name,
|
|
1494
|
+
reason: event.reason,
|
|
1495
|
+
unitType: event.unitType,
|
|
1496
|
+
unitId: event.unitId,
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1499
|
+
},
|
|
1500
|
+
},
|
|
1501
|
+
notifications: {
|
|
1502
|
+
async notifyLifecycle(event) {
|
|
1503
|
+
if (event.name === "error") {
|
|
1504
|
+
ctx.ui.notify(event.detail ?? "auto orchestration error", "error");
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
},
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
return createAutoOrchestrator(deps);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function ensureOrchestrationModule(ctx: ExtensionContext, pi: ExtensionAPI, basePath: string): void {
|
|
1514
|
+
s.orchestration = createWiredAutoOrchestrationModule(ctx, pi, basePath, lockBase());
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1366
1517
|
/**
|
|
1367
1518
|
* Build the LoopDeps object from auto.ts private scope.
|
|
1368
1519
|
* This bundles all private functions that autoLoop needs without exporting them.
|
|
@@ -1785,6 +1936,7 @@ export async function startAuto(
|
|
|
1785
1936
|
rebuildScope(s.basePath, s.currentMilestoneId);
|
|
1786
1937
|
}
|
|
1787
1938
|
|
|
1939
|
+
ensureOrchestrationModule(ctx, pi, s.basePath || base);
|
|
1788
1940
|
registerSigtermHandler(lockBase());
|
|
1789
1941
|
|
|
1790
1942
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
@@ -1868,6 +2020,11 @@ export async function startAuto(
|
|
|
1868
2020
|
}
|
|
1869
2021
|
pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", level: "progress" });
|
|
1870
2022
|
|
|
2023
|
+
try {
|
|
2024
|
+
await s.orchestration?.resume();
|
|
2025
|
+
} catch (err) {
|
|
2026
|
+
debugLog("resume-orchestration-resume", { error: err instanceof Error ? err.message : String(err) });
|
|
2027
|
+
}
|
|
1871
2028
|
startAutoCommandPolling(s.basePath);
|
|
1872
2029
|
await runAutoLoopWithUok({
|
|
1873
2030
|
ctx,
|
|
@@ -1904,7 +2061,7 @@ export async function startAuto(
|
|
|
1904
2061
|
// Build scope after bootstrap has populated s.basePath / s.originalBasePath /
|
|
1905
2062
|
// s.currentMilestoneId (including worktree setup inside bootstrapAutoSession).
|
|
1906
2063
|
rebuildScope(s.basePath, s.currentMilestoneId);
|
|
1907
|
-
|
|
2064
|
+
ensureOrchestrationModule(ctx, pi, s.basePath || base);
|
|
1908
2065
|
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
|
1909
2066
|
registerAutoWorkerForSession(s);
|
|
1910
2067
|
try {
|
|
@@ -1915,6 +2072,12 @@ export async function startAuto(
|
|
|
1915
2072
|
}
|
|
1916
2073
|
pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: requestedStepMode ? "Step-mode started." : "Auto-mode started.", level: "progress" });
|
|
1917
2074
|
|
|
2075
|
+
try {
|
|
2076
|
+
await s.orchestration?.start({ basePath: s.basePath, trigger: "auto-loop" });
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
debugLog("start-orchestration-start", { error: err instanceof Error ? err.message : String(err) });
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1918
2081
|
startAutoCommandPolling(s.basePath);
|
|
1919
2082
|
|
|
1920
2083
|
// Dispatch the first unit
|
|
@@ -2037,6 +2200,9 @@ export async function dispatchHookUnit(
|
|
|
2037
2200
|
}
|
|
2038
2201
|
|
|
2039
2202
|
s.basePath = targetBasePath;
|
|
2203
|
+
if (!s.orchestration) {
|
|
2204
|
+
ensureOrchestrationModule(ctx, pi, s.basePath);
|
|
2205
|
+
}
|
|
2040
2206
|
|
|
2041
2207
|
const hookUnitType = `hook/${hookName}`;
|
|
2042
2208
|
const hookStartedAt = Date.now();
|
|
@@ -56,6 +56,19 @@ function resolveAgentEndBasePath(): string | undefined {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export function _buildAbortedPauseContext(lastMsg: { errorMessage?: unknown }): {
|
|
60
|
+
message: string;
|
|
61
|
+
category: "aborted";
|
|
62
|
+
isTransient: true;
|
|
63
|
+
} {
|
|
64
|
+
const hasErrorMessage = Object.prototype.hasOwnProperty.call(lastMsg, "errorMessage") && !!lastMsg.errorMessage;
|
|
65
|
+
return {
|
|
66
|
+
message: hasErrorMessage ? String(lastMsg.errorMessage) : "Operation aborted",
|
|
67
|
+
category: "aborted",
|
|
68
|
+
isTransient: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
async function pauseTransientWithBackoff(
|
|
60
73
|
cls: ErrorClass,
|
|
61
74
|
pi: ExtensionAPI,
|
|
@@ -159,7 +172,7 @@ export async function handleAgentEnd(
|
|
|
159
172
|
return;
|
|
160
173
|
}
|
|
161
174
|
|
|
162
|
-
await pauseAuto(ctx, pi);
|
|
175
|
+
await pauseAuto(ctx, pi, _buildAbortedPauseContext(lastMsg as { errorMessage?: unknown }));
|
|
163
176
|
return;
|
|
164
177
|
}
|
|
165
178
|
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GSD2 — Exec (context-mode) tool registration.
|
|
2
2
|
//
|
|
3
|
-
// Exposes the
|
|
4
|
-
// `context_mode.enabled:
|
|
3
|
+
// Exposes the Context Mode runtime tools in-process. Default-on; opt out with
|
|
4
|
+
// `context_mode.enabled: false` in preferences.
|
|
5
5
|
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
@@ -64,6 +64,38 @@ async function applyDisabledModelProviderPolicy(ctx: ExtensionContext): Promise<
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
async function writeContextModeCompactionSnapshot(basePath: string): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
70
|
+
const { isContextModeEnabled } = await import("../preferences-types.js");
|
|
71
|
+
const prefs = loadEffectiveGSDPreferences(basePath);
|
|
72
|
+
if (!isContextModeEnabled(prefs?.preferences)) return;
|
|
73
|
+
|
|
74
|
+
const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
|
|
75
|
+
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
76
|
+
await ensureDbOpen(basePath);
|
|
77
|
+
|
|
78
|
+
let activeContext: string | null = null;
|
|
79
|
+
try {
|
|
80
|
+
const state = await deriveGsdState(basePath);
|
|
81
|
+
if (state.activeMilestone && state.activeSlice && state.activeTask) {
|
|
82
|
+
activeContext =
|
|
83
|
+
`Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
|
|
84
|
+
(state.activeTask.title ? ` - ${state.activeTask.title}` : "");
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
/* non-fatal */
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeCompactionSnapshot(basePath, { activeContext });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
safetyLogWarning(
|
|
93
|
+
"context-mode",
|
|
94
|
+
`failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
67
99
|
export function registerHooks(
|
|
68
100
|
pi: ExtensionAPI,
|
|
69
101
|
ecosystemHandlers: GSDEcosystemBeforeAgentStartHandler[],
|
|
@@ -156,7 +188,7 @@ export function registerHooks(
|
|
|
156
188
|
await getEcosystemReadyPromise();
|
|
157
189
|
|
|
158
190
|
const beforeAgentBasePath = process.cwd();
|
|
159
|
-
const pendingApprovalGate = getPendingGate();
|
|
191
|
+
const pendingApprovalGate = getPendingGate(beforeAgentBasePath);
|
|
160
192
|
if (pendingApprovalGate && isExplicitApprovalResponse(event.prompt, pendingApprovalGate)) {
|
|
161
193
|
markApprovalGateVerified(pendingApprovalGate, beforeAgentBasePath);
|
|
162
194
|
const milestoneId = extractDepthVerificationMilestoneId(pendingApprovalGate);
|
|
@@ -229,15 +261,19 @@ export function registerHooks(
|
|
|
229
261
|
});
|
|
230
262
|
|
|
231
263
|
pi.on("session_before_compact", async () => {
|
|
264
|
+
const basePath = process.cwd();
|
|
265
|
+
// Context Mode is default-on. Write the resumable snapshot before any
|
|
266
|
+
// active-auto cancel return so auto sessions still leave re-entry context.
|
|
267
|
+
await writeContextModeCompactionSnapshot(basePath);
|
|
268
|
+
|
|
232
269
|
// Only cancel compaction while auto-mode is actively running.
|
|
233
270
|
// Paused auto-mode should allow compaction — the user may be doing
|
|
234
271
|
// interactive work (#3165).
|
|
235
272
|
if (isAutoActive()) {
|
|
236
273
|
return { cancel: true };
|
|
237
274
|
}
|
|
238
|
-
const basePath = process.cwd();
|
|
239
275
|
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
240
|
-
await ensureDbOpen();
|
|
276
|
+
await ensureDbOpen(basePath);
|
|
241
277
|
const state = await deriveGsdState(basePath);
|
|
242
278
|
if (!state.activeMilestone || !state.activeSlice) return;
|
|
243
279
|
// Write checkpoint for ALL phases, not just "executing" — discuss, research,
|
|
@@ -282,42 +318,6 @@ export function registerHooks(
|
|
|
282
318
|
}));
|
|
283
319
|
});
|
|
284
320
|
|
|
285
|
-
// Context-mode snapshot: write .gsd/last-snapshot.md before compaction so
|
|
286
|
-
// agents can call gsd_resume (or Read the file) to re-orient. Opt-in via
|
|
287
|
-
// preferences.context_mode.enabled. Runs after the auto-cancel handler
|
|
288
|
-
// above — if that one returned cancel:true, pi still fires us but the
|
|
289
|
-
// compaction won't actually happen; the snapshot is still useful then,
|
|
290
|
-
// since auto may pause and resume later.
|
|
291
|
-
pi.on("session_before_compact", async () => {
|
|
292
|
-
try {
|
|
293
|
-
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
294
|
-
const { isContextModeEnabled } = await import("../preferences-types.js");
|
|
295
|
-
const prefs = loadEffectiveGSDPreferences();
|
|
296
|
-
if (!isContextModeEnabled(prefs?.preferences)) return;
|
|
297
|
-
const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
|
|
298
|
-
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
299
|
-
await ensureDbOpen();
|
|
300
|
-
const basePath = process.cwd();
|
|
301
|
-
let activeContext: string | null = null;
|
|
302
|
-
try {
|
|
303
|
-
const state = await deriveGsdState(basePath);
|
|
304
|
-
if (state.activeMilestone && state.activeSlice && state.activeTask) {
|
|
305
|
-
activeContext =
|
|
306
|
-
`Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
|
|
307
|
-
(state.activeTask.title ? ` — ${state.activeTask.title}` : "");
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
/* non-fatal */
|
|
311
|
-
}
|
|
312
|
-
writeCompactionSnapshot(basePath, { activeContext });
|
|
313
|
-
} catch (err) {
|
|
314
|
-
safetyLogWarning(
|
|
315
|
-
"context-mode",
|
|
316
|
-
`failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
321
|
pi.on("message_update", async (event, ctx: ExtensionContext) => {
|
|
322
322
|
if (approvalQuestionAbortInFlight) return;
|
|
323
323
|
|
|
@@ -401,20 +401,22 @@ export function registerHooks(
|
|
|
401
401
|
// ── Discussion gate enforcement: block tool calls while gate is pending ──
|
|
402
402
|
// If ask_user_questions was called with a gate ID but hasn't been confirmed,
|
|
403
403
|
// block all non-read-only tool calls to prevent the model from skipping gates.
|
|
404
|
-
if (getPendingGate()) {
|
|
404
|
+
if (getPendingGate(discussionBasePath)) {
|
|
405
405
|
const milestoneId = await getDiscussionMilestoneIdFor(discussionBasePath);
|
|
406
406
|
if (isToolCallEventType("bash", event)) {
|
|
407
407
|
const bashGuard = shouldBlockPendingGateBash(
|
|
408
408
|
event.input.command,
|
|
409
409
|
milestoneId,
|
|
410
|
-
isQueuePhaseActive(),
|
|
410
|
+
isQueuePhaseActive(discussionBasePath),
|
|
411
|
+
discussionBasePath,
|
|
411
412
|
);
|
|
412
413
|
if (bashGuard.block) return bashGuard;
|
|
413
414
|
} else {
|
|
414
415
|
const gateGuard = shouldBlockPendingGate(
|
|
415
416
|
toolName,
|
|
416
417
|
milestoneId,
|
|
417
|
-
isQueuePhaseActive(),
|
|
418
|
+
isQueuePhaseActive(discussionBasePath),
|
|
419
|
+
discussionBasePath,
|
|
418
420
|
);
|
|
419
421
|
if (gateGuard.block) return gateGuard;
|
|
420
422
|
}
|
|
@@ -424,7 +426,7 @@ export function registerHooks(
|
|
|
424
426
|
// When /gsd queue is active, the agent should only create milestones,
|
|
425
427
|
// not execute work. Block write/edit to non-.gsd/ paths and bash commands
|
|
426
428
|
// that would modify files.
|
|
427
|
-
if (isQueuePhaseActive()) {
|
|
429
|
+
if (isQueuePhaseActive(discussionBasePath)) {
|
|
428
430
|
let queueInput = "";
|
|
429
431
|
if (isToolCallEventType("write", event)) {
|
|
430
432
|
queueInput = event.input.path;
|
|
@@ -498,7 +500,8 @@ export function registerHooks(
|
|
|
498
500
|
event.toolName,
|
|
499
501
|
event.input.path,
|
|
500
502
|
await getDiscussionMilestoneIdFor(discussionBasePath),
|
|
501
|
-
isQueuePhaseActive(),
|
|
503
|
+
isQueuePhaseActive(discussionBasePath),
|
|
504
|
+
discussionBasePath,
|
|
502
505
|
);
|
|
503
506
|
if (result.block) return result;
|
|
504
507
|
});
|
|
@@ -558,7 +561,7 @@ export function registerHooks(
|
|
|
558
561
|
if (toolName !== "ask_user_questions") return;
|
|
559
562
|
const basePath = process.cwd();
|
|
560
563
|
const milestoneId = await getDiscussionMilestoneIdFor(basePath);
|
|
561
|
-
const queueActive = isQueuePhaseActive();
|
|
564
|
+
const queueActive = isQueuePhaseActive(basePath);
|
|
562
565
|
|
|
563
566
|
const details = event.details as any;
|
|
564
567
|
|
|
@@ -568,7 +571,7 @@ export function registerHooks(
|
|
|
568
571
|
// If the user responded at all (even "needs adjustment"), clear the pending gate
|
|
569
572
|
// because the user engaged — the prompt handles the re-ask-after-adjustment flow.
|
|
570
573
|
const questions: any[] = (event.input as any)?.questions ?? [];
|
|
571
|
-
const currentPendingGate = getPendingGate();
|
|
574
|
+
const currentPendingGate = getPendingGate(basePath);
|
|
572
575
|
if (currentPendingGate) {
|
|
573
576
|
if (details?.cancelled || !details?.response) {
|
|
574
577
|
// Gate stays pending. Direct the agent to the most reliable recovery
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// GSD-2 write-gate bootstrap — regression tests for basePath threading on
|
|
2
|
+
// shouldBlockContextWrite / shouldBlockPendingGate (R1).
|
|
3
|
+
//
|
|
4
|
+
// The underlying bug: readers defaulted to process.cwd() and so missed the
|
|
5
|
+
// per-basePath state Map entry written by markDepthVerified(..., baseDirA)
|
|
6
|
+
// when cwd had drifted to baseDirB. With basePath threaded explicitly to
|
|
7
|
+
// the readers, the depth-gate sees the verified state regardless of cwd.
|
|
8
|
+
|
|
9
|
+
import { test, describe, before, after } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
markDepthVerified,
|
|
17
|
+
setPendingGate,
|
|
18
|
+
shouldBlockContextWrite,
|
|
19
|
+
shouldBlockPendingGate,
|
|
20
|
+
clearDiscussionFlowState,
|
|
21
|
+
} from "../write-gate.js";
|
|
22
|
+
|
|
23
|
+
function makeTempDir(): string {
|
|
24
|
+
return mkdtempSync(join(tmpdir(), "wg-shouldblock-basepath-"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let originalCwd: string;
|
|
28
|
+
before(() => {
|
|
29
|
+
originalCwd = process.cwd();
|
|
30
|
+
});
|
|
31
|
+
after(() => {
|
|
32
|
+
if (process.cwd() !== originalCwd) {
|
|
33
|
+
process.chdir(originalCwd);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("write-gate shouldBlock readers respect explicit basePath", () => {
|
|
38
|
+
let baseDirA: string;
|
|
39
|
+
let baseDirB: string;
|
|
40
|
+
let prevPersist: string | undefined;
|
|
41
|
+
|
|
42
|
+
before(() => {
|
|
43
|
+
baseDirA = makeTempDir();
|
|
44
|
+
baseDirB = makeTempDir();
|
|
45
|
+
prevPersist = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
46
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
after(() => {
|
|
50
|
+
process.chdir(originalCwd);
|
|
51
|
+
if (prevPersist === undefined) {
|
|
52
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
53
|
+
} else {
|
|
54
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = prevPersist;
|
|
55
|
+
}
|
|
56
|
+
rmSync(baseDirA, { recursive: true, force: true });
|
|
57
|
+
rmSync(baseDirB, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("shouldBlockContextWrite with explicit basePath sees verified state after cwd drift", () => {
|
|
61
|
+
clearDiscussionFlowState(baseDirA);
|
|
62
|
+
clearDiscussionFlowState(baseDirB);
|
|
63
|
+
|
|
64
|
+
markDepthVerified("M001", baseDirA);
|
|
65
|
+
process.chdir(baseDirB);
|
|
66
|
+
|
|
67
|
+
const contextPath = join(baseDirA, ".gsd", "milestones", "M001", "M001-CONTEXT.md");
|
|
68
|
+
const result = shouldBlockContextWrite("write", contextPath, "M001", undefined, baseDirA);
|
|
69
|
+
|
|
70
|
+
assert.equal(result.block, false, "explicit basePath should resolve to baseDirA's verified state");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("shouldBlockContextWrite without basePath defaults to cwd and misses verified state (bug repro)", () => {
|
|
74
|
+
clearDiscussionFlowState(baseDirA);
|
|
75
|
+
clearDiscussionFlowState(baseDirB);
|
|
76
|
+
|
|
77
|
+
markDepthVerified("M001", baseDirA);
|
|
78
|
+
process.chdir(baseDirB);
|
|
79
|
+
|
|
80
|
+
const contextPath = join(baseDirA, ".gsd", "milestones", "M001", "M001-CONTEXT.md");
|
|
81
|
+
const result = shouldBlockContextWrite("write", contextPath, "M001");
|
|
82
|
+
|
|
83
|
+
assert.equal(result.block, true, "default-to-cwd path resolves to baseDirB and misses baseDirA state");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("shouldBlockPendingGate with explicit basePath sees pending gate after cwd drift", () => {
|
|
87
|
+
clearDiscussionFlowState(baseDirA);
|
|
88
|
+
clearDiscussionFlowState(baseDirB);
|
|
89
|
+
|
|
90
|
+
setPendingGate("depth_verification_M001_confirm", baseDirA);
|
|
91
|
+
process.chdir(baseDirB);
|
|
92
|
+
|
|
93
|
+
const result = shouldBlockPendingGate("write", "M001", false, baseDirA);
|
|
94
|
+
|
|
95
|
+
assert.equal(result.block, true, "explicit basePath should resolve to baseDirA's pending gate state");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -351,8 +351,9 @@ export function shouldBlockPendingGate(
|
|
|
351
351
|
toolName: string,
|
|
352
352
|
milestoneId: string | null,
|
|
353
353
|
queuePhaseActive?: boolean,
|
|
354
|
+
basePath: string = process.cwd(),
|
|
354
355
|
): { block: boolean; reason?: string } {
|
|
355
|
-
return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(), toolName, milestoneId, queuePhaseActive);
|
|
356
|
+
return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(basePath), toolName, milestoneId, queuePhaseActive);
|
|
356
357
|
}
|
|
357
358
|
|
|
358
359
|
export function shouldBlockPendingGateInSnapshot(
|
|
@@ -386,8 +387,9 @@ export function shouldBlockPendingGateBash(
|
|
|
386
387
|
command: string,
|
|
387
388
|
milestoneId: string | null,
|
|
388
389
|
queuePhaseActive?: boolean,
|
|
390
|
+
basePath: string = process.cwd(),
|
|
389
391
|
): { block: boolean; reason?: string } {
|
|
390
|
-
return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(), command, milestoneId, queuePhaseActive);
|
|
392
|
+
return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(basePath), command, milestoneId, queuePhaseActive);
|
|
391
393
|
}
|
|
392
394
|
|
|
393
395
|
export function shouldBlockPendingGateBashInSnapshot(
|
|
@@ -444,6 +446,7 @@ export function shouldBlockContextWrite(
|
|
|
444
446
|
inputPath: string,
|
|
445
447
|
milestoneId: string | null,
|
|
446
448
|
_queuePhaseActive?: boolean,
|
|
449
|
+
basePath: string = process.cwd(),
|
|
447
450
|
): { block: boolean; reason?: string } {
|
|
448
451
|
if (toolName !== "write") return { block: false };
|
|
449
452
|
if (!MILESTONE_CONTEXT_RE.test(inputPath)) return { block: false };
|
|
@@ -460,7 +463,7 @@ export function shouldBlockContextWrite(
|
|
|
460
463
|
};
|
|
461
464
|
}
|
|
462
465
|
|
|
463
|
-
if (isMilestoneDepthVerified(targetMilestoneId)) return { block: false };
|
|
466
|
+
if (isMilestoneDepthVerified(targetMilestoneId, basePath)) return { block: false };
|
|
464
467
|
|
|
465
468
|
return {
|
|
466
469
|
block: true,
|
|
@@ -483,8 +486,9 @@ export function shouldBlockContextArtifactSave(
|
|
|
483
486
|
artifactType: string,
|
|
484
487
|
milestoneId: string | null,
|
|
485
488
|
sliceId?: string | null,
|
|
489
|
+
basePath: string = process.cwd(),
|
|
486
490
|
): { block: boolean; reason?: string } {
|
|
487
|
-
return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(), artifactType, milestoneId, sliceId);
|
|
491
|
+
return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(basePath), artifactType, milestoneId, sliceId);
|
|
488
492
|
}
|
|
489
493
|
|
|
490
494
|
export function shouldBlockContextArtifactSaveInSnapshot(
|
|
@@ -753,6 +753,11 @@ function columnExists(db: DbAdapter, table: string, column: string): boolean {
|
|
|
753
753
|
return rows.some((row) => row["name"] === column);
|
|
754
754
|
}
|
|
755
755
|
|
|
756
|
+
function formatFtsUnavailableError(err: unknown): string {
|
|
757
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
758
|
+
return message.replace(/\bmoduel\s*:\s*/gi, "module: ");
|
|
759
|
+
}
|
|
760
|
+
|
|
756
761
|
/**
|
|
757
762
|
* Create the FTS5 virtual table for memories plus the triggers that keep it
|
|
758
763
|
* in sync with the base table. FTS5 may be unavailable on stripped-down
|
|
@@ -787,7 +792,7 @@ export function tryCreateMemoriesFts(db: DbAdapter): boolean {
|
|
|
787
792
|
`);
|
|
788
793
|
return true;
|
|
789
794
|
} catch (err) {
|
|
790
|
-
logWarning("db", `FTS5 unavailable — memory queries will use LIKE fallback: ${(err
|
|
795
|
+
logWarning("db", `FTS5 unavailable — memory queries will use LIKE fallback: ${formatFtsUnavailableError(err)}`);
|
|
791
796
|
return false;
|
|
792
797
|
}
|
|
793
798
|
}
|
|
@@ -1740,6 +1745,35 @@ export function closeDatabase(): void {
|
|
|
1740
1745
|
_lastDbPhase = null;
|
|
1741
1746
|
}
|
|
1742
1747
|
|
|
1748
|
+
/**
|
|
1749
|
+
* Re-open the active database connection from disk.
|
|
1750
|
+
*
|
|
1751
|
+
* Auto-mode can observe artifacts written by a workflow server running in a
|
|
1752
|
+
* different process before its long-lived singleton has re-synchronized. The
|
|
1753
|
+
* recovery path uses this to force the next state derivation to read from the
|
|
1754
|
+
* current on-disk database instead of continuing with a possibly stale handle.
|
|
1755
|
+
*/
|
|
1756
|
+
export function refreshOpenDatabaseFromDisk(): boolean {
|
|
1757
|
+
if (!currentDb || !currentPath) return false;
|
|
1758
|
+
if (currentPath === ":memory:") return false;
|
|
1759
|
+
|
|
1760
|
+
const dbPath = currentPath;
|
|
1761
|
+
const identityKey = _currentIdentityKey;
|
|
1762
|
+
|
|
1763
|
+
try {
|
|
1764
|
+
closeDatabase();
|
|
1765
|
+
const opened = openDatabase(dbPath);
|
|
1766
|
+
if (opened && identityKey && currentDb) {
|
|
1767
|
+
_dbCache.set(identityKey, { dbPath, db: currentDb });
|
|
1768
|
+
_currentIdentityKey = identityKey;
|
|
1769
|
+
}
|
|
1770
|
+
return opened;
|
|
1771
|
+
} catch (e) {
|
|
1772
|
+
logWarning("db", `database refresh failed: ${(e as Error).message}`);
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1743
1777
|
/** Run a full VACUUM — call sparingly (e.g. after milestone completion). */
|
|
1744
1778
|
export function vacuumDatabase(): void {
|
|
1745
1779
|
if (!currentDb) return;
|