mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 22:24:16 +01:00
fix(native): use current authority for warm plugin completion (#17444)
This commit is contained in:
1 parent
988fe0180b
commit
410e5c6eed
2 files changed
+28
-4
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
import * as environmentTaskRuntime from "../environment-task-runtime.js";
|
import * as environmentTaskRuntime from "../environment-task-runtime.js";
|
||||||
import * as pluginTaskRuntime from "./plugin-runner-task.js";
|
import * as pluginTaskRuntime from "./plugin-runner-task.js";
|
||||||
|
import * as completionFeedback from "./native-completion-feedback.js";
|
||||||
import { environmentTaskOperationSchema } from "@paperclipai/plugin-sdk";
|
import { environmentTaskOperationSchema } from "@paperclipai/plugin-sdk";
|
||||||
import { trace } from "@opentelemetry/api";
|
import { trace } from "@opentelemetry/api";
|
||||||
import { InMemorySpanExporter, SimpleSpanProcessor, TracerProvider } from "@opentelemetry/sdk-trace";
|
import { InMemorySpanExporter, SimpleSpanProcessor, TracerProvider } from "@opentelemetry/sdk-trace";
|
||||||
@@ -119,6 +120,7 @@ type BackendFactoryOptions = {
|
|||||||
};
|
};
|
||||||
}) => unknown;
|
}) => unknown;
|
||||||
dynamicToolHandler?: (call: unknown) => Promise<unknown>;
|
dynamicToolHandler?: (call: unknown) => Promise<unknown>;
|
||||||
|
completionFeedback?: (result: Parameters<typeof completionFeedback.nativeCompletionFeedback>[2]) => Promise<string>;
|
||||||
onSpawn?: (meta: {
|
onSpawn?: (meta: {
|
||||||
pid: number;
|
pid: number;
|
||||||
processGroupId: number | null;
|
processGroupId: number | null;
|
||||||
@@ -10521,15 +10523,32 @@ describe("runnerd provider runtime wiring", () => {
|
|||||||
await writeRunnerAuthority(root, identity, "suspended");
|
await writeRunnerAuthority(root, identity, "suspended");
|
||||||
}) };
|
}) };
|
||||||
const nextSession = { close: vi.fn(async () => undefined) };
|
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,
|
turnId: "turn", normalizedSessionId: first.session.normalizedSessionId,
|
||||||
providerSessionId: "provider", driverKind: "test", driverVersion: "1",
|
providerSessionId: "provider", driverKind: "test", driverVersion: "1",
|
||||||
nativeEventCount: 1, highestContiguousSourceSeq: 1, usage: null };
|
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 => {
|
state.execute.mockReset().mockImplementationOnce(async options => {
|
||||||
|
retainedFeedback = state.createBackend.mock.calls.at(-1)![1].completionFeedback;
|
||||||
await options.onSession?.(firstSession);
|
await options.onSession?.(firstSession);
|
||||||
return result;
|
return result;
|
||||||
}).mockImplementationOnce(async options => {
|
}).mockImplementationOnce(async options => {
|
||||||
order.push("execute-next");
|
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);
|
expect(options.existingSession).toBe(reusable ? firstSession : undefined);
|
||||||
await options.onSession?.(reusable ? firstSession : nextSession);
|
await options.onSession?.(reusable ? firstSession : nextSession);
|
||||||
return result;
|
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)
|
? priorNativeRunDb(scenario === "stale-owner" ? older : first, scenario === "active-owner" ? "running" : "succeeded").select(fields as never)
|
||||||
: ordinarySelect(fields as never)) as Db["select"];
|
: ordinarySelect(fields as never)) as Db["select"];
|
||||||
try {
|
try {
|
||||||
await executePaperclipNativeSession({ db: leaseDb(first), execution: first,
|
await executePaperclipNativeSession({ db: firstDb, execution: first,
|
||||||
runnerInstanceId: identity.runnerInstanceId, useRunnerd: true, runnerExecutionTarget: pluginTarget as never,...pluginOptions });
|
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 === "stale-owner") await writeRunnerAuthority(root, { ...identity, runId: older.binding.runId }, "ready");
|
||||||
if (scenario === "missing-owner") {
|
if (scenario === "missing-owner") {
|
||||||
@@ -10598,7 +10617,7 @@ describe("runnerd provider runtime wiring", () => {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
failRetirement = false;
|
failRetirement = false;
|
||||||
resume?.mockRestore();owners?.mockRestore();
|
resume?.mockRestore();owners?.mockRestore();feedback.mockRestore();
|
||||||
await closeWarmNativeSessionsForEnvironment({ environmentId, reason: "test cleanup" });
|
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.
|
// its host environment out of the plugin and records the prepared directory.
|
||||||
runnerInstanceId: prior?.runnerInstanceId ?? effectiveRunnerInstanceId, environment: {},
|
runnerInstanceId: prior?.runnerInstanceId ?? effectiveRunnerInstanceId, environment: {},
|
||||||
workingDirectoryAuthority: "runner_prepared_launch", dynamicTools, dynamicToolHandler: executeTool,
|
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,
|
codexTransportFactory: recovery => createRunnerdCodexTransport({ provider: provider.kind, stateDirectory: root,
|
||||||
...owner.driver, runnerArtifact: description.runner,
|
...owner.driver, runnerArtifact: description.runner,
|
||||||
...(provider.kind === "acpx" ? { acpxAgent: provider.agent,
|
...(provider.kind === "acpx" ? { acpxAgent: provider.agent,
|
||||||
|
|||||||
Reference in new issue
Block a user