gsd-pi 2.24.0 → 2.25.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 +2 -1
- package/dist/models-resolver.d.ts +0 -11
- package/dist/models-resolver.js +0 -15
- package/dist/resource-loader.d.ts +0 -1
- package/dist/resource-loader.js +0 -9
- package/dist/resources/GSD-WORKFLOW.md +12 -9
- package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
- package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
- package/dist/resources/extensions/gsd/activity-log.ts +5 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
- package/dist/resources/extensions/gsd/auto-worktree.ts +119 -1
- package/dist/resources/extensions/gsd/auto.ts +184 -36
- package/dist/resources/extensions/gsd/cache.ts +3 -1
- package/dist/resources/extensions/gsd/doctor.ts +2 -0
- package/dist/resources/extensions/gsd/git-service.ts +74 -14
- package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +34 -12
- package/dist/resources/extensions/gsd/index.ts +14 -1
- package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/dist/resources/extensions/gsd/memory-store.ts +441 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/dist/resources/extensions/gsd/worktree.ts +9 -2
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +2 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +39 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +3 -0
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +23 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +38 -1
- package/packages/pi-ai/src/providers/mistral.ts +3 -0
- package/packages/pi-ai/src/types.ts +19 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
- package/src/resources/GSD-WORKFLOW.md +12 -9
- package/src/resources/extensions/bg-shell/overlay.ts +18 -17
- package/src/resources/extensions/get-secrets-from-user.ts +5 -23
- package/src/resources/extensions/gsd/activity-log.ts +5 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +119 -1
- package/src/resources/extensions/gsd/auto.ts +184 -36
- package/src/resources/extensions/gsd/cache.ts +3 -1
- package/src/resources/extensions/gsd/doctor.ts +2 -0
- package/src/resources/extensions/gsd/git-service.ts +74 -14
- package/src/resources/extensions/gsd/gsd-db.ts +78 -1
- package/src/resources/extensions/gsd/guided-flow.ts +34 -12
- package/src/resources/extensions/gsd/index.ts +14 -1
- package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/src/resources/extensions/gsd/memory-store.ts +441 -0
- package/src/resources/extensions/gsd/migrate/command.ts +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/src/resources/extensions/gsd/triage-ui.ts +1 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/src/resources/extensions/gsd/worktree.ts +9 -2
- package/src/resources/extensions/search-the-web/native-search.ts +15 -5
|
@@ -16,9 +16,9 @@ import type {
|
|
|
16
16
|
ExtensionCommandContext,
|
|
17
17
|
} from "@gsd/pi-coding-agent";
|
|
18
18
|
|
|
19
|
-
import { deriveState
|
|
19
|
+
import { deriveState } from "./state.js";
|
|
20
20
|
import type { BudgetEnforcementMode, GSDState } from "./types.js";
|
|
21
|
-
import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides } from "./files.js";
|
|
21
|
+
import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides, parseSummary } from "./files.js";
|
|
22
22
|
import { loadPrompt } from "./prompt-loader.js";
|
|
23
23
|
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
24
24
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
@@ -94,7 +94,7 @@ import {
|
|
|
94
94
|
parseSliceBranch,
|
|
95
95
|
setActiveMilestoneId,
|
|
96
96
|
} from "./worktree.js";
|
|
97
|
-
import { GitServiceImpl } from "./git-service.js";
|
|
97
|
+
import { GitServiceImpl, type TaskCommitContext } from "./git-service.js";
|
|
98
98
|
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
|
99
99
|
import { formatGitError } from "./git-self-heal.js";
|
|
100
100
|
import {
|
|
@@ -889,23 +889,37 @@ export async function startAuto(
|
|
|
889
889
|
);
|
|
890
890
|
return;
|
|
891
891
|
}
|
|
892
|
-
// Stale lock from a dead process —
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
892
|
+
// Stale lock from a dead process — validate before synthesizing recovery context.
|
|
893
|
+
// If the recovered unit belongs to a fully-completed milestone (SUMMARY exists),
|
|
894
|
+
// discard recovery context to prevent phantom skip loops (#790).
|
|
895
|
+
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
896
|
+
const milestoneAlreadyComplete = recoveredMid
|
|
897
|
+
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
898
|
+
: false;
|
|
899
|
+
|
|
900
|
+
if (milestoneAlreadyComplete) {
|
|
900
901
|
ctx.ui.notify(
|
|
901
|
-
|
|
902
|
-
"
|
|
902
|
+
`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`,
|
|
903
|
+
"info",
|
|
903
904
|
);
|
|
904
905
|
} else {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
906
|
+
const activityDir = join(gsdRoot(base), "activity");
|
|
907
|
+
const recovery = synthesizeCrashRecovery(
|
|
908
|
+
base, crashLock.unitType, crashLock.unitId,
|
|
909
|
+
crashLock.sessionFile, activityDir,
|
|
908
910
|
);
|
|
911
|
+
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
912
|
+
pendingCrashRecovery = recovery.prompt;
|
|
913
|
+
ctx.ui.notify(
|
|
914
|
+
`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
|
|
915
|
+
"warning",
|
|
916
|
+
);
|
|
917
|
+
} else {
|
|
918
|
+
ctx.ui.notify(
|
|
919
|
+
`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
|
|
920
|
+
"warning",
|
|
921
|
+
);
|
|
922
|
+
}
|
|
909
923
|
}
|
|
910
924
|
clearLock(base);
|
|
911
925
|
}
|
|
@@ -1314,12 +1328,41 @@ export async function handleAgentEnd(
|
|
|
1314
1328
|
// Small delay to let files settle (git commits, file writes)
|
|
1315
1329
|
await new Promise(r => setTimeout(r, 500));
|
|
1316
1330
|
|
|
1317
|
-
//
|
|
1331
|
+
// Commit any dirty files the LLM left behind on the current branch.
|
|
1332
|
+
// For execute-task units, build a meaningful commit message from the
|
|
1333
|
+
// task summary (one-liner, key_files, inferred type). For other unit
|
|
1334
|
+
// types, fall back to the generic chore() message.
|
|
1318
1335
|
if (currentUnit) {
|
|
1319
1336
|
try {
|
|
1320
|
-
|
|
1337
|
+
let taskContext: TaskCommitContext | undefined;
|
|
1338
|
+
|
|
1339
|
+
if (currentUnit.type === "execute-task") {
|
|
1340
|
+
const parts = currentUnit.id.split("/");
|
|
1341
|
+
const [mid, sid, tid] = parts;
|
|
1342
|
+
if (mid && sid && tid) {
|
|
1343
|
+
const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
|
|
1344
|
+
if (summaryPath) {
|
|
1345
|
+
try {
|
|
1346
|
+
const summaryContent = await loadFile(summaryPath);
|
|
1347
|
+
if (summaryContent) {
|
|
1348
|
+
const summary = parseSummary(summaryContent);
|
|
1349
|
+
taskContext = {
|
|
1350
|
+
taskId: `${sid}/${tid}`,
|
|
1351
|
+
taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
|
|
1352
|
+
oneLiner: summary.oneLiner || undefined,
|
|
1353
|
+
keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
} catch {
|
|
1357
|
+
// Non-fatal — fall back to generic message
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const commitMsg = autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id, taskContext);
|
|
1321
1364
|
if (commitMsg) {
|
|
1322
|
-
ctx.ui.notify(`
|
|
1365
|
+
ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
|
|
1323
1366
|
}
|
|
1324
1367
|
} catch {
|
|
1325
1368
|
// Non-fatal
|
|
@@ -1372,7 +1415,8 @@ export async function handleAgentEnd(
|
|
|
1372
1415
|
}
|
|
1373
1416
|
try {
|
|
1374
1417
|
await rebuildState(basePath);
|
|
1375
|
-
|
|
1418
|
+
// State rebuild commit is bookkeeping — generic message is appropriate
|
|
1419
|
+
autoCommitCurrentBranch(basePath, "state-rebuild", currentUnit.id);
|
|
1376
1420
|
} catch {
|
|
1377
1421
|
// Non-fatal
|
|
1378
1422
|
}
|
|
@@ -1465,7 +1509,7 @@ export async function handleAgentEnd(
|
|
|
1465
1509
|
persistCompletedKey(basePath, completionKey);
|
|
1466
1510
|
completedKeySet.add(completionKey);
|
|
1467
1511
|
}
|
|
1468
|
-
|
|
1512
|
+
invalidateAllCaches();
|
|
1469
1513
|
}
|
|
1470
1514
|
} catch {
|
|
1471
1515
|
// Non-fatal — worst case we fall through to normal dispatch which has its own checks
|
|
@@ -1504,7 +1548,16 @@ export async function handleAgentEnd(
|
|
|
1504
1548
|
if (currentUnit) {
|
|
1505
1549
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1506
1550
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1507
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1551
|
+
const hookActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1552
|
+
if (hookActivityFile) {
|
|
1553
|
+
try {
|
|
1554
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1555
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1556
|
+
if (llmCallFn) {
|
|
1557
|
+
extractMemoriesFromUnit(hookActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1558
|
+
}
|
|
1559
|
+
} catch { /* non-fatal */ }
|
|
1560
|
+
}
|
|
1508
1561
|
}
|
|
1509
1562
|
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
1510
1563
|
writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
|
|
@@ -1646,7 +1699,16 @@ export async function handleAgentEnd(
|
|
|
1646
1699
|
if (currentUnit) {
|
|
1647
1700
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1648
1701
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1649
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1702
|
+
const triageActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1703
|
+
if (triageActivityFile) {
|
|
1704
|
+
try {
|
|
1705
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1706
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1707
|
+
if (llmCallFn) {
|
|
1708
|
+
extractMemoriesFromUnit(triageActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1709
|
+
}
|
|
1710
|
+
} catch { /* non-fatal */ }
|
|
1711
|
+
}
|
|
1650
1712
|
}
|
|
1651
1713
|
|
|
1652
1714
|
// Dispatch triage as a new unit (early-dispatch-and-return)
|
|
@@ -1724,7 +1786,16 @@ export async function handleAgentEnd(
|
|
|
1724
1786
|
if (currentUnit) {
|
|
1725
1787
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1726
1788
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1727
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1789
|
+
const qtActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1790
|
+
if (qtActivityFile) {
|
|
1791
|
+
try {
|
|
1792
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1793
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1794
|
+
if (llmCallFn) {
|
|
1795
|
+
extractMemoriesFromUnit(qtActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1796
|
+
}
|
|
1797
|
+
} catch { /* non-fatal */ }
|
|
1798
|
+
}
|
|
1728
1799
|
}
|
|
1729
1800
|
|
|
1730
1801
|
// Dispatch quick-task as a new unit
|
|
@@ -2418,26 +2489,61 @@ async function dispatchNextUnit(
|
|
|
2418
2489
|
const skipCount = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2419
2490
|
unitConsecutiveSkips.set(idempotencyKey, skipCount);
|
|
2420
2491
|
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
|
|
2492
|
+
// Cross-check: verify deriveState actually returns this unit (#790).
|
|
2493
|
+
// If the unit's milestone is already complete, this is a phantom skip
|
|
2494
|
+
// loop from stale crash recovery context — don't evict.
|
|
2495
|
+
const skippedMid = unitId.split("/")[0];
|
|
2496
|
+
const skippedMilestoneComplete = skippedMid
|
|
2497
|
+
? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
|
|
2498
|
+
: false;
|
|
2499
|
+
if (skippedMilestoneComplete) {
|
|
2500
|
+
// Milestone is complete — evicting this key would fight self-heal.
|
|
2501
|
+
// Clear skip counter and re-dispatch from fresh state.
|
|
2502
|
+
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2503
|
+
invalidateAllCaches();
|
|
2504
|
+
ctx.ui.notify(
|
|
2505
|
+
`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid}. Re-dispatching from fresh state.`,
|
|
2506
|
+
"info",
|
|
2507
|
+
);
|
|
2508
|
+
_skipDepth++;
|
|
2509
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2510
|
+
await dispatchNextUnit(ctx, pi);
|
|
2511
|
+
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2421
2514
|
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2422
2515
|
completedKeySet.delete(idempotencyKey);
|
|
2423
2516
|
removePersistedKey(basePath, idempotencyKey);
|
|
2424
|
-
|
|
2517
|
+
invalidateAllCaches();
|
|
2425
2518
|
ctx.ui.notify(
|
|
2426
2519
|
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2427
2520
|
"warning",
|
|
2428
2521
|
);
|
|
2522
|
+
if (!active) return;
|
|
2429
2523
|
_skipDepth++;
|
|
2430
|
-
await new Promise(r => setTimeout(r,
|
|
2524
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2431
2525
|
await dispatchNextUnit(ctx, pi);
|
|
2432
2526
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2433
2527
|
return;
|
|
2434
2528
|
}
|
|
2529
|
+
// Count toward lifetime cap so hard-stop fires during skip loops (#792)
|
|
2530
|
+
const lifeSkip = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
|
|
2531
|
+
unitLifetimeDispatches.set(idempotencyKey, lifeSkip);
|
|
2532
|
+
if (lifeSkip > MAX_LIFETIME_DISPATCHES) {
|
|
2533
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
|
|
2534
|
+
ctx.ui.notify(
|
|
2535
|
+
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip} iterations).`,
|
|
2536
|
+
"error",
|
|
2537
|
+
);
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2435
2540
|
ctx.ui.notify(
|
|
2436
2541
|
`Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
|
|
2437
2542
|
"info",
|
|
2438
2543
|
);
|
|
2544
|
+
if (!active) return;
|
|
2439
2545
|
_skipDepth++;
|
|
2440
|
-
await new Promise(r => setTimeout(r,
|
|
2546
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2441
2547
|
await dispatchNextUnit(ctx, pi);
|
|
2442
2548
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2443
2549
|
return;
|
|
@@ -2460,31 +2566,62 @@ async function dispatchNextUnit(
|
|
|
2460
2566
|
if (verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
2461
2567
|
persistCompletedKey(basePath, idempotencyKey);
|
|
2462
2568
|
completedKeySet.add(idempotencyKey);
|
|
2463
|
-
|
|
2569
|
+
invalidateAllCaches();
|
|
2464
2570
|
// Same consecutive-skip guard as the idempotency path above.
|
|
2465
2571
|
const skipCount2 = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2466
2572
|
unitConsecutiveSkips.set(idempotencyKey, skipCount2);
|
|
2467
2573
|
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
|
|
2574
|
+
// Cross-check: verify the unit's milestone is still active (#790).
|
|
2575
|
+
const skippedMid2 = unitId.split("/")[0];
|
|
2576
|
+
const skippedMilestoneComplete2 = skippedMid2
|
|
2577
|
+
? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
|
|
2578
|
+
: false;
|
|
2579
|
+
if (skippedMilestoneComplete2) {
|
|
2580
|
+
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2581
|
+
invalidateAllCaches();
|
|
2582
|
+
ctx.ui.notify(
|
|
2583
|
+
`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid2}. Re-dispatching from fresh state.`,
|
|
2584
|
+
"info",
|
|
2585
|
+
);
|
|
2586
|
+
_skipDepth++;
|
|
2587
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2588
|
+
await dispatchNextUnit(ctx, pi);
|
|
2589
|
+
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2468
2592
|
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2469
2593
|
completedKeySet.delete(idempotencyKey);
|
|
2470
2594
|
removePersistedKey(basePath, idempotencyKey);
|
|
2471
|
-
|
|
2595
|
+
invalidateAllCaches();
|
|
2472
2596
|
ctx.ui.notify(
|
|
2473
2597
|
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2474
2598
|
"warning",
|
|
2475
2599
|
);
|
|
2600
|
+
if (!active) return;
|
|
2476
2601
|
_skipDepth++;
|
|
2477
|
-
await new Promise(r => setTimeout(r,
|
|
2602
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2478
2603
|
await dispatchNextUnit(ctx, pi);
|
|
2479
2604
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2480
2605
|
return;
|
|
2481
2606
|
}
|
|
2607
|
+
// Count toward lifetime cap so hard-stop fires during skip loops (#792)
|
|
2608
|
+
const lifeSkip2 = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
|
|
2609
|
+
unitLifetimeDispatches.set(idempotencyKey, lifeSkip2);
|
|
2610
|
+
if (lifeSkip2 > MAX_LIFETIME_DISPATCHES) {
|
|
2611
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
|
|
2612
|
+
ctx.ui.notify(
|
|
2613
|
+
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip2} iterations).`,
|
|
2614
|
+
"error",
|
|
2615
|
+
);
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2482
2618
|
ctx.ui.notify(
|
|
2483
2619
|
`Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`,
|
|
2484
2620
|
"info",
|
|
2485
2621
|
);
|
|
2622
|
+
if (!active) return;
|
|
2486
2623
|
_skipDepth++;
|
|
2487
|
-
await new Promise(r => setTimeout(r,
|
|
2624
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2488
2625
|
await dispatchNextUnit(ctx, pi);
|
|
2489
2626
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2490
2627
|
return;
|
|
@@ -2554,7 +2691,7 @@ async function dispatchNextUnit(
|
|
|
2554
2691
|
persistCompletedKey(basePath, reconciledKey);
|
|
2555
2692
|
completedKeySet.add(reconciledKey);
|
|
2556
2693
|
unitDispatchCount.delete(dispatchKey);
|
|
2557
|
-
|
|
2694
|
+
invalidateAllCaches();
|
|
2558
2695
|
await new Promise(r => setImmediate(r));
|
|
2559
2696
|
await dispatchNextUnit(ctx, pi);
|
|
2560
2697
|
return;
|
|
@@ -2581,7 +2718,7 @@ async function dispatchNextUnit(
|
|
|
2581
2718
|
persistCompletedKey(basePath, dispatchKey);
|
|
2582
2719
|
completedKeySet.add(dispatchKey);
|
|
2583
2720
|
unitDispatchCount.delete(dispatchKey);
|
|
2584
|
-
|
|
2721
|
+
invalidateAllCaches();
|
|
2585
2722
|
await new Promise(r => setImmediate(r));
|
|
2586
2723
|
await dispatchNextUnit(ctx, pi);
|
|
2587
2724
|
return;
|
|
@@ -2601,7 +2738,7 @@ async function dispatchNextUnit(
|
|
|
2601
2738
|
persistCompletedKey(basePath, dispatchKey);
|
|
2602
2739
|
completedKeySet.add(dispatchKey);
|
|
2603
2740
|
unitDispatchCount.delete(dispatchKey);
|
|
2604
|
-
|
|
2741
|
+
invalidateAllCaches();
|
|
2605
2742
|
await new Promise(r => setImmediate(r));
|
|
2606
2743
|
await dispatchNextUnit(ctx, pi);
|
|
2607
2744
|
return;
|
|
@@ -2642,7 +2779,7 @@ async function dispatchNextUnit(
|
|
|
2642
2779
|
persistCompletedKey(basePath, repairedKey);
|
|
2643
2780
|
completedKeySet.add(repairedKey);
|
|
2644
2781
|
unitDispatchCount.delete(dispatchKey);
|
|
2645
|
-
|
|
2782
|
+
invalidateAllCaches();
|
|
2646
2783
|
await new Promise(r => setImmediate(r));
|
|
2647
2784
|
await dispatchNextUnit(ctx, pi);
|
|
2648
2785
|
return;
|
|
@@ -2686,7 +2823,18 @@ async function dispatchNextUnit(
|
|
|
2686
2823
|
if (currentUnit) {
|
|
2687
2824
|
const modelId = ctx.model?.id ?? "unknown";
|
|
2688
2825
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2689
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2826
|
+
const activityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2827
|
+
|
|
2828
|
+
// Fire-and-forget memory extraction from completed unit
|
|
2829
|
+
if (activityFile) {
|
|
2830
|
+
try {
|
|
2831
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
2832
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
2833
|
+
if (llmCallFn) {
|
|
2834
|
+
extractMemoriesFromUnit(activityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
2835
|
+
}
|
|
2836
|
+
} catch { /* non-fatal */ }
|
|
2837
|
+
}
|
|
2690
2838
|
|
|
2691
2839
|
// Record routing outcome for adaptive learning
|
|
2692
2840
|
if (currentUnitRouting) {
|
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
import { invalidateStateCache } from './state.js';
|
|
13
13
|
import { clearPathCache } from './paths.js';
|
|
14
14
|
import { clearParseCache } from './files.js';
|
|
15
|
+
import { clearArtifacts } from './gsd-db.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Invalidate all GSD runtime caches in one call.
|
|
18
19
|
*
|
|
19
20
|
* Call this after file writes, milestone transitions, merge reconciliation,
|
|
20
21
|
* or any operation that changes .gsd/ contents on disk. Forgetting to clear
|
|
21
|
-
* any single cache causes stale reads (see #431).
|
|
22
|
+
* any single cache causes stale reads (see #431, #793).
|
|
22
23
|
*/
|
|
23
24
|
export function invalidateAllCaches(): void {
|
|
24
25
|
invalidateStateCache();
|
|
25
26
|
clearPathCache();
|
|
26
27
|
clearParseCache();
|
|
28
|
+
clearArtifacts();
|
|
27
29
|
}
|
|
@@ -4,6 +4,7 @@ import { join, sep } from "node:path";
|
|
|
4
4
|
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
5
5
|
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
|
|
6
6
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
7
|
+
import { invalidateAllCaches } from "./cache.js";
|
|
7
8
|
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
|
8
9
|
import { listWorktrees, resolveGitDir } from "./worktree-manager.js";
|
|
9
10
|
import { abortAndReset } from "./git-self-heal.js";
|
|
@@ -200,6 +201,7 @@ async function updateStateFile(basePath: string, fixesApplied: string[]): Promis
|
|
|
200
201
|
|
|
201
202
|
/** Rebuild STATE.md from current disk state. Exported for auto-mode post-hooks. */
|
|
202
203
|
export async function rebuildState(basePath: string): Promise<void> {
|
|
204
|
+
invalidateAllCaches();
|
|
203
205
|
const state = await deriveState(basePath);
|
|
204
206
|
const path = resolveGsdRootFile(basePath, "STATE");
|
|
205
207
|
await saveFile(path, buildStateMarkdown(state));
|
|
@@ -68,6 +68,50 @@ export interface CommitOptions {
|
|
|
68
68
|
allowEmpty?: boolean;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// ─── Meaningful Commit Message Generation ───────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/** Context for generating a meaningful commit message from task execution results. */
|
|
74
|
+
export interface TaskCommitContext {
|
|
75
|
+
taskId: string;
|
|
76
|
+
taskTitle: string;
|
|
77
|
+
/** The one-liner from the task summary (e.g. "Added retry-aware worker status logging") */
|
|
78
|
+
oneLiner?: string;
|
|
79
|
+
/** Files modified by this task (from task summary frontmatter) */
|
|
80
|
+
keyFiles?: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a meaningful conventional commit message from task execution context.
|
|
85
|
+
* Format: `{type}({sliceId}/{taskId}): {description}`
|
|
86
|
+
*
|
|
87
|
+
* The description is the task summary one-liner if available (it describes
|
|
88
|
+
* what was actually built), falling back to the task title (what was planned).
|
|
89
|
+
*/
|
|
90
|
+
export function buildTaskCommitMessage(ctx: TaskCommitContext): string {
|
|
91
|
+
const scope = ctx.taskId; // e.g. "S01/T02" or just "T02"
|
|
92
|
+
const description = ctx.oneLiner || ctx.taskTitle;
|
|
93
|
+
const type = inferCommitType(ctx.taskTitle, ctx.oneLiner);
|
|
94
|
+
|
|
95
|
+
// Truncate description to ~72 chars for subject line
|
|
96
|
+
const maxDescLen = 68 - type.length - scope.length;
|
|
97
|
+
const truncated = description.length > maxDescLen
|
|
98
|
+
? description.slice(0, maxDescLen - 1).trimEnd() + "…"
|
|
99
|
+
: description;
|
|
100
|
+
|
|
101
|
+
const subject = `${type}(${scope}): ${truncated}`;
|
|
102
|
+
|
|
103
|
+
// Build body with key files if available
|
|
104
|
+
if (ctx.keyFiles && ctx.keyFiles.length > 0) {
|
|
105
|
+
const fileLines = ctx.keyFiles
|
|
106
|
+
.slice(0, 8) // cap at 8 files to keep commit concise
|
|
107
|
+
.map(f => `- ${f}`)
|
|
108
|
+
.join("\n");
|
|
109
|
+
return `${subject}\n\n${fileLines}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return subject;
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
/**
|
|
72
116
|
* Thrown when a slice merge hits code conflicts in non-.gsd files.
|
|
73
117
|
* The working tree is left in a conflicted state (no reset) so the
|
|
@@ -253,18 +297,14 @@ export function runGit(basePath: string, args: string[], options: { allowFailure
|
|
|
253
297
|
* Each entry: [keywords[], commitType]
|
|
254
298
|
*/
|
|
255
299
|
const COMMIT_TYPE_RULES: [string[], string][] = [
|
|
256
|
-
[["fix", "bug", "patch", "hotfix"], "fix"],
|
|
300
|
+
[["fix", "fixed", "fixes", "bug", "patch", "hotfix", "repair", "correct"], "fix"],
|
|
257
301
|
[["refactor", "restructure", "reorganize"], "refactor"],
|
|
258
|
-
[["doc", "docs", "documentation"], "docs"],
|
|
259
|
-
[["test", "tests", "testing"], "test"],
|
|
260
|
-
[["
|
|
302
|
+
[["doc", "docs", "documentation", "readme", "changelog"], "docs"],
|
|
303
|
+
[["test", "tests", "testing", "spec", "coverage"], "test"],
|
|
304
|
+
[["perf", "performance", "optimize", "speed", "cache"], "perf"],
|
|
305
|
+
[["chore", "cleanup", "clean up", "dependencies", "deps", "bump", "config", "ci", "archive", "remove", "delete"], "chore"],
|
|
261
306
|
];
|
|
262
307
|
|
|
263
|
-
/**
|
|
264
|
-
* Infer a conventional commit type from a slice title.
|
|
265
|
-
* Uses case-insensitive word-boundary matching against known keywords.
|
|
266
|
-
* Returns "feat" when no keywords match.
|
|
267
|
-
*/
|
|
268
308
|
// ─── GitServiceImpl ────────────────────────────────────────────────────
|
|
269
309
|
|
|
270
310
|
export class GitServiceImpl {
|
|
@@ -356,11 +396,22 @@ export class GitServiceImpl {
|
|
|
356
396
|
}
|
|
357
397
|
|
|
358
398
|
/**
|
|
359
|
-
* Auto-commit dirty working tree
|
|
399
|
+
* Auto-commit dirty working tree.
|
|
400
|
+
*
|
|
401
|
+
* When `taskContext` is provided, generates a meaningful conventional commit
|
|
402
|
+
* message from the task execution results (one-liner, title, inferred type).
|
|
403
|
+
* Falls back to a generic `chore()` message when no context is available
|
|
404
|
+
* (e.g. pre-switch commits, stop commits, state rebuild commits).
|
|
405
|
+
*
|
|
360
406
|
* Returns the commit message on success, or null if nothing to commit.
|
|
361
407
|
* @param extraExclusions Additional paths to exclude from staging (e.g. [".gsd/"] for pre-switch commits).
|
|
362
408
|
*/
|
|
363
|
-
autoCommit(
|
|
409
|
+
autoCommit(
|
|
410
|
+
unitType: string,
|
|
411
|
+
unitId: string,
|
|
412
|
+
extraExclusions: readonly string[] = [],
|
|
413
|
+
taskContext?: TaskCommitContext,
|
|
414
|
+
): string | null {
|
|
364
415
|
// Quick check: is there anything dirty at all?
|
|
365
416
|
// Native path uses libgit2 (single syscall), fallback spawns git.
|
|
366
417
|
if (!nativeHasChanges(this.basePath)) return null;
|
|
@@ -371,7 +422,9 @@ export class GitServiceImpl {
|
|
|
371
422
|
// (all changes might have been runtime files that got excluded)
|
|
372
423
|
if (!nativeHasStagedChanges(this.basePath)) return null;
|
|
373
424
|
|
|
374
|
-
const message =
|
|
425
|
+
const message = taskContext
|
|
426
|
+
? buildTaskCommitMessage(taskContext)
|
|
427
|
+
: `chore(${unitId}): auto-commit after ${unitType}`;
|
|
375
428
|
nativeCommit(this.basePath, message, { allowEmpty: false });
|
|
376
429
|
return message;
|
|
377
430
|
}
|
|
@@ -497,8 +550,15 @@ export class GitServiceImpl {
|
|
|
497
550
|
|
|
498
551
|
// ─── Commit Type Inference ─────────────────────────────────────────────────
|
|
499
552
|
|
|
500
|
-
|
|
501
|
-
|
|
553
|
+
/**
|
|
554
|
+
* Infer a conventional commit type from a title (and optional one-liner).
|
|
555
|
+
* Uses case-insensitive word-boundary matching against known keywords.
|
|
556
|
+
* Returns "feat" when no keywords match.
|
|
557
|
+
*
|
|
558
|
+
* Used for both slice squash-merge titles and task commit messages.
|
|
559
|
+
*/
|
|
560
|
+
export function inferCommitType(title: string, oneLiner?: string): string {
|
|
561
|
+
const lower = `${title} ${oneLiner || ""}`.toLowerCase();
|
|
502
562
|
|
|
503
563
|
for (const [keywords, commitType] of COMMIT_TYPE_RULES) {
|
|
504
564
|
for (const keyword of keywords) {
|
|
@@ -161,7 +161,7 @@ function openRawDb(path: string): unknown {
|
|
|
161
161
|
|
|
162
162
|
// ─── Schema ────────────────────────────────────────────────────────────────
|
|
163
163
|
|
|
164
|
-
const SCHEMA_VERSION =
|
|
164
|
+
const SCHEMA_VERSION = 3;
|
|
165
165
|
|
|
166
166
|
function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
167
167
|
// WAL mode for file-backed databases (must be outside transaction)
|
|
@@ -221,9 +221,36 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
|
221
221
|
)
|
|
222
222
|
`);
|
|
223
223
|
|
|
224
|
+
db.exec(`
|
|
225
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
226
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
227
|
+
id TEXT NOT NULL UNIQUE,
|
|
228
|
+
category TEXT NOT NULL,
|
|
229
|
+
content TEXT NOT NULL,
|
|
230
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
231
|
+
source_unit_type TEXT,
|
|
232
|
+
source_unit_id TEXT,
|
|
233
|
+
created_at TEXT NOT NULL,
|
|
234
|
+
updated_at TEXT NOT NULL,
|
|
235
|
+
superseded_by TEXT DEFAULT NULL,
|
|
236
|
+
hit_count INTEGER NOT NULL DEFAULT 0
|
|
237
|
+
)
|
|
238
|
+
`);
|
|
239
|
+
|
|
240
|
+
db.exec(`
|
|
241
|
+
CREATE TABLE IF NOT EXISTS memory_processed_units (
|
|
242
|
+
unit_key TEXT PRIMARY KEY,
|
|
243
|
+
activity_file TEXT,
|
|
244
|
+
processed_at TEXT NOT NULL
|
|
245
|
+
)
|
|
246
|
+
`);
|
|
247
|
+
|
|
248
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)');
|
|
249
|
+
|
|
224
250
|
// Views — DROP + CREATE since CREATE VIEW IF NOT EXISTS doesn't update definitions
|
|
225
251
|
db.exec(`CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`);
|
|
226
252
|
db.exec(`CREATE VIEW IF NOT EXISTS active_requirements AS SELECT * FROM requirements WHERE superseded_by IS NULL`);
|
|
253
|
+
db.exec(`CREATE VIEW IF NOT EXISTS active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL`);
|
|
227
254
|
|
|
228
255
|
// Insert schema version if not already present
|
|
229
256
|
const existing = db.prepare('SELECT count(*) as cnt FROM schema_version').get();
|
|
@@ -274,6 +301,41 @@ function migrateSchema(db: DbAdapter): void {
|
|
|
274
301
|
);
|
|
275
302
|
}
|
|
276
303
|
|
|
304
|
+
// v2 → v3: add memories + memory_processed_units tables
|
|
305
|
+
if (currentVersion < 3) {
|
|
306
|
+
db.exec(`
|
|
307
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
308
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
309
|
+
id TEXT NOT NULL UNIQUE,
|
|
310
|
+
category TEXT NOT NULL,
|
|
311
|
+
content TEXT NOT NULL,
|
|
312
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
313
|
+
source_unit_type TEXT,
|
|
314
|
+
source_unit_id TEXT,
|
|
315
|
+
created_at TEXT NOT NULL,
|
|
316
|
+
updated_at TEXT NOT NULL,
|
|
317
|
+
superseded_by TEXT DEFAULT NULL,
|
|
318
|
+
hit_count INTEGER NOT NULL DEFAULT 0
|
|
319
|
+
)
|
|
320
|
+
`);
|
|
321
|
+
|
|
322
|
+
db.exec(`
|
|
323
|
+
CREATE TABLE IF NOT EXISTS memory_processed_units (
|
|
324
|
+
unit_key TEXT PRIMARY KEY,
|
|
325
|
+
activity_file TEXT,
|
|
326
|
+
processed_at TEXT NOT NULL
|
|
327
|
+
)
|
|
328
|
+
`);
|
|
329
|
+
|
|
330
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)');
|
|
331
|
+
db.exec('DROP VIEW IF EXISTS active_memories');
|
|
332
|
+
db.exec('CREATE VIEW active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL');
|
|
333
|
+
|
|
334
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run(
|
|
335
|
+
{ ':version': 3, ':applied_at': new Date().toISOString() },
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
277
339
|
db.exec('COMMIT');
|
|
278
340
|
} catch (err) {
|
|
279
341
|
db.exec('ROLLBACK');
|
|
@@ -728,6 +790,21 @@ export function upsertRequirement(r: Requirement): void {
|
|
|
728
790
|
/**
|
|
729
791
|
* Insert or replace an artifact. Uses the `path` PK for idempotency.
|
|
730
792
|
*/
|
|
793
|
+
/**
|
|
794
|
+
* Delete all rows from the artifacts table.
|
|
795
|
+
* The artifacts table is a read cache — clearing it forces the next
|
|
796
|
+
* deriveState() to fall through to disk reads (native Rust batch parse).
|
|
797
|
+
* Safe to call when no database is open (no-op).
|
|
798
|
+
*/
|
|
799
|
+
export function clearArtifacts(): void {
|
|
800
|
+
if (!currentDb) return;
|
|
801
|
+
try {
|
|
802
|
+
currentDb.exec('DELETE FROM artifacts');
|
|
803
|
+
} catch {
|
|
804
|
+
// Clearing a cache should never be fatal
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
731
808
|
export function insertArtifact(a: {
|
|
732
809
|
path: string;
|
|
733
810
|
artifact_type: string;
|