Merge the verified task-source and conversion baseline repairs

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

* codex/dependable-repository-conversion:
  test: create conversion-compatible Claude baseline settings
  fix: preserve plain task sources through shared workspace reuse
This commit is contained in:
DottaandPaperclip committed 2026-10-21 08:40:08 -05:00
commit a6eaf005a2
7 files changed
+87 -9

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,
+1 -1
View File
@@ -69,7 +69,7 @@ hard stops. The conversion journey creates a real legacy agent and selects
Paperclip Runner through the public agent update API. It checks retained
configuration, encrypted secret references, environment, instructions, and
corresponding permission policy before submitting work through the browser.
The disposable legacy profile starts without optional CLI-only extra arguments;
The disposable legacy profile starts without optional CLI-only extra arguments or turn limits;
the journey does not remove settings from an existing user agent. For either
provider, Daytona's public environment probe must save platform qualification
before the same agent selects Runner. Probe leases and uncertain probe accounting
@@ -18,6 +18,7 @@ describe("real legacy conversion prerequisites", () => {
expect(agent.adapterType).toBe(profile.provider === "codex" ? "codex_local" : "claude_local");
expect(agent.adapterConfig).toMatchObject({ model: profile.model, timeoutSec: 360, cwd: "/owned/workspace" });
expect(agent.adapterConfig).not.toHaveProperty("extraArgs");
expect(agent.adapterConfig).not.toHaveProperty("maxTurnsPerRun");
expect((agent.adapterConfig as Record<string, any>).env).toMatchObject(profile.provider === "codex"
? { OPENAI_API_KEY: { type: "secret_ref", secretId: "codex-reference", version: "latest" } }
: { ANTHROPIC_API_KEY: { type: "secret_ref", secretId: "claude-reference", version: "latest" } });
+4 -1
View File
@@ -10,7 +10,10 @@ export function repositoryProofProfile(native: RunnerProfileFixture, legacy?: Ru
return { ...native, buildAgent(input) {
const { instructionsBundle: _manual, ...agent } = (legacy ?? native).buildAgent(input);
const config = { ...(agent.adapterConfig as Record<string, unknown>), model: native.model };
if (legacy) delete (config as Record<string, unknown>).extraArgs;
if (legacy) {
delete (config as Record<string, unknown>).extraArgs;
delete (config as Record<string, unknown>).maxTurnsPerRun;
}
return { ...agent, ...(legacy ? { runner: "legacy" } : {}), budgetMonthlyCents: 1000, adapterConfig: config };
} };
}