diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 800655b94..40feac13d 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -6903,7 +6903,7 @@ export class AgentSession { this.#syncAgentSessionId(); this.#rekeyHindsightMemoryForCurrentSessionId(); this.#rekeyMnemopiMemoryForCurrentSessionId(); - this.#resetMnemopiConversationTrackingIfMnemopi(); + await this.#resetMemoryContextForNewTranscript(); // Emit session_switch event with reason "fork" to hooks if (this.#extensionRunner) { diff --git a/packages/coding-agent/test/agent-session-message-pipeline.test.ts b/packages/coding-agent/test/agent-session-message-pipeline.test.ts index 979328ea7..5c1b81910 100644 --- a/packages/coding-agent/test/agent-session-message-pipeline.test.ts +++ b/packages/coding-agent/test/agent-session-message-pipeline.test.ts @@ -782,4 +782,96 @@ describe("AgentSession message pipeline", () => { expect(contexts).toHaveLength(2); expect(contexts[1]!.systemPrompt?.join("\n")).not.toContain(injected); }); + + it("does not duplicate promoted memory in the base prompt when forking", async () => { + using tempDir = TempDir.createSync("@pi-injected-memory-fork-"); + const sessionManager = SessionManager.create(tempDir.path(), tempDir.join("sessions")); + expect(sessionManager.getSessionFile()).toBeString(); + await sessionManager.flush(); + + const api = "test-injected-memory-fork-cache"; + const contexts: Context[] = []; + let remembered = false; + const injected = "forked recall"; + const fakeBackend: MemoryBackend = { + id: "mnemopi", + async start() {}, + async buildDeveloperInstructions() { + return remembered ? `static memory instructions\n\n${injected}` : "static memory instructions"; + }, + async clear() {}, + async enqueue() {}, + async beforeAgentStartPrompt() { + if (remembered) return undefined; + remembered = true; + return injected; + }, + }; + vi.spyOn(memoryBackend, "resolveMemoryBackend").mockResolvedValue(fakeBackend); + registerCustomApi(api, (_model, context) => { + contexts.push(context); + const stream = new AssistantMessageEventStream(); + queueMicrotask(() => { + const message = createAssistantMessage("ok"); + stream.push({ type: "text_delta", contentIndex: 0, delta: "ok", partial: message }); + stream.push({ type: "done", reason: "stop", message }); + }); + return stream; + }); + const model = buildModel({ + id: "local-model", + name: "Local Model", + api, + provider: "ollama", + baseUrl: "http://127.0.0.1:11434", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 4096, + maxTokens: 1024, + } as ModelSpec) as Model; + const agent = new Agent({ + initialState: { + model, + systemPrompt: ["base", "static memory instructions"], + messages: [], + tools: [], + }, + }); + agent.setAppendOnlyContext(new AppendOnlyContextManager()); + const session = new AgentSession({ + agent, + sessionManager, + settings: Settings.isolated({ + "compaction.enabled": false, + "memory.backend": "mnemopi", + "provider.appendOnlyContext": "on", + }), + modelRegistry: createModelRegistryStub() as never, + rebuildSystemPrompt: async () => ({ + systemPrompt: remembered + ? ["base", `static memory instructions\n\n${injected}`] + : ["base", "static memory instructions"], + }), + }); + sessions.push(session); + setMnemopiSessionState(session, { + aliasOf: undefined, + setSessionId(_sessionId: string) {}, + resetConversationTracking() { + remembered = false; + }, + async dispose() {}, + } as unknown as MnemopiSessionState); + + await session.sendUserMessage("first"); + expect(session.systemPrompt.join("\n")).toContain(injected); + + await session.fork(); + await session.sendUserMessage("second"); + + const forkedPrompt = contexts[1]!.systemPrompt?.join("\n") ?? ""; + const occurrences = forkedPrompt.split(injected).length - 1; + expect(occurrences).toBe(1); + }); });