From d9ffc1aa63460f5a325bd4cc2e08de7cf897b912 Mon Sep 17 00:00:00 2001 From: Alex Knight Date: Wed, 6 May 2026 18:57:32 +1000 Subject: [PATCH] fix cron run binding route (#78373) Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com> --- CHANGELOG.md | 1 + ...onversation-route.base-session-key.test.ts | 59 ++++++++++++++++++- src/channels/plugins/binding-routing.test.ts | 22 +++++++ src/channels/plugins/binding-routing.ts | 11 ++++ 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db8875d6fe1..35eb8cd9402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,7 @@ Docs: https://docs.openclaw.ai - Providers/xAI: clamp the bundled xAI thinking profile to `off` so live Gateway runs cannot send unsupported reasoning levels to native Grok Responses models. - Matrix/approvals: retry approval delivery up to 3 times with a short backoff so transient Matrix send failures do not strand pending approval prompts. (#78179) Thanks @Patrick-Erichsen. - Discord/gateway: measure heartbeat ACK timeouts from the actual heartbeat send, preventing late initial heartbeats from triggering false reconnect loops while the channel is still awaiting readiness. Fixes #77668. (#78087) Thanks @bryce-d-greybeard and @NikolaFC. +- Channels/cron: ignore stale runtime conversation bindings that point at completed isolated cron run sessions, so follow-up DMs fall back to their normal route instead of reusing a closed cron task prompt. Fixes #78074. Thanks @amknight. - Discord/guilds: route plain text control commands such as `/steer` through the normal authorization and mention gate instead of silently dropping them before an agent session can see them. Fixes #78080. Thanks @ramitrkar-hash. - Control UI/Sessions: make the compaction count a compact `N Checkpoint(s)` disclosure and show expanded session-level details with modern checkpoint history cards across responsive table layouts. Thanks @BunsDev. - Control UI/performance: keep chat and channel tabs responsive while history payloads and channel probes are slow, label partial channel status, and record slow chat/config render timings in the event log. Thanks @BunsDev. diff --git a/extensions/telegram/src/conversation-route.base-session-key.test.ts b/extensions/telegram/src/conversation-route.base-session-key.test.ts index 4df685afbdb..c97c8922fd1 100644 --- a/extensions/telegram/src/conversation-route.base-session-key.test.ts +++ b/extensions/telegram/src/conversation-route.base-session-key.test.ts @@ -1,11 +1,23 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import { + __testing as conversationBindingTesting, + registerSessionBindingAdapter, + type SessionBindingAdapter, +} from "openclaw/plugin-sdk/conversation-runtime"; import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing"; -import { describe, expect, it } from "vitest"; -import { resolveTelegramConversationBaseSessionKey } from "./conversation-route.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + resolveTelegramConversationBaseSessionKey, + resolveTelegramConversationRoute, +} from "./conversation-route.js"; describe("resolveTelegramConversationBaseSessionKey", () => { const cfg: OpenClawConfig = {}; + beforeEach(() => { + conversationBindingTesting.resetSessionBindingAdaptersForTests(); + }); + it("keeps default-account DMs on the route session key", () => { expect( resolveTelegramConversationBaseSessionKey({ @@ -105,4 +117,47 @@ describe("resolveTelegramConversationBaseSessionKey", () => { }).sessionKey, ).toBe("agent:main:telegram:personal:direct:12345:thread:12345:99"); }); + + it("keeps inbound DMs on the main route when a stale runtime binding points at a cron run", () => { + const touch = vi.fn>(); + registerSessionBindingAdapter({ + channel: "telegram", + accountId: "default", + listBySession: () => [], + resolveByConversation: () => ({ + bindingId: "binding-cron-run", + targetSessionKey: "agent:youtube:cron:monthly-report:run:closed-run-1", + targetKind: "session", + conversation: { + channel: "telegram", + accountId: "default", + conversationId: "12345", + }, + status: "active", + boundAt: 1, + }), + touch, + }); + + const result = resolveTelegramConversationRoute({ + cfg: { + session: { + dmScope: "main", + }, + }, + accountId: "default", + chatId: 12345, + isGroup: false, + senderId: 12345, + }); + + expect(touch).not.toHaveBeenCalled(); + expect(result.configuredBinding).toBeNull(); + expect(result.configuredBindingSessionKey).toBe(""); + expect(result.route).toMatchObject({ + agentId: "main", + sessionKey: "agent:main:main", + matchedBy: "default", + }); + }); }); diff --git a/src/channels/plugins/binding-routing.test.ts b/src/channels/plugins/binding-routing.test.ts index 1428832ca57..c958fa07334 100644 --- a/src/channels/plugins/binding-routing.test.ts +++ b/src/channels/plugins/binding-routing.test.ts @@ -118,6 +118,28 @@ describe("runtime conversation binding route", () => { expect(result.boundSessionKey).toBeUndefined(); expect(result.route).toBe(route); }); + + it("ignores runtime bindings that target isolated cron run sessions", () => { + const route = createRoute(); + const binding = createBinding({ + targetSessionKey: "agent:youtube:cron:monthly-report:run:closed-run-1", + }); + const { touch } = registerAdapter(binding); + + const result = resolveRuntimeConversationBindingRoute({ + route, + conversation: { + channel: "demo", + accountId: "default", + conversationId: "room-1", + }, + }); + + expect(touch).not.toHaveBeenCalled(); + expect(result.bindingRecord).toBeNull(); + expect(result.boundSessionKey).toBeUndefined(); + expect(result.route).toBe(route); + }); }); describe("ensureConfiguredBindingRouteReady", () => { diff --git a/src/channels/plugins/binding-routing.ts b/src/channels/plugins/binding-routing.ts index bcf93d95b9c..e6b9add09e3 100644 --- a/src/channels/plugins/binding-routing.ts +++ b/src/channels/plugins/binding-routing.ts @@ -8,6 +8,7 @@ import { import type { ResolvedAgentRoute } from "../../routing/resolve-route.js"; import { deriveLastRoutePolicy } from "../../routing/resolve-route.js"; import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; +import { isCronRunSessionKey } from "../../sessions/session-key-utils.js"; import { resolveConfiguredBinding } from "./binding-registry.js"; import { ensureConfiguredBindingTargetReady } from "./binding-targets.js"; import type { ConfiguredBindingResolution } from "./binding-types.js"; @@ -125,6 +126,16 @@ export function resolveRuntimeConversationBindingRoute( }; } + if (isCronRunSessionKey(boundSessionKey)) { + logVerbose( + `ignored runtime conversation binding ${bindingRecord.bindingId} to isolated cron run session ${boundSessionKey}`, + ); + return { + bindingRecord: null, + route: params.route, + }; + } + getSessionBindingService().touch(bindingRecord.bindingId); if (isPluginOwnedRuntimeBindingRecord(bindingRecord)) { return {