Fix active-memory recall runs when mx-claw is enabled (#65049)

* fix(active-memory): preserve parent channel context for recall runs

* fix(active-memory): keep recall runs on the resolved channel

* fix(active-memory): prefer resolved recall channel over wrapper hints

* fix(active-memory): trust explicit recall channel hints

* fix(active-memory): rank recall channel fallbacks by trust
This commit is contained in:
Tak Hoffman
2026-04-11 21:08:57 -05:00
committed by GitHub
parent f5bf733575
commit 9d126dc645
3 changed files with 113 additions and 18 deletions

View File

@@ -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] = {

View File

@@ -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({});
}
}

View File

@@ -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 {