fix(mnemopi): retained only new auto-retain turns

Auto-retain now slices the transcript after the last retained user turn before storing an episode, preventing cumulative duplicate session transcripts in mnemopi banks.

Fixes #4396
This commit is contained in:
roboomp
2026-07-03 04:50:44 +00:00
parent d0c1890a6c
commit 5b6385df19
3 changed files with 27 additions and 5 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed mnemopi auto-retain storing cumulative full-session transcripts on every retention interval; subsequent retains now store only newly completed user-turn suffixes. ([#4396](https://github.com/can1357/oh-my-pi/issues/4396))
## [16.3.4] - 2026-07-03
### Fixed
+20 -1
View File
@@ -116,6 +116,22 @@ interface MnemopiStoredMemoryRow {
session_id?: unknown;
}
type MnemopiRetentionMessage = { role: string; content: string };
function sliceUnretainedMessages(
messages: MnemopiRetentionMessage[],
lastRetainedTurn: number,
): MnemopiRetentionMessage[] {
if (lastRetainedTurn <= 0) return messages;
let userTurns = 0;
for (let index = 0; index < messages.length; index++) {
if (messages[index].role !== "user") continue;
userTurns++;
if (userTurns > lastRetainedTurn) return messages.slice(index);
}
return [];
}
export function getMnemopiSessionState(session: AgentSession | undefined): MnemopiSessionState | undefined {
return session ? (session as AgentSessionWithMnemopiState)[kMnemopiSessionState] : undefined;
}
@@ -339,7 +355,10 @@ export class MnemopiSessionState {
const flat = extractMessages(this.session.sessionManager);
const userTurns = flat.filter(message => message.role === "user").length;
if (userTurns - this.lastRetainedTurn < this.config.retainEveryNTurns) return;
await this.retainMessages(flat, `${this.sessionId}-${Date.now()}`);
await this.retainMessages(
sliceUnretainedMessages(flat, this.lastRetainedTurn),
`${this.sessionId}-${Date.now()}`,
);
this.lastRetainedTurn = userTurns;
}
@@ -450,14 +450,15 @@ describe("Mnemopi backend lifecycle", () => {
tempDbPath = undefined;
});
it("auto-retain uses the cumulative transcript turn count", async () => {
it("auto-retain stores only the not-yet-retained suffix", async () => {
const entries = Array.from({ length: 4 }, (_, index) => ({
type: "message",
message: { role: "user", content: `turn ${index + 1}` },
}));
const state = registerMnemopiState(makeMnemopiConfig({ retainEveryNTurns: 4 }), {
const state = registerMnemopiState(makeMnemopiConfig({ retainEveryNTurns: 2 }), {
cwd: "/work/project-alpha",
});
state.lastRetainedTurn = 2;
(state.session.sessionManager as { getEntries: () => unknown[] }).getEntries = () => entries;
const retainSpy = vi.spyOn(state, "retainMessages").mockResolvedValue();
@@ -465,8 +466,6 @@ describe("Mnemopi backend lifecycle", () => {
expect(retainSpy).toHaveBeenCalledTimes(1);
expect(retainSpy.mock.calls[0][0]).toEqual([
{ role: "user", content: "turn 1" },
{ role: "user", content: "turn 2" },
{ role: "user", content: "turn 3" },
{ role: "user", content: "turn 4" },
]);