fix: normalize session keys and outbound mirroring

This commit is contained in:
Peter Steinberger
2026-01-24 11:57:04 +00:00
parent eaeb52f70a
commit 4b6cdd1d3c
33 changed files with 1357 additions and 145 deletions

View File

@@ -70,7 +70,8 @@ export function resolveSessionAgentIds(params: { sessionKey?: string; config?: C
} {
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
const sessionKey = params.sessionKey?.trim();
const parsed = sessionKey ? parseAgentSessionKey(sessionKey) : null;
const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : undefined;
const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null;
const sessionAgentId = parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId;
return { defaultAgentId, sessionAgentId };
}

View File

@@ -128,7 +128,7 @@ describe("getDmHistoryLimitFromSessionKey", () => {
slack: { dmHistoryLimit: 10 },
},
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:C1", config)).toBeUndefined();
expect(getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:c1", config)).toBeUndefined();
expect(getDmHistoryLimitFromSessionKey("telegram:slash:123", config)).toBeUndefined();
});
it("returns undefined for unknown provider", () => {

View File

@@ -126,7 +126,7 @@ describe("resolveSessionAgentIds", () => {
});
it("keeps the agent id for provider-qualified agent sessions", () => {
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: "agent:beta:slack:channel:C1",
sessionKey: "agent:beta:slack:channel:c1",
config: cfg,
});
expect(sessionAgentId).toBe("beta");

View File

@@ -106,7 +106,7 @@ describe("sandbox explain helpers", () => {
const msg = formatSandboxToolPolicyBlockedMessage({
cfg,
sessionKey: "agent:main:whatsapp:group:G1",
sessionKey: "agent:main:whatsapp:group:g1",
toolName: "browser",
});
expect(msg).toBeTruthy();

View File

@@ -8,7 +8,6 @@ import { createMessageTool } from "./message-tool.js";
const mocks = vi.hoisted(() => ({
runMessageAction: vi.fn(),
appendAssistantMessageToSessionTranscript: vi.fn(async () => ({ ok: true, sessionFile: "x" })),
}));
vi.mock("../../infra/outbound/message-action-runner.js", async () => {
@@ -21,47 +20,9 @@ vi.mock("../../infra/outbound/message-action-runner.js", async () => {
};
});
vi.mock("../../config/sessions.js", async () => {
const actual = await vi.importActual<typeof import("../../config/sessions.js")>(
"../../config/sessions.js",
);
return {
...actual,
appendAssistantMessageToSessionTranscript: mocks.appendAssistantMessageToSessionTranscript,
};
});
describe("message tool mirroring", () => {
it("mirrors media filename for plugin-handled sends", async () => {
mocks.appendAssistantMessageToSessionTranscript.mockClear();
mocks.runMessageAction.mockResolvedValue({
kind: "send",
action: "send",
channel: "telegram",
handledBy: "plugin",
payload: {},
dryRun: false,
} satisfies MessageActionRunResult);
const tool = createMessageTool({
agentSessionKey: "agent:main:main",
config: {} as never,
});
await tool.execute("1", {
action: "send",
target: "telegram:123",
message: "",
media: "https://example.com/files/report.pdf?sig=1",
});
expect(mocks.appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith(
expect.objectContaining({ text: "report.pdf" }),
);
});
it("does not mirror on dry-run", async () => {
mocks.appendAssistantMessageToSessionTranscript.mockClear();
describe("message tool agent routing", () => {
it("derives agentId from the session key", async () => {
mocks.runMessageAction.mockClear();
mocks.runMessageAction.mockResolvedValue({
kind: "send",
action: "send",
@@ -72,7 +33,7 @@ describe("message tool mirroring", () => {
} satisfies MessageActionRunResult);
const tool = createMessageTool({
agentSessionKey: "agent:main:main",
agentSessionKey: "agent:alpha:main",
config: {} as never,
});
@@ -82,7 +43,9 @@ describe("message tool mirroring", () => {
message: "hi",
});
expect(mocks.appendAssistantMessageToSessionTranscript).not.toHaveBeenCalled();
const call = mocks.runMessageAction.mock.calls[0]?.[0];
expect(call?.agentId).toBe("alpha");
expect(call?.sessionKey).toBeUndefined();
});
});

View File

@@ -11,10 +11,6 @@ import {
import { BLUEBUBBLES_GROUP_ACTIONS } from "../../channels/plugins/bluebubbles-actions.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import {
appendAssistantMessageToSessionTranscript,
resolveMirroredTranscriptText,
} from "../../config/sessions.js";
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../gateway/protocol/client-info.js";
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
import { getToolResult, runMessageAction } from "../../infra/outbound/message-action-runner.js";
@@ -377,36 +373,11 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
defaultAccountId: accountId ?? undefined,
gateway,
toolContext,
sessionKey: options?.agentSessionKey,
agentId: options?.agentSessionKey
? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg })
: undefined,
});
if (
action === "send" &&
options?.agentSessionKey &&
!result.dryRun &&
result.handledBy === "plugin"
) {
const mediaUrl = typeof params.media === "string" ? params.media : undefined;
const mirrorText = resolveMirroredTranscriptText({
text: typeof params.message === "string" ? params.message : undefined,
mediaUrls: mediaUrl ? [mediaUrl] : undefined,
});
if (mirrorText) {
const agentId = resolveSessionAgentId({
sessionKey: options.agentSessionKey,
config: cfg,
});
await appendAssistantMessageToSessionTranscript({
agentId,
sessionKey: options.agentSessionKey,
text: mirrorText,
});
}
}
const toolResult = getToolResult(result);
if (toolResult) return toolResult;
return jsonResult(result.payload);