mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 08:12:21 +00:00
fix(webchat): preserve session channel routing on internal turns (#23258)
Co-authored-by: binary64 <1680627+binary64@users.noreply.github.com>
This commit is contained in:
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Sandbox/Docker: apply custom bind mounts after workspace mounts and prioritize bind-source resolution on overlapping paths, so explicit workspace binds are no longer ignored. (#22669) Thanks @tasaankaeris.
|
||||
- Exec approvals/Forwarding: restore Discord text forwarding when component approvals are not configured, and carry request snapshots through resolve events so resolved notices still forward after cache misses/restarts. (#22988) Thanks @bubmiller.
|
||||
- Security/Elevated: match `tools.elevated.allowFrom` against sender identities only (not recipient `ctx.To`), closing a recipient-token bypass for `/elevated` authorization. (#11022) Thanks @coygeek.
|
||||
- Webchat/Sessions: preserve external session routing metadata when internal `chat.send` turns run under `webchat`, so explicit channel-keyed sessions (for example Telegram) no longer get rewritten to `webchat` and misroute follow-up delivery. (#23258) Thanks @binary64.
|
||||
- Config/Memory: allow `"mistral"` in `agents.defaults.memorySearch.provider` and `agents.defaults.memorySearch.fallback` schema validation. (#14934) Thanks @ThomsenDrake.
|
||||
- Security/Feishu: enforce ID-only allowlist matching for DM/group sender authorization, normalize Feishu ID prefixes during checks, and ignore mutable display names so display-name collisions cannot satisfy allowlist entries. This ships in the next npm release. Thanks @jiseoung for reporting.
|
||||
- Security/Group policy: harden `channels.*.groups.*.toolsBySender` matching by requiring explicit sender-key types (`id:`, `e164:`, `username:`, `name:`), preventing cross-identifier collisions across mutable/display-name fields while keeping legacy untyped keys on a deprecated ID-only path. This ships in the next npm release. Thanks @jiseoung for reporting.
|
||||
|
||||
@@ -1371,3 +1371,72 @@ describe("initSessionState stale threadId fallback", () => {
|
||||
expect(result.sessionEntry.lastThreadId).toBe(99);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initSessionState internal channel routing preservation", () => {
|
||||
it("keeps persisted external lastChannel when OriginatingChannel is internal webchat", async () => {
|
||||
const storePath = await createStorePath("preserve-external-channel-");
|
||||
const sessionKey = "agent:main:telegram:group:12345";
|
||||
await saveSessionStore(storePath, {
|
||||
[sessionKey]: {
|
||||
sessionId: "sess-1",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "telegram",
|
||||
lastTo: "group:12345",
|
||||
deliveryContext: {
|
||||
channel: "telegram",
|
||||
to: "group:12345",
|
||||
},
|
||||
},
|
||||
});
|
||||
const cfg = { session: { store: storePath } } as OpenClawConfig;
|
||||
|
||||
const result = await initSessionState({
|
||||
ctx: {
|
||||
Body: "internal follow-up",
|
||||
SessionKey: sessionKey,
|
||||
OriginatingChannel: "webchat",
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.sessionEntry.lastChannel).toBe("telegram");
|
||||
expect(result.sessionEntry.deliveryContext?.channel).toBe("telegram");
|
||||
});
|
||||
|
||||
it("uses session key channel hint when first turn is internal webchat", async () => {
|
||||
const storePath = await createStorePath("session-key-channel-hint-");
|
||||
const sessionKey = "agent:main:telegram:group:98765";
|
||||
const cfg = { session: { store: storePath } } as OpenClawConfig;
|
||||
|
||||
const result = await initSessionState({
|
||||
ctx: {
|
||||
Body: "hello",
|
||||
SessionKey: sessionKey,
|
||||
OriginatingChannel: "webchat",
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.sessionEntry.lastChannel).toBe("telegram");
|
||||
expect(result.sessionEntry.deliveryContext?.channel).toBe("telegram");
|
||||
});
|
||||
|
||||
it("keeps webchat channel for webchat/main sessions", async () => {
|
||||
const storePath = await createStorePath("preserve-webchat-main-");
|
||||
const cfg = { session: { store: storePath } } as OpenClawConfig;
|
||||
|
||||
const result = await initSessionState({
|
||||
ctx: {
|
||||
Body: "hello",
|
||||
SessionKey: "agent:main:main",
|
||||
OriginatingChannel: "webchat",
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.sessionEntry.lastChannel).toBe("webchat");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,13 @@ import { deliverSessionMaintenanceWarning } from "../../infra/session-maintenanc
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
|
||||
import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.js";
|
||||
import {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
isDeliverableMessageChannel,
|
||||
normalizeMessageChannel,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import { normalizeInboundTextNewlines } from "./inbound-text.js";
|
||||
@@ -39,6 +45,18 @@ import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
|
||||
const log = createSubsystemLogger("session-init");
|
||||
|
||||
function resolveSessionKeyChannelHint(sessionKey?: string): string | undefined {
|
||||
const parsed = parseAgentSessionKey(sessionKey);
|
||||
if (!parsed?.rest) {
|
||||
return undefined;
|
||||
}
|
||||
const head = parsed.rest.split(":")[0]?.trim().toLowerCase();
|
||||
if (!head || head === "main" || head === "cron" || head === "subagent" || head === "acp") {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeMessageChannel(head);
|
||||
}
|
||||
|
||||
export type SessionInitResult = {
|
||||
sessionCtx: TemplateContext;
|
||||
sessionEntry: SessionEntry;
|
||||
@@ -267,7 +285,28 @@ export async function initSessionState(params: {
|
||||
|
||||
const baseEntry = !isNewSession && freshEntry ? entry : undefined;
|
||||
// Track the originating channel/to for announce routing (subagent announce-back).
|
||||
const lastChannelRaw = (ctx.OriginatingChannel as string | undefined) || baseEntry?.lastChannel;
|
||||
const originatingChannelRaw = ctx.OriginatingChannel as string | undefined;
|
||||
const originatingChannel = normalizeMessageChannel(originatingChannelRaw);
|
||||
const persistedChannel = normalizeMessageChannel(baseEntry?.lastChannel);
|
||||
const sessionKeyChannelHint = resolveSessionKeyChannelHint(sessionKey);
|
||||
let lastChannelRaw = originatingChannelRaw || baseEntry?.lastChannel;
|
||||
// Internal webchat/system turns should not overwrite previously known external
|
||||
// delivery routes (or explicit channel hints encoded in the session key).
|
||||
if (originatingChannel === INTERNAL_MESSAGE_CHANNEL) {
|
||||
if (
|
||||
persistedChannel &&
|
||||
persistedChannel !== INTERNAL_MESSAGE_CHANNEL &&
|
||||
isDeliverableMessageChannel(persistedChannel)
|
||||
) {
|
||||
lastChannelRaw = persistedChannel;
|
||||
} else if (
|
||||
sessionKeyChannelHint &&
|
||||
sessionKeyChannelHint !== INTERNAL_MESSAGE_CHANNEL &&
|
||||
isDeliverableMessageChannel(sessionKeyChannelHint)
|
||||
) {
|
||||
lastChannelRaw = sessionKeyChannelHint;
|
||||
}
|
||||
}
|
||||
const lastToRaw = ctx.OriginatingTo || ctx.To || baseEntry?.lastTo;
|
||||
const lastAccountIdRaw = ctx.AccountId || baseEntry?.lastAccountId;
|
||||
// Only fall back to persisted threadId for thread sessions. Non-thread
|
||||
|
||||
Reference in New Issue
Block a user