fix cron run binding route (#78373)

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
This commit is contained in:
Alex Knight
2026-05-06 18:57:32 +10:00
committed by GitHub
parent 0b88d6286c
commit d9ffc1aa63
4 changed files with 91 additions and 2 deletions

View File

@@ -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.

View File

@@ -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<NonNullable<SessionBindingAdapter["touch"]>>();
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",
});
});
});

View File

@@ -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", () => {

View File

@@ -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 {