From b923fe8143375ff4f15fd8d2b5763dff6486ebbd Mon Sep 17 00:00:00 2001 From: Otoris Date: Sun, 15 Mar 2026 10:44:08 -0400 Subject: [PATCH 1/7] feat: named DM session infrastructure (key utils, types, store helpers) --- CHANGELOG.md | 47 +------ src/config/sessions/types.ts | 6 + src/gateway/session-utils.ts | 50 ++++++- src/sessions/session-key-utils.test.ts | 185 +++++++++++++++++++++++++ src/sessions/session-key-utils.ts | 60 ++++++++ 5 files changed, 304 insertions(+), 44 deletions(-) create mode 100644 src/sessions/session-key-utils.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bf37c1757e6..df3ab49955e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,70 +6,32 @@ Docs: https://docs.openclaw.ai ### Changes +- Sessions/DM: add `/resume ` command for named DM session switching — users can create, switch between, and list isolated named sessions within a single DM, each with its own context and history. (#5284) Thanks @Otoris. - Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl. - Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman. - Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327. - Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl. -- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029) +- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. - Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob. -- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280. -- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898. -- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs. ### Fixes -- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles. - Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong. -- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom. -- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc. -- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw. +- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc. +- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. Thanks @Coobiw. - Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob. - Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc. - Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults. - Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969) -- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao. - Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28. - Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146) -- Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts. - Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. -- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup. -- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. Thanks @vincentkoc. -- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc. -- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc. -- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc. -- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc. -- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc. -- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc. -- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason. -- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava. -- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT. -- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason. -- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026. -- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`) -- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc. - Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898. - CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob. - Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0. - Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark. - Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc. - Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411) -- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc. -- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc. -- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent. -- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274) -- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. Thanks @vincentkoc. -- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc. -- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc. -- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc. -- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman. -- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc. -- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc. -- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc. -- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc. -- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. -- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) thanks @gumadeiras. -- Gateway/watch mode: recreate bundled plugin runtime metadata after clean or stale `dist` states, so `pnpm gateway:watch` no longer fails on missing `dist/extensions/*/openclaw.plugin.json` manifests after a rebuild. Thanks @gumadeiras. -- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc. ## 2026.3.13 @@ -105,7 +67,6 @@ Docs: https://docs.openclaw.ai - macOS/exec approvals: respect per-agent exec approval settings in the gateway prompter, including allowlist fallback when the native prompt cannot be shown, so gateway-triggered `system.run` requests follow configured policy instead of always prompting or denying unexpectedly. (#13707) Thanks @sliekens. - Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus. - Telegram/inbound media IPv4 fallback: retry SSRF-guarded Telegram file downloads once with the same IPv4 fallback policy as Bot API calls so fresh installs on IPv6-broken hosts no longer fail to download inbound images. -- Commands/onboarding: split static auth-choice help from the plugin-backed onboarding catalog so `openclaw onboard` registration no longer pulls provider-wizard imports just to describe `--auth-choice`. (#47545) Thanks @vincentkoc. - Windows/gateway install: bound `schtasks` calls and fall back to the Startup-folder login item when task creation hangs, so native `openclaw gateway install` fails fast instead of wedging forever on broken Scheduled Task setups. - Windows/gateway stop: resolve Startup-folder fallback listeners from the installed `gateway.cmd` port, so `openclaw gateway stop` now actually kills fallback-launched gateway processes before restart. - Windows/gateway status: reuse the installed service command environment when reading runtime status, so startup-fallback gateways keep reporting the configured port and running state in `gateway status --json` instead of falling back to `gateway port unknown`. diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index 4ba9b336127..4efa4578156 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -76,6 +76,12 @@ export type SessionEntry = { sessionId: string; updatedAt: number; sessionFile?: string; + /** + * Active named session for DM routing (ETH-608). + * When set, DM messages from this peer route to the named session instead of main. + * Format: "valorant", "work", etc. (session name only, not full key). + */ + activeNamedSession?: string; /** Parent session key that spawned this session (used for sandbox session-tool scoping). */ spawnedBy?: string; /** Workspace inherited by spawned sessions and reused on later turns for the same child session. */ diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 00a2cb7747e..800bda7f215 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -30,7 +30,7 @@ import { normalizeMainKey, parseAgentSessionKey, } from "../routing/session-key.js"; -import { isCronRunSessionKey } from "../sessions/session-key-utils.js"; +import { buildNamedDmSessionKey, isCronRunSessionKey } from "../sessions/session-key-utils.js"; import { AVATAR_MAX_BYTES, isAvatarDataUrl, @@ -843,6 +843,54 @@ export function resolveSessionModelIdentityRef( return { provider: resolved.provider, model: resolved.model }; } +/** + * Get the active named session key for a DM peer, if one is set. + * Returns the full named session key if active, or null otherwise. + */ +export function getActiveNamedSessionKey(params: { + mainEntry: SessionEntry | undefined; + agentId: string; + peerId: string; +}): string | null { + if (!params.mainEntry || !params.mainEntry.activeNamedSession) { + return null; + } + const name = params.mainEntry.activeNamedSession.trim(); + if (!name) { + return null; + } + return buildNamedDmSessionKey({ + agentId: params.agentId, + peerId: params.peerId, + name, + }); +} + +/** + * Set or clear the active named session for a DM peer on the main session entry. + * If name is null/empty, clears the active named session (returns to main). + * Returns true if the update was applied. + */ +export function setActiveNamedSession(params: { + mainEntry: SessionEntry; + name: string | null | undefined; +}): boolean { + const trimmed = params.name?.trim(); + if (!trimmed) { + if (params.mainEntry.activeNamedSession !== undefined) { + delete params.mainEntry.activeNamedSession; + return true; + } + return false; + } + const normalized = trimmed.toLowerCase(); + if (params.mainEntry.activeNamedSession !== normalized) { + params.mainEntry.activeNamedSession = normalized; + return true; + } + return false; +} + export function listSessionsFromStore(params: { cfg: OpenClawConfig; storePath: string; diff --git a/src/sessions/session-key-utils.test.ts b/src/sessions/session-key-utils.test.ts new file mode 100644 index 00000000000..7e9a830ed2c --- /dev/null +++ b/src/sessions/session-key-utils.test.ts @@ -0,0 +1,185 @@ +import { describe, expect, it } from "vitest"; +import { + buildNamedDmSessionKey, + isNamedDmSessionKey, + parseNamedDmSessionKey, +} from "./session-key-utils.js"; + +describe("Named DM Session Keys (ETH-608)", () => { + describe("buildNamedDmSessionKey", () => { + it("builds a valid named DM session key", () => { + const key = buildNamedDmSessionKey({ + agentId: "main", + peerId: "123456789", + name: "valorant", + }); + expect(key).toBe("agent:main:dm-named:123456789:valorant"); + }); + + it("normalizes inputs to lowercase", () => { + const key = buildNamedDmSessionKey({ + agentId: "MAIN", + peerId: "USER123", + name: "MySession", + }); + expect(key).toBe("agent:main:dm-named:user123:mysession"); + }); + + it("throws on empty agentId", () => { + expect(() => + buildNamedDmSessionKey({ + agentId: "", + peerId: "123", + name: "work", + }), + ).toThrow("agentId, peerId, and name are required"); + }); + + it("throws on empty peerId", () => { + expect(() => + buildNamedDmSessionKey({ + agentId: "main", + peerId: "", + name: "work", + }), + ).toThrow("agentId, peerId, and name are required"); + }); + + it("throws on empty name", () => { + expect(() => + buildNamedDmSessionKey({ + agentId: "main", + peerId: "123", + name: "", + }), + ).toThrow("agentId, peerId, and name are required"); + }); + + it("trims whitespace", () => { + const key = buildNamedDmSessionKey({ + agentId: " main ", + peerId: " 123 ", + name: " work ", + }); + expect(key).toBe("agent:main:dm-named:123:work"); + }); + }); + + describe("isNamedDmSessionKey", () => { + it("returns true for valid named DM session keys", () => { + expect(isNamedDmSessionKey("agent:main:dm-named:123456789:valorant")).toBe(true); + expect(isNamedDmSessionKey("agent:ops:dm-named:999:work")).toBe(true); + }); + + it("returns false for non-agent keys", () => { + expect(isNamedDmSessionKey("main")).toBe(false); + expect(isNamedDmSessionKey("discord:direct:123")).toBe(false); + }); + + it("returns false for agent keys that are not named DM keys", () => { + expect(isNamedDmSessionKey("agent:main:main")).toBe(false); + expect(isNamedDmSessionKey("agent:main:direct:123")).toBe(false); + expect(isNamedDmSessionKey("agent:main:group:456")).toBe(false); + }); + + it("returns false for malformed named DM keys", () => { + expect(isNamedDmSessionKey("agent:main:dm-named")).toBe(false); + expect(isNamedDmSessionKey("agent:main:dm-named:123")).toBe(false); + expect(isNamedDmSessionKey("agent:main:dm-named:123:work:extra")).toBe(false); + }); + + it("returns false for null/undefined/empty", () => { + expect(isNamedDmSessionKey(null)).toBe(false); + expect(isNamedDmSessionKey(undefined)).toBe(false); + expect(isNamedDmSessionKey("")).toBe(false); + }); + }); + + describe("parseNamedDmSessionKey", () => { + it("parses valid named DM session keys", () => { + const result = parseNamedDmSessionKey("agent:main:dm-named:123456789:valorant"); + expect(result).toEqual({ + agentId: "main", + peerId: "123456789", + name: "valorant", + }); + }); + + it("normalizes to lowercase", () => { + const result = parseNamedDmSessionKey("AGENT:MAIN:DM-NAMED:USER123:WORK"); + expect(result).toEqual({ + agentId: "main", + peerId: "user123", + name: "work", + }); + }); + + it("returns null for non-agent keys", () => { + expect(parseNamedDmSessionKey("main")).toBe(null); + expect(parseNamedDmSessionKey("discord:direct:123")).toBe(null); + }); + + it("returns null for non-named-DM agent keys", () => { + expect(parseNamedDmSessionKey("agent:main:main")).toBe(null); + expect(parseNamedDmSessionKey("agent:main:direct:123")).toBe(null); + }); + + it("returns null for malformed named DM keys", () => { + expect(parseNamedDmSessionKey("agent:main:dm-named")).toBe(null); + expect(parseNamedDmSessionKey("agent:main:dm-named:123")).toBe(null); + }); + + it("returns null for null/undefined/empty", () => { + expect(parseNamedDmSessionKey(null)).toBe(null); + expect(parseNamedDmSessionKey(undefined)).toBe(null); + expect(parseNamedDmSessionKey("")).toBe(null); + }); + + it("handles multiple agents", () => { + const result1 = parseNamedDmSessionKey("agent:ops:dm-named:456:project-x"); + expect(result1).toEqual({ + agentId: "ops", + peerId: "456", + name: "project-x", + }); + + const result2 = parseNamedDmSessionKey("agent:dev:dm-named:789:testing"); + expect(result2).toEqual({ + agentId: "dev", + peerId: "789", + name: "testing", + }); + }); + }); + + describe("round-trip", () => { + it("build and parse round-trip correctly", () => { + const original = { + agentId: "main", + peerId: "123456789", + name: "valorant", + }; + + const key = buildNamedDmSessionKey(original); + const parsed = parseNamedDmSessionKey(key); + + expect(parsed).toEqual(original); + }); + + it("round-trip with normalization", () => { + const key = buildNamedDmSessionKey({ + agentId: "MAIN", + peerId: "USER123", + name: "MySession", + }); + + const parsed = parseNamedDmSessionKey(key); + + expect(parsed).toEqual({ + agentId: "main", + peerId: "user123", + name: "mysession", + }); + }); + }); +}); diff --git a/src/sessions/session-key-utils.ts b/src/sessions/session-key-utils.ts index c405df3a5ff..fe954cd7180 100644 --- a/src/sessions/session-key-utils.ts +++ b/src/sessions/session-key-utils.ts @@ -130,3 +130,63 @@ export function resolveThreadParentSessionKey( const parent = raw.slice(0, idx).trim(); return parent ? parent : null; } + +/** + * Check if a session key is a named DM session key. + * Format: agent:main:dm-named:: + */ +export function isNamedDmSessionKey(sessionKey: string | undefined | null): boolean { + const parsed = parseAgentSessionKey(sessionKey); + if (!parsed) { + return false; + } + return /^dm-named:[^:]+:[^:]+$/.test(parsed.rest); +} + +/** + * Build a named DM session key. + * Format: agent::dm-named:: + */ +export function buildNamedDmSessionKey(params: { + agentId: string; + peerId: string; + name: string; +}): string { + const agentId = params.agentId.trim().toLowerCase(); + const peerId = params.peerId.trim().toLowerCase(); + const name = params.name.trim().toLowerCase(); + if (!agentId || !peerId || !name) { + throw new Error("buildNamedDmSessionKey: agentId, peerId, and name are required"); + } + return `agent:${agentId}:dm-named:${peerId}:${name}`; +} + +/** + * Parse a named DM session key. + * Returns { agentId, peerId, name } or null if not a named DM key. + */ +export function parseNamedDmSessionKey( + sessionKey: string | undefined | null, +): { agentId: string; peerId: string; name: string } | null { + const parsed = parseAgentSessionKey(sessionKey); + if (!parsed) { + return null; + } + const parts = parsed.rest.split(":"); + if (parts.length !== 3) { + return null; + } + if (parts[0] !== "dm-named") { + return null; + } + const peerId = parts[1]?.trim(); + const name = parts[2]?.trim(); + if (!peerId || !name) { + return null; + } + return { + agentId: parsed.agentId, + peerId, + name, + }; +} From bd6c65f3f82d5838b78891dd97f1144178f55a97 Mon Sep 17 00:00:00 2001 From: Otoris Date: Sun, 15 Mar 2026 13:01:17 -0400 Subject: [PATCH 2/7] fix: restore CHANGELOG, add colon guard, fix deriveSessionChatType, add store helper tests --- CHANGELOG.md | 32 +++++++++-- src/gateway/session-utils.ts | 3 ++ src/sessions/session-key-utils.test.ts | 73 ++++++++++++++++++++++++++ src/sessions/session-key-utils.ts | 5 +- 4 files changed, 108 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3ab49955e..99e550e9eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,32 +6,56 @@ Docs: https://docs.openclaw.ai ### Changes -- Sessions/DM: add `/resume ` command for named DM session switching — users can create, switch between, and list isolated named sessions within a single DM, each with its own context and history. (#5284) Thanks @Otoris. +- Sessions/named DM: add `buildNamedDmSessionKey`, `parseNamedDmSessionKey`, and `isNamedDmSessionKey` key-format helpers, an `activeNamedSession` field on `SessionEntry`, and `setActiveNamedSession`/`getActiveNamedSessionKey` store helpers as infrastructure for named DM session switching. - Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl. - Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman. - Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327. - Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl. -- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. +- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029) - Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob. +- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280. ### Fixes +- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles. - Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong. -- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc. -- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. Thanks @Coobiw. +- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc. +- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw. - Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob. - Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc. - Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults. - Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969) +- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao. - Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28. - Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146) +- Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts. - Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. +- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc. +- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc. +- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc. +- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc. +- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc. +- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason. +- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava. +- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT. +- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason. +- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026. +- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`) +- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc. - Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898. - CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob. - Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0. - Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark. - Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc. - Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411) +- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc. +- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc. +- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent. +- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274) +- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. Thanks @vincentkoc. +- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc. +- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc. +- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc. ## 2026.3.13 diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 800bda7f215..6d2db1f3d99 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -884,6 +884,9 @@ export function setActiveNamedSession(params: { return false; } const normalized = trimmed.toLowerCase(); + if (normalized.includes(":")) { + return false; + } if (params.mainEntry.activeNamedSession !== normalized) { params.mainEntry.activeNamedSession = normalized; return true; diff --git a/src/sessions/session-key-utils.test.ts b/src/sessions/session-key-utils.test.ts index 7e9a830ed2c..95b6bb306d3 100644 --- a/src/sessions/session-key-utils.test.ts +++ b/src/sessions/session-key-utils.test.ts @@ -4,6 +4,11 @@ import { isNamedDmSessionKey, parseNamedDmSessionKey, } from "./session-key-utils.js"; +import { + getActiveNamedSessionKey, + setActiveNamedSession, +} from "../gateway/session-utils.js"; +import type { SessionEntry } from "../config/sessions.js"; describe("Named DM Session Keys (ETH-608)", () => { describe("buildNamedDmSessionKey", () => { @@ -182,4 +187,72 @@ describe("Named DM Session Keys (ETH-608)", () => { }); }); }); + + describe("setActiveNamedSession", () => { + it("sets activeNamedSession on the entry and returns true", () => { + const entry = {} as SessionEntry; + const result = setActiveNamedSession({ mainEntry: entry, name: "work" }); + expect(result).toBe(true); + expect(entry.activeNamedSession).toBe("work"); + }); + + it("is idempotent — returns false when called twice with same name", () => { + const entry = {} as SessionEntry; + setActiveNamedSession({ mainEntry: entry, name: "work" }); + const result = setActiveNamedSession({ mainEntry: entry, name: "work" }); + expect(result).toBe(false); + }); + + it("clears activeNamedSession when name is null and returns true", () => { + const entry = { activeNamedSession: "work" } as SessionEntry; + const result = setActiveNamedSession({ mainEntry: entry, name: null }); + expect(result).toBe(true); + expect(entry.activeNamedSession).toBeUndefined(); + }); + + it("returns false when clearing an already-cleared entry", () => { + const entry = {} as SessionEntry; + const result = setActiveNamedSession({ mainEntry: entry, name: null }); + expect(result).toBe(false); + }); + + it("rejects names containing colons and returns false", () => { + const entry = {} as SessionEntry; + const result = setActiveNamedSession({ mainEntry: entry, name: "foo:bar" }); + expect(result).toBe(false); + expect(entry.activeNamedSession).toBeUndefined(); + }); + + it("normalizes name to lowercase", () => { + const entry = {} as SessionEntry; + setActiveNamedSession({ mainEntry: entry, name: "Work" }); + expect(entry.activeNamedSession).toBe("work"); + }); + }); + + describe("getActiveNamedSessionKey", () => { + it("returns null when mainEntry is undefined", () => { + const result = getActiveNamedSessionKey({ mainEntry: undefined, agentId: "main", peerId: "123" }); + expect(result).toBeNull(); + }); + + it("returns null when activeNamedSession is not set", () => { + const entry = {} as SessionEntry; + const result = getActiveNamedSessionKey({ mainEntry: entry, agentId: "main", peerId: "123" }); + expect(result).toBeNull(); + }); + + it("returns null when activeNamedSession is whitespace-only", () => { + const entry = { activeNamedSession: " " } as SessionEntry; + const result = getActiveNamedSessionKey({ mainEntry: entry, agentId: "main", peerId: "123" }); + expect(result).toBeNull(); + }); + + it("returns the correct named session key on round-trip", () => { + const entry = {} as SessionEntry; + setActiveNamedSession({ mainEntry: entry, name: "work" }); + const result = getActiveNamedSessionKey({ mainEntry: entry, agentId: "main", peerId: "123" }); + expect(result).toBe("agent:main:dm-named:123:work"); + }); + }); }); diff --git a/src/sessions/session-key-utils.ts b/src/sessions/session-key-utils.ts index fe954cd7180..339f601eaba 100644 --- a/src/sessions/session-key-utils.ts +++ b/src/sessions/session-key-utils.ts @@ -47,7 +47,7 @@ export function deriveSessionChatType(sessionKey: string | undefined | null): Se if (tokens.has("channel")) { return "channel"; } - if (tokens.has("direct") || tokens.has("dm")) { + if (tokens.has("direct") || tokens.has("dm") || tokens.has("dm-named")) { return "direct"; } // Legacy Discord keys can be shaped like: @@ -158,6 +158,9 @@ export function buildNamedDmSessionKey(params: { if (!agentId || !peerId || !name) { throw new Error("buildNamedDmSessionKey: agentId, peerId, and name are required"); } + if (name.includes(":")) { + throw new Error(`buildNamedDmSessionKey: name must not contain ":" (got: "${name}")`); + } return `agent:${agentId}:dm-named:${peerId}:${name}`; } From 369cea10dfc3fb97ee48d7e49078f61ea400cd83 Mon Sep 17 00:00:00 2001 From: Otoris Date: Sun, 15 Mar 2026 14:28:41 -0400 Subject: [PATCH 3/7] fix: add peerId colon guard, restore CHANGELOG --- CHANGELOG.md | 7 +++++++ src/sessions/session-key-utils.test.ts | 10 ++++++++++ src/sessions/session-key-utils.ts | 3 +++ 3 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99e550e9eb2..a70de1701ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles. - Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong. +- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom. - Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc. - Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw. - Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob. @@ -30,6 +31,8 @@ Docs: https://docs.openclaw.ai - Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146) - Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts. - Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. +- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. Thanks @vincentkoc. +- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc. - Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc. - Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc. - Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc. @@ -56,6 +59,10 @@ Docs: https://docs.openclaw.ai - CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc. - Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc. - ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc. +- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman. +- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc. +- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc. +- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc. ## 2026.3.13 diff --git a/src/sessions/session-key-utils.test.ts b/src/sessions/session-key-utils.test.ts index 95b6bb306d3..018c5df40f0 100644 --- a/src/sessions/session-key-utils.test.ts +++ b/src/sessions/session-key-utils.test.ts @@ -60,6 +60,16 @@ describe("Named DM Session Keys (ETH-608)", () => { ).toThrow("agentId, peerId, and name are required"); }); + it("throws when peerId contains a colon", () => { + expect(() => + buildNamedDmSessionKey({ + agentId: "main", + peerId: "123:456", + name: "work", + }), + ).toThrow(); + }); + it("trims whitespace", () => { const key = buildNamedDmSessionKey({ agentId: " main ", diff --git a/src/sessions/session-key-utils.ts b/src/sessions/session-key-utils.ts index 339f601eaba..f84692ed762 100644 --- a/src/sessions/session-key-utils.ts +++ b/src/sessions/session-key-utils.ts @@ -158,6 +158,9 @@ export function buildNamedDmSessionKey(params: { if (!agentId || !peerId || !name) { throw new Error("buildNamedDmSessionKey: agentId, peerId, and name are required"); } + if (peerId.includes(":")) { + throw new Error(`buildNamedDmSessionKey: peerId must not contain ":" (got: "${peerId}")`); + } if (name.includes(":")) { throw new Error(`buildNamedDmSessionKey: name must not contain ":" (got: "${name}")`); } From 3d6e0cfa42703cafdc0908e69f203ebd818b5c32 Mon Sep 17 00:00:00 2001 From: Otoris Date: Sun, 15 Mar 2026 15:36:47 -0400 Subject: [PATCH 4/7] fix: sync CHANGELOG to latest main, add agentId colon guard --- CHANGELOG.md | 5 ++++- src/sessions/session-key-utils.test.ts | 10 ++++++++++ src/sessions/session-key-utils.ts | 3 +++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a70de1701ca..134750de66a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ Docs: https://docs.openclaw.ai ## Unreleased ### Changes - - Sessions/named DM: add `buildNamedDmSessionKey`, `parseNamedDmSessionKey`, and `isNamedDmSessionKey` key-format helpers, an `activeNamedSession` field on `SessionEntry`, and `setActiveNamedSession`/`getActiveNamedSessionKey` store helpers as infrastructure for named DM session switching. + - Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl. - Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman. - Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327. @@ -63,6 +63,8 @@ Docs: https://docs.openclaw.ai - CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc. - CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc. - CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc. +- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc. +- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. ## 2026.3.13 @@ -98,6 +100,7 @@ Docs: https://docs.openclaw.ai - macOS/exec approvals: respect per-agent exec approval settings in the gateway prompter, including allowlist fallback when the native prompt cannot be shown, so gateway-triggered `system.run` requests follow configured policy instead of always prompting or denying unexpectedly. (#13707) Thanks @sliekens. - Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus. - Telegram/inbound media IPv4 fallback: retry SSRF-guarded Telegram file downloads once with the same IPv4 fallback policy as Bot API calls so fresh installs on IPv6-broken hosts no longer fail to download inbound images. +- Commands/onboarding: split static auth-choice help from the plugin-backed onboarding catalog so `openclaw onboard` registration no longer pulls provider-wizard imports just to describe `--auth-choice`. (#47545) Thanks @vincentkoc. - Windows/gateway install: bound `schtasks` calls and fall back to the Startup-folder login item when task creation hangs, so native `openclaw gateway install` fails fast instead of wedging forever on broken Scheduled Task setups. - Windows/gateway stop: resolve Startup-folder fallback listeners from the installed `gateway.cmd` port, so `openclaw gateway stop` now actually kills fallback-launched gateway processes before restart. - Windows/gateway status: reuse the installed service command environment when reading runtime status, so startup-fallback gateways keep reporting the configured port and running state in `gateway status --json` instead of falling back to `gateway port unknown`. diff --git a/src/sessions/session-key-utils.test.ts b/src/sessions/session-key-utils.test.ts index 018c5df40f0..cd84700314b 100644 --- a/src/sessions/session-key-utils.test.ts +++ b/src/sessions/session-key-utils.test.ts @@ -60,6 +60,16 @@ describe("Named DM Session Keys (ETH-608)", () => { ).toThrow("agentId, peerId, and name are required"); }); + it("throws when agentId contains a colon", () => { + expect(() => + buildNamedDmSessionKey({ + agentId: "ma:in", + peerId: "123", + name: "work", + }), + ).toThrow(); + }); + it("throws when peerId contains a colon", () => { expect(() => buildNamedDmSessionKey({ diff --git a/src/sessions/session-key-utils.ts b/src/sessions/session-key-utils.ts index f84692ed762..e8a6bfab72b 100644 --- a/src/sessions/session-key-utils.ts +++ b/src/sessions/session-key-utils.ts @@ -158,6 +158,9 @@ export function buildNamedDmSessionKey(params: { if (!agentId || !peerId || !name) { throw new Error("buildNamedDmSessionKey: agentId, peerId, and name are required"); } + if (agentId.includes(":")) { + throw new Error(`buildNamedDmSessionKey: agentId must not contain ":" (got: "${agentId}")`); + } if (peerId.includes(":")) { throw new Error(`buildNamedDmSessionKey: peerId must not contain ":" (got: "${peerId}")`); } From dfdebb45b774edd46994349eb0ff7e1dc2f6c64e Mon Sep 17 00:00:00 2001 From: Otoris Date: Sun, 15 Mar 2026 17:34:16 -0400 Subject: [PATCH 5/7] fix: sync CHANGELOG after rebase to latest main --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 134750de66a..1dd8b207509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Docs: https://docs.openclaw.ai - Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029) - Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob. - Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280. +- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898. +- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs. ### Fixes @@ -31,6 +33,7 @@ Docs: https://docs.openclaw.ai - Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146) - Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts. - Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. +- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup. - Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. Thanks @vincentkoc. - Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc. - Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc. @@ -65,6 +68,9 @@ Docs: https://docs.openclaw.ai - CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc. - CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc. - Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. +- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) thanks @gumadeiras. +- Gateway/watch mode: recreate bundled plugin runtime metadata after clean or stale `dist` states, so `pnpm gateway:watch` no longer fails on missing `dist/extensions/*/openclaw.plugin.json` manifests after a rebuild. Thanks @gumadeiras. +- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc. ## 2026.3.13 From 16a35cfbb49e18401a82665e3c5d48ce8e8f700c Mon Sep 17 00:00:00 2001 From: Otoris Date: Sun, 15 Mar 2026 17:36:03 -0400 Subject: [PATCH 6/7] fix: throw on colon in setActiveNamedSession instead of silent false --- src/gateway/session-utils.ts | 2 +- src/sessions/session-key-utils.test.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 6d2db1f3d99..5e2a22e08f9 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -885,7 +885,7 @@ export function setActiveNamedSession(params: { } const normalized = trimmed.toLowerCase(); if (normalized.includes(":")) { - return false; + throw new Error(`setActiveNamedSession: name must not contain ":" (got: "${normalized}")`); } if (params.mainEntry.activeNamedSession !== normalized) { params.mainEntry.activeNamedSession = normalized; diff --git a/src/sessions/session-key-utils.test.ts b/src/sessions/session-key-utils.test.ts index cd84700314b..85f986ea2d8 100644 --- a/src/sessions/session-key-utils.test.ts +++ b/src/sessions/session-key-utils.test.ts @@ -236,10 +236,9 @@ describe("Named DM Session Keys (ETH-608)", () => { expect(result).toBe(false); }); - it("rejects names containing colons and returns false", () => { + it("throws when name contains a colon", () => { const entry = {} as SessionEntry; - const result = setActiveNamedSession({ mainEntry: entry, name: "foo:bar" }); - expect(result).toBe(false); + expect(() => setActiveNamedSession({ mainEntry: entry, name: "foo:bar" })).toThrow(); expect(entry.activeNamedSession).toBeUndefined(); }); From a0cbe0a87c61af7d3ab860a6d941f1dec0a1eb4d Mon Sep 17 00:00:00 2001 From: Otoris Date: Sun, 15 Mar 2026 18:36:09 -0400 Subject: [PATCH 7/7] fix: apply oxfmt formatting to CHANGELOG and test file --- CHANGELOG.md | 1 + src/sessions/session-key-utils.test.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd8b207509..1e2c5972c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Docs: https://docs.openclaw.ai ## Unreleased ### Changes + - Sessions/named DM: add `buildNamedDmSessionKey`, `parseNamedDmSessionKey`, and `isNamedDmSessionKey` key-format helpers, an `activeNamedSession` field on `SessionEntry`, and `setActiveNamedSession`/`getActiveNamedSessionKey` store helpers as infrastructure for named DM session switching. - Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl. diff --git a/src/sessions/session-key-utils.test.ts b/src/sessions/session-key-utils.test.ts index 85f986ea2d8..9e55bd5312b 100644 --- a/src/sessions/session-key-utils.test.ts +++ b/src/sessions/session-key-utils.test.ts @@ -1,14 +1,11 @@ import { describe, expect, it } from "vitest"; +import type { SessionEntry } from "../config/sessions.js"; +import { getActiveNamedSessionKey, setActiveNamedSession } from "../gateway/session-utils.js"; import { buildNamedDmSessionKey, isNamedDmSessionKey, parseNamedDmSessionKey, } from "./session-key-utils.js"; -import { - getActiveNamedSessionKey, - setActiveNamedSession, -} from "../gateway/session-utils.js"; -import type { SessionEntry } from "../config/sessions.js"; describe("Named DM Session Keys (ETH-608)", () => { describe("buildNamedDmSessionKey", () => { @@ -251,7 +248,11 @@ describe("Named DM Session Keys (ETH-608)", () => { describe("getActiveNamedSessionKey", () => { it("returns null when mainEntry is undefined", () => { - const result = getActiveNamedSessionKey({ mainEntry: undefined, agentId: "main", peerId: "123" }); + const result = getActiveNamedSessionKey({ + mainEntry: undefined, + agentId: "main", + peerId: "123", + }); expect(result).toBeNull(); });