mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 10:13:11 +02:00
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:
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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user