Add minimal worktree seed mode

This commit is contained in:
Dotta
2026-03-10 07:41:01 -05:00
parent 0704854926
commit 4a67db6a4d
5 changed files with 269 additions and 41 deletions

View File

@@ -4,6 +4,7 @@ import {
buildWorktreeConfig,
buildWorktreeEnvEntries,
formatShellExports,
resolveWorktreeSeedPlan,
resolveWorktreeLocalPaths,
rewriteLocalUrlPort,
sanitizeWorktreeInstanceId,
@@ -107,4 +108,18 @@ describe("worktree helpers", () => {
expect(env.PAPERCLIP_INSTANCE_ID).toBe("feature-worktree-support");
expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'");
});
it("uses minimal seed mode to keep app state but drop heavy runtime history", () => {
const minimal = resolveWorktreeSeedPlan("minimal");
const full = resolveWorktreeSeedPlan("full");
expect(minimal.excludedTables).toContain("heartbeat_runs");
expect(minimal.excludedTables).toContain("heartbeat_run_events");
expect(minimal.excludedTables).toContain("workspace_runtime_services");
expect(minimal.excludedTables).toContain("agent_task_sessions");
expect(minimal.nullifyColumns.issues).toEqual(["checkout_run_id", "execution_run_id"]);
expect(full.excludedTables).toEqual([]);
expect(full.nullifyColumns).toEqual({});
});
});

View File

@@ -3,6 +3,30 @@ import type { PaperclipConfig } from "../config/schema.js";
import { expandHomePrefix } from "../config/home.js";
export const DEFAULT_WORKTREE_HOME = "~/.paperclip-worktrees";
export const WORKTREE_SEED_MODES = ["minimal", "full"] as const;
export type WorktreeSeedMode = (typeof WORKTREE_SEED_MODES)[number];
export type WorktreeSeedPlan = {
mode: WorktreeSeedMode;
excludedTables: string[];
nullifyColumns: Record<string, string[]>;
};
const MINIMAL_WORKTREE_EXCLUDED_TABLES = [
"activity_log",
"agent_runtime_state",
"agent_task_sessions",
"agent_wakeup_requests",
"cost_events",
"heartbeat_run_events",
"heartbeat_runs",
"workspace_runtime_services",
];
const MINIMAL_WORKTREE_NULLIFIED_COLUMNS: Record<string, string[]> = {
issues: ["checkout_run_id", "execution_run_id"],
};
export type WorktreeLocalPaths = {
cwd: string;
@@ -20,6 +44,27 @@ export type WorktreeLocalPaths = {
storageDir: string;
};
export function isWorktreeSeedMode(value: string): value is WorktreeSeedMode {
return (WORKTREE_SEED_MODES as readonly string[]).includes(value);
}
export function resolveWorktreeSeedPlan(mode: WorktreeSeedMode): WorktreeSeedPlan {
if (mode === "full") {
return {
mode,
excludedTables: [],
nullifyColumns: {},
};
}
return {
mode,
excludedTables: [...MINIMAL_WORKTREE_EXCLUDED_TABLES],
nullifyColumns: {
...MINIMAL_WORKTREE_NULLIFIED_COLUMNS,
},
};
}
function nonEmpty(value: string | null | undefined): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}

View File

@@ -22,9 +22,12 @@ import {
buildWorktreeEnvEntries,
DEFAULT_WORKTREE_HOME,
formatShellExports,
isWorktreeSeedMode,
resolveSuggestedWorktreeName,
resolveWorktreeSeedPlan,
resolveWorktreeLocalPaths,
sanitizeWorktreeInstanceId,
type WorktreeSeedMode,
type WorktreeLocalPaths,
} from "./worktree-lib.js";
@@ -38,6 +41,7 @@ type WorktreeInitOptions = {
serverPort?: number;
dbPort?: number;
seed?: boolean;
seedMode?: string;
force?: boolean;
};
@@ -178,6 +182,8 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P
password: "paperclip",
port,
persistent: true,
onLog: () => {},
onError: () => {},
});
if (!existsSync(path.resolve(dataDir, "PG_VERSION"))) {
@@ -203,7 +209,9 @@ async function seedWorktreeDatabase(input: {
targetConfig: PaperclipConfig;
targetPaths: WorktreeLocalPaths;
instanceId: string;
seedMode: WorktreeSeedMode;
}): Promise<string> {
const seedPlan = resolveWorktreeSeedPlan(input.seedMode);
const sourceEnvFile = resolvePaperclipEnvFile(input.sourceConfigPath);
const sourceEnvEntries = readPaperclipEnvEntries(sourceEnvFile);
let sourceHandle: EmbeddedPostgresHandle | null = null;
@@ -227,6 +235,8 @@ async function seedWorktreeDatabase(input: {
retentionDays: 7,
filenamePrefix: `${input.instanceId}-seed`,
includeMigrationJournal: true,
excludeTables: seedPlan.excludedTables,
nullifyColumns: seedPlan.nullifyColumns,
});
targetHandle = await ensureEmbeddedPostgres(
@@ -262,6 +272,10 @@ export async function worktreeInitCommand(opts: WorktreeInitOptions): Promise<vo
cwd,
opts.name ?? detectGitBranchName(cwd) ?? undefined,
);
const seedMode = opts.seedMode ?? "minimal";
if (!isWorktreeSeedMode(seedMode)) {
throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`);
}
const instanceId = sanitizeWorktreeInstanceId(opts.instance ?? name);
const paths = resolveWorktreeLocalPaths({
cwd,
@@ -306,7 +320,7 @@ export async function worktreeInitCommand(opts: WorktreeInitOptions): Promise<vo
);
}
const spinner = p.spinner();
spinner.start("Seeding isolated worktree database from source instance...");
spinner.start(`Seeding isolated worktree database from source instance (${seedMode})...`);
try {
seedSummary = await seedWorktreeDatabase({
sourceConfigPath,
@@ -314,8 +328,9 @@ export async function worktreeInitCommand(opts: WorktreeInitOptions): Promise<vo
targetConfig,
targetPaths: paths,
instanceId,
seedMode,
});
spinner.stop("Seeded isolated worktree database.");
spinner.stop(`Seeded isolated worktree database (${seedMode}).`);
} catch (error) {
spinner.stop(pc.red("Failed to seed worktree database."));
throw error;
@@ -328,6 +343,7 @@ export async function worktreeInitCommand(opts: WorktreeInitOptions): Promise<vo
p.log.message(pc.dim(`Instance: ${paths.instanceId}`));
p.log.message(pc.dim(`Server port: ${serverPort} | DB port: ${databasePort}`));
if (seedSummary) {
p.log.message(pc.dim(`Seed mode: ${seedMode}`));
p.log.message(pc.dim(`Seed snapshot: ${seedSummary}`));
}
p.outro(
@@ -371,6 +387,7 @@ export function registerWorktreeCommands(program: Command): void {
.option("--from-instance <id>", "Source instance id when deriving the source config", "default")
.option("--server-port <port>", "Preferred server port", (value) => Number(value))
.option("--db-port <port>", "Preferred embedded Postgres port", (value) => Number(value))
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
.option("--no-seed", "Skip database seeding from the source instance")
.option("--force", "Replace existing repo-local config and isolated instance data", false)
.action(worktreeInitCommand);