diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index cda3d38a5d2b..a13a4181b0ec 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -103,7 +103,7 @@ import { resolveInternalEventTranscriptBody, } from "./command/attempt-execution.shared.js"; import { resolveAgentRunContext } from "./command/run-context.js"; -import { resolveSession } from "./command/session.js"; +import { clearRotatedSessionMetadata, resolveSession } from "./command/session.js"; import type { AgentCommandIngressOpts, AgentCommandOpts } from "./command/types.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; import { @@ -953,8 +953,12 @@ async function agentCommandInternal( const currentStoreEntry = sessionStore[sessionKey]; const allowCreateRestartRecoveryEntry = currentStoreEntry === undefined && sessionEntry === undefined; - const entry = currentStoreEntry ?? + const initialEntry = currentStoreEntry ?? sessionEntry ?? { sessionId, updatedAt: now, sessionStartedAt: now }; + const isSessionRollover = isNewSession && initialEntry.sessionId !== sessionId; + const entry = isSessionRollover + ? clearRotatedSessionMetadata(initialEntry) + : initialEntry; currentRunDeliveryContext = await resolveCurrentRunDeliveryContext({ cfg, opts, @@ -965,6 +969,8 @@ async function agentCommandInternal( ...entry, sessionId, updatedAt: now, + sessionStartedAt: isSessionRollover ? now : entry.sessionStartedAt, + lastInteractionAt: isSessionRollover ? now : entry.lastInteractionAt, restartRecoveryDeliveryContext: currentRunDeliveryContext, restartRecoveryDeliveryRunId: currentRunDeliveryContext ? runId : undefined, }; @@ -972,15 +978,17 @@ async function agentCommandInternal( sessionStore, sessionKey, storePath, - initialEntry: entry, + initialEntry, entry: next, shouldPersist: (current) => - shouldPersistRestartRecoveryContextClaim( - current, - sessionId, - runId, - allowCreateRestartRecoveryEntry, - ), + isSessionRollover + ? current?.sessionId === initialEntry.sessionId + : shouldPersistRestartRecoveryContextClaim( + current, + sessionId, + runId, + allowCreateRestartRecoveryEntry, + ), }); sessionEntry = persisted ?? sessionEntry; trackedRestartRecoveryDeliveryContext = diff --git a/src/agents/command/session.ts b/src/agents/command/session.ts index 0c91cc55da24..bc523964a1c2 100644 --- a/src/agents/command/session.ts +++ b/src/agents/command/session.ts @@ -59,12 +59,7 @@ type SessionKeyResolution = { storePath: string; }; -function clearRotatedTerminalMainSessionMetadata( - entry: SessionEntry | undefined, -): SessionEntry | undefined { - if (!entry) { - return undefined; - } +export function clearRotatedSessionMetadata(entry: SessionEntry): SessionEntry { const next = { ...entry, sessionFile: undefined, @@ -406,8 +401,8 @@ export function resolveSession(opts: { const sessionId = requestedSessionId || (fresh ? sessionEntry?.sessionId : undefined) || crypto.randomUUID(); const isNewSession = !fresh && !requestedSessionId; - const resolvedSessionEntry = terminalMainTranscriptNewerThanRegistry - ? clearRotatedTerminalMainSessionMetadata(sessionEntry) + const resolvedSessionEntry = isNewSession && sessionEntry + ? clearRotatedSessionMetadata(sessionEntry) : sessionEntry; clearBootstrapSnapshotOnSessionRollover({ diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 823f5ff8cddf..4ba397234df2 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -460,6 +460,56 @@ describe("agentCommand", () => { ).rejects.toThrow("allowModelOverride must be explicitly set for ingress agent runs."); }); + it("reuses a Discord voice session after one stale-session rollover", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + const sessionKey = "agent:main:discord:channel:voice-1"; + const staleStartedAt = Date.now() - 2 * 24 * 60 * 60_000; + mockConfig(home, store); + writeSessionStoreSeed(store, { + [sessionKey]: { + sessionId: "stale-voice-session", + updatedAt: staleStartedAt, + sessionStartedAt: staleStartedAt, + }, + }); + + const runVoiceTurn = async (message: string) => + await agentCommandFromIngress( + { + message, + sessionKey, + agentId: "main", + messageChannel: "discord", + messageProvider: "discord-voice", + allowModelOverride: false, + deliver: false, + }, + runtime, + ); + + await runVoiceTurn("remember 42"); + const firstSessionId = getLastEmbeddedCall()?.sessionId; + expect(firstSessionId).toBeTruthy(); + expect(firstSessionId).not.toBe("stale-voice-session"); + const firstPersisted = readSessionStore<{ + sessionId: string; + sessionStartedAt?: number; + }>(store)[sessionKey]; + expect(firstPersisted?.sessionId).toBe(firstSessionId); + expect(firstPersisted?.sessionStartedAt).toBeGreaterThan(staleStartedAt); + + await runVoiceTurn("what number?"); + expect(getLastEmbeddedCall()?.sessionId).toBe(firstSessionId); + + const persisted = readSessionStore<{ sessionId: string; sessionStartedAt?: number }>(store)[ + sessionKey + ]; + expect(persisted?.sessionId).toBe(firstSessionId); + expect(persisted?.sessionStartedAt).toBeGreaterThan(staleStartedAt); + }); + }); + it("rejects archived sessions selected by session id", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json");