mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
* fix(gateway): accept heartbeat/cron/webhook channel hints in agent params (#73237) * test(gateway): cover internal reply channel hints * test(openai): include codex mini catalog expectation * test(openai): follow codex catalog fixture split --------- Co-authored-by: Ke Wang <ke@pika.art> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -70,6 +70,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Channels/Telegram: keep Bot API network fallbacks sticky after failed attempts and retry timed-out startup control calls once on the fallback route, so `deleteWebhook` IPv6 stalls no longer trigger slow multi-account retry storms. Fixes #73255. Thanks @ttomiczek and @sktbrd.
|
||||
- Gateway/agents: accept heartbeat, cron, and webhook as internal channel hints for agent runs so `sessions_spawn` works from non-delivery parent sessions while unknown channel hints still fail closed. Fixes #73237. Thanks @KeWang0622.
|
||||
- Gateway/models: merge explicit `models.providers.*.models` rows into the Gateway model catalog with normalized provider/model dedupe, and use normalized image-capability lookup so custom vision models keep native image attachments even when Pi discovery omits them or model ID casing differs. Fixes #64213 and #65165. Thanks @billonese and @202233a.
|
||||
- Gateway/reload: publish canonical post-write source config to in-process reloaders so simple config saves no longer create phantom plugin diffs or trigger unnecessary Gateway restarts. (#73267) Thanks @szsip239.
|
||||
- Gateway/Docker: keep config-triggered restarts in-process inside containers instead of spawning a detached child and exiting PID 1 cleanly, so Docker Swarm and other on-failure supervisors do not leave the service stuck at 0/1 replicas. Fixes #73178. Thanks @du-nguyen-IT007.
|
||||
|
||||
@@ -966,6 +966,60 @@ describe("gateway agent handler", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each(
|
||||
(["channel", "replyChannel"] as const).flatMap((field) =>
|
||||
(["heartbeat", "cron", "webhook"] as const).map((channel) => [field, channel] as const),
|
||||
),
|
||||
)("accepts internal non-delivery %s hint %s", async (field, channel) => {
|
||||
primeMainAgentRun();
|
||||
mocks.agentCommand.mockClear();
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeAgent(
|
||||
{
|
||||
message: "spawn from internal source",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
[field]: channel,
|
||||
idempotencyKey: `internal-channel-${field}-${channel}`,
|
||||
} as AgentParams,
|
||||
{ reqId: `internal-channel-${field}-${channel}-1`, respond },
|
||||
);
|
||||
|
||||
const rejection = respond.mock.calls.find(
|
||||
(call: unknown[]) =>
|
||||
call[0] === false &&
|
||||
typeof (call[2] as { message?: string } | undefined)?.message === "string" &&
|
||||
(call[2] as { message: string }).message.includes("unknown channel"),
|
||||
);
|
||||
expect(rejection).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each(["channel", "replyChannel"] as const)("rejects unknown %s hints", async (field) => {
|
||||
primeMainAgentRun();
|
||||
mocks.agentCommand.mockClear();
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeAgent(
|
||||
{
|
||||
message: "bogus channel",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
[field]: "not-a-real-channel",
|
||||
idempotencyKey: `unknown-${field}`,
|
||||
} as AgentParams,
|
||||
{ reqId: `unknown-${field}-1`, respond },
|
||||
);
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("unknown channel: not-a-real-channel"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts music generation internal events", async () => {
|
||||
primeMainAgentRun();
|
||||
mocks.agentCommand.mockClear();
|
||||
|
||||
@@ -75,6 +75,7 @@ import {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
isDeliverableMessageChannel,
|
||||
isGatewayMessageChannel,
|
||||
isInternalNonDeliveryChannel,
|
||||
normalizeMessageChannel,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { resolveAssistantIdentity } from "../assistant-identity.js";
|
||||
@@ -540,7 +541,10 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
}
|
||||
|
||||
const isKnownGatewayChannel = (value: string): boolean => isGatewayMessageChannel(value);
|
||||
// Accept internal non-delivery sources (heartbeat, cron, webhook) as valid
|
||||
// channel hints so subagent spawns from those parent runs are not rejected.
|
||||
const isKnownGatewayChannel = (value: string): boolean =>
|
||||
isGatewayMessageChannel(value) || isInternalNonDeliveryChannel(value);
|
||||
const channelHints = [request.channel, request.replyChannel]
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => value.trim())
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
export const INTERNAL_MESSAGE_CHANNEL = "webchat" as const;
|
||||
export type InternalMessageChannel = typeof INTERNAL_MESSAGE_CHANNEL;
|
||||
|
||||
// Internal, non-delivery sources that may surface as a `channel` hint when an
|
||||
// agent run is triggered by something other than a chat message — heartbeat
|
||||
// ticks, cron jobs, or webhook receivers. They are not deliverable on their
|
||||
// own, but they should still pass agent-param channel validation so internal
|
||||
// callers (e.g. sessions_spawn from a heartbeat-driven parent run) are not
|
||||
// rejected as "unknown channel".
|
||||
export const INTERNAL_NON_DELIVERY_CHANNELS = ["heartbeat", "cron", "webhook"] as const;
|
||||
export type InternalNonDeliveryChannel = (typeof INTERNAL_NON_DELIVERY_CHANNELS)[number];
|
||||
|
||||
export function isInternalNonDeliveryChannel(value: string): value is InternalNonDeliveryChannel {
|
||||
return (INTERNAL_NON_DELIVERY_CHANNELS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import {
|
||||
INTERNAL_NON_DELIVERY_CHANNELS,
|
||||
isInternalNonDeliveryChannel,
|
||||
isMarkdownCapableMessageChannel,
|
||||
resolveGatewayMessageChannel,
|
||||
} from "./message-channel.js";
|
||||
@@ -58,6 +60,16 @@ describe("message-channel", () => {
|
||||
expect(resolveGatewayMessageChannel("workspace-chat")).toBe("demo-alias-channel");
|
||||
});
|
||||
|
||||
it("recognises internal non-delivery channel sources", () => {
|
||||
for (const channel of INTERNAL_NON_DELIVERY_CHANNELS) {
|
||||
expect(isInternalNonDeliveryChannel(channel)).toBe(true);
|
||||
}
|
||||
expect(isInternalNonDeliveryChannel("telegram")).toBe(false);
|
||||
expect(isInternalNonDeliveryChannel("webchat")).toBe(false);
|
||||
expect(isInternalNonDeliveryChannel("")).toBe(false);
|
||||
expect(isInternalNonDeliveryChannel("HEARTBEAT")).toBe(false);
|
||||
});
|
||||
|
||||
it("reads markdown capability from channel metadata", () => {
|
||||
expect(isMarkdownCapableMessageChannel("telegram")).toBe(true);
|
||||
expect(isMarkdownCapableMessageChannel("whatsapp")).toBe(false);
|
||||
|
||||
@@ -24,7 +24,10 @@ export {
|
||||
} from "./message-channel-normalize.js";
|
||||
export {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
INTERNAL_NON_DELIVERY_CHANNELS,
|
||||
isInternalNonDeliveryChannel,
|
||||
type InternalMessageChannel,
|
||||
type InternalNonDeliveryChannel,
|
||||
} from "./message-channel-constants.js";
|
||||
import {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
|
||||
Reference in New Issue
Block a user