From df04ca7da38e8565f425ae4705f7f9f9c9060e7a Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:23:13 -0500 Subject: [PATCH] fix: preserve metadata on voice session touches --- src/gateway/server-node-events.test.ts | 58 ++++++++++++++++++++++++++ src/gateway/server-node-events.ts | 1 + 2 files changed, 59 insertions(+) diff --git a/src/gateway/server-node-events.test.ts b/src/gateway/server-node-events.test.ts index dbf1bde579f..645672bb0e0 100644 --- a/src/gateway/server-node-events.test.ts +++ b/src/gateway/server-node-events.test.ts @@ -9,6 +9,9 @@ const buildSessionLookup = ( lastChannel?: string; lastTo?: string; updatedAt?: number; + label?: string; + spawnedBy?: string; + parentSessionKey?: string; } = {}, ): ReturnType => ({ cfg: { session: { mainKey: "agent:main:main" } } as OpenClawConfig, @@ -19,6 +22,9 @@ const buildSessionLookup = ( updatedAt: entry.updatedAt ?? Date.now(), lastChannel: entry.lastChannel, lastTo: entry.lastTo, + label: entry.label, + spawnedBy: entry.spawnedBy, + parentSessionKey: entry.parentSessionKey, }, canonicalKey: sessionKey, legacyKey: undefined, @@ -460,6 +466,58 @@ describe("voice transcript events", () => { expect(agentCommandMock).toHaveBeenCalledTimes(1); expect(warn).toHaveBeenCalledWith(expect.stringContaining("voice session-store update failed")); }); + + it("preserves existing session metadata when touching the store for voice transcripts", async () => { + const ctx = buildCtx(); + loadSessionEntryMock.mockImplementation((sessionKey: string) => + buildSessionLookup(sessionKey, { + sessionId: "sess-preserve", + updatedAt: 10, + label: "existing label", + spawnedBy: "agent:main:parent", + parentSessionKey: "agent:main:parent", + lastChannel: "discord", + lastTo: "thread-1", + }), + ); + + let updatedStore: Record | undefined; + updateSessionStoreMock.mockImplementationOnce(async (_storePath, update) => { + const store = { + "voice-preserve-session": { + sessionId: "sess-preserve", + updatedAt: 10, + label: "existing label", + spawnedBy: "agent:main:parent", + parentSessionKey: "agent:main:parent", + lastChannel: "discord", + lastTo: "thread-1", + }, + }; + update(store); + updatedStore = structuredClone(store); + }); + + await handleNodeEvent(ctx, "node-v4", { + event: "voice.transcript", + payloadJSON: JSON.stringify({ + text: "preserve metadata", + sessionKey: "voice-preserve-session", + }), + }); + await Promise.resolve(); + + expect(updatedStore).toMatchObject({ + "voice-preserve-session": { + sessionId: "sess-preserve", + label: "existing label", + spawnedBy: "agent:main:parent", + parentSessionKey: "agent:main:parent", + lastChannel: "discord", + lastTo: "thread-1", + }, + }); + }); }); describe("notifications changed events", () => { diff --git a/src/gateway/server-node-events.ts b/src/gateway/server-node-events.ts index 2e9e911725a..55086fc2154 100644 --- a/src/gateway/server-node-events.ts +++ b/src/gateway/server-node-events.ts @@ -154,6 +154,7 @@ async function touchSessionStore(params: { store, }); store[primaryKey] = { + ...store[primaryKey], sessionId: params.sessionId, updatedAt: params.now, thinkingLevel: params.entry?.thinkingLevel,