From 01ed3ca20d5302dcafb519a9fb50bbd1e6759bf2 Mon Sep 17 00:00:00 2001 From: atlas-bot Date: Sat, 3 Oct 2026 19:06:50 +0000 Subject: [PATCH] paperclip: keep per-run managed AI home out of ACPX session fingerprint (REN-563) Co-Authored-By: Claude Opus 5.5 --- ci/images/paperclip/README.md | 9 +++ ...en-563-acpx-managed-home-fingerprint.patch | 74 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 ci/images/paperclip/ren-563-acpx-managed-home-fingerprint.patch diff --git a/ci/images/paperclip/README.md b/ci/images/paperclip/README.md index a6bcb8c1..3131c9bf 100644 --- a/ci/images/paperclip/README.md +++ b/ci/images/paperclip/README.md @@ -29,6 +29,15 @@ every run, local and in k8s pods. The value is not part of the session fingerprint, so resume is unchanged. No upstream PR yet; rename the patch to `pr--…` once there is one. +Claude sessions never resume with a managed AI connection (REN-563): the +server gives every run a fresh temp home (`HOME`, `XDG_*`, +`CLAUDE_CONFIG_DIR`, …), and the ACPX engine hashed those paths into the +session fingerprint (`adapterEnvHash`), so a resume never matched. +`ren-563-acpx-managed-home-fingerprint.patch` hashes a fixed placeholder for +those paths when they follow the managed layout, as the server's own +fingerprint already does (`managedAiSessionFingerprintConfig`). No upstream +PR yet. + ## Files - `upstream.env` — pinned commit and the version string stamped into the diff --git a/ci/images/paperclip/ren-563-acpx-managed-home-fingerprint.patch b/ci/images/paperclip/ren-563-acpx-managed-home-fingerprint.patch new file mode 100644 index 00000000..acad1062 --- /dev/null +++ b/ci/images/paperclip/ren-563-acpx-managed-home-fingerprint.patch @@ -0,0 +1,74 @@ +diff --git a/packages/adapter-utils/src/acpx-engine/execute.test.ts b/packages/adapter-utils/src/acpx-engine/execute.test.ts +index 112e2eb..67ea043 100644 +--- a/packages/adapter-utils/src/acpx-engine/execute.test.ts ++++ b/packages/adapter-utils/src/acpx-engine/execute.test.ts +@@ -1857,6 +1857,37 @@ describe("shared ACPX engine runtime behavior", () => { + expect(changed.result.sessionParams?.configFingerprint).not.toBe(first.result.sessionParams?.configFingerprint); + }); + ++ it("keeps the per-run managed AI home out of session identity", async () => { ++ const root = await makeTempRoot(); ++ const config = { agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") }; ++ async function withManagedHome(home: string, claudeConfigDir = path.join(home, "provider")) { ++ return runExecutor({ ++ ...config, ++ managedAiConnection: { identity: "grant-1:shared:abc" }, ++ env: { ++ HOME: home, ++ XDG_CONFIG_HOME: path.join(home, "config"), ++ XDG_DATA_HOME: path.join(home, "data"), ++ CODEX_HOME: path.join(home, "provider"), ++ GROK_HOME: path.join(home, "provider"), ++ CLAUDE_CONFIG_DIR: claudeConfigDir, ++ }, ++ }, { context: { taskId: "issue-1" } }); ++ } ++ const fp = (run: Awaited>) => run.result.sessionParams?.configFingerprint; ++ const first = await withManagedHome(path.join(root, "paperclip-ai-a")); ++ const second = await withManagedHome(path.join(root, "paperclip-ai-b")); ++ const overridden = await withManagedHome(path.join(root, "paperclip-ai-c"), "/custom/claude"); ++ expect(fp(first)).toBeTypeOf("string"); ++ expect(fp(second)).toBe(fp(first)); ++ expect(fp(overridden)).not.toBe(fp(first)); ++ ++ // Without a managed AI connection the same paths are user config and still count. ++ const plainA = await runExecutor({ ...config, env: { HOME: path.join(root, "home-a") } }, { context: { taskId: "issue-1" } }); ++ const plainB = await runExecutor({ ...config, env: { HOME: path.join(root, "home-b") } }, { context: { taskId: "issue-1" } }); ++ expect(fp(plainB)).not.toBe(fp(plainA)); ++ }); ++ + it("busts the session fingerprint when a stable configured PAPERCLIP_* value rotates", async () => { + const root = await makeTempRoot(); + const stateDir = path.join(root, "state"); +diff --git a/packages/adapter-utils/src/acpx-engine/execute.ts b/packages/adapter-utils/src/acpx-engine/execute.ts +index e5681b8..b1d0207 100644 +--- a/packages/adapter-utils/src/acpx-engine/execute.ts ++++ b/packages/adapter-utils/src/acpx-engine/execute.ts +@@ -2027,6 +2027,27 @@ async function buildRuntime(input: { + // are absent from tempKeysApplied and keep their compatibility protection. + if (!scratchKeys.has(key) || value !== scratch.dir) resolvedAdapterEnv[key] = value; + } ++ // A managed AI connection gets a fresh server-made temp home on every run ++ // (HOME, XDG_*, and the provider home under it). Hash a fixed placeholder for ++ // those paths so the session can resume; the server's own fingerprint does ++ // the same (managedAiSessionFingerprintConfig). Values that differ from the ++ // managed layout are real overrides and stay in the hash. ++ const managedHome = config.managedAiConnection ? resolvedAdapterEnv.HOME : undefined; ++ if (managedHome) { ++ const managedLayout: Record = { ++ HOME: "", ++ XDG_CONFIG_HOME: "config", ++ XDG_DATA_HOME: "data", ++ CODEX_HOME: "provider", ++ GROK_HOME: "provider", ++ CLAUDE_CONFIG_DIR: "provider", ++ }; ++ for (const [key, sub] of Object.entries(managedLayout)) { ++ if (resolvedAdapterEnv[key] === (sub ? path.join(managedHome, sub) : managedHome)) { ++ resolvedAdapterEnv[key] = sub ? `/${sub}` : ""; ++ } ++ } ++ } + if (authToken) env.PAPERCLIP_API_KEY = authToken; + // For the claude agent, set model via ANTHROPIC_MODEL at startup rather than + // via session/set_config_option — the ACP server's set_config_option handler