From 77a0ee7f9de7f6eb7bc1caa1fa21d43d01e6af4a Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Sat, 11 Apr 2026 09:06:49 -0700 Subject: [PATCH] fix: canonicalize topic session transcript fallback (#64869) * fix: canonicalize topic session transcript fallback When initSessionState has a topic-scoped SessionKey but no MessageThreadId, fallback transcript selection should still land on the topic-qualified JSONL path instead of the bare session file. Match the existing transcript resolver by parsing the thread id from the session key, and cover the regression with a session init test that loads the Telegram session-conversation grammar. Regeneration-Prompt: | Investigate why a Telegram topic session could alternate between .jsonl and -topic-.jsonl for the same logical session. The fix should be in OpenClaw's session initialization path, not in lossless-claw. Keep behavior unchanged when MessageThreadId is present, but when the inbound turn only carries a topic-scoped SessionKey, derive the same topic-specific transcript path that the canonical transcript resolver would use. Add a regression test that proves initSessionState chooses the topic-qualified file even without MessageThreadId, and make the test load the session-conversation registry needed to parse Telegram :topic: grammar. * fix: preserve topic session transcript history --- CHANGELOG.md | 2 ++ src/auto-reply/reply/session.test.ts | 32 +++++++++++++++++++++++++++- src/auto-reply/reply/session.ts | 10 ++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91872d300ce..409b429df82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Docs: https://docs.openclaw.ai ### Fixes +- Telegram/sessions: keep topic-scoped session initialization on the canonical topic transcript path when inbound turns omit `MessageThreadId`, so one topic session no longer alternates between bare and topic-qualified transcript files. (#64869) thanks @jalehman. + ## 2026.4.11-beta.1 ### Changes diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index 9c78c3ba3d1..46ad10141c6 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -16,11 +16,12 @@ import { registerSessionBindingAdapter, } from "../../infra/outbound/session-binding-service.js"; import { enqueueSystemEvent, resetSystemEventsForTest } from "../../infra/system-events.js"; -import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js"; import { createChannelTestPluginBase, createTestRegistry, } from "../../test-utils/channel-plugins.js"; +import { createSessionConversationTestRegistry } from "../../test-utils/session-conversation-registry.js"; import { drainFormattedSystemEvents } from "./session-updates.js"; import { persistSessionUsageUpdate } from "./session-usage.js"; import { initSessionState } from "./session.js"; @@ -561,6 +562,35 @@ describe("initSessionState thread forking", () => { `${result.sessionEntry.sessionId}-topic-456.jsonl`, ); }); + + it("records topic-specific session files from SessionKey when MessageThreadId is absent", async () => { + const root = await makeCaseDir("openclaw-topic-session-key-"); + const storePath = path.join(root, "sessions.json"); + + const cfg = { + session: { store: storePath }, + } as OpenClawConfig; + + setActivePluginRegistry(createSessionConversationTestRegistry()); + try { + const result = await initSessionState({ + ctx: { + Body: "Hello topic", + SessionKey: "agent:main:telegram:group:123:topic:456", + }, + cfg, + commandAuthorized: true, + }); + + const sessionFile = result.sessionEntry.sessionFile; + expect(sessionFile).toBeTruthy(); + expect(path.basename(sessionFile ?? "")).toBe( + `${result.sessionEntry.sessionId}-topic-456.jsonl`, + ); + } finally { + resetPluginRuntimeStateForTest(); + } + }); }); describe("initSessionState RawBody", () => { diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 6b4bd6f12ac..ddab2929aee 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -21,6 +21,7 @@ import { resolveAndPersistSessionFile } from "../../config/sessions/session-file import { resolveSessionKey } from "../../config/sessions/session-key.js"; import { resolveMaintenanceConfigFromInput } from "../../config/sessions/store-maintenance.js"; import { loadSessionStore, updateSessionStore } from "../../config/sessions/store.js"; +import { parseSessionThreadInfo } from "../../config/sessions/thread-info.js"; import { DEFAULT_RESET_TRIGGERS, type GroupKeyResolution, @@ -620,8 +621,15 @@ export async function initSessionState(params: { } } } + const threadIdFromSessionKey = parseSessionThreadInfo( + sessionCtxForState.SessionKey ?? sessionKey, + ).threadId; const fallbackSessionFile = !sessionEntry.sessionFile - ? resolveSessionTranscriptPath(sessionEntry.sessionId, agentId, ctx.MessageThreadId) + ? resolveSessionTranscriptPath( + sessionEntry.sessionId, + agentId, + ctx.MessageThreadId ?? threadIdFromSessionKey, + ) : undefined; const resolvedSessionFile = await resolveAndPersistSessionFile({ sessionId: sessionEntry.sessionId,