Compare commits

...

10 Commits

6 changed files with 72 additions and 3 deletions

View File

@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
- Agents/compaction safeguard pre-check: skip embedded compaction before entering the Pi SDK when a session has no real conversation messages, avoiding unnecessary LLM API calls on idle sessions. (#36451) thanks @Sid-Qin.
- Config/schema cache key stability: build merged schema cache keys with incremental hashing to avoid large single-string serialization and prevent `RangeError: Invalid string length` on high-cardinality plugin/channel metadata. (#36603) Thanks @powermaster888.
- iMessage/cron completion announces: strip leaked inline reply tags (for example `[[reply_to:6100]]`) from user-visible completion text so announcement deliveries do not expose threading metadata. (#24600) Thanks @vincentkoc.
- Control UI/iMessage duplicate reply routing: keep internal webchat turns on dispatcher delivery (instead of origin-channel reroute) so Control UI chats do not duplicate replies into iMessage, while preserving webchat-provider relayed routing for external surfaces. Fixes #33483. Thanks @alicexmolt.
- Sessions/daily reset transcript archival: archive prior transcript files during stale-session scheduled/daily resets by capturing the previous session entry before rollover, preventing orphaned transcript files on disk. (#35493) Thanks @byungsker.
- Feishu/group slash command detection: normalize group mention wrappers before command-authorization probing so mention-prefixed commands (for example `@Bot/model` and `@Bot /reset`) are recognized as gateway commands instead of being forwarded to the agent. (#35994) Thanks @liuxiaopai-ai.
- Agents/context pruning: guard assistant thinking/text char estimation against malformed blocks (missing `thinking`/`text` strings or null entries) so pruning no longer crashes with malformed provider content. (openclaw#35146) thanks @Sid-Qin.

View File

@ -399,6 +399,58 @@ describe("dispatchReplyFromConfig", () => {
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
});
it("does not route external origin replies when current surface is internal webchat without explicit delivery", async () => {
setNoAbort();
mocks.routeReply.mockClear();
const cfg = emptyConfig;
const dispatcher = createDispatcher();
const ctx = buildTestCtx({
Provider: "webchat",
Surface: "webchat",
OriginatingChannel: "imessage",
OriginatingTo: "imessage:+15550001111",
});
const replyResolver = async (
_ctx: MsgContext,
_opts?: GetReplyOptions,
_cfg?: OpenClawConfig,
) => ({ text: "hi" }) satisfies ReplyPayload;
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
expect(mocks.routeReply).not.toHaveBeenCalled();
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
});
it("routes external origin replies for internal webchat turns when explicit delivery is set", async () => {
setNoAbort();
mocks.routeReply.mockClear();
const cfg = emptyConfig;
const dispatcher = createDispatcher();
const ctx = buildTestCtx({
Provider: "webchat",
Surface: "webchat",
OriginatingChannel: "imessage",
OriginatingTo: "imessage:+15550001111",
ExplicitDeliverRoute: true,
});
const replyResolver = async (
_ctx: MsgContext,
_opts?: GetReplyOptions,
_cfg?: OpenClawConfig,
) => ({ text: "hi" }) satisfies ReplyPayload;
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
expect(mocks.routeReply).toHaveBeenCalledWith(
expect.objectContaining({
channel: "imessage",
to: "imessage:+15550001111",
}),
);
});
it("routes media-only tool results when summaries are suppressed", async () => {
setNoAbort();
mocks.routeReply.mockClear();

View File

@ -215,8 +215,15 @@ export async function dispatchReplyFromConfig(params: {
const surfaceChannel = normalizeMessageChannel(ctx.Surface);
// Prefer provider channel because surface may carry origin metadata in relayed flows.
const currentSurface = providerChannel ?? surfaceChannel;
const isInternalWebchatTurn =
currentSurface === INTERNAL_MESSAGE_CHANNEL &&
(surfaceChannel === INTERNAL_MESSAGE_CHANNEL || !surfaceChannel) &&
ctx.ExplicitDeliverRoute !== true;
const shouldRouteToOriginating = Boolean(
isRoutableChannel(originatingChannel) && originatingTo && originatingChannel !== currentSurface,
!isInternalWebchatTurn &&
isRoutableChannel(originatingChannel) &&
originatingTo &&
originatingChannel !== currentSurface,
);
const shouldSuppressTyping =
shouldRouteToOriginating || originatingChannel === INTERNAL_MESSAGE_CHANNEL;

View File

@ -159,6 +159,11 @@ export type MsgContext = {
* The chat/channel/user ID where the reply should be sent.
*/
OriginatingTo?: string;
/**
* True when the current turn intentionally requested external delivery to
* OriginatingChannel/OriginatingTo, rather than inheriting stale session route metadata.
*/
ExplicitDeliverRoute?: boolean;
/**
* Provider-specific parent conversation id for threaded contexts.
* For Discord threads, this is the parent channel id.

View File

@ -393,6 +393,7 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
expect.objectContaining({
OriginatingChannel: "telegram",
OriginatingTo: "telegram:6812765697",
ExplicitDeliverRoute: true,
AccountId: "default",
MessageThreadId: 42,
}),
@ -566,6 +567,7 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
expect.objectContaining({
OriginatingChannel: "webchat",
OriginatingTo: undefined,
ExplicitDeliverRoute: false,
AccountId: undefined,
}),
);

View File

@ -906,13 +906,14 @@ export const chatHandlers: GatewayRequestHandlers = {
(isChannelScopedSession || hasLegacyChannelPeerShape)) ||
(isConfiguredMainSessionScope && client?.connect !== undefined && !isFromWebchatClient)),
);
const hasDeliverableRoute =
const hasDeliverableRoute = Boolean(
shouldDeliverExternally &&
canInheritDeliverableRoute &&
routeChannelCandidate &&
routeChannelCandidate !== INTERNAL_MESSAGE_CHANNEL &&
typeof routeToCandidate === "string" &&
routeToCandidate.trim().length > 0;
routeToCandidate.trim().length > 0,
);
const originatingChannel = hasDeliverableRoute
? routeChannelCandidate
: INTERNAL_MESSAGE_CHANNEL;
@ -935,6 +936,7 @@ export const chatHandlers: GatewayRequestHandlers = {
Surface: INTERNAL_MESSAGE_CHANNEL,
OriginatingChannel: originatingChannel,
OriginatingTo: originatingTo,
ExplicitDeliverRoute: hasDeliverableRoute,
AccountId: accountId,
MessageThreadId: messageThreadId,
ChatType: "direct",