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

@@ -7,6 +7,7 @@ import {
readStringArrayParam,
readStringParam,
} from "../../agents/tools/common.js";
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js";
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
import type {
@@ -26,6 +27,7 @@ import {
resolveMessageChannelSelection,
} from "./channel-selection.js";
import { applyTargetToParams } from "./channel-target.js";
import { ensureOutboundSessionEntry, resolveOutboundSessionRoute } from "./outbound-session.js";
import type { OutboundSendDeps } from "./deliver.js";
import type { MessagePollResult, MessageSendResult } from "./message.js";
import {
@@ -37,9 +39,10 @@ import {
} from "./outbound-policy.js";
import { executePollAction, executeSendAction } from "./outbound-send-service.js";
import { actionHasTarget, actionRequiresTarget } from "./message-action-spec.js";
import { resolveChannelTarget } from "./target-resolver.js";
import { resolveChannelTarget, type ResolvedMessagingTarget } from "./target-resolver.js";
import { loadWebMedia } from "../../web/media.js";
import { extensionForMime } from "../../media/mime.js";
import { parseSlackTarget } from "../../slack/targets.js";
export type MessageActionRunnerGateway = {
url?: string;
@@ -204,6 +207,21 @@ function readBooleanParam(params: Record<string, unknown>, key: string): boolean
return undefined;
}
function resolveSlackAutoThreadId(params: {
to: string;
toolContext?: ChannelThreadingToolContext;
}): string | undefined {
const context = params.toolContext;
if (!context?.currentThreadTs || !context.currentChannelId) return undefined;
// Only mirror auto-threading when Slack would reply in the active thread for this channel.
if (context.replyToMode !== "all" && context.replyToMode !== "first") return undefined;
const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
if (!parsedTarget || parsedTarget.kind !== "channel") return undefined;
if (parsedTarget.id !== context.currentChannelId) return undefined;
if (context.replyToMode === "first" && context.hasRepliedRef?.value) return undefined;
return context.currentThreadTs;
}
function resolveAttachmentMaxBytes(params: {
cfg: ClawdbotConfig;
channel: ChannelId;
@@ -440,7 +458,8 @@ async function resolveActionTarget(params: {
action: ChannelMessageActionName;
args: Record<string, unknown>;
accountId?: string | null;
}): Promise<void> {
}): Promise<ResolvedMessagingTarget | undefined> {
let resolvedTarget: ResolvedMessagingTarget | undefined;
const toRaw = typeof params.args.to === "string" ? params.args.to.trim() : "";
if (toRaw) {
const resolved = await resolveChannelTarget({
@@ -451,6 +470,7 @@ async function resolveActionTarget(params: {
});
if (resolved.ok) {
params.args.to = resolved.target.to;
resolvedTarget = resolved.target;
} else {
throw resolved.error;
}
@@ -474,6 +494,7 @@ async function resolveActionTarget(params: {
throw resolved.error;
}
}
return resolvedTarget;
}
type ResolvedActionContext = {
@@ -484,6 +505,8 @@ type ResolvedActionContext = {
dryRun: boolean;
gateway?: MessageActionRunnerGateway;
input: RunMessageActionParams;
agentId?: string;
resolvedTarget?: ResolvedMessagingTarget;
};
function resolveGateway(input: RunMessageActionParams): MessageActionRunnerGateway | undefined {
if (!input.gateway) return undefined;
@@ -570,7 +593,7 @@ async function handleBroadcastAction(
}
async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActionRunResult> {
const { cfg, params, channel, accountId, dryRun, gateway, input } = ctx;
const { cfg, params, channel, accountId, dryRun, gateway, input, agentId, resolvedTarget } = ctx;
const action: ChannelMessageActionName = "send";
const to = readStringParam(params, "to", { required: true });
// Support media, path, and filePath parameters for attachments
@@ -621,6 +644,38 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
const mediaUrl = readStringParam(params, "media", { trim: false });
const gifPlayback = readBooleanParam(params, "gifPlayback") ?? false;
const bestEffort = readBooleanParam(params, "bestEffort");
const replyToId = readStringParam(params, "replyTo");
const threadId = readStringParam(params, "threadId");
// Slack auto-threading can inject threadTs without explicit params; mirror to that session key.
const slackAutoThreadId =
channel === "slack" && !replyToId && !threadId
? resolveSlackAutoThreadId({ to, toolContext: input.toolContext })
: undefined;
const outboundRoute =
agentId && !dryRun
? await resolveOutboundSessionRoute({
cfg,
channel,
agentId,
accountId,
target: to,
resolvedTarget,
replyToId,
threadId: threadId ?? slackAutoThreadId,
})
: null;
if (outboundRoute && !dryRun) {
await ensureOutboundSessionEntry({
cfg,
agentId,
channel,
accountId,
route: outboundRoute,
});
}
const mirrorMediaUrls =
mergedMediaUrls.length > 0 ? mergedMediaUrls : mediaUrl ? [mediaUrl] : undefined;
const send = await executeSendAction({
ctx: {
cfg,
@@ -632,10 +687,12 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
deps: input.deps,
dryRun,
mirror:
input.sessionKey && !dryRun
outboundRoute && !dryRun
? {
sessionKey: input.sessionKey,
agentId: input.agentId,
sessionKey: outboundRoute.sessionKey,
agentId,
text: message,
mediaUrls: mirrorMediaUrls,
}
: undefined,
},
@@ -762,6 +819,11 @@ export async function runMessageAction(
): Promise<MessageActionRunResult> {
const cfg = input.cfg;
const params = { ...input.params };
const resolvedAgentId =
input.agentId ??
(input.sessionKey
? resolveSessionAgentId({ sessionKey: input.sessionKey, config: cfg })
: undefined);
parseButtonsParam(params);
parseCardParam(params);
@@ -839,7 +901,7 @@ export async function runMessageAction(
dryRun,
});
await resolveActionTarget({
const resolvedTarget = await resolveActionTarget({
cfg,
channel,
action,
@@ -866,6 +928,8 @@ export async function runMessageAction(
dryRun,
gateway,
input,
agentId: resolvedAgentId,
resolvedTarget,
});
}