fix: delete transcripts on role ordering reset

This commit is contained in:
Peter Steinberger
2026-01-16 09:20:11 +00:00
parent 949fa1051f
commit 6fa437613b
2 changed files with 31 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { SessionEntry } from "../../config/sessions.js"; import type { SessionEntry } from "../../config/sessions.js";
import * as sessions from "../../config/sessions.js";
import type { TypingMode } from "../../config/types.js"; import type { TypingMode } from "../../config/types.js";
import type { TemplateContext } from "../templating.js"; import type { TemplateContext } from "../templating.js";
import type { GetReplyOptions } from "../types.js"; import type { GetReplyOptions } from "../types.js";
@@ -127,11 +128,14 @@ describe("runReplyAgent typing (heartbeat)", () => {
try { try {
const sessionId = "session"; const sessionId = "session";
const storePath = path.join(stateDir, "sessions", "sessions.json"); const storePath = path.join(stateDir, "sessions", "sessions.json");
const sessionEntry = { sessionId, updatedAt: Date.now() }; const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
const sessionEntry = { sessionId, updatedAt: Date.now(), sessionFile: transcriptPath };
const sessionStore = { main: sessionEntry }; const sessionStore = { main: sessionEntry };
await fs.mkdir(path.dirname(storePath), { recursive: true }); await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8");
await fs.mkdir(path.dirname(transcriptPath), { recursive: true });
await fs.writeFile(transcriptPath, "ok", "utf-8");
runEmbeddedPiAgentMock runEmbeddedPiAgentMock
.mockImplementationOnce(async () => { .mockImplementationOnce(async () => {
@@ -175,11 +179,14 @@ describe("runReplyAgent typing (heartbeat)", () => {
try { try {
const sessionId = "session"; const sessionId = "session";
const storePath = path.join(stateDir, "sessions", "sessions.json"); const storePath = path.join(stateDir, "sessions", "sessions.json");
const sessionEntry = { sessionId, updatedAt: Date.now() }; const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
const sessionEntry = { sessionId, updatedAt: Date.now(), sessionFile: transcriptPath };
const sessionStore = { main: sessionEntry }; const sessionStore = { main: sessionEntry };
await fs.mkdir(path.dirname(storePath), { recursive: true }); await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8");
await fs.mkdir(path.dirname(transcriptPath), { recursive: true });
await fs.writeFile(transcriptPath, "ok", "utf-8");
runEmbeddedPiAgentMock runEmbeddedPiAgentMock
.mockImplementationOnce(async () => ({ .mockImplementationOnce(async () => ({
@@ -229,11 +236,14 @@ describe("runReplyAgent typing (heartbeat)", () => {
try { try {
const sessionId = "session"; const sessionId = "session";
const storePath = path.join(stateDir, "sessions", "sessions.json"); const storePath = path.join(stateDir, "sessions", "sessions.json");
const sessionEntry = { sessionId, updatedAt: Date.now() }; const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
const sessionEntry = { sessionId, updatedAt: Date.now(), sessionFile: transcriptPath };
const sessionStore = { main: sessionEntry }; const sessionStore = { main: sessionEntry };
await fs.mkdir(path.dirname(storePath), { recursive: true }); await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8");
await fs.mkdir(path.dirname(transcriptPath), { recursive: true });
await fs.writeFile(transcriptPath, "ok", "utf-8");
runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({
payloads: [{ text: "Message ordering conflict - please try again.", isError: true }], payloads: [{ text: "Message ordering conflict - please try again.", isError: true }],
@@ -260,6 +270,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
}); });
expect(payload.text?.toLowerCase()).toContain("reset"); expect(payload.text?.toLowerCase()).toContain("reset");
expect(sessionStore.main.sessionId).not.toBe(sessionId); expect(sessionStore.main.sessionId).not.toBe(sessionId);
await expect(fs.access(transcriptPath)).rejects.toBeDefined();
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(persisted.main.sessionId).toBe(sessionStore.main.sessionId); expect(persisted.main.sessionId).toBe(sessionStore.main.sessionId);

View File

@@ -1,4 +1,5 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import fs from "node:fs";
import { setCliSessionId } from "../../agents/cli-session.js"; import { setCliSessionId } from "../../agents/cli-session.js";
import { lookupContextTokens } from "../../agents/context.js"; import { lookupContextTokens } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js"; import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
@@ -8,6 +9,7 @@ import { queueEmbeddedPiMessage } from "../../agents/pi-embedded.js";
import { hasNonzeroUsage } from "../../agents/usage.js"; import { hasNonzeroUsage } from "../../agents/usage.js";
import { import {
resolveAgentIdFromSessionKey, resolveAgentIdFromSessionKey,
resolveSessionFilePath,
resolveSessionTranscriptPath, resolveSessionTranscriptPath,
type SessionEntry, type SessionEntry,
updateSessionStore, updateSessionStore,
@@ -247,6 +249,8 @@ export async function runReplyAgent(params: {
}; };
const resetSessionAfterRoleOrderingConflict = async (reason: string): Promise<boolean> => { const resetSessionAfterRoleOrderingConflict = async (reason: string): Promise<boolean> => {
if (!sessionKey || !activeSessionStore || !storePath) return false; if (!sessionKey || !activeSessionStore || !storePath) return false;
const prevEntry = activeSessionStore[sessionKey] ?? activeSessionEntry;
const prevSessionId = prevEntry?.sessionId;
const nextSessionId = crypto.randomUUID(); const nextSessionId = crypto.randomUUID();
const nextEntry: SessionEntry = { const nextEntry: SessionEntry = {
...(activeSessionStore[sessionKey] ?? activeSessionEntry), ...(activeSessionStore[sessionKey] ?? activeSessionEntry),
@@ -279,6 +283,19 @@ export async function runReplyAgent(params: {
defaultRuntime.error( defaultRuntime.error(
`Role ordering conflict (${reason}). Restarting session ${sessionKey} -> ${nextSessionId}.`, `Role ordering conflict (${reason}). Restarting session ${sessionKey} -> ${nextSessionId}.`,
); );
if (prevSessionId) {
const transcriptCandidates = new Set<string>();
const resolved = resolveSessionFilePath(prevSessionId, prevEntry, { agentId });
if (resolved) transcriptCandidates.add(resolved);
transcriptCandidates.add(resolveSessionTranscriptPath(prevSessionId, agentId));
for (const candidate of transcriptCandidates) {
try {
fs.unlinkSync(candidate);
} catch {
// Best-effort cleanup.
}
}
}
return true; return true;
}; };
try { try {