mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
fix(runner): preserve workspace binding on recovery
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
baf49f5e97
commit
f34591016e
2 files changed
+45
-8
No files matched your search
@@ -241,6 +241,7 @@ describe("P6-25 persisted reaper-to-finalization recovery", () => {
|
||||
const projectId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
const newerExecutionWorkspaceId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
const freshIssueId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
@@ -403,8 +404,23 @@ describe("P6-25 persisted reaper-to-finalization recovery", () => {
|
||||
cwd: repoRoot,
|
||||
providerType: "local_fs",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: newerExecutionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "shared_workspace",
|
||||
strategyType: "project_primary",
|
||||
name: "Newer issue workspace",
|
||||
status: "active",
|
||||
cwd: repoRoot,
|
||||
providerType: "local_fs",
|
||||
});
|
||||
await db.update(issues).set({
|
||||
executionWorkspaceId,
|
||||
// Simulate a newer run moving the issue-level pointer before the older native run is
|
||||
// recovered. The older run must still restore its own immutable workspace binding.
|
||||
executionWorkspaceId: newerExecutionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: { mode: "shared_workspace" },
|
||||
}).where(eq(issues.id, issueId));
|
||||
@@ -523,8 +539,15 @@ describe("P6-25 persisted reaper-to-finalization recovery", () => {
|
||||
expect(new Set(effects.map((effect) => effect.decisionId))).toEqual(new Set([decisions[0]!.id]));
|
||||
expect(effects.map((effect) => effect.effectKind).sort()).toEqual(["issue_status_projection", "release_checkout"]);
|
||||
await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toEqual([
|
||||
expect.objectContaining({ status: "done", statusVersion: 1, lastStatusDecisionId: decisions[0]!.id }),
|
||||
expect.objectContaining({
|
||||
status: "done",
|
||||
statusVersion: 1,
|
||||
lastStatusDecisionId: decisions[0]!.id,
|
||||
executionWorkspaceId: newerExecutionWorkspaceId,
|
||||
}),
|
||||
]);
|
||||
await expect(db.select().from(executionWorkspaces).where(eq(executionWorkspaces.companyId, companyId)))
|
||||
.resolves.toHaveLength(2);
|
||||
await expect(db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, runId))).resolves.toEqual([
|
||||
expect.objectContaining({ phase: "committed", resultId: expect.any(String), assessmentId: expect.any(String), decisionId: decisions[0]!.id }),
|
||||
]);
|
||||
|
||||
@@ -14433,12 +14433,26 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
context.paperclipTaskMarkdownCompact = redactedWakeContext.paperclipTaskMarkdownCompact;
|
||||
}
|
||||
}
|
||||
const requestedExecutionWorkspaceId = readNonEmptyString(issueRef?.executionWorkspaceId);
|
||||
// A native run's execution input is immutable once persisted. Recovery must therefore
|
||||
// restore the workspace bound to that input rather than consulting the issue's current
|
||||
// workspace pointer: a newer run may already have moved or cleared the issue binding while
|
||||
// this older provider session is still recoverable.
|
||||
const persistedRunnerProfile = parseObject(run.runnerProfileJson);
|
||||
const persistedNativeExecutionInput =
|
||||
run.runtimeMode === "native" && persistedRunnerProfile.nativeExecutionInput !== undefined
|
||||
? parseNativeExecutionInput(persistedRunnerProfile.nativeExecutionInput)
|
||||
: null;
|
||||
const nativeRecoveryExecutionWorkspaceId =
|
||||
persistedNativeExecutionInput?.binding.executionWorkspaceId ?? null;
|
||||
const requestedExecutionWorkspaceId = nativeRecoveryExecutionWorkspaceId
|
||||
?? readNonEmptyString(issueRef?.executionWorkspaceId);
|
||||
const existingExecutionWorkspace =
|
||||
requestedExecutionWorkspaceId ? await executionWorkspacesSvc.getById(requestedExecutionWorkspaceId) : null;
|
||||
const workspaceReuseRequest = resolveExecutionWorkspaceReuseRequestForIssue({
|
||||
issueExecutionWorkspaceId: requestedExecutionWorkspaceId,
|
||||
issueExecutionWorkspacePreference: issueRef?.executionWorkspacePreference ?? null,
|
||||
issueExecutionWorkspacePreference: nativeRecoveryExecutionWorkspaceId
|
||||
? "reuse_existing"
|
||||
: issueRef?.executionWorkspacePreference ?? null,
|
||||
existingExecutionWorkspaceStatus: existingExecutionWorkspace?.status ?? null,
|
||||
});
|
||||
const requestedShouldReuseExisting = workspaceReuseRequest.requestedShouldReuseExisting;
|
||||
@@ -15187,7 +15201,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
cleanupReason: null,
|
||||
});
|
||||
}
|
||||
if (issueId && persistedExecutionWorkspace) {
|
||||
if (issueId && persistedExecutionWorkspace && !nativeRecoveryExecutionWorkspaceId) {
|
||||
const nextIssueWorkspaceMode = issueExecutionWorkspaceModeForPersistedWorkspace(persistedExecutionWorkspace.mode);
|
||||
const shouldSwitchIssueToExistingWorkspace =
|
||||
issueRef?.executionWorkspacePreference === "reuse_existing" ||
|
||||
@@ -15887,9 +15901,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
});
|
||||
nativeRunnerInstanceId = run.runnerInstanceId ?? randomUUID();
|
||||
const nativeSessionId = run.nativeSessionId ?? randomUUID();
|
||||
const persistedProfile = parseObject(run.runnerProfileJson);
|
||||
if (persistedProfile.nativeExecutionInput !== undefined) {
|
||||
nativeExecution = parseNativeExecutionInput(persistedProfile.nativeExecutionInput);
|
||||
const persistedProfile = persistedRunnerProfile;
|
||||
if (persistedNativeExecutionInput) {
|
||||
nativeExecution = persistedNativeExecutionInput;
|
||||
if (
|
||||
nativeExecution.binding.companyId !== agent.companyId
|
||||
|| nativeExecution.binding.runId !== run.id
|
||||
|
||||
Reference in new issue
Block a user