Merge current master before controller evidence review

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

* origin/master:
  fix(native): use current authority for warm plugin completion (#17444)
  fix(heartbeat): keep source-free project policy stable across turns (#17447)
This commit is contained in:
DottaandPaperclip committed 2026-10-24 12:11:18 -07:00
commit 31656ded92
4 files changed
+54 -6

No files matched your search

@@ -1,9 +1,10 @@
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { agents, agentTaskSessions, authUsers, companies, companyMemberships, createDb, heartbeatRuns, issues } from "@paperclipai/db";
import { agents, agentTaskSessions, authUsers, companies, companyMemberships, createDb, heartbeatRuns, issues, plugins, projects } from "@paperclipai/db";
import { registerServerAdapter, unregisterServerAdapter, type AdapterExecutionContext } from "../adapters/index.js";
import { heartbeatService } from "../services/heartbeat.js";
import { createPluginWorkspaceFixture } from "./helpers/plugin-workspaces.js";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
const support = await getEmbeddedPostgresTestSupport();
@@ -81,6 +82,29 @@ describePostgres("heartbeat session preparation through adapter dispatch", () =>
expect(resumed.finished.sessionIdBefore).toBe("saved-session");
});
it("keeps inherited project policy stable after binding a repository-free workspace", async () => {
const f = await seed();
const storage = await createPluginWorkspaceFixture(db, f.companyId);
const [plugin] = await db.select().from(plugins).where(eq(plugins.id, storage.pluginId));
await db.update(plugins).set({ manifestJson: { ...plugin.manifestJson, workspaceProvider: {
...plugin.manifestJson.workspaceProvider!, defaultForProjects: { mode: "isolated_workspace", strategy: "git_worktree" },
} } }).where(eq(plugins.id, storage.pluginId));
const [project] = await db.insert(projects).values({ companyId: f.companyId, name: "Directory project" }).returning();
await db.update(issues).set({ projectId: project.id }).where(eq(issues.id, f.issueId));
try {
const first = await execute(f);
const [bound] = await db.select().from(issues).where(eq(issues.id, f.issueId));
expect(bound.executionWorkspaceId).toEqual(expect.any(String));
const next = await execute(f);
expect(next.input.runtime.sessionId).toBe("saved-session");
expect(next.input.config.workspaceStrategy).toEqual(first.input.config.workspaceStrategy);
expect(next.finished.resultJson).toMatchObject({ configFreshness: {
session: { reset: false, taskSessionReused: true, changedCategories: [] },
workspace: { action: "reuse", workspaceReused: true, changedCategories: [], activeWorkspaceId: bound.executionWorkspaceId },
} });
} finally { await db.delete(plugins).where(eq(plugins.id, storage.pluginId)); }
});
it("starts fresh when effective model configuration changes and persists the reason", async () => {
const f = await seed();
await execute(f);
+1 -1
View File
@@ -2371,7 +2371,7 @@ export function heartbeatService(
!boundSourceWorkspace && !taskWorkspaceIntentRow?.intent && !issueContext?.projectWorkspaceId
? taskProjectContext : null
);
const parsedProjectExecutionWorkspacePolicy = workspacePolicyContext && !boundSourceWorkspace
const parsedProjectExecutionWorkspacePolicy = workspacePolicyContext?.hasWorkspace && !boundSourceWorkspace
? await effectiveProjectWorkspacePolicy(db, agent.companyId, workspacePolicyContext.executionWorkspacePolicy, workspacePolicyContext.hasGitSource)
: parseProjectExecutionWorkspacePolicy(workspacePolicyContext?.executionWorkspacePolicy);
const projectExecutionWorkspacePolicy =
@@ -1,5 +1,6 @@
import * as environmentTaskRuntime from "../environment-task-runtime.js";
import * as pluginTaskRuntime from "./plugin-runner-task.js";
import * as completionFeedback from "./native-completion-feedback.js";
import { environmentTaskOperationSchema } from "@paperclipai/plugin-sdk";
import { trace } from "@opentelemetry/api";
import { InMemorySpanExporter, SimpleSpanProcessor, TracerProvider } from "@opentelemetry/sdk-trace";
@@ -119,6 +120,7 @@ type BackendFactoryOptions = {
};
}) => unknown;
dynamicToolHandler?: (call: unknown) => Promise<unknown>;
completionFeedback?: (result: Parameters<typeof completionFeedback.nativeCompletionFeedback>[2]) => Promise<string>;
onSpawn?: (meta: {
pid: number;
processGroupId: number | null;
@@ -10521,15 +10523,32 @@ describe("runnerd provider runtime wiring", () => {
await writeRunnerAuthority(root, identity, "suspended");
}) };
const nextSession = { close: vi.fn(async () => undefined) };
const result = { result: { summary: "done" }, terminal: { runTerminalState: "succeeded" },
const validated = validatePrpStructuredRunResult({ schema: "paperclip.run_result.v1", reportedWorkDisposition: "done",
summary: "done", completionClaim: { contractRevision: first.completionContract.contract.revision, objectiveSatisfied: true,
criteria: [], remainingWork: [] }, evidence: [], verification: [], artifacts: [], attentionRequests: [] });
if (!validated.ok) throw new Error("Invalid completion fixture");
const result = { result: validated.result, terminal: { runTerminalState: "succeeded" },
turnId: "turn", normalizedSessionId: first.session.normalizedSessionId,
providerSessionId: "provider", driverKind: "test", driverVersion: "1",
nativeEventCount: 1, highestContiguousSourceSeq: 1, usage: null };
const feedback = vi.spyOn(completionFeedback, "nativeCompletionFeedback").mockResolvedValue("accepted");
const firstDb = leaseDb(first);
let retainedFeedback: BackendFactoryOptions["completionFeedback"];
state.execute.mockReset().mockImplementationOnce(async options => {
retainedFeedback = state.createBackend.mock.calls.at(-1)![1].completionFeedback;
await options.onSession?.(firstSession);
return result;
}).mockImplementationOnce(async options => {
order.push("execute-next");
if (reusable) {
await expect(retainedFeedback!(result.result)).resolves.toBe("accepted");
expect(feedback).toHaveBeenLastCalledWith(firstDb, next.binding.runId, result.result);
expect(state.toolAuthorityDefinitions).toHaveBeenLastCalledWith(expect.objectContaining({ runId: next.binding.runId }));
feedback.mockClear();
state.toolAuthorityDefinitions.mockRejectedValueOnce(new Error("current authority denied"));
await expect(retainedFeedback!(result.result)).rejects.toThrow("current authority denied");
expect(feedback).not.toHaveBeenCalled();
}
expect(options.existingSession).toBe(reusable ? firstSession : undefined);
await options.onSession?.(reusable ? firstSession : nextSession);
return result;
@@ -10542,7 +10561,7 @@ describe("runnerd provider runtime wiring", () => {
? priorNativeRunDb(scenario === "stale-owner" ? older : first, scenario === "active-owner" ? "running" : "succeeded").select(fields as never)
: ordinarySelect(fields as never)) as Db["select"];
try {
await executePaperclipNativeSession({ db: leaseDb(first), execution: first,
await executePaperclipNativeSession({ db: firstDb, execution: first,
runnerInstanceId: identity.runnerInstanceId, useRunnerd: true, runnerExecutionTarget: pluginTarget as never,...pluginOptions });
if (scenario === "stale-owner") await writeRunnerAuthority(root, { ...identity, runId: older.binding.runId }, "ready");
if (scenario === "missing-owner") {
@@ -10598,7 +10617,7 @@ describe("runnerd provider runtime wiring", () => {
}
} finally {
failRetirement = false;
resume?.mockRestore();owners?.mockRestore();
resume?.mockRestore();owners?.mockRestore();feedback.mockRestore();
await closeWarmNativeSessionsForEnvironment({ environmentId, reason: "test cleanup" });
}
});
@@ -11916,7 +11916,12 @@ async function createRunnerdBackendWithinSessionClaim(
// its host environment out of the plugin and records the prepared directory.
runnerInstanceId: prior?.runnerInstanceId ?? effectiveRunnerInstanceId, environment: {},
workingDirectoryAuthority: "runner_prepared_launch", dynamicTools, dynamicToolHandler: executeTool,
completionFeedback: async result => { await authorityEpoch.definitions(); return nativeCompletionFeedback(input.db, input.execution.binding.runId, result); },
completionFeedback: async result => {
const current = sessionToolAuthorityEpochs.get(sessionScopeId);
if (!current) throw new Error("native_session_tool_authority_unavailable");
await current.definitions();
return nativeCompletionFeedback(input.db, current.runId, result);
},
codexTransportFactory: recovery => createRunnerdCodexTransport({ provider: provider.kind, stateDirectory: root,
...owner.driver, runnerArtifact: description.runner,
...(provider.kind === "acpx" ? { acpxAgent: provider.agent,