From 5a77a30c186b64b7bd463a7609934102901047fe Mon Sep 17 00:00:00 2001 From: atlas-bot Date: Sat, 3 Oct 2026 16:46:18 +0000 Subject: [PATCH] feat(paperclip): keep Claude auto memory in AGENT_HOME (REN-541) Claude auto memory defaults to $CLAUDE_CONFIG_DIR/projects/

/memory. Paperclip makes CLAUDE_CONFIG_DIR fresh per run and deletes it, so every memory is lost. New image patch: the ACPX engine writes autoMemoryDirectory=/memory into the per-run .claude/settings.local.json. AGENT_HOME is restored and saved around each run, local and in k8s pods. Not part of the session fingerprint, so resume is unchanged. Co-Authored-By: Claude Opus 5.5 --- ci/images/paperclip/README.md | 13 +- .../ren-541-claude-auto-memory.patch | 118 ++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 ci/images/paperclip/ren-541-claude-auto-memory.patch diff --git a/ci/images/paperclip/README.md b/ci/images/paperclip/README.md index 355f2b31..a6bcb8c1 100644 --- a/ci/images/paperclip/README.md +++ b/ci/images/paperclip/README.md @@ -20,6 +20,15 @@ paperclipai/paperclip#14769, also open: the plugin sets `remoteCwd: /workspace` when it acquires or resumes a lease. `pr-14769-probe-remote-cwd.patch` is that PR's diff (head `10dc7371`). +Claude auto memory is lost after every run (REN-541): it lives under +`CLAUDE_CONFIG_DIR`, which Paperclip makes fresh per run and deletes. +`ren-541-claude-auto-memory.patch` makes the ACPX engine write +`autoMemoryDirectory: /memory` into the per-run +`.claude/settings.local.json`. `AGENT_HOME` is restored before and saved after +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. + ## Files - `upstream.env` — pinned commit and the version string stamped into the @@ -39,10 +48,10 @@ Set `PAPERCLIP_COMMIT` to the new master commit and `PAPERCLIP_VERSION` to `git describe --tags --match 'v*' --long `, then run `sh prepare.sh /tmp/src`: it fails if a patch no longer applies. -## Exit: when #13733 and #14769 are merged upstream +## Exit: when every patch here is merged upstream Renovate cannot watch an upstream PR, so this is a manual step. Once -upstream nightly contains both fixes: +upstream nightly contains all of them: 1. `kubernetes/infrastructure/image-automation/paperclip.yaml`: back to `ghcr.io/paperclipai/paperclip` with `filterTags.pattern: '^nightly$'` diff --git a/ci/images/paperclip/ren-541-claude-auto-memory.patch b/ci/images/paperclip/ren-541-claude-auto-memory.patch new file mode 100644 index 00000000..2f35f340 --- /dev/null +++ b/ci/images/paperclip/ren-541-claude-auto-memory.patch @@ -0,0 +1,118 @@ +diff --git a/packages/adapter-utils/src/acpx-engine/execute.test.ts b/packages/adapter-utils/src/acpx-engine/execute.test.ts +index a26f980..112e2eb 100644 +--- a/packages/adapter-utils/src/acpx-engine/execute.test.ts ++++ b/packages/adapter-utils/src/acpx-engine/execute.test.ts +@@ -2544,6 +2544,37 @@ describe("shared ACPX engine runtime behavior", () => { + expect(note).toBeTruthy(); + }); + ++ it("points Claude auto memory at the run's AGENT_HOME without changing the session fingerprint", async () => { ++ const root = await makeTempRoot(); ++ const stateDir = path.join(root, "state"); ++ const cwd = path.join(root, "worktree"); ++ await fs.mkdir(cwd, { recursive: true }); ++ const stableHome = path.join(root, "agent-home"); ++ const runAgentHome = (runId: string) => path.join(root, "file-sync", "runs", runId, "live"); ++ const readSettings = async () => ++ JSON.parse(await fs.readFile(path.join(cwd, ".claude", "settings.local.json"), "utf8")) as { ++ autoMemoryDirectory?: string; ++ permissions?: { additionalDirectories?: string[] }; ++ }; ++ ++ const first = await runExecutor( ++ { agent: "claude", stateDir, cwd }, ++ { context: { paperclipWorkspace: { cwd, agentHome: runAgentHome("a"), agentHomeForPermissions: stableHome } } }, ++ ); ++ expect((await readSettings()).autoMemoryDirectory).toBe(path.join(runAgentHome("a"), "memory")); ++ ++ const second = await runExecutor( ++ { agent: "claude", stateDir, cwd }, ++ { context: { paperclipWorkspace: { cwd, agentHome: runAgentHome("b"), agentHomeForPermissions: stableHome } } }, ++ ); ++ const written = await readSettings(); ++ expect(written.autoMemoryDirectory).toBe(path.join(runAgentHome("b"), "memory")); ++ expect(written.permissions?.additionalDirectories).toContain(stableHome); ++ expect(written.permissions?.additionalDirectories).not.toContain(runAgentHome("b")); ++ expect(second.result.sessionParams?.configFingerprint).toBeTypeOf("string"); ++ expect(second.result.sessionParams?.configFingerprint).toBe(first.result.sessionParams?.configFingerprint); ++ }); ++ + it("merges Paperclip allowlist into an existing .claude/settings.local.json without losing user entries", 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 b1dc569..e5681b8 100644 +--- a/packages/adapter-utils/src/acpx-engine/execute.ts ++++ b/packages/adapter-utils/src/acpx-engine/execute.ts +@@ -1558,6 +1558,7 @@ interface PaperclipClaudeSettingsResult { + additionalDirectories: string[]; + defaultMode: string; + overrodeDontAsk: boolean; ++ autoMemoryDirectory: string | null; + } + + function uniqueSorted(values: Array): string[] { +@@ -1571,10 +1572,19 @@ function uniqueSorted(values: Array): string[] { + // denies every non-allowlisted tool and never reaches `canUseTool`), and we + // widen the SDK's Read sandbox to include the Paperclip state dirs the agent + // needs to talk to its own control plane. ++// ++// It also points Claude's auto memory at `/memory`. The default ++// location lives under CLAUDE_CONFIG_DIR, which is a per-run temp home, so ++// memories were lost after every run. AGENT_HOME is restored before and saved ++// after each run, locally and in remote sandboxes. Claude ignores ++// `autoMemoryDirectory` only in checked-in project settings, not local ones. + async function writePaperclipClaudeSettings(input: { + cwd: string; + stateDir: string; + agentHome: string; ++ // The run's actual AGENT_HOME (the in-sandbox path for remote runs). It may ++ // differ from `agentHome`, which stays stable for session identity. ++ runAgentHome: string; + companyId: string; + }): Promise { + const filePath = path.join(input.cwd, ".claude", "settings.local.json"); +@@ -1630,7 +1640,14 @@ async function writePaperclipClaudeSettings(input: { + additionalDirectories: mergedAdditionalDirectories, + defaultMode, + }; +- const next: Record = { ...existing, permissions: nextPermissions }; ++ const autoMemoryDirectory = input.runAgentHome ++ ? path.posix.join(input.runAgentHome, "memory") ++ : null; ++ const next: Record = { ++ ...existing, ++ permissions: nextPermissions, ++ ...(autoMemoryDirectory ? { autoMemoryDirectory } : {}), ++ }; + await writeFileAtomically({ + target: filePath, + contents: `${JSON.stringify(next, null, 2)}\n`, +@@ -1642,6 +1659,7 @@ async function writePaperclipClaudeSettings(input: { + additionalDirectories: mergedAdditionalDirectories, + defaultMode, + overrodeDontAsk, ++ autoMemoryDirectory, + }; + } + +@@ -2064,12 +2082,18 @@ async function buildRuntime(input: { + // by the company root already granted in these settings; the process env + // still receives this run's actual AGENT_HOME. + agentHome: asString(workspaceContext.agentHomeForPermissions, agentHome), ++ // Auto memory follows the actual AGENT_HOME so it is saved with the run. ++ // It is not part of the session fingerprint: a moved memory directory ++ // must not invalidate a resumable conversation. ++ runAgentHome: agentHome, + companyId: agent.companyId, + }); + skillCommandNotes.push( + `Wrote Paperclip-managed Claude settings to ${paperclipClaudeSettings.filePath} (defaultMode=${paperclipClaudeSettings.defaultMode}${ + paperclipClaudeSettings.overrodeDontAsk ? "; overrode user dontAsk" : "" +- }, +${paperclipClaudeSettings.additionalDirectories.length} read root(s), +${paperclipClaudeSettings.allow.length} allow rule(s)).`, ++ }, +${paperclipClaudeSettings.additionalDirectories.length} read root(s), +${paperclipClaudeSettings.allow.length} allow rule(s)${ ++ paperclipClaudeSettings.autoMemoryDirectory ? `, autoMemoryDirectory=${paperclipClaudeSettings.autoMemoryDirectory}` : "" ++ }).`, + ); + } else if (acpxAgent === "codex") { + // Step 2 — codex-home.seed: the codex managed-home + skills preparation. -- 2.54.0