mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(cron): resolve accountId from agent bindings in isolated sessions
When an isolated cron session has no lastAccountId (e.g. first-run or fresh session), the message tool receives an undefined accountId which defaults to "default". In multi-account setups where accounts are named (e.g. "willy", "betty"), this causes resolveTelegramToken() to fail because accounts["default"] doesn't exist. This change adds a fallback in resolveDeliveryTarget(): when the session-derived accountId is undefined, look up the agent's bound account from the bindings config using buildChannelAccountBindings(). This mirrors the same binding resolution used for inbound routing, closing the gap between inbound and outbound account resolution. Session-derived accountId still takes precedence when present. Fixes #17889 Related: #12628, #16259 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
e9f2e6a829
commit
2ed43fd7b4
131
src/cron/isolated-agent/delivery-target.test.ts
Normal file
131
src/cron/isolated-agent/delivery-target.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
vi.mock("../../config/sessions.js", () => ({
|
||||
loadSessionStore: vi.fn().mockReturnValue({}),
|
||||
resolveAgentMainSessionKey: vi.fn().mockReturnValue("agent:test:main"),
|
||||
resolveStorePath: vi.fn().mockReturnValue("/tmp/test-store.json"),
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/outbound/channel-selection.js", () => ({
|
||||
resolveMessageChannelSelection: vi.fn().mockResolvedValue({ channel: "telegram" }),
|
||||
}));
|
||||
|
||||
import { loadSessionStore } from "../../config/sessions.js";
|
||||
import { resolveDeliveryTarget } from "./delivery-target.js";
|
||||
|
||||
function makeCfg(overrides?: Partial<OpenClawConfig>): OpenClawConfig {
|
||||
return {
|
||||
bindings: [],
|
||||
channels: {},
|
||||
...overrides,
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
describe("resolveDeliveryTarget", () => {
|
||||
it("falls back to bound accountId when session has no lastAccountId", async () => {
|
||||
vi.mocked(loadSessionStore).mockReturnValue({});
|
||||
|
||||
const cfg = makeCfg({
|
||||
bindings: [
|
||||
{
|
||||
agentId: "agent-b",
|
||||
match: { channel: "telegram", accountId: "account-b" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await resolveDeliveryTarget(cfg, "agent-b", {
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
});
|
||||
|
||||
expect(result.accountId).toBe("account-b");
|
||||
});
|
||||
|
||||
it("preserves session lastAccountId when present", async () => {
|
||||
vi.mocked(loadSessionStore).mockReturnValue({
|
||||
"agent:test:main": {
|
||||
sessionId: "sess-1",
|
||||
updatedAt: 1000,
|
||||
lastChannel: "telegram",
|
||||
lastTo: "123456",
|
||||
lastAccountId: "session-account",
|
||||
},
|
||||
});
|
||||
|
||||
const cfg = makeCfg({
|
||||
bindings: [
|
||||
{
|
||||
agentId: "agent-b",
|
||||
match: { channel: "telegram", accountId: "account-b" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await resolveDeliveryTarget(cfg, "agent-b", {
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
});
|
||||
|
||||
// Session-derived accountId should take precedence over binding
|
||||
expect(result.accountId).toBe("session-account");
|
||||
});
|
||||
|
||||
it("returns undefined accountId when no binding and no session", async () => {
|
||||
vi.mocked(loadSessionStore).mockReturnValue({});
|
||||
|
||||
const cfg = makeCfg({ bindings: [] });
|
||||
|
||||
const result = await resolveDeliveryTarget(cfg, "agent-b", {
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
});
|
||||
|
||||
expect(result.accountId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("selects correct binding when multiple agents have bindings", async () => {
|
||||
vi.mocked(loadSessionStore).mockReturnValue({});
|
||||
|
||||
const cfg = makeCfg({
|
||||
bindings: [
|
||||
{
|
||||
agentId: "agent-a",
|
||||
match: { channel: "telegram", accountId: "account-a" },
|
||||
},
|
||||
{
|
||||
agentId: "agent-b",
|
||||
match: { channel: "telegram", accountId: "account-b" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await resolveDeliveryTarget(cfg, "agent-b", {
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
});
|
||||
|
||||
expect(result.accountId).toBe("account-b");
|
||||
});
|
||||
|
||||
it("ignores bindings for different channels", async () => {
|
||||
vi.mocked(loadSessionStore).mockReturnValue({});
|
||||
|
||||
const cfg = makeCfg({
|
||||
bindings: [
|
||||
{
|
||||
agentId: "agent-b",
|
||||
match: { channel: "discord", accountId: "discord-account" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await resolveDeliveryTarget(cfg, "agent-b", {
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
});
|
||||
|
||||
expect(result.accountId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
resolveOutboundTarget,
|
||||
resolveSessionDeliveryTarget,
|
||||
} from "../../infra/outbound/targets.js";
|
||||
import { buildChannelAccountBindings } from "../../routing/bindings.js";
|
||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||
|
||||
export async function resolveDeliveryTarget(
|
||||
cfg: OpenClawConfig,
|
||||
@@ -70,6 +72,20 @@ export async function resolveDeliveryTarget(
|
||||
const mode = resolved.mode as "explicit" | "implicit";
|
||||
const toCandidate = resolved.to;
|
||||
|
||||
// When the session has no lastAccountId (e.g. first-run isolated cron
|
||||
// session), fall back to the agent's bound account from bindings config.
|
||||
// This ensures the message tool in isolated sessions resolves the correct
|
||||
// bot token for multi-account setups.
|
||||
let accountId = resolved.accountId;
|
||||
if (!accountId && channel) {
|
||||
const bindings = buildChannelAccountBindings(cfg);
|
||||
const byAgent = bindings.get(channel);
|
||||
const boundAccounts = byAgent?.get(normalizeAgentId(agentId));
|
||||
if (boundAccounts && boundAccounts.length > 0) {
|
||||
accountId = boundAccounts[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Only carry threadId when delivering to the same recipient as the session's
|
||||
// last conversation. This prevents stale thread IDs (e.g. from a Telegram
|
||||
// supergroup topic) from being sent to a different target (e.g. a private
|
||||
@@ -83,7 +99,7 @@ export async function resolveDeliveryTarget(
|
||||
return {
|
||||
channel,
|
||||
to: undefined,
|
||||
accountId: resolved.accountId,
|
||||
accountId,
|
||||
threadId,
|
||||
mode,
|
||||
};
|
||||
@@ -93,13 +109,13 @@ export async function resolveDeliveryTarget(
|
||||
channel,
|
||||
to: toCandidate,
|
||||
cfg,
|
||||
accountId: resolved.accountId,
|
||||
accountId,
|
||||
mode,
|
||||
});
|
||||
return {
|
||||
channel,
|
||||
to: docked.ok ? docked.to : undefined,
|
||||
accountId: resolved.accountId,
|
||||
accountId,
|
||||
threadId,
|
||||
mode,
|
||||
error: docked.ok ? undefined : docked.error,
|
||||
|
||||
Reference in New Issue
Block a user