fix(gateway): harden message action channel fallback and startup grace

Take the safe, tested subset from #32367:\n- per-channel startup connect grace in health monitor\n- tool-context channel-provider fallback for message actions\n\nCo-authored-by: Munem Hashmi <munem.hashmi@gmail.com>
This commit is contained in:
Peter Steinberger
2026-03-03 01:17:07 +00:00
parent 4d04e1a41f
commit 71cd337137
5 changed files with 103 additions and 10 deletions

View File

@@ -349,6 +349,37 @@ describe("runMessageAction context isolation", () => {
expect(result.channel).toBe("slack");
});
it("falls back to tool-context provider when channel param is an id", async () => {
const result = await runDrySend({
cfg: slackConfig,
actionParams: {
channel: "C12345678",
target: "#C12345678",
message: "hi",
},
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
});
expect(result.kind).toBe("send");
expect(result.channel).toBe("slack");
});
it("falls back to tool-context provider for broadcast channel ids", async () => {
const result = await runDryAction({
cfg: slackConfig,
action: "broadcast",
actionParams: {
targets: ["channel:C12345678"],
channel: "C12345678",
message: "hi",
},
toolContext: { currentChannelProvider: "slack" },
});
expect(result.kind).toBe("broadcast");
expect(result.channel).toBe("slack");
});
it("blocks cross-provider sends by default", async () => {
await expect(
runDrySend({

View File

@@ -217,13 +217,28 @@ async function maybeApplyCrossContextMarker(params: {
});
}
async function resolveChannel(cfg: OpenClawConfig, params: Record<string, unknown>) {
async function resolveChannel(
cfg: OpenClawConfig,
params: Record<string, unknown>,
toolContext?: { currentChannelProvider?: string },
) {
const channelHint = readStringParam(params, "channel");
const selection = await resolveMessageChannelSelection({
cfg,
channel: channelHint,
});
return selection.channel;
try {
const selection = await resolveMessageChannelSelection({
cfg,
channel: channelHint,
});
return selection.channel;
} catch (error) {
if (channelHint && toolContext?.currentChannelProvider) {
const fallback = normalizeMessageChannel(toolContext.currentChannelProvider);
if (fallback && isDeliverableMessageChannel(fallback)) {
params.channel = fallback;
return fallback;
}
}
throw error;
}
}
async function resolveActionTarget(params: {
@@ -317,7 +332,7 @@ async function handleBroadcastAction(
}
const targetChannels =
channelHint && channelHint.trim().toLowerCase() !== "all"
? [await resolveChannel(input.cfg, { channel: channelHint })]
? [await resolveChannel(input.cfg, { channel: channelHint }, input.toolContext)]
: configured;
const results: Array<{
channel: ChannelId;
@@ -754,7 +769,7 @@ export async function runMessageAction(
}
}
const channel = await resolveChannel(cfg, params);
const channel = await resolveChannel(cfg, params, input.toolContext);
let accountId = readStringParam(params, "accountId") ?? input.defaultAccountId;
if (!accountId && resolvedAgentId) {
const byAgent = buildChannelAccountBindings(cfg).get(channel);