diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index 941f384913b..936f9e6a9cc 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -471,7 +471,6 @@ describe("active-memory plugin", () => { expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({ provider: "github-copilot", model: "gpt-5.4-mini", - messageChannel: "webchat", messageProvider: "webchat", sessionKey: expect.stringMatching(/^agent:main:main:active-memory:[a-f0-9]{12}$/), config: { @@ -1155,7 +1154,7 @@ describe("active-memory plugin", () => { ); expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({ messageChannel: "telegram", - messageProvider: "webchat", + messageProvider: "telegram", }); expect(hoisted.sessionStore["agent:main:telegram:direct:12345"]?.pluginDebugEntries).toEqual([ { @@ -1192,6 +1191,82 @@ describe("active-memory plugin", () => { }); }); + it("prefers the resolved session channel over a wrapper channel hint", async () => { + hoisted.sessionStore["agent:main:telegram:direct:12345"] = { + sessionId: "session-a", + updatedAt: 25, + channel: "telegram", + }; + + await hooks.before_prompt_build( + { prompt: "what wings should i order? wrapper channel hint", messages: [] }, + { + agentId: "main", + trigger: "user", + sessionKey: "agent:main:telegram:direct:12345", + messageProvider: "webchat", + channelId: "webchat", + }, + ); + + expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({ + messageChannel: "telegram", + messageProvider: "telegram", + }); + }); + + it("preserves an explicit real channel hint over a stale stored wrapper channel", async () => { + hoisted.sessionStore["agent:main:telegram:direct:12345"] = { + sessionId: "session-a", + updatedAt: 25, + origin: { + provider: "webchat", + }, + }; + + await hooks.before_prompt_build( + { prompt: "what wings should i order? explicit channel hint", messages: [] }, + { + agentId: "main", + trigger: "user", + sessionKey: "agent:main:telegram:direct:12345", + messageProvider: "webchat", + channelId: "telegram", + }, + ); + + expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({ + messageChannel: "telegram", + messageProvider: "telegram", + }); + }); + + it("preserves a direct explicit channel when weak legacy fallback disagrees", async () => { + hoisted.sessionStore["agent:main:telegram:direct:12345"] = { + sessionId: "session-a", + updatedAt: 25, + origin: { + provider: "webchat", + }, + }; + + await hooks.before_prompt_build( + { prompt: "what wings should i order? direct explicit channel", messages: [] }, + { + agentId: "main", + trigger: "user", + sessionKey: "agent:main:telegram:direct:12345", + messageProvider: "telegram", + channelId: "telegram", + }, + ); + + expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({ + messageChannel: "telegram", + messageProvider: "telegram", + }); + }); + it("clears stale status on skipped non-interactive turns even when agentId is missing", async () => { const sessionKey = "noncanonical-session"; hoisted.sessionStore[sessionKey] = { diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 8f25eacf36b..075c4719f4e 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -369,6 +369,28 @@ function resolveRecallRunChannelContext(params: { } { const explicitChannel = normalizeOptionalString(params.channelId); const explicitProvider = normalizeOptionalString(params.messageProvider); + const trustedExplicitChannel = + explicitChannel && explicitChannel !== explicitProvider ? explicitChannel : undefined; + const resolveReturnValue = (params: { + resolvedChannel?: string; + resolvedChannelStrength?: "strong" | "weak"; + }) => { + const trustedResolvedChannel = + params.resolvedChannelStrength === "strong" ? params.resolvedChannel : undefined; + return { + messageChannel: + trustedExplicitChannel ?? + trustedResolvedChannel ?? + explicitChannel ?? + params.resolvedChannel, + messageProvider: + trustedExplicitChannel ?? + trustedResolvedChannel ?? + explicitProvider ?? + explicitChannel ?? + params.resolvedChannel, + }; + }; const resolvedSessionKey = normalizeOptionalString(params.sessionKey) ?? resolveCanonicalSessionKeyFromSessionId({ @@ -377,10 +399,7 @@ function resolveRecallRunChannelContext(params: { sessionId: params.sessionId, }); if (!resolvedSessionKey) { - return { - messageChannel: explicitChannel ?? explicitProvider, - messageProvider: explicitProvider ?? explicitChannel, - }; + return resolveReturnValue({}); } try { @@ -395,19 +414,20 @@ function resolveRecallRunChannelContext(params: { store, sessionKey: resolvedSessionKey, }).existing; - const entryChannel = + const strongEntryChannel = normalizeOptionalString(sessionEntry?.lastChannel) ?? - normalizeOptionalString(sessionEntry?.channel) ?? - normalizeOptionalString(sessionEntry?.origin?.provider); - return { - messageChannel: explicitChannel ?? entryChannel ?? explicitProvider, - messageProvider: explicitProvider ?? explicitChannel ?? entryChannel, - }; + normalizeOptionalString(sessionEntry?.channel); + const weakEntryChannel = normalizeOptionalString(sessionEntry?.origin?.provider); + return resolveReturnValue({ + resolvedChannel: strongEntryChannel ?? weakEntryChannel, + resolvedChannelStrength: strongEntryChannel + ? "strong" + : weakEntryChannel + ? "weak" + : undefined, + }); } catch { - return { - messageChannel: explicitChannel ?? explicitProvider, - messageProvider: explicitProvider ?? explicitChannel, - }; + return resolveReturnValue({}); } } diff --git a/extensions/msteams/src/channel.test.ts b/extensions/msteams/src/channel.test.ts index 924e7e476fb..b6982388b32 100644 --- a/extensions/msteams/src/channel.test.ts +++ b/extensions/msteams/src/channel.test.ts @@ -1,7 +1,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { describe, expect, it } from "vitest"; -import { msteamsPlugin } from "./channel.js"; import { msTeamsApprovalAuth } from "./approval-auth.js"; +import { msteamsPlugin } from "./channel.js"; function createConfiguredMSTeamsCfg(): OpenClawConfig { return {