mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
Preserve plugin session continuity through warm execution and checkpoint recovery (#17820)
This commit is contained in:
1 parent
dcfc2acfa8
commit
ea727dc3c2
23 files changed
+433
-82
No files matched your search
@@ -23,8 +23,11 @@ const requirements: RunnerProviderLaunchRequirements = {
|
||||
settings: {}, approvalPolicy: "never", readOnly: false, preparedConversation: false,
|
||||
includeCollaborationModeInstructions: false,
|
||||
};
|
||||
async function prepare(owner: ReturnType<typeof createPluginRunnerTaskOwner>) {
|
||||
await owner.driver.prepareLaunch!(requirements);
|
||||
function launchRequirements(harness: string): RunnerProviderLaunchRequirements {
|
||||
return harness === "claude" ? { ...requirements, provider: "acpx", settings: { acpxAgent: "claude" } } : requirements;
|
||||
}
|
||||
async function prepare(owner: ReturnType<typeof createPluginRunnerTaskOwner>, current = requirements) {
|
||||
await owner.driver.prepareLaunch!(current);
|
||||
}
|
||||
function provider() {
|
||||
let stopped = false;
|
||||
@@ -323,14 +326,14 @@ describe("plugin task workspace file transfer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses a warm Codex process across run authorities without idle status polling", async () => {
|
||||
it.each(["codex", "claude"])("reuses a warm %s process across run authorities without idle status polling", async harness => {
|
||||
const {task} = provider();
|
||||
ingress.connect.mockReturnValue({ready:Promise.resolve(),close:async()=>{}});
|
||||
const retained = vi.fn(async()=>{});
|
||||
const input = {task,projectIds:[],harness:"codex",projectAuthCheckVersion:1 as const,warmTaskVersion:1 as const,
|
||||
const input = {task,projectIds:[],harness,projectAuthCheckVersion:1 as const,warmTaskVersion:1 as const,
|
||||
accountIdentity:"account",credentialIdentity:"revision",pollMs:1,onSessionRetained:retained};
|
||||
const owner = createPluginRunnerTaskOwner(input);
|
||||
await prepare(owner);
|
||||
await prepare(owner, launchRequirements(harness));
|
||||
const registration = await owner.registration({} as never,"run");
|
||||
const spec = launchSpec(); spec.nativeRunner!.lifecyclePolicy = {mode:"warm",idleTimeoutMs:300000};
|
||||
const handle = owner.launcher(spec); registration.activate(); await registration.ready();
|
||||
@@ -345,7 +348,7 @@ it("reuses a warm Codex process across run authorities without idle status polli
|
||||
owner.rebind({...input,task:nextTask});
|
||||
const nextRegistration=await owner.registration({} as never,"next-run");
|
||||
const nextIdentity={...requirements.identity,runId:"next-run",turnId:"next-turn",itemId:"next-item"};
|
||||
await owner.driver.prepareLaunch!({...requirements,identity:nextIdentity});
|
||||
await owner.driver.prepareLaunch!({...launchRequirements(harness),identity:nextIdentity});
|
||||
expect(nextTask.mock.calls[0][0]).toMatchObject({kind:"prepare",priorIdentity:requirements.identity,accountIdentity:"account"});
|
||||
expect(nextTask.mock.calls.map(([op])=>op.kind)).toEqual(["prepare","check_project_auth"]);
|
||||
await registration.release(); nextRegistration.activate(); await nextRegistration.ready();
|
||||
@@ -355,13 +358,13 @@ it("reuses a warm Codex process across run authorities without idle status polli
|
||||
expect(nextTask.mock.calls.filter(([op])=>op.kind==="stop")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("adopts a verified retained Codex task without submitting a replacement", async () => {
|
||||
it.each(["codex", "claude"])("adopts a verified retained %s task without submitting a replacement", async harness => {
|
||||
const {task}=provider();
|
||||
ingress.connect.mockReturnValue({ready:Promise.resolve(),close:async()=>{}});
|
||||
const owner=createPluginRunnerTaskOwner({task,projectIds:[],harness:"codex",projectAuthCheckVersion:1,
|
||||
const owner=createPluginRunnerTaskOwner({task,projectIds:[],harness,projectAuthCheckVersion:1,
|
||||
warmTaskVersion:1,accountIdentity:"account",adoptIdentity:requirements.identity,pollMs:1});
|
||||
const recovered = {configurationId:"saved",requirementsDigest:`sha256:${"a".repeat(64)}`,provider:{}};
|
||||
await expect(owner.driver.prepareLaunch!(requirements,recovered)).resolves.toEqual(recovered);
|
||||
await expect(owner.driver.prepareLaunch!(launchRequirements(harness),recovered)).resolves.toEqual(recovered);
|
||||
expect(task.mock.calls.map(([op])=>op.kind)).toEqual(["check_project_auth"]);
|
||||
expect(task.mock.calls[0][0]).toMatchObject({configurationId:"saved",projectIds:[]});
|
||||
expect(await owner.driver.adoptExistingRunner!.isAlive()).toBe(true);
|
||||
@@ -372,28 +375,30 @@ it("adopts a verified retained Codex task without submitting a replacement", asy
|
||||
expect(task.mock.calls.some(([op])=>op.kind==="complete")).toBe(true);
|
||||
});
|
||||
|
||||
it("denies retained process adoption when current project authentication is revoked",async()=>{
|
||||
it.each(["codex", "claude"])("denies retained %s process adoption when current project authentication is revoked",async harness=>{
|
||||
const {task}=provider();
|
||||
task.mockImplementation(async op=>op.kind === "check_project_auth"
|
||||
? {kind:"project_auth",taskId:"task",configurationId:op.configurationId,provider:op.provider,allowed:false} as never
|
||||
: {kind:"accepted",taskId:"task"} as never);
|
||||
const owner=createPluginRunnerTaskOwner({task,projectIds:["project"],harness:"codex",projectAuthCheckVersion:1,
|
||||
const owner=createPluginRunnerTaskOwner({task,projectIds:["project"],harness,projectAuthCheckVersion:1,
|
||||
warmTaskVersion:1,accountIdentity:"account",adoptIdentity:requirements.identity});
|
||||
await expect(owner.driver.prepareLaunch!(requirements,{configurationId:"saved",requirementsDigest:`sha256:${"a".repeat(64)}`,provider:{}}))
|
||||
await expect(owner.driver.prepareLaunch!(launchRequirements(harness),{configurationId:"saved",requirementsDigest:`sha256:${"a".repeat(64)}`,provider:{}}))
|
||||
.rejects.toThrow("project authentication conflicts");
|
||||
expect(task.mock.calls.map(([op])=>op.kind)).toEqual(["check_project_auth"]);
|
||||
});
|
||||
|
||||
it("refuses warm account or credential generation changes", async()=>{
|
||||
it.each(["codex", "claude"])("refuses warm %s account, harness, project, or credential generation changes", async harness=>{
|
||||
const {task}=provider();
|
||||
const input={task,projectIds:[],harness:"codex",projectAuthCheckVersion:1 as const,warmTaskVersion:1 as const,
|
||||
const input={task,projectIds:[],harness,projectAuthCheckVersion:1 as const,warmTaskVersion:1 as const,
|
||||
accountIdentity:"account",credentialIdentity:"revision",pollMs:1};
|
||||
const owner=createPluginRunnerTaskOwner(input);
|
||||
await prepare(owner);
|
||||
await prepare(owner, launchRequirements(harness));
|
||||
const spec=launchSpec();spec.nativeRunner!.lifecyclePolicy={mode:"warm",idleTimeoutMs:300000};
|
||||
const process=owner.launcher(spec);
|
||||
expect(()=>owner.rebind({...input,accountIdentity:"other"})).toThrow("cannot be rebound");
|
||||
expect(()=>owner.rebind({...input,credentialIdentity:"rotated"})).toThrow("cannot be rebound");
|
||||
expect(()=>owner.rebind({...input,harness:harness === "codex" ? "claude" : "codex"})).toThrow("cannot be rebound");
|
||||
expect(()=>owner.rebind({...input,projectIds:["foreign"]})).toThrow("cannot be rebound");
|
||||
const registration=await owner.registration({} as never,"run");
|
||||
await registration.checkpoint("unsettled"); await process.completion;
|
||||
});
|
||||
Reference in new issue
Block a user