feat(KB-154): persist worktree pool across engine restarts

- Add rehydrate() method and scanIdleWorktrees() to restore idle worktrees from disk on startup
- Add cleanupOrphanedWorktrees() to remove stale worktrees when recycling is disabled
- Wire startup rehydrate/cleanup into dashboard command based on recycleWorktrees setting
- Export new functions from @kb/engine and update dashboard imports
- Add unit tests for rehydrate/scan/cleanup and integration tests for restart resilience
This commit is contained in:
Dustin Byrne
2026-03-28 02:45:47 -04:00
parent 5e3cafe05b
commit a2a12f94eb
6 changed files with 566 additions and 6 deletions

View File

@@ -2,7 +2,7 @@ import { exec } from "node:child_process";
import type { AddressInfo } from "node:net";
import { TaskStore } from "@kb/core";
import { createServer } from "@kb/dashboard";
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask, UsageLimitPauser, PRIORITY_MERGE } from "@kb/engine";
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask, UsageLimitPauser, PRIORITY_MERGE, scanIdleWorktrees, cleanupOrphanedWorktrees } from "@kb/engine";
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
function openBrowser(url: string): void {
@@ -47,6 +47,28 @@ export async function runDashboard(port: number, opts: { open?: boolean } = {})
//
const pool = new WorktreePool();
// ── Startup: rehydrate or clean up worktrees from previous runs ────
//
// When `recycleWorktrees` is true, scan the .worktrees/ directory for
// idle worktrees (not assigned to any active task) and load them into
// the pool so new tasks can reuse them instead of creating fresh ones.
//
// When `recycleWorktrees` is false, clean up orphaned worktrees left
// behind by previous engine runs to avoid disk waste.
//
if (initialSettings.recycleWorktrees) {
const idlePaths = await scanIdleWorktrees(cwd, store);
if (idlePaths.length > 0) {
pool.rehydrate(idlePaths);
console.log(`[engine] Rehydrated pool with ${idlePaths.length} idle worktree(s)`);
}
} else {
const cleaned = await cleanupOrphanedWorktrees(cwd, store);
if (cleaned > 0) {
console.log(`[engine] Cleaned up ${cleaned} orphaned worktree(s)`);
}
}
// ── Usage limit pauser ──────────────────────────────────────────────
//
// Shared pauser that triggers globalPause when any agent hits an API