mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 22:23:56 +00:00
fix(discord): reuse stored sessionId across voice (stt-tts) turns (#97746)
* fix(discord): reuse stored sessionId across voice (stt-tts) turns Discord voice turns previously called agentCommandFromIngress without a sessionId, so the core session resolver minted a fresh UUID per turn. Because the codex app-server thread is bound beside the per-session file, every voice turn produced a new codex thread, a brand-new rollout-*.jsonl, and lost all prior context. The fix reads the persisted session entry for the route's sessionKey (the same supervisor entry already used for the route) and threads that sessionId into agentCommandFromIngress, mirroring how voice-call's response-generator and the gateway 'voice.transcript' dispatcher already keep voice conversations stable. Adds extensions/discord/src/voice/ingress.test.ts covering both directions: reuses the stored id when present, and lets core mint a fresh one when the store is empty. Fixes #97688 * fix(discord): preserve voice session after rollover * refactor(discord): keep voice session ownership in core --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user