Merge retained task-source reuse repair

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/plain-task-repository-admission:
  fix: preserve plain task sources through shared workspace reuse
This commit is contained in:
DottaandPaperclip committed 2026-10-21 08:38:37 -05:00
commit 8748c2b5df
4 files changed
+81 -7

No files matched your search

@@ -143,3 +143,16 @@ blocking remainingWork entries. A real-database reproduction accepts that invali
report before repair. The same gate now also checks an unfinished objective.
Current questions, approvals, pause holds, conversations, and authorized external
response waits retain their earlier checks. No accepted result is rewritten.
Review then found that shared-workspace restoration relabeled a retained task
source as project_primary before repository admission. A new regression performs
first workspace preparation and reuse against the real database; it fails at the
source transition before repair. The corrected path retains task_session for an
explicitly repository-free task directory, including its absent repository and
ref instead of inheriting organizational defaults. Configured source pointers
keep project classification. The composed regression also stages a repository
through the real service and prepares verified bytes on the reused task root,
with only remote clone transport replaced by an owned local repository. The
queued sixth campaign was cancelled during its build, before provider execution,
when this review finding made that candidate an uninformative paid retry.
@@ -22,13 +22,27 @@ describe("task workspace admission authority", () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-source-authority-"));
try {
const restored = await ensurePersistedExecutionWorkspaceAvailable({
base: { baseCwd: cwd, source: "project_primary", projectId: "org-project", workspaceId: "org-source", repoUrl: null, repoRef: null },
base: { baseCwd: cwd, source: "project_primary", projectId: "org-project", workspaceId: "org-source", repoUrl: "https://github.com/organization/source", repoRef: "organization-main" },
workspace: { mode: "shared_workspace", strategyType: "project_primary", cwd, providerRef: cwd,
projectId: null, projectWorkspaceId: null, repoUrl: null, baseRef: null, branchName: null },
projectId: null, projectWorkspaceId: null, repoUrl: null, baseRef: null, branchName: null, metadata: { source: "task_session" } },
issue: { id: "task", identifier: null, title: "Retain files" },
agent: { id: "agent", companyId: "company", name: "Agent" },
});
expect(restored).toMatchObject({ cwd, projectId: null, workspaceId: null });
expect(restored).toMatchObject({ cwd, projectId: null, workspaceId: null, repoUrl: null, repoRef: null, source: "task_session" });
} finally { await fs.rm(cwd, { recursive: true, force: true }); }
});
it.each([
{ projectId: "configured-project" }, { projectWorkspaceId: "configured-source" }, { repoUrl: "https://github.com/configured/source" },
])("retains configured-source classification despite task_session metadata: %j", async configured => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-configured-source-"));
try {
const restored = await ensurePersistedExecutionWorkspaceAvailable({
base: { baseCwd: cwd, source: "task_session", projectId: null, workspaceId: null, repoUrl: null, repoRef: null },
workspace: { mode: "shared_workspace", strategyType: "project_primary", cwd, providerRef: cwd,
projectId: null, projectWorkspaceId: null, repoUrl: null, baseRef: null, branchName: null, metadata: { source: "task_session" }, ...configured },
issue: { id: "task", identifier: null, title: "Configured source" }, agent: { id: "agent", companyId: "company", name: "Agent" },
});
expect(restored?.source).toBe("project_primary");
} finally { await fs.rm(cwd, { recursive: true, force: true }); }
});
it("permits new admission to apply explicit pending intent", () => {
@@ -488,6 +488,41 @@ describe.skipIf(!support.supported)("heartbeat workspace preparation boundary",
expect((await readIssue(issue.id)).projectId).toBe(organization.id);
});
it("retains task-directory provenance through shared workspace preparation", async () => {
const { input, issue, source } = await fixture();
await db.update(issues).set({ projectId: null, projectWorkspaceId: null,
workspaceSelection: { version: 1, source: "explicit", selection: { kind: "task_directory" } },
}).where(eq(issues.id, issue.id));
input.task.issueRef = await readIssue(issue.id);
input.task.context = { issueId: issue.id };
const first = await prepareHeartbeatWorkspace(db, input);
expect(first.executionWorkspace.source).toBe("task_session");
expect(first.persistedExecutionWorkspace).toMatchObject({ mode: "shared_workspace", strategyType: "project_primary",
sourceIssueId: issue.id, projectId: null, projectWorkspaceId: null, repoUrl: null, metadata: { source: "task_session" } });
await mkdir(join(first.executionWorkspace.cwd, ".git", "info"), { recursive: true });
await writeFile(join(first.executionWorkspace.cwd, ".git", "info", "exclude"), "/.paperclip-repositories/\n");
await writeFile(join(first.executionWorkspace.cwd, "original-context.txt"), "Preserve the original task bytes\n");
await db.update(issues).set({ executionRunId: input.run.id }).where(eq(issues.id, issue.id));
const taskRepositories = executionWorkspaceService(db);
const receipt = await taskRepositories.requestTaskRepository({ companyId: issue.companyId, issueId: issue.id,
actor: { type: "board", source: "local_implicit", userId: "local-board" },
request: { repository: { kind: "url", url: "https://github.com/public/source" }, requestKey: "retained-task-directory" } });
const materialize = managedRepositoryCheckout.materializeManagedProjectWorkspace;
vi.spyOn(managedRepositoryCheckout, "materializeManagedProjectWorkspace").mockImplementation((cwd, options) =>
materialize(cwd, { ...options, localSource: source }));
await reuse(input, first.persistedExecutionWorkspace!);
await db.update(issues).set({ executionRunId: input.run.id }).where(eq(issues.id, issue.id));
input.task.issueRef = await readIssue(issue.id);
const next = await prepareHeartbeatWorkspace(db, input);
expect(next.executionWorkspace.source).toBe("task_session");
expect(next.persistedExecutionWorkspace!.metadata?.source).toBe("task_session");
expect(next.persistedExecutionWorkspace!.id).toBe(first.persistedExecutionWorkspace!.id);
expect(await readFile(join(next.executionWorkspace.cwd, "original-context.txt"), "utf8")).toBe("Preserve the original task bytes\n");
const [repository] = await taskRepositories.listTaskRepositories(issue.companyId, next.persistedExecutionWorkspace!.id);
expect(repository).toMatchObject({ id: receipt.operationId, state: "ready" });
expect(await readFile(join(next.executionWorkspace.cwd, repository.relativePath, "README.md"), "utf8")).toBe("source content\n");
});
it("creates a real isolated worktree and reuses its recorded workspace", async () => {
const { input, source, issue } = await fixture(true);
const first = await prepareHeartbeatWorkspace(db, input);
+16 -4
View File
@@ -3770,6 +3770,17 @@ export async function realizeExecutionWorkspace(input: {
};
}
function isPersistedTaskDirectory(workspace: {
strategyType: string | null | undefined;
projectId: string | null | undefined;
projectWorkspaceId: string | null | undefined;
repoUrl: string | null | undefined;
metadata?: Record<string, unknown> | null;
}): boolean {
return workspace.strategyType === "project_primary" && workspace.metadata?.source === "task_session"
&& workspace.projectId === null && workspace.projectWorkspaceId === null && workspace.repoUrl === null;
}
export async function ensurePersistedExecutionWorkspaceAvailable(input: {
prepareRestoredLocalSource?: (sourceCwd: string) => Promise<() => Promise<void>>;
persistContentsRestoredAt?: (at: string | null) => Promise<void>;
@@ -3807,13 +3818,14 @@ export async function ensurePersistedExecutionWorkspaceAvailable(input: {
if (!cwd) return null;
const strategy = input.workspace.strategyType === "git_worktree" ? "git_worktree" : "project_primary";
const taskDirectory = isPersistedTaskDirectory(input.workspace);
const realized: RealizedExecutionWorkspace = {
baseCwd: input.base.baseCwd,
source: input.workspace.mode === "shared_workspace" ? "project_primary" : "task_session",
source: input.workspace.mode === "shared_workspace" && !taskDirectory ? "project_primary" : "task_session",
projectId: input.workspace.projectId === undefined ? input.base.projectId : input.workspace.projectId,
workspaceId: input.workspace.projectWorkspaceId === undefined ? input.base.workspaceId : input.workspace.projectWorkspaceId,
repoUrl: input.workspace.repoUrl ?? input.base.repoUrl,
repoRef: input.workspace.baseRef ?? input.base.repoRef,
repoUrl: taskDirectory ? null : input.workspace.repoUrl ?? input.base.repoUrl,
repoRef: taskDirectory ? null : input.workspace.baseRef ?? input.base.repoRef,
additionalWorkspaces: input.base.additionalWorkspaces ?? [],
strategy,
cwd,
@@ -8898,7 +8910,7 @@ export async function restartDesiredRuntimeServicesOnStartup(db: Db) {
: null,
workspace: {
baseCwd: row.cwd,
source: row.mode === "shared_workspace" ? "project_primary" : "task_session",
source: row.mode === "shared_workspace" && !isPersistedTaskDirectory(row) ? "project_primary" : "task_session",
projectId: row.projectId,
workspaceId: row.projectWorkspaceId ?? null,
repoUrl: row.repoUrl ?? null,