From 030fa62b79f0c7aa1c0fc91eacc0d8f57209503f Mon Sep 17 00:00:00 2001 From: Eva Date: Sun, 12 Jul 2026 04:05:44 +0700 Subject: [PATCH] fix(telegram): preserve codex login profile identity --- .../bot-native-commands.session-meta.test.ts | 35 ++++++++++++++++-- .../telegram/src/bot-native-commands.ts | 36 +++++++++++++++++-- .../provider-auth-login-flow-runtime.ts | 11 ++++-- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/extensions/telegram/src/bot-native-commands.session-meta.test.ts b/extensions/telegram/src/bot-native-commands.session-meta.test.ts index 146adea0707a..4fe25d430504 100644 --- a/extensions/telegram/src/bot-native-commands.session-meta.test.ts +++ b/extensions/telegram/src/bot-native-commands.session-meta.test.ts @@ -55,6 +55,7 @@ const persistentBindingMocks = vi.hoisted(() => ({ const sessionMocks = vi.hoisted(() => ({ getSessionEntry: vi.fn(), loadSessionStore: vi.fn(), + patchSessionEntry: vi.fn(), recordSessionMetaFromInbound: vi.fn(), resolveStorePath: vi.fn(), })); @@ -175,6 +176,7 @@ vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => { ...actual, getSessionEntry: sessionMocks.getSessionEntry, loadSessionStore: sessionMocks.loadSessionStore, + patchSessionEntry: sessionMocks.patchSessionEntry, resolveStorePath: sessionMocks.resolveStorePath, }; }); @@ -640,6 +642,7 @@ function resetSessionMetaMocks() { ({ storePath, sessionKey }: { storePath: string; sessionKey: string }) => sessionMocks.loadSessionStore(storePath)[sessionKey], ); + sessionMocks.patchSessionEntry.mockClear().mockResolvedValue(null); sessionMocks.recordSessionMetaFromInbound.mockClear().mockResolvedValue(undefined); sessionMocks.resolveStorePath.mockClear().mockReturnValue("/tmp/openclaw-sessions.json"); pluginRuntimeMocks.executePluginCommand.mockClear().mockResolvedValue({ text: "ok" }); @@ -1617,7 +1620,7 @@ describe("registerTelegramNativeCommands — session metadata", () => { ); }); - it("passes the target session auth profile to Telegram /login codex", async () => { + it("moves the target session to the profile returned by Telegram /login codex", async () => { sessionMocks.loadSessionStore.mockReturnValue({ "agent:main:main": { authProfileOverride: "openai:owner@example.com", @@ -1633,7 +1636,9 @@ describe("registerTelegramNativeCommands — session metadata", () => { return { providerId: "openai", methodId: "device-code", - profiles: [{ profileId: "openai:owner@example.com", provider: "openai", mode: "oauth" }], + profiles: [ + { profileId: "openai:new-owner@example.com", provider: "openai", mode: "oauth" }, + ], }; }); @@ -1653,9 +1658,33 @@ describe("registerTelegramNativeCommands — session metadata", () => { provider: "openai", method: "device-code", agent: "main", - profileId: "openai:owner@example.com", }), ); + expect( + (runModelsAuthLoginFlow.mock.calls[0]?.[0] as { profileId?: string } | undefined)?.profileId, + ).toBeUndefined(); + expect(sessionMocks.patchSessionEntry).toHaveBeenCalledWith({ + agentId: "main", + sessionKey: "agent:main:main", + storePath: "/tmp/openclaw-sessions.json", + fallbackEntry: { + authProfileOverride: "openai:owner@example.com", + sessionId: "sess-main", + updatedAt: 1, + }, + preserveActivity: true, + update: expect.any(Function), + }); + const patchUpdate = ( + sessionMocks.patchSessionEntry.mock.calls[0]?.[0] as { + update?: () => Record; + } + )?.update?.(); + expect(patchUpdate).toEqual({ + authProfileOverride: "openai:new-owner@example.com", + authProfileOverrideSource: "user", + authProfileOverrideCompactionCount: undefined, + }); }); it("passes session identity to plugin commands when the entry has no file", async () => { diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index 3e6533a517f2..e29da4eb7c08 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -43,6 +43,7 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { formatSqliteSessionFileMarker, getSessionEntry, + patchSessionEntry, resolveStorePath, type SessionEntry, } from "openclaw/plugin-sdk/session-store-runtime"; @@ -1338,21 +1339,50 @@ export const registerTelegramNativeCommands = ({ agentId: route.agentId, sessionKey: targetSessionKey, }); - const profileId = codexChannelLoginRuntime.resolveProviderScopedProfileId( + const previousProfileId = codexChannelLoginRuntime.resolveProviderScopedProfileId( targetSessionEntry?.authProfileOverride, loginProvider, ); - await codexChannelLoginRuntime.runDeviceLoginFlow({ + const loginResult = await codexChannelLoginRuntime.runDeviceLoginFlow({ runLoginFlow: loginFlow, provider: loginProvider, agentId: route.agentId, - ...(profileId ? { profileId } : {}), config: runtimeCfg, runtime, sendMessage: sendLoginMessage, unsupportedPromptMessage: "Telegram /login supports only fixed Codex device-code auth.", }); + const nextProfileId = loginResult.profiles.find( + (profile) => profile.provider === loginProvider, + )?.profileId; + if (targetSessionEntry && nextProfileId && nextProfileId !== previousProfileId) { + try { + const storePath = resolveStorePath(runtimeCfg.session?.store, { + agentId: route.agentId, + }); + await patchSessionEntry({ + agentId: route.agentId, + sessionKey: targetSessionKey, + storePath, + fallbackEntry: targetSessionEntry, + preserveActivity: true, + update: () => ({ + authProfileOverride: nextProfileId, + authProfileOverrideSource: "user", + authProfileOverrideCompactionCount: undefined, + }), + }); + } catch (error) { + runtime.error?.( + danger( + `telegram /login codex completed but failed to update session auth profile: ${String( + error, + )}`, + ), + ); + } + } await sendLoginMessage("Codex login complete. Try your request again now."); } catch { runtime.error?.(danger("telegram /login codex failed")); diff --git a/src/plugin-sdk/provider-auth-login-flow-runtime.ts b/src/plugin-sdk/provider-auth-login-flow-runtime.ts index 8b25a4f605b5..522d9ee02dbc 100644 --- a/src/plugin-sdk/provider-auth-login-flow-runtime.ts +++ b/src/plugin-sdk/provider-auth-login-flow-runtime.ts @@ -10,10 +10,15 @@ export type { ModelsAuthLoginFlowOptions, ModelsAuthLoginFlowResult, } from "../commands/models/auth.js"; -import type { ModelsAuthLoginFlowOptions } from "../commands/models/auth.js"; +import type { + ModelsAuthLoginFlowOptions, + ModelsAuthLoginFlowResult, +} from "../commands/models/auth.js"; type ProviderAuthLoginFlowRuntime = typeof import("../commands/models/auth.js"); -type RunModelsAuthLoginFlow = (opts: ModelsAuthLoginFlowOptions) => Promise; +type RunModelsAuthLoginFlow = ( + opts: ModelsAuthLoginFlowOptions, +) => Promise; const CODEX_LOGIN_PROVIDER = "openai"; const CODEX_LOGIN_METHOD = "device-code"; @@ -134,7 +139,7 @@ async function runCodexDeviceLoginFlow(params: { sendMessage: (message: string) => Promise; unsupportedPromptMessage: string; runLoginFlow?: RunModelsAuthLoginFlow; -}): Promise { +}): Promise { return await (params.runLoginFlow ?? runModelsAuthLoginFlow)({ provider: params.provider, method: CODEX_LOGIN_METHOD,