mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
fix: retain warm sandbox bindings independently of worktree settings
Validate live engine coverage and physical sandbox reuse in deployed acceptance. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a5d7df55f6
commit
e8a6fb02bf
7 files changed
+116
-8
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { retainUnsavedWorkFolderLease, workFolderSandboxKey } from "./work-folder-retention.js";
|
||||
import { prepareSandboxWorkFolders } from "./sandbox-work-folders.js";
|
||||
import { bindWarmSandboxWorkspace } from "./sandbox-workspace-binding.js";
|
||||
import path from "node:path";
|
||||
import { execFile as execFileCallback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
@@ -19362,7 +19363,13 @@ export function heartbeatService(
|
||||
};
|
||||
}
|
||||
if (Object.keys(nextIssuePatch).length > 0) {
|
||||
await issuesSvc.update(issueId, nextIssuePatch);
|
||||
if (warmReusableExecutionWorkspace && !isolatedWorkspacesEnabled) {
|
||||
await bindWarmSandboxWorkspace(db, {
|
||||
companyId: agent.companyId, issueId, runId: run.id, agentId: agent.id, workspaceId: workspace.id,
|
||||
});
|
||||
} else {
|
||||
await issuesSvc.update(issueId, nextIssuePatch);
|
||||
}
|
||||
issueExecutionWorkspaceIdForRun = workspace.id;
|
||||
issueProjectWorkspaceIdForRun =
|
||||
resolvedProjectWorkspaceId ?? issueProjectWorkspaceIdForRun;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { executionWorkspaces, heartbeatRuns, issues, type Db } from "@paperclipai/db";
|
||||
import { logActivity, publishActivity, type ActivityPublication } from "./activity-log.js";
|
||||
|
||||
/** Host runtime state must survive even when user-configurable worktrees are disabled. */
|
||||
export async function bindWarmSandboxWorkspace(db: Db, input: {
|
||||
companyId: string; issueId: string; runId: string; agentId: string; workspaceId: string;
|
||||
}) {
|
||||
const publications: ActivityPublication[] = [];
|
||||
await db.transaction(async (tx) => {
|
||||
const [issue] = await tx.select().from(issues).where(and(
|
||||
eq(issues.id, input.issueId), eq(issues.companyId, input.companyId),
|
||||
eq(issues.executionRunId, input.runId),
|
||||
)).for("update");
|
||||
const [workspace] = await tx.select().from(executionWorkspaces).where(and(
|
||||
eq(executionWorkspaces.id, input.workspaceId), eq(executionWorkspaces.companyId, input.companyId),
|
||||
));
|
||||
const [run] = await tx.select().from(heartbeatRuns).where(and(
|
||||
eq(heartbeatRuns.id, input.runId), eq(heartbeatRuns.companyId, input.companyId),
|
||||
eq(heartbeatRuns.agentId, input.agentId), eq(heartbeatRuns.status, "running"),
|
||||
));
|
||||
if (!issue || !run || !workspace || workspace.projectId !== issue.projectId || workspace.status !== "active"
|
||||
|| (workspace.sourceIssueId !== null && workspace.sourceIssueId !== issue.id)) {
|
||||
throw new Error("Warm sandbox workspace no longer belongs to this active task run");
|
||||
}
|
||||
await tx.update(issues).set({
|
||||
executionWorkspaceId: workspace.id, executionWorkspacePreference: "reuse_existing",
|
||||
...(workspace.projectWorkspaceId ? { projectWorkspaceId: workspace.projectWorkspaceId } : {}),
|
||||
updatedAt: new Date(),
|
||||
}).where(eq(issues.id, issue.id));
|
||||
await logActivity(tx as unknown as Db, {
|
||||
companyId: input.companyId, actorType: "agent", actorId: input.agentId, agentId: input.agentId,
|
||||
runId: input.runId, issueId: issue.id, action: "execution_workspace.sandbox_bound",
|
||||
entityType: "execution_workspace", entityId: workspace.id,
|
||||
details: { issueId: issue.id, reason: "warm_sandbox_reuse" },
|
||||
}, publications);
|
||||
});
|
||||
for (const publication of publications) publishActivity(publication);
|
||||
}
|
||||
Reference in new issue
Block a user