gsd-pi 2.80.0-dev.e146beb20 → 2.80.0-dev.e51d2c88c
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 +4 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +29 -15
- package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
- package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
- package/dist/resources/extensions/gsd/auto.js +66 -4
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
- package/dist/resources/extensions/gsd/context-budget.js +37 -2
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
- package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
- package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
- package/dist/resources/extensions/gsd/gsd-db.js +46 -13
- package/dist/resources/extensions/gsd/guided-flow.js +33 -4
- package/dist/resources/extensions/gsd/memory-store.js +69 -12
- package/dist/resources/extensions/gsd/migrate/command.js +40 -1
- package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/quick.js +34 -2
- package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
- package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
- package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -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 +18 -18
- 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 +18 -18
- 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 +3 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +22 -17
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
- package/packages/mcp-server/src/workflow-tools.ts +30 -16
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- 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 +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
- 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 +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
- package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +18 -8
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +20 -8
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +35 -20
- package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
- package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
- package/src/resources/extensions/gsd/auto.ts +78 -3
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/context-budget.ts +44 -2
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
- package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
- package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
- package/src/resources/extensions/gsd/gsd-db.ts +50 -13
- package/src/resources/extensions/gsd/guided-flow.ts +49 -4
- package/src/resources/extensions/gsd/memory-store.ts +77 -12
- package/src/resources/extensions/gsd/migrate/command.ts +47 -1
- package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/quick.ts +37 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
- package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
- package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
- package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
- /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 8F5YpnZNBaooIWGF4GBV3}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 8F5YpnZNBaooIWGF4GBV3}/_ssgManifest.js +0 -0
|
@@ -71,14 +71,14 @@ export async function recoverTimedOutUnit(
|
|
|
71
71
|
recovery: status,
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
const durableComplete = status.summaryExists && status.taskChecked && status.nextActionAdvanced;
|
|
74
|
+
const durableComplete = status.dbComplete || (status.summaryExists && status.taskChecked && status.nextActionAdvanced);
|
|
75
75
|
if (durableComplete) {
|
|
76
76
|
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
|
|
77
77
|
phase: "finalized",
|
|
78
78
|
recovery: status,
|
|
79
79
|
});
|
|
80
80
|
ctx.ui.notify(
|
|
81
|
-
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed
|
|
81
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed. Continuing auto-mode. (attempt ${attemptNumber})`,
|
|
82
82
|
"info",
|
|
83
83
|
);
|
|
84
84
|
unitRecoveryCount.delete(recoveryKey);
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
isLockProcessAlive,
|
|
60
60
|
formatCrashInfo,
|
|
61
61
|
emitCrashRecoveredUnitEnd,
|
|
62
|
+
emitOpenUnitEndForUnit,
|
|
62
63
|
} from "./crash-recovery.js";
|
|
63
64
|
import {
|
|
64
65
|
acquireSessionLock,
|
|
@@ -200,6 +201,8 @@ import {
|
|
|
200
201
|
detectWorkingTreeActivity,
|
|
201
202
|
} from "./auto-supervisor.js";
|
|
202
203
|
import { isDbAvailable, getMilestone } from "./gsd-db.js";
|
|
204
|
+
import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
|
|
205
|
+
import { writeUnitRuntimeRecord } from "./unit-runtime.js";
|
|
203
206
|
import { countPendingCaptures } from "./captures.js";
|
|
204
207
|
import { CMUX_CHANNELS, type CmuxLogLevel } from "../shared/cmux-events.js";
|
|
205
208
|
import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
|
|
@@ -242,6 +245,20 @@ import {
|
|
|
242
245
|
type WorktreeResolverDeps,
|
|
243
246
|
} from "./worktree-resolver.js";
|
|
244
247
|
import { reorderForCaching } from "./prompt-ordering.js";
|
|
248
|
+
import { initTokenCounter } from "./token-counter.js";
|
|
249
|
+
|
|
250
|
+
// Warm the tiktoken encoder at extension startup so context-budget computations
|
|
251
|
+
// can use accurate token counts via countTokensSync without paying the load
|
|
252
|
+
// cost mid-prompt-build. Fire-and-forget — failure falls back to the
|
|
253
|
+
// provider-aware char-ratio estimator already used by getCharsPerToken().
|
|
254
|
+
// Catch rejections explicitly: an unhandled rejection at module-import time
|
|
255
|
+
// can destabilize startup before the engine logger is configured.
|
|
256
|
+
void initTokenCounter().catch((err) => {
|
|
257
|
+
logWarning(
|
|
258
|
+
"engine",
|
|
259
|
+
`token counter warm-up failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
260
|
+
);
|
|
261
|
+
});
|
|
245
262
|
|
|
246
263
|
// ─── Session State ─────────────────────────────────────────────────────────
|
|
247
264
|
|
|
@@ -293,11 +310,15 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
|
293
310
|
* the DB is unavailable (e.g. fresh project before init) we skip registration
|
|
294
311
|
* silently rather than blocking session start.
|
|
295
312
|
*/
|
|
296
|
-
function registerAutoWorkerForSession(
|
|
313
|
+
function registerAutoWorkerForSession(
|
|
314
|
+
session: AutoSession,
|
|
315
|
+
projectRootOverride?: string,
|
|
316
|
+
): void {
|
|
297
317
|
if (session.workerId) return; // already registered (e.g. resume re-runs)
|
|
298
318
|
try {
|
|
299
319
|
const projectRootRealpath = normalizeRealPath(
|
|
300
|
-
|
|
320
|
+
projectRootOverride
|
|
321
|
+
?? session.scope?.workspace.projectRoot
|
|
301
322
|
?? (session.originalBasePath || session.basePath),
|
|
302
323
|
);
|
|
303
324
|
session.workerId = registerAutoWorker({ projectRootRealpath });
|
|
@@ -501,9 +522,54 @@ export {
|
|
|
501
522
|
getBudgetEnforcementAction,
|
|
502
523
|
} from "./auto-budget.js";
|
|
503
524
|
|
|
525
|
+
function closeOutSignalInterruptedUnit(currentBasePath: string): void {
|
|
526
|
+
const currentUnit = s.currentUnit;
|
|
527
|
+
if (!currentUnit) return;
|
|
528
|
+
|
|
529
|
+
const reason = "Auto-mode process received a termination signal";
|
|
530
|
+
const errorContext: ErrorContext = {
|
|
531
|
+
message: reason,
|
|
532
|
+
category: "aborted",
|
|
533
|
+
isTransient: false,
|
|
534
|
+
};
|
|
535
|
+
const basePath = s.basePath || currentBasePath;
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
emitOpenUnitEndForUnit(basePath, currentUnit.type, currentUnit.id, "cancelled", errorContext);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
logWarning("engine", `signal unit-end cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
writeUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id, currentUnit.startedAt, {
|
|
545
|
+
phase: "crashed",
|
|
546
|
+
lastProgressAt: Date.now(),
|
|
547
|
+
lastProgressKind: "signal",
|
|
548
|
+
});
|
|
549
|
+
} catch (err) {
|
|
550
|
+
logWarning("engine", `signal runtime cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
if (s.workerId) markLatestActiveForWorkerCanceled(s.workerId, "signal-exit");
|
|
555
|
+
} catch (err) {
|
|
556
|
+
logWarning("engine", `signal dispatch cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
resolveAgentEndCancelled(errorContext);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
logWarning("engine", `signal resolve cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
504
566
|
/** Wrapper: register SIGTERM handler and store reference. */
|
|
505
567
|
function registerSigtermHandler(currentBasePath: string): void {
|
|
506
|
-
s.sigtermHandler = _registerSigtermHandler(
|
|
568
|
+
s.sigtermHandler = _registerSigtermHandler(
|
|
569
|
+
currentBasePath,
|
|
570
|
+
s.sigtermHandler,
|
|
571
|
+
() => closeOutSignalInterruptedUnit(currentBasePath),
|
|
572
|
+
);
|
|
507
573
|
}
|
|
508
574
|
|
|
509
575
|
/** Wrapper: deregister SIGTERM handler and clear reference. */
|
|
@@ -1971,6 +2037,10 @@ export async function startAuto(
|
|
|
1971
2037
|
: new URL("../../../resource-loader.js", import.meta.url).href;
|
|
1972
2038
|
const { initResources } = await import(resourceLoaderPath);
|
|
1973
2039
|
initResources(agentDir);
|
|
2040
|
+
// initResources() uses synchronous fs APIs, so the prompt-template cache
|
|
2041
|
+
// can be primed immediately — no need for the legacy 1s setTimeout deferral.
|
|
2042
|
+
const { primeCache } = await import("./prompt-loader.js");
|
|
2043
|
+
primeCache();
|
|
1974
2044
|
// Open the project DB before rebuild/derive so resume uses DB-backed
|
|
1975
2045
|
// state instead of falling back to stale markdown parsing (#2940).
|
|
1976
2046
|
await openProjectDbIfPresent(s.basePath);
|
|
@@ -2057,6 +2127,11 @@ export async function startAuto(
|
|
|
2057
2127
|
buildResolver,
|
|
2058
2128
|
};
|
|
2059
2129
|
|
|
2130
|
+
// Register the worker before bootstrap enters a milestone worktree.
|
|
2131
|
+
// This ensures enterMilestone can claim a lease and seed dispatch claims
|
|
2132
|
+
// for crash-recovery fidelity (#5405).
|
|
2133
|
+
registerAutoWorkerForSession(s, base);
|
|
2134
|
+
|
|
2060
2135
|
const ready = await bootstrapAutoSession(
|
|
2061
2136
|
s,
|
|
2062
2137
|
ctx,
|
|
@@ -153,9 +153,29 @@ export async function handleAgentEnd(
|
|
|
153
153
|
if (maybeHandleEmptyIntentTurn(event, isAutoActive())) return;
|
|
154
154
|
|
|
155
155
|
if (!isAutoActive()) return;
|
|
156
|
-
if (isSessionSwitchInFlight()) return;
|
|
157
156
|
|
|
158
157
|
const lastMsg = event.messages[event.messages.length - 1];
|
|
158
|
+
if (isSessionSwitchInFlight()) {
|
|
159
|
+
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
160
|
+
const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
|
|
161
|
+
if (isUserInitiatedAbortMessage(rawErrorMsg)) {
|
|
162
|
+
resolveAgentEndCancelled({
|
|
163
|
+
message: rawErrorMsg,
|
|
164
|
+
category: "aborted",
|
|
165
|
+
isTransient: false,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} else if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
|
|
169
|
+
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
170
|
+
const hasEmptyContent = Array.isArray(content) && content.length === 0;
|
|
171
|
+
const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
|
|
172
|
+
if (!hasEmptyContent || hasErrorMessage) {
|
|
173
|
+
resolveAgentEndCancelled(_buildAbortedPauseContext(lastMsg as { errorMessage?: unknown }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
|
|
160
180
|
// Empty content with aborted stopReason is a non-fatal agent stop (the LLM
|
|
161
181
|
// chose to end without producing output). Only pause on genuine fatal aborts
|
|
@@ -6,23 +6,36 @@
|
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
8
8
|
|
|
9
|
+
async function loadContextModePreferences(baseDir: string) {
|
|
10
|
+
const [{ loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
|
|
11
|
+
import("../preferences.js"),
|
|
12
|
+
import("../workflow-logger.js"),
|
|
13
|
+
]);
|
|
14
|
+
try {
|
|
15
|
+
return loadEffectiveGSDPreferences(baseDir)?.preferences ?? null;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
logWarning("tool", `Context Mode tool could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export function registerExecTools(pi: ExtensionAPI): void {
|
|
10
23
|
pi.registerTool({
|
|
11
24
|
name: "gsd_exec",
|
|
12
25
|
label: "Exec (Sandboxed)",
|
|
13
26
|
description:
|
|
14
|
-
"Run a short script (bash/node/python) in a subprocess.
|
|
27
|
+
"Run a short script (bash/node/python) in a subprocess. Capped stdout/stderr and metadata persist to " +
|
|
15
28
|
".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
|
|
16
29
|
"this instead of reading many files or emitting large tool outputs — e.g. have the script " +
|
|
17
30
|
"count/grep/summarize and log the finding. Enabled by default; opt out via " +
|
|
18
31
|
"preferences.context_mode.enabled=false.",
|
|
19
32
|
promptSnippet:
|
|
20
|
-
"Run a bash/node/python script in a sandbox;
|
|
33
|
+
"Run a bash/node/python script in a sandbox; capped output is saved to disk and only a digest returns",
|
|
21
34
|
promptGuidelines: [
|
|
22
35
|
"Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
|
|
23
36
|
"Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
|
|
24
37
|
"The digest is the last ~300 chars of stdout — size your log output accordingly.",
|
|
25
|
-
"Need
|
|
38
|
+
"Need persisted output? Read the stdout_path returned in details (file on local disk).",
|
|
26
39
|
],
|
|
27
40
|
parameters: Type.Object({
|
|
28
41
|
runtime: Type.Union(
|
|
@@ -40,20 +53,11 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
40
53
|
),
|
|
41
54
|
}),
|
|
42
55
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
import("../preferences.js"),
|
|
46
|
-
import("../workflow-logger.js"),
|
|
47
|
-
]);
|
|
48
|
-
let prefs: ReturnType<typeof loadEffectiveGSDPreferences> | null = null;
|
|
49
|
-
try {
|
|
50
|
-
prefs = loadEffectiveGSDPreferences();
|
|
51
|
-
} catch (err) {
|
|
52
|
-
logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
-
}
|
|
56
|
+
const { executeGsdExec } = await import("../tools/exec-tool.js");
|
|
57
|
+
const baseDir = process.cwd();
|
|
54
58
|
return executeGsdExec(params as Parameters<typeof executeGsdExec>[0], {
|
|
55
|
-
baseDir
|
|
56
|
-
preferences:
|
|
59
|
+
baseDir,
|
|
60
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
57
61
|
});
|
|
58
62
|
},
|
|
59
63
|
});
|
|
@@ -67,7 +71,7 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
67
71
|
promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
|
|
68
72
|
promptGuidelines: [
|
|
69
73
|
"Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
|
|
70
|
-
"The preview shows the trailing ~300 chars of stdout; read stdout_path for
|
|
74
|
+
"The preview shows the trailing ~300 chars of stdout; read stdout_path for persisted output.",
|
|
71
75
|
],
|
|
72
76
|
parameters: Type.Object({
|
|
73
77
|
query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
|
|
@@ -81,8 +85,10 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
81
85
|
}),
|
|
82
86
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
83
87
|
const { executeExecSearch } = await import("../tools/exec-search-tool.js");
|
|
88
|
+
const baseDir = process.cwd();
|
|
84
89
|
return executeExecSearch(params as Parameters<typeof executeExecSearch>[0], {
|
|
85
|
-
baseDir
|
|
90
|
+
baseDir,
|
|
91
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
86
92
|
});
|
|
87
93
|
},
|
|
88
94
|
});
|
|
@@ -102,8 +108,10 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
102
108
|
parameters: Type.Object({}),
|
|
103
109
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
104
110
|
const { executeResume } = await import("../tools/resume-tool.js");
|
|
111
|
+
const baseDir = process.cwd();
|
|
105
112
|
return executeResume(params as Parameters<typeof executeResume>[0], {
|
|
106
|
-
baseDir
|
|
113
|
+
baseDir,
|
|
114
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
107
115
|
});
|
|
108
116
|
},
|
|
109
117
|
});
|
|
@@ -65,6 +65,26 @@ async function applyDisabledModelProviderPolicy(ctx: ExtensionContext): Promise<
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Bridge `context_management.compaction_threshold_percent` from GSD preferences
|
|
70
|
+
* into the agent's runtime compaction settings (#5475). The preference is
|
|
71
|
+
* validated to (0.5, 0.95) at load time, but defense-in-depth normalization
|
|
72
|
+
* here protects against a stale or hand-edited prefs file. Calling with
|
|
73
|
+
* `undefined` clears any prior override so a removed preference does not leak.
|
|
74
|
+
*/
|
|
75
|
+
async function applyCompactionThresholdOverride(ctx: ExtensionContext): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
78
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
79
|
+
const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
|
|
80
|
+
const value =
|
|
81
|
+
typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
|
|
82
|
+
ctx.setCompactionThresholdOverride(value);
|
|
83
|
+
} catch {
|
|
84
|
+
// Non-fatal: leave any existing override in place.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
export function resolveNotificationStoreBasePath(cwd: string = process.cwd()): string {
|
|
69
89
|
return resolveWorktreeProjectRoot(cwd);
|
|
70
90
|
}
|
|
@@ -123,6 +143,7 @@ export function registerHooks(
|
|
|
123
143
|
await resetAskUserQuestionsTurnCache();
|
|
124
144
|
await syncServiceTierStatus(ctx);
|
|
125
145
|
await applyDisabledModelProviderPolicy(ctx);
|
|
146
|
+
await applyCompactionThresholdOverride(ctx);
|
|
126
147
|
// Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
|
|
127
148
|
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
128
149
|
if (!isInAutoWorktree(process.cwd())) {
|
|
@@ -172,6 +193,7 @@ export function registerHooks(
|
|
|
172
193
|
clearDiscussionFlowState(process.cwd());
|
|
173
194
|
await syncServiceTierStatus(ctx);
|
|
174
195
|
await applyDisabledModelProviderPolicy(ctx);
|
|
196
|
+
await applyCompactionThresholdOverride(ctx);
|
|
175
197
|
// Skip MCP auto-prep when running inside an auto-worktree. The worktree
|
|
176
198
|
// already has .mcp.json from createAutoWorktree, and re-running the writer
|
|
177
199
|
// post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
* @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type TokenProvider,
|
|
13
|
+
getCharsPerToken,
|
|
14
|
+
isAccurateCountingAvailable,
|
|
15
|
+
countTokensSync,
|
|
16
|
+
} from "./token-counter.js";
|
|
12
17
|
|
|
13
18
|
// ─── Budget ratio constants ──────────────────────────────────────────────────
|
|
14
19
|
// Percentages of total context window allocated to each budget category.
|
|
@@ -32,6 +37,24 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
|
32
37
|
/** Conservative effective context for Claude Code subscription routing (#4676) */
|
|
33
38
|
const CLAUDE_CODE_EFFECTIVE_CONTEXT_WINDOW = 200_000;
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Cached empirical chars-per-token from a tiktoken probe, keyed by provider.
|
|
42
|
+
* countTokensSync's fallback path is provider-aware, so we cache per-provider
|
|
43
|
+
* to preserve that distinction once the encoder warms. The cl100k_base encoder
|
|
44
|
+
* itself gives a stable ratio for ASCII English so a single probe per provider
|
|
45
|
+
* key is sufficient. Empty map means "not yet probed" or "encoder unavailable".
|
|
46
|
+
*/
|
|
47
|
+
const _empiricalCharsPerTokenByProvider = new Map<string, number>();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Test hook — clears the empirical chars-per-token cache so test cases that
|
|
51
|
+
* assert against the static char-ratio fallback aren't polluted by a prior
|
|
52
|
+
* tiktoken-warmed run in the same process. Production code must not call this.
|
|
53
|
+
*/
|
|
54
|
+
export function _resetEmpiricalCacheForTest(): void {
|
|
55
|
+
_empiricalCharsPerTokenByProvider.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
/** Percentage of context consumed before suggesting a continue-here checkpoint */
|
|
36
59
|
const CONTINUE_THRESHOLD_PERCENT = 70;
|
|
37
60
|
|
|
@@ -101,7 +124,26 @@ export interface MinimalPreferences {
|
|
|
101
124
|
export function computeBudgets(contextWindow: number, provider?: TokenProvider): BudgetAllocation {
|
|
102
125
|
const effectiveWindow = contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW;
|
|
103
126
|
const charsPerToken = provider ? getCharsPerToken(provider) : CHARS_PER_TOKEN;
|
|
104
|
-
|
|
127
|
+
|
|
128
|
+
// Prefer the tiktoken encoder for total-char estimation when it has been
|
|
129
|
+
// warmed (initTokenCounter resolved). The cl100k_base ratio is stable for
|
|
130
|
+
// ASCII English, so probe once per provider and cache — computeBudgets is
|
|
131
|
+
// called multiple times per prompt build and the probe encode is otherwise
|
|
132
|
+
// wasted work.
|
|
133
|
+
let totalChars: number;
|
|
134
|
+
if (isAccurateCountingAvailable()) {
|
|
135
|
+
const providerKey = provider ?? "__default__";
|
|
136
|
+
let empirical = _empiricalCharsPerTokenByProvider.get(providerKey);
|
|
137
|
+
if (empirical === undefined) {
|
|
138
|
+
const probe = "the quick brown fox jumps over the lazy dog ".repeat(64);
|
|
139
|
+
const probeTokens = countTokensSync(probe, provider);
|
|
140
|
+
empirical = probeTokens > 0 ? probe.length / probeTokens : charsPerToken;
|
|
141
|
+
_empiricalCharsPerTokenByProvider.set(providerKey, empirical);
|
|
142
|
+
}
|
|
143
|
+
totalChars = effectiveWindow * empirical;
|
|
144
|
+
} else {
|
|
145
|
+
totalChars = effectiveWindow * charsPerToken;
|
|
146
|
+
}
|
|
105
147
|
|
|
106
148
|
return {
|
|
107
149
|
summaryBudgetChars: Math.floor(totalChars * SUMMARY_RATIO),
|
|
@@ -359,6 +359,47 @@ export function markCanceled(dispatchId: number, reason: string): void {
|
|
|
359
359
|
).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
|
|
364
|
+
* a worker when the process is exiting before the normal loop can settle it.
|
|
365
|
+
*/
|
|
366
|
+
export function markLatestActiveForWorkerCanceled(workerId: string, reason: string): boolean {
|
|
367
|
+
if (!isDbAvailable()) return false;
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
const db = _getAdapter()!;
|
|
370
|
+
const result = transaction(() => {
|
|
371
|
+
return db.prepare(
|
|
372
|
+
`UPDATE unit_dispatches
|
|
373
|
+
SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
|
|
374
|
+
WHERE id = (
|
|
375
|
+
SELECT id FROM unit_dispatches
|
|
376
|
+
WHERE worker_id = :worker_id
|
|
377
|
+
AND status IN ('pending','claimed','running')
|
|
378
|
+
ORDER BY id DESC
|
|
379
|
+
LIMIT 1
|
|
380
|
+
)`,
|
|
381
|
+
).run({
|
|
382
|
+
":ended_at": now,
|
|
383
|
+
":reason": reason,
|
|
384
|
+
":worker_id": workerId,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
const changes =
|
|
388
|
+
typeof (result as { changes?: unknown }).changes === "number"
|
|
389
|
+
? (result as { changes: number }).changes
|
|
390
|
+
: 0;
|
|
391
|
+
if (changes <= 0) return false;
|
|
392
|
+
insertAuditEvent({
|
|
393
|
+
eventId: randomUUID(),
|
|
394
|
+
traceId: workerId,
|
|
395
|
+
category: "orchestration",
|
|
396
|
+
type: "dispatch-canceled",
|
|
397
|
+
ts: now,
|
|
398
|
+
payload: { workerId, reason },
|
|
399
|
+
});
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
362
403
|
/**
|
|
363
404
|
* Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
|
|
364
405
|
* callers to compute attempt_n and by detect-stuck.ts (B3) to consult
|
|
@@ -57,7 +57,8 @@ export function createBaseSchemaObjects(db: DbAdapter, hooks: BaseSchemaHooks):
|
|
|
57
57
|
slice_id TEXT DEFAULT NULL,
|
|
58
58
|
task_id TEXT DEFAULT NULL,
|
|
59
59
|
full_content TEXT NOT NULL DEFAULT '',
|
|
60
|
-
imported_at TEXT NOT NULL DEFAULT ''
|
|
60
|
+
imported_at TEXT NOT NULL DEFAULT '',
|
|
61
|
+
content_hash TEXT DEFAULT NULL
|
|
61
62
|
)
|
|
62
63
|
`);
|
|
63
64
|
|
|
@@ -76,7 +77,8 @@ export function createBaseSchemaObjects(db: DbAdapter, hooks: BaseSchemaHooks):
|
|
|
76
77
|
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
77
78
|
scope TEXT NOT NULL DEFAULT 'project',
|
|
78
79
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
79
|
-
structured_fields TEXT DEFAULT NULL
|
|
80
|
+
structured_fields TEXT DEFAULT NULL,
|
|
81
|
+
last_hit_at TEXT DEFAULT NULL
|
|
80
82
|
)
|
|
81
83
|
`);
|
|
82
84
|
|
|
@@ -416,6 +416,14 @@ export function applyMigrationV26MilestoneCommitAttributions(db: DbAdapter): voi
|
|
|
416
416
|
db.exec("CREATE INDEX IF NOT EXISTS idx_milestone_commit_attr_milestone ON milestone_commit_attributions(milestone_id)");
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
export function applyMigrationV27ArtifactHash(db: DbAdapter): void {
|
|
420
|
+
ensureColumn(db, "artifacts", "content_hash", "ALTER TABLE artifacts ADD COLUMN content_hash TEXT DEFAULT NULL");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function applyMigrationV28MemoryLastHitAt(db: DbAdapter): void {
|
|
424
|
+
ensureColumn(db, "memories", "last_hit_at", "ALTER TABLE memories ADD COLUMN last_hit_at TEXT DEFAULT NULL");
|
|
425
|
+
}
|
|
426
|
+
|
|
419
427
|
export interface MigrationV22Hooks {
|
|
420
428
|
copyQualityGateRowsToRepairedTable(db: DbAdapter): void;
|
|
421
429
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// excluded from this invariant.
|
|
24
24
|
|
|
25
25
|
import { createRequire } from "node:module";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
26
27
|
import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
27
28
|
import { dirname } from "node:path";
|
|
28
29
|
import type { Decision, Requirement, GateRow, GateId, GateScope, GateStatus, GateVerdict } from "./types.js";
|
|
@@ -78,6 +79,8 @@ import {
|
|
|
78
79
|
applyMigrationV22QualityGateRepair,
|
|
79
80
|
applyMigrationV23MilestoneQueue,
|
|
80
81
|
applyMigrationV26MilestoneCommitAttributions,
|
|
82
|
+
applyMigrationV27ArtifactHash,
|
|
83
|
+
applyMigrationV28MemoryLastHitAt,
|
|
81
84
|
} from "./db-migration-steps.js";
|
|
82
85
|
import { isMemoriesFtsAvailableSchema, tryCreateMemoriesFtsSchema } from "./db-memory-fts-schema.js";
|
|
83
86
|
import { createDbOpenState, type DbOpenPhase } from "./db-open-state.js";
|
|
@@ -106,7 +109,7 @@ const providerLoader = createSqliteProviderLoader({
|
|
|
106
109
|
writeStderr: (message: string) => process.stderr.write(message),
|
|
107
110
|
});
|
|
108
111
|
|
|
109
|
-
export const SCHEMA_VERSION =
|
|
112
|
+
export const SCHEMA_VERSION = 28;
|
|
110
113
|
|
|
111
114
|
function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
112
115
|
if (fileBacked) db.exec("PRAGMA journal_mode=WAL");
|
|
@@ -335,6 +338,16 @@ function migrateSchema(db: DbAdapter): void {
|
|
|
335
338
|
recordSchemaVersion(db, 26);
|
|
336
339
|
}
|
|
337
340
|
|
|
341
|
+
if (currentVersion < 27) {
|
|
342
|
+
applyMigrationV27ArtifactHash(db);
|
|
343
|
+
recordSchemaVersion(db, 27);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (currentVersion < 28) {
|
|
347
|
+
applyMigrationV28MemoryLastHitAt(db);
|
|
348
|
+
recordSchemaVersion(db, 28);
|
|
349
|
+
}
|
|
350
|
+
|
|
338
351
|
db.exec("COMMIT");
|
|
339
352
|
} catch (err) {
|
|
340
353
|
db.exec("ROLLBACK");
|
|
@@ -913,9 +926,10 @@ export function insertArtifact(a: {
|
|
|
913
926
|
full_content: string;
|
|
914
927
|
}): void {
|
|
915
928
|
if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
929
|
+
const contentHash = createHash("sha256").update(a.full_content).digest("hex");
|
|
916
930
|
currentDb.prepare(
|
|
917
|
-
`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
|
|
918
|
-
VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`,
|
|
931
|
+
`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash)
|
|
932
|
+
VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at, :content_hash)`,
|
|
919
933
|
).run({
|
|
920
934
|
":path": a.path,
|
|
921
935
|
":artifact_type": a.artifact_type,
|
|
@@ -924,6 +938,7 @@ export function insertArtifact(a: {
|
|
|
924
938
|
":task_id": a.task_id,
|
|
925
939
|
":full_content": a.full_content,
|
|
926
940
|
":imported_at": new Date().toISOString(),
|
|
941
|
+
":content_hash": contentHash,
|
|
927
942
|
});
|
|
928
943
|
}
|
|
929
944
|
|
|
@@ -1795,6 +1810,13 @@ export function reconcileWorktreeDb(
|
|
|
1795
1810
|
const hasEscalationAwaiting = wtTaskInfo.some((col) => col["name"] === "escalation_awaiting_review");
|
|
1796
1811
|
const hasEscalationArtifact = wtTaskInfo.some((col) => col["name"] === "escalation_artifact_path");
|
|
1797
1812
|
const hasEscalationOverride = wtTaskInfo.some((col) => col["name"] === "escalation_override_applied_at");
|
|
1813
|
+
const wtArtifactInfo = adapter.prepare("PRAGMA wt.table_info('artifacts')").all();
|
|
1814
|
+
const hasArtifactContentHash = wtArtifactInfo.some((col) => col["name"] === "content_hash");
|
|
1815
|
+
const wtMemoryInfo = adapter.prepare("PRAGMA wt.table_info('memories')").all();
|
|
1816
|
+
const hasMemoryScope = wtMemoryInfo.some((col) => col["name"] === "scope");
|
|
1817
|
+
const hasMemoryTags = wtMemoryInfo.some((col) => col["name"] === "tags");
|
|
1818
|
+
const hasMemoryStructuredFields = wtMemoryInfo.some((col) => col["name"] === "structured_fields");
|
|
1819
|
+
const hasMemoryLastHitAt = wtMemoryInfo.some((col) => col["name"] === "last_hit_at");
|
|
1798
1820
|
|
|
1799
1821
|
const decConf = adapter.prepare(
|
|
1800
1822
|
`SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR ${
|
|
@@ -1842,12 +1864,17 @@ export function reconcileWorktreeDb(
|
|
|
1842
1864
|
FROM wt.requirements
|
|
1843
1865
|
`).run());
|
|
1844
1866
|
|
|
1867
|
+
// V27: preserve content_hash. If the worktree predates V27 (no column),
|
|
1868
|
+
// fall back to the main DB's existing hash so reconcile doesn't null
|
|
1869
|
+
// out integrity fingerprints on artifacts that were unchanged in wt.
|
|
1845
1870
|
merged.artifacts = countChanges(adapter.prepare(`
|
|
1846
1871
|
INSERT OR REPLACE INTO artifacts (
|
|
1847
|
-
path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
|
|
1872
|
+
path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
|
|
1848
1873
|
)
|
|
1849
|
-
SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
|
|
1850
|
-
|
|
1874
|
+
SELECT w.path, w.artifact_type, w.milestone_id, w.slice_id, w.task_id, w.full_content, w.imported_at,
|
|
1875
|
+
${hasArtifactContentHash ? "w.content_hash" : "m.content_hash"}
|
|
1876
|
+
FROM wt.artifacts w
|
|
1877
|
+
LEFT JOIN artifacts m ON m.path = w.path
|
|
1851
1878
|
`).run());
|
|
1852
1879
|
|
|
1853
1880
|
// Merge milestones — worktree may have updated status/planning fields.
|
|
@@ -1949,15 +1976,25 @@ export function reconcileWorktreeDb(
|
|
|
1949
1976
|
LEFT JOIN tasks m ON m.milestone_id = w.milestone_id AND m.slice_id = w.slice_id AND m.id = w.id
|
|
1950
1977
|
`).run());
|
|
1951
1978
|
|
|
1952
|
-
// Merge memories — keep worktree-learned insights
|
|
1979
|
+
// Merge memories — keep worktree-learned insights.
|
|
1980
|
+
// V18 (scope, tags), V21 (structured_fields), V28 (last_hit_at): for each
|
|
1981
|
+
// column the wt may not yet have (older worktree DB), fall back to the
|
|
1982
|
+
// main DB's existing value via LEFT JOIN so reconcile never silently
|
|
1983
|
+
// resets these fields to defaults on rows that already had them.
|
|
1953
1984
|
merged.memories = countChanges(adapter.prepare(`
|
|
1954
1985
|
INSERT OR REPLACE INTO memories (
|
|
1955
1986
|
seq, id, category, content, confidence, source_unit_type, source_unit_id,
|
|
1956
|
-
created_at, updated_at, superseded_by, hit_count
|
|
1987
|
+
created_at, updated_at, superseded_by, hit_count,
|
|
1988
|
+
scope, tags, structured_fields, last_hit_at
|
|
1957
1989
|
)
|
|
1958
|
-
SELECT seq, id, category, content, confidence, source_unit_type, source_unit_id,
|
|
1959
|
-
created_at, updated_at, superseded_by, hit_count
|
|
1960
|
-
|
|
1990
|
+
SELECT w.seq, w.id, w.category, w.content, w.confidence, w.source_unit_type, w.source_unit_id,
|
|
1991
|
+
w.created_at, w.updated_at, w.superseded_by, w.hit_count,
|
|
1992
|
+
${hasMemoryScope ? "w.scope" : "COALESCE(m.scope, 'project')"},
|
|
1993
|
+
${hasMemoryTags ? "w.tags" : "COALESCE(m.tags, '[]')"},
|
|
1994
|
+
${hasMemoryStructuredFields ? "w.structured_fields" : "m.structured_fields"},
|
|
1995
|
+
${hasMemoryLastHitAt ? "w.last_hit_at" : "m.last_hit_at"}
|
|
1996
|
+
FROM wt.memories w
|
|
1997
|
+
LEFT JOIN memories m ON m.id = w.id
|
|
1961
1998
|
`).run());
|
|
1962
1999
|
|
|
1963
2000
|
// Merge verification evidence — append-only, use INSERT OR IGNORE to avoid duplicates
|
|
@@ -3062,8 +3099,8 @@ export function updateMemoryContentRow(
|
|
|
3062
3099
|
export function incrementMemoryHitCount(id: string, updatedAt: string): void {
|
|
3063
3100
|
if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
3064
3101
|
currentDb.prepare(
|
|
3065
|
-
"UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id",
|
|
3066
|
-
).run({ ":updated_at": updatedAt, ":id": id });
|
|
3102
|
+
"UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at, last_hit_at = :last_hit_at WHERE id = :id",
|
|
3103
|
+
).run({ ":updated_at": updatedAt, ":last_hit_at": updatedAt, ":id": id });
|
|
3067
3104
|
}
|
|
3068
3105
|
|
|
3069
3106
|
export function supersedeMemoryRow(oldId: string, newId: string, updatedAt: string): void {
|