From 41ad8f00ebb25dba4e54370f3334f6dba557cd52 Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 25 May 2026 16:59:08 +0100 Subject: [PATCH] refactor: persist followup cli user turns through sessions --- src/auto-reply/reply/followup-runner.test.ts | 34 ++++++++++++++++++++ src/auto-reply/reply/followup-runner.ts | 29 +++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts index af6ff48bc6d..2fc3adc022f 100644 --- a/src/auto-reply/reply/followup-runner.test.ts +++ b/src/auto-reply/reply/followup-runner.test.ts @@ -10,6 +10,7 @@ import type { FollowupRun, QueueSettings } from "./queue.js"; const runEmbeddedPiAgentMock = vi.fn(); const runCliAgentMock = vi.fn(); const runWithModelFallbackMock = vi.fn(); +const persistUserTurnTranscriptMock = vi.fn(); const compactEmbeddedPiSessionMock = vi.fn(); const routeReplyMock = vi.fn(); const isRoutableChannelMock = vi.fn(); @@ -352,6 +353,15 @@ async function loadFreshFollowupRunnerModuleForTest() { vi.doMock("../../agents/cli-runner.js", () => ({ runCliAgent: (params: unknown) => runCliAgentMock(params), })); + vi.doMock("../../sessions/user-turn-transcript.js", async () => { + const actual = await vi.importActual( + "../../sessions/user-turn-transcript.js", + ); + return { + ...actual, + persistUserTurnTranscript: (params: unknown) => persistUserTurnTranscriptMock(params), + }; + }); vi.doMock("./queue.js", () => ({ clearFollowupQueue: clearFollowupQueueForFollowupTest, completeFollowupRunLifecycle: (run: Pick) => @@ -461,6 +471,11 @@ beforeEach(() => { clearRuntimeConfigSnapshot?.(); runEmbeddedPiAgentMock.mockReset(); runCliAgentMock.mockReset(); + persistUserTurnTranscriptMock.mockReset(); + persistUserTurnTranscriptMock.mockResolvedValue({ + message: { role: "user", content: [{ type: "text", text: "hello" }] }, + sessionFile: "/tmp/session.jsonl", + }); runWithModelFallbackMock.mockReset(); runWithModelFallbackMock.mockImplementation( async (params: { @@ -800,6 +815,7 @@ describe("createFollowupRunner runtime config", () => { originatingChannel: "telegram", run: { config: runtimeConfig, + sessionId: "session-cli-followup", provider: "anthropic", model: "claude-opus-4-7", messageProvider: "telegram", @@ -815,6 +831,24 @@ describe("createFollowupRunner runtime config", () => { expect(call.config).toBe(runtimeConfig); expect(call.cliSessionId).toBe("cli-session-1"); expect(call.messageChannel).toBe("telegram"); + expect(persistUserTurnTranscriptMock).toHaveBeenCalledOnce(); + const persistenceCall = requireLastMockCallArg( + persistUserTurnTranscriptMock, + "persist user turn transcript", + ); + expect(persistenceCall).toMatchObject({ + sessionId: "session-cli-followup", + sessionKey: "main", + sessionEntry, + sessionStore, + agentId: "agent", + cwd: "/tmp", + config: runtimeConfig, + updateMode: "inline", + }); + expect(persistenceCall.input).toMatchObject({ + text: "hello", + }); }); it("defers queued CLI attempt terminal lifecycle events until fallback settles", async () => { diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index c48e7ccd1c5..e4728e95025 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -26,6 +26,7 @@ import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-event import { formatErrorMessage } from "../../infra/errors.js"; import { defaultRuntime } from "../../runtime.js"; import { shouldPreserveUserFacingSessionStateForInputProvenance } from "../../sessions/input-provenance.js"; +import { persistUserTurnTranscript } from "../../sessions/user-turn-transcript.js"; import { readStringValue } from "../../shared/string-coerce.js"; import { isInternalMessageChannel } from "../../utils/message-channel.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; @@ -776,6 +777,34 @@ export function createFollowupRunner(params: { })() : rawResult, }); + if (!suppressQueuedUserPersistenceForCandidate) { + try { + const persistedUserTurn = await persistUserTurnTranscript({ + ...(effectiveQueued.userMessageForPersistence + ? { message: effectiveQueued.userMessageForPersistence } + : { + input: { + text: effectiveQueued.transcriptPrompt ?? effectiveQueued.prompt, + timestamp: Date.now(), + }, + }), + sessionId: run.sessionId, + sessionKey: replySessionKey ?? run.sessionId, + sessionEntry: activeSessionEntry, + ...(sessionStore ? { sessionStore } : {}), + ...(storePath ? { storePath } : {}), + agentId: run.agentId, + cwd: run.workspaceDir, + config: runtimeConfig, + updateMode: "inline", + }); + if (persistedUserTurn) { + queuedUserMessagePersistedAcrossFallback = true; + } + } catch (error) { + logVerbose(`failed to persist CLI user turn transcript: ${String(error)}`); + } + } bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen( result.meta?.systemPromptReport, );