diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe720c79..55dacd73e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - Config/heartbeat legacy-path handling: auto-migrate top-level `heartbeat` into `agents.defaults.heartbeat` (with merge semantics that preserve explicit defaults), and keep startup failures on non-migratable legacy entries in the detailed invalid-config path instead of generic migration-failed errors. (#32706) thanks @xiwan. - Plugins/SDK subpath parity: add channel-specific plugin SDK subpaths for Discord, Slack, Signal, iMessage, WhatsApp, and LINE; migrate bundled plugin entrypoints to scoped subpaths/core with CI guardrails; and keep `openclaw/plugin-sdk` root import compatibility for existing external plugins. (#33737) thanks @gumadeiras. - Routing/session duplicate suppression synthesis: align shared session delivery-context inheritance, channel-paired route-field merges, and reply-surface target matching so dmScope=main turns avoid cross-surface duplicate replies while thread-aware forwarding keeps intended routing semantics. (from #33629, #26889, #17337, #33250) Thanks @Yuandiaodiaodiao, @kevinwildenradt, @Glucksberg, and @bmendonca3. +- Routing/legacy session route inheritance: preserve external route metadata inheritance for legacy channel session keys (`agent:::` and `...:thread:`) so `chat.send` does not incorrectly fall back to webchat when valid delivery context exists. Follow-up to #33786. - Security/auth labels: remove token and API-key snippets from user-facing auth status labels so `/status` and `/models` do not expose credential fragments. (#33262) thanks @cu1ch3n. - Auth/credential semantics: align profile eligibility + probe diagnostics with SecretRef/expiry rules and harden browser download atomic writes. (#33733) thanks @joshavant. - Security/audit denyCommands guidance: suggest likely exact node command IDs for unknown `gateway.nodes.denyCommands` entries so ineffective denylist entries are easier to correct. (#29713) thanks @liquidhorizon88-bot. diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index 4ab6875ff..b6f6fce38 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -448,6 +448,75 @@ describe("chat directive tag stripping for non-streaming final payloads", () => ); }); + it("chat.send inherits routing metadata for legacy channel-peer session keys", async () => { + createTranscriptFixture("openclaw-chat-send-legacy-channel-peer-routing-"); + mockState.finalText = "ok"; + mockState.sessionEntry = { + deliveryContext: { + channel: "telegram", + to: "telegram:6812765697", + accountId: "default", + }, + lastChannel: "telegram", + lastTo: "telegram:6812765697", + lastAccountId: "default", + }; + const respond = vi.fn(); + const context = createChatContext(); + + await runNonStreamingChatSend({ + context, + respond, + idempotencyKey: "idem-legacy-channel-peer-routing", + sessionKey: "agent:main:telegram:6812765697", + expectBroadcast: false, + }); + + expect(mockState.lastDispatchCtx).toEqual( + expect.objectContaining({ + OriginatingChannel: "telegram", + OriginatingTo: "telegram:6812765697", + AccountId: "default", + }), + ); + }); + + it("chat.send inherits routing metadata for legacy channel-peer thread session keys", async () => { + createTranscriptFixture("openclaw-chat-send-legacy-thread-channel-peer-routing-"); + mockState.finalText = "ok"; + mockState.sessionEntry = { + deliveryContext: { + channel: "telegram", + to: "telegram:6812765697", + accountId: "default", + threadId: "42", + }, + lastChannel: "telegram", + lastTo: "telegram:6812765697", + lastAccountId: "default", + lastThreadId: "42", + }; + const respond = vi.fn(); + const context = createChatContext(); + + await runNonStreamingChatSend({ + context, + respond, + idempotencyKey: "idem-legacy-thread-channel-peer-routing", + sessionKey: "agent:main:telegram:6812765697:thread:42", + expectBroadcast: false, + }); + + expect(mockState.lastDispatchCtx).toEqual( + expect.objectContaining({ + OriginatingChannel: "telegram", + OriginatingTo: "telegram:6812765697", + AccountId: "default", + MessageThreadId: "42", + }), + ); + }); + it("chat.send does not inherit external delivery context for shared main sessions", async () => { createTranscriptFixture("openclaw-chat-send-main-no-cross-route-"); mockState.finalText = "ok"; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 382a39a8e..7c8db7344 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -875,6 +875,8 @@ export const chatHandlers: GatewayRequestHandlers = { const isChannelScopedSession = sessionPeerShapeCandidates.some((part) => CHANNEL_SCOPED_SESSION_SHAPES.has(part), ); + const hasLegacyChannelPeerShape = + !isChannelScopedSession && typeof sessionScopeParts[1] === "string"; // Only inherit prior external route metadata for channel-scoped sessions. // Channel-agnostic sessions (main, direct:, etc.) can otherwise // leak stale routes across surfaces. @@ -882,7 +884,7 @@ export const chatHandlers: GatewayRequestHandlers = { sessionChannelHint && sessionChannelHint !== INTERNAL_MESSAGE_CHANNEL && !isChannelAgnosticSessionScope && - isChannelScopedSession, + (isChannelScopedSession || hasLegacyChannelPeerShape), ); const hasDeliverableRoute = canInheritDeliverableRoute &&