diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index f5db93d0ff71..9626a9b51d6f 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -29,7 +29,7 @@ c0f910ebfa3dbf283145fb1e3b9c016d03e853ef13e70402b09f3b9d9c2f4ab0 module/channel 9a5aaf650f9242523bb57bdc2556c323ab64e55aa11e25e2685b73b23ee12534 module/channel-dm-policy fbf353eb38ae68d8ded3f2a60b432c7bb2c245d2ec7e7c9f53c6da19a0db0938 module/channel-entry-contract 982f29a18e07228e3da82cae67d06ff38249592a29c2fd28f01f0d2016ff80d9 module/channel-feedback -6cf2da836db49a267aed470ce6f5f55e68d8d86e1efe980cdc71349cfce30bc2 module/channel-inbound +d645d24bcb7a5f68cc46c692ad0d1fbd19be0a99996f31e9479ce9cffce301c1 module/channel-inbound 76bb7f531f3702c801e8fe7479e9e499f601fb361a4303afdcb45fc0da440e4b module/channel-inbound-debounce 4a7ada095f0f483525dcbd848fbccab26473749eba87e6a6c6e5074fd04ee1d1 module/channel-ingress-runtime c97dd36cdf8f83c2893c33e9430a93cd131a03d855725783ca5b545de0cf84f8 module/channel-lifecycle diff --git a/docs/plugins/sdk-channel-inbound.md b/docs/plugins/sdk-channel-inbound.md index f0cebe56fcb3..d94a65fdffde 100644 --- a/docs/plugins/sdk-channel-inbound.md +++ b/docs/plugins/sdk-channel-inbound.md @@ -38,78 +38,6 @@ import { - `dispatchChannelInboundReply(...)`: records and dispatches an already assembled inbound reply with a delivery adapter. -## Prepared inbound envelope - -Channels that finish transport admission and normalization before entering the -shared runner can use `PreparedChannelInbound` as the boundary: - -```ts -import { - projectPreparedChannelInbound, - resolveChannelInboundReplyPolicy, - runChannelInboundEvent, - type PreparedChannelInbound, -} from "openclaw/plugin-sdk/channel-inbound"; - -const inbound = { - channel: "demo", - event: { id: nativeEvent.id, timestamp: nativeEvent.timestamp }, - from: conversation.id, - sender: { id: sender.id, name: sender.name }, - conversation: { kind: "group", id: conversation.id }, - route: { agentId, accountId, routeSessionKey }, - reply: { to: conversation.id, replyToId: quote?.id }, - message: { - rawBody, - body: formattedBody, - bodyForAgent, - commandBody, - }, - command: { - kind: "text-slash", - body: commandBody, - authorization: { kind: "authorized" }, - }, - media, -} satisfies PreparedChannelInbound; - -const prepared = projectPreparedChannelInbound({ - inbound, - control: { messageReceivedHooks: "core" }, -}); -const replyPolicy = resolveChannelInboundReplyPolicy({ - cfg, - ctx: prepared.context, - blockStreamingEnabled, -}); - -await runChannelInboundEvent({ - channel: inbound.channel, - accountId, - raw: inbound, - adapter: { - ingest: () => prepared.input, - resolveTurn: () => buildTurnPlan(prepared.context, replyPolicy), - }, -}); -``` - -The envelope contains prepared product facts only. Keep native callbacks, -socket objects, provider message records, and transport lifecycle symbols in a -channel-private context beside it. Represent command authorization explicitly -as `not_checked`, `authorized`, or `denied`; do not make core infer it from a -transport payload. - -Use `resolveChannelInboundReplyPolicy(...)` after building the portable inbound -context. Core resolves product policy for source reply delivery, block -streaming, and typing suppression. The channel supplies prepared context facts, -including mention state, plus native capabilities such as whether block -streaming is enabled. - -Keep quote encoding, reply callbacks, native target lookup, and other -transport mechanics in the channel. Do not duplicate visible-reply policy in a -channel adapter. - For media-only inbound events, keep the message body and command text empty and pass one `ChannelInboundMediaInput` fact per native attachment. When an ambient history line or another text-only carrier must describe those facts, use diff --git a/extensions/whatsapp/src/auto-reply/monitor.ts b/extensions/whatsapp/src/auto-reply/monitor.ts index de9db4ca0163..18cf782ae8d0 100644 --- a/extensions/whatsapp/src/auto-reply/monitor.ts +++ b/extensions/whatsapp/src/auto-reply/monitor.ts @@ -27,13 +27,13 @@ import { type ManagedWhatsAppListener, } from "../connection-controller.js"; import { resolveWhatsAppInboundPolicy } from "../inbound-policy.js"; -import { normalizeAdmittedWebInboundMessage } from "../inbound/message-aliases.js"; import { readWhatsAppBaileysCacheEntry, type WhatsAppBaileysGroupMetadataCache, type WhatsAppBaileysMessageCache, } from "../inbound/baileys-cache.js"; import type { WhatsAppGroupMetadataCache } from "../inbound/group-metadata-cache.js"; +import { normalizeAdmittedWebInboundMessage } from "../inbound/message-aliases.js"; import { attachWebInboxToSocket } from "../inbound/monitor.js"; import type { WebInboundMessageInput } from "../inbound/types.js"; import { diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.message-sent.test.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.message-sent.test.ts index c98c55785d42..8bff4d517d7c 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.message-sent.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.message-sent.test.ts @@ -17,7 +17,10 @@ import { import { clearInternalHooks, registerInternalHook } from "openclaw/plugin-sdk/hook-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestWebInboundMessage } from "../../inbound/test-message.test-helper.js"; -import { createWhatsAppReplyPlan } from "./inbound-dispatch.js"; +import { + buildWhatsAppInboundTransportContext, + createWhatsAppReplyPlan, +} from "./inbound-dispatch.js"; const sessionKey = "agent:main:whatsapp:direct:+1000"; type InternalHookEvent = Parameters[1]>[0]; @@ -68,7 +71,25 @@ function createPlan(deliverReply: Parameters[0][ groupHistories: new Map(), groupHistoryKey: sessionKey, maxMediaBytes: 1024, - msg, + inbound: { + channel: "whatsapp", + event: { id: "wa-inbound-1" }, + from: "+1000", + sender: { id: "1000@s.whatsapp.net" }, + conversation: { kind: "direct", id: "+1000" }, + route: { + agentId: "main", + accountId: "default", + routeSessionKey: sessionKey, + }, + reply: { to: "+2000", originatingTo: "+1000" }, + message: { + body: "show me the result", + bodyForAgent: "show me the result", + rawBody: "show me the result", + commandBody: "show me the result", + }, + }, rememberSentText: vi.fn(), replyLogger: { info: vi.fn(), @@ -88,6 +109,7 @@ function createPlan(deliverReply: Parameters[0][ matchedBy: "default", }, shouldClearGroupHistory: false, + transport: buildWhatsAppInboundTransportContext(msg), }); return { context, plan }; } diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts index 89e2fef8bcd7..71117c7316e5 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts @@ -51,14 +51,7 @@ vi.mock("openclaw/plugin-sdk/channel-outbound", async (importOriginal) => { return { ...actual, deliverInboundReplyWithMessageSendContext: deliverInboundReplyWithMessageSendContextMock, - }; -}); - -vi.mock("openclaw/plugin-sdk/channel-inbound", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveChannelInboundReplyPolicy: (params: { + resolveChannelMessageSourceReplyDeliveryMode: (params: { cfg: { messages?: { visibleReplies?: "automatic" | "message_tool"; @@ -69,34 +62,16 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async (importOriginal) => { ChatType?: string; CommandSource?: "native" | "text"; CommandAuthorized?: boolean; - WasMentioned?: boolean; }; - blockStreamingEnabled?: boolean; }) => { sourceReplyDeliveryModeContexts.push(params.ctx); - const isRoom = params.ctx.ChatType === "group" || params.ctx.ChatType === "channel"; - const sourceReplyDeliveryMode = !isRoom - ? undefined - : params.ctx.CommandSource === "native" || - (params.ctx.CommandSource === "text" && params.ctx.CommandAuthorized === true) + return params.ctx.CommandSource === "native" || + (params.ctx.CommandSource === "text" && params.ctx.CommandAuthorized === true) + ? "automatic" + : (params.cfg.messages?.groupChat?.visibleReplies ?? + params.cfg.messages?.visibleReplies) === "automatic" ? "automatic" - : (params.cfg.messages?.groupChat?.visibleReplies ?? - params.cfg.messages?.visibleReplies) === "automatic" - ? "automatic" - : "message_tool_only"; - const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only"; - return { - sourceReplyDeliveryMode, - disableBlockStreaming: sourceRepliesAreToolOnly - ? true - : typeof params.blockStreamingEnabled === "boolean" - ? !params.blockStreamingEnabled - : undefined, - suppressTyping: - sourceRepliesAreToolOnly && - params.ctx.ChatType === "group" && - params.ctx.WasMentioned !== true, - }; + : "message_tool_only"; }, }; }); diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts index 68fbaa52a3fa..657c77cd7604 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts @@ -1,18 +1,13 @@ // Whatsapp plugin module implements inbound dispatch behavior. import type { StatusReactionController } from "openclaw/plugin-sdk/channel-feedback"; import { - buildChannelInboundEventContext, createChannelPartialDeliveryError, isChannelPartialDeliveryError, - projectPreparedChannelInbound, - resolveChannelInboundReplyPolicy, type ChannelInboundTurnPlan, - type PreparedChannelInbound, toInboundMediaFactsWithMetadata, } from "openclaw/plugin-sdk/channel-inbound"; import { hasVisibleInboundReplyDispatch } from "openclaw/plugin-sdk/channel-inbound"; import { - bindIngressLifecycleToReplyOptions, listMessageReceiptPlatformIds, resolveChannelStreamingBlockEnabled, } from "openclaw/plugin-sdk/channel-outbound"; @@ -54,6 +49,11 @@ import { type ReplyPayload, type resolveAgentRoute, } from "./inbound-dispatch.runtime.js"; +import { + projectPreparedChannelInbound, + resolveWhatsAppInboundReplyPolicy, + type PreparedChannelInbound, +} from "./prepared-inbound.js"; type ReplyLifecycleKind = "tool" | "block" | "final"; type ChannelReplyOnModelSelected = NonNullable< @@ -641,7 +641,7 @@ export function createWhatsAppReplyPlan(params: { accountId: params.route.accountId, }); const mediaLocalRoots = getAgentScopedMediaLocalRoots(params.cfg, params.route.agentId); - const replyPolicy = resolveChannelInboundReplyPolicy({ + const replyPolicy = resolveWhatsAppInboundReplyPolicy({ cfg: params.cfg, ctx: params.context, blockStreamingEnabled: resolveChannelStreamingBlockEnabled(params.cfg.channels?.whatsapp), diff --git a/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts b/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts index e40393c5b2e3..e0dd7267496b 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts @@ -95,8 +95,7 @@ vi.mock("openclaw/plugin-sdk/routing", () => ({ }), })); -import { requireAdmittedWhatsAppInboundMessage } from "../../inbound/admission.js"; -import { normalizeWebInboundMessage } from "../../inbound/message-aliases.js"; +import { normalizeAdmittedWebInboundMessage } from "../../inbound/message-aliases.js"; import { createTestLegacyFlatWebInboundMessage, createTestWebAudioInboundMessage, @@ -105,7 +104,7 @@ import type { AdmittedWebInboundMessage, WebInboundMessageInput } from "../../in import { createWebOnMessageHandler } from "./on-message.js"; function admitTestMessage(msg: WebInboundMessageInput): AdmittedWebInboundMessage { - return requireAdmittedWhatsAppInboundMessage(normalizeWebInboundMessage(msg)); + return normalizeAdmittedWebInboundMessage(msg); } function makeAudioMsg(): AdmittedWebInboundMessage { diff --git a/extensions/whatsapp/src/auto-reply/monitor/prepared-inbound.test.ts b/extensions/whatsapp/src/auto-reply/monitor/prepared-inbound.test.ts new file mode 100644 index 000000000000..6373d15c44d4 --- /dev/null +++ b/extensions/whatsapp/src/auto-reply/monitor/prepared-inbound.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from "vitest"; +import { projectPreparedChannelInbound, type PreparedChannelInbound } from "./prepared-inbound.js"; + +describe("WhatsApp prepared inbound", () => { + it("projects portable facts without WhatsApp transport state", () => { + const inbound = { + channel: "whatsapp", + accountId: "work", + event: { + id: "event-1", + fullId: "whatsapp:event-1", + timestamp: 1_710_000_000, + }, + from: "whatsapp:user:u1", + sender: { + id: "u1", + name: "Alice", + }, + conversation: { + kind: "group", + id: "room-1", + label: "Example Room", + }, + route: { + agentId: "main", + accountId: "work", + routeSessionKey: "agent:main:whatsapp:group:room-1", + }, + reply: { + to: "whatsapp:room:room-1", + replyToId: "quoted-1", + }, + message: { + body: "agent body", + bodyForAgent: "agent body", + rawBody: "raw body", + commandBody: "/status", + }, + command: { + kind: "text-slash", + body: "/status", + authorization: { + kind: "denied", + reason: "sender_not_allowed", + }, + }, + media: [ + { + path: "/tmp/example.jpg", + contentType: "image/jpeg", + kind: "image", + }, + ], + context: { + senderE164: "+15550001111", + }, + } satisfies PreparedChannelInbound; + + const projected = projectPreparedChannelInbound({ + inbound, + control: { messageReceivedHooks: "core" }, + }); + + expect(projected.input).toEqual({ + id: "event-1", + timestamp: 1_710_000_000, + rawText: "raw body", + textForAgent: "agent body", + textForCommands: "/status", + raw: inbound, + }); + expect(projected.context).toMatchObject({ + MessageSid: "event-1", + MessageSidFull: "whatsapp:event-1", + BodyForAgent: "agent body", + RawBody: "raw body", + CommandBody: "/status", + ReplyToId: "quoted-1", + CommandAuthorized: false, + ConversationLabel: "Example Room", + GroupSubject: "Example Room", + SenderE164: "+15550001111", + media: [ + { + path: "/tmp/example.jpg", + contentType: "image/jpeg", + kind: "image", + }, + ], + }); + }); +}); diff --git a/src/channels/inbound-event/prepared.ts b/extensions/whatsapp/src/auto-reply/monitor/prepared-inbound.ts similarity index 66% rename from src/channels/inbound-event/prepared.ts rename to extensions/whatsapp/src/auto-reply/monitor/prepared-inbound.ts index d3ebf871f28a..ba4b43f9de0f 100644 --- a/src/channels/inbound-event/prepared.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/prepared-inbound.ts @@ -1,12 +1,14 @@ -import type { ReplyThreadingPolicy } from "../../auto-reply/types.js"; -import type { NormalizedLocation } from "../location.js"; -import { toLocationContext } from "../location.js"; -import type { CommandFacts, NormalizedTurnInput, SupplementalContextFacts } from "../turn/types.js"; import { buildChannelInboundEventContext, + toLocationContext, type BuildChannelInboundEventContextParams, type BuiltChannelInboundEventContext, -} from "./context.js"; + type CommandFacts, + type NormalizedLocation, + type SupplementalContextFacts, +} from "openclaw/plugin-sdk/channel-inbound"; +import { resolveChannelMessageSourceReplyDeliveryMode } from "openclaw/plugin-sdk/channel-outbound"; +import type { ReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference"; type PreparedChannelInboundCommandAuthorization = | { kind: "not_checked" } @@ -77,7 +79,14 @@ export function projectPreparedChannelInbound(params: { inbound: PreparedChannelInbound; control: PreparedChannelInboundControl; }): { - input: NormalizedTurnInput; + input: { + id: string; + timestamp?: number; + rawText: string; + textForAgent?: string; + textForCommands?: string; + raw: PreparedChannelInbound; + }; context: BuiltChannelInboundEventContext; } { const { inbound, control } = params; @@ -123,3 +132,40 @@ export function projectPreparedChannelInbound(params: { }), }; } + +type ChannelInboundReplyPolicy = { + sourceReplyDeliveryMode?: "automatic" | "message_tool_only"; + disableBlockStreaming?: boolean; + suppressTyping: boolean; +}; + +/** Keeps WhatsApp reply-policy mapping beside the transport that applies it. */ +export function resolveWhatsAppInboundReplyPolicy(params: { + cfg: Parameters[0]["cfg"]; + ctx: Parameters[0]["ctx"] & { + ChatType?: string; + WasMentioned?: boolean; + }; + blockStreamingEnabled?: boolean; +}): ChannelInboundReplyPolicy { + const isRoom = params.ctx.ChatType === "group" || params.ctx.ChatType === "channel"; + const sourceReplyDeliveryMode = isRoom + ? resolveChannelMessageSourceReplyDeliveryMode({ + cfg: params.cfg, + ctx: params.ctx, + }) + : undefined; + const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only"; + return { + sourceReplyDeliveryMode, + disableBlockStreaming: sourceRepliesAreToolOnly + ? true + : typeof params.blockStreamingEnabled === "boolean" + ? !params.blockStreamingEnabled + : undefined, + suppressTyping: + sourceRepliesAreToolOnly && + params.ctx.ChatType === "group" && + params.ctx.WasMentioned !== true, + }; +} diff --git a/extensions/whatsapp/src/inbound/message-aliases.test.ts b/extensions/whatsapp/src/inbound/message-aliases.test.ts index 3152ad592634..b06c740f6bf8 100644 --- a/extensions/whatsapp/src/inbound/message-aliases.test.ts +++ b/extensions/whatsapp/src/inbound/message-aliases.test.ts @@ -2,7 +2,6 @@ import { describe, expect, expectTypeOf, it, vi } from "vitest"; import { normalizeAdmittedWebInboundMessage, - normalizeWebInboundMessage, withDeprecatedWebInboundMessageFlatAliases, } from "./message-aliases.js"; import type { monitorWebInbox } from "./monitor.js"; @@ -281,42 +280,33 @@ describe("WhatsApp inbound flat aliases", () => { expect(msg.accessControlPassed).toBe(true); }); - it("normalizes canonical messages without admission without rebuilding admission facts", () => { - const normalized = normalizeWebInboundMessage({ - event: { - id: "group-no-admission", - timestamp: 1_700_000_456, - }, - payload: { - body: "hello group", - }, - platform: { - chatJid: "123@g.us", - recipientJid: "+15550000001", - senderJid: "15550000002@s.whatsapp.net", - senderE164: "+15550000002", - senderName: "Alice", - sendComposing: vi.fn(async () => undefined), - reply: vi.fn(async () => createAcceptedWhatsAppSendResult("text", "reply-group")), - sendMedia: vi.fn(async () => createAcceptedWhatsAppSendResult("media", "media-group")), - }, - from: "123@g.us", - conversationId: "123@g.us", - accountId: "work", - accessControlPassed: true, - chatType: "group", - }); - - expect(normalized.admission).toBeUndefined(); - expect(normalized.from).toBe("123@g.us"); - expect(normalized.conversationId).toBe("123@g.us"); - expect(normalized.accountId).toBe("work"); - expect(normalized.chatType).toBe("group"); - - normalized.conversationId = "456@g.us"; - expect(normalized.admission).toBeUndefined(); - expect(normalized.from).toBe("456@g.us"); - expect(normalized.conversationId).toBe("456@g.us"); + it("rejects canonical messages without admission at the listener boundary", () => { + expect(() => + normalizeAdmittedWebInboundMessage({ + event: { + id: "group-no-admission", + timestamp: 1_700_000_456, + }, + payload: { + body: "hello group", + }, + platform: { + chatJid: "123@g.us", + recipientJid: "+15550000001", + senderJid: "15550000002@s.whatsapp.net", + senderE164: "+15550000002", + senderName: "Alice", + sendComposing: vi.fn(async () => undefined), + reply: vi.fn(async () => createAcceptedWhatsAppSendResult("text", "reply-group")), + sendMedia: vi.fn(async () => createAcceptedWhatsAppSendResult("media", "media-group")), + }, + from: "123@g.us", + conversationId: "123@g.us", + accountId: "work", + accessControlPassed: true, + chatType: "group", + }), + ).toThrow("WhatsApp inbound message is missing admission facts"); }); it("normalizes legacy flat messages into canonical contexts with live aliases", () => { @@ -351,7 +341,7 @@ describe("WhatsApp inbound flat aliases", () => { isBatched: true, }; - const normalized = normalizeWebInboundMessage(legacy); + const normalized = normalizeAdmittedWebInboundMessage(legacy); expect(normalized.event).toMatchObject({ id: "legacy-1", @@ -405,7 +395,7 @@ describe("WhatsApp inbound flat aliases", () => { }); it("normalizes blocked legacy flat admission fields through the compatibility seam", () => { - const normalized = normalizeWebInboundMessage({ + const normalized = normalizeAdmittedWebInboundMessage({ id: "legacy-blocked", from: "+15550000002", conversationId: "+15550000002", @@ -437,7 +427,7 @@ describe("WhatsApp inbound flat aliases", () => { }); it("preserves explicit legacy access proof through normalization", () => { - const normalized = normalizeWebInboundMessage({ + const normalized = normalizeAdmittedWebInboundMessage({ ...createTestLegacyFlatWebInboundMessage(), accessControlPassed: true, }); diff --git a/extensions/whatsapp/src/inbound/message-aliases.ts b/extensions/whatsapp/src/inbound/message-aliases.ts index 3785540a0e7c..688359aa0a85 100644 --- a/extensions/whatsapp/src/inbound/message-aliases.ts +++ b/extensions/whatsapp/src/inbound/message-aliases.ts @@ -445,7 +445,7 @@ function normalizeLegacyFlatWebInboundMessage(msg: LegacyFlatWebInboundMessage): }); } -export function normalizeWebInboundMessage(msg: WebInboundMessageInput): WebInboundMessage { +function normalizeWebInboundMessage(msg: WebInboundMessageInput): WebInboundMessage { if (msg.event && msg.payload && msg.platform) { return withDeprecatedWebInboundMessageFlatAliases(msg); } diff --git a/extensions/whatsapp/src/inbound/message-debounce.ts b/extensions/whatsapp/src/inbound/message-debounce.ts index 953f1fcd963b..97dbf449d473 100644 --- a/extensions/whatsapp/src/inbound/message-debounce.ts +++ b/extensions/whatsapp/src/inbound/message-debounce.ts @@ -6,10 +6,7 @@ import { requireWhatsAppInboundAdmission } from "./admission.js"; import type { WhatsAppIngressLifecycle, WhatsAppReadReceiptTarget } from "./durable-receive.js"; import { attachWhatsAppIngressLifecycle } from "./ingress-lifecycle.js"; import { withDeprecatedWebInboundMessageFlatAliases } from "./message-aliases.js"; -import type { - AdmittedWebInboundCallbackMessage, - WebInboundMessageInput, -} from "./types.js"; +import type { AdmittedWebInboundCallbackMessage, WebInboundMessageInput } from "./types.js"; export type WhatsAppQueuedInboundMessage = AdmittedWebInboundCallbackMessage & { debounceKey?: string; diff --git a/extensions/whatsapp/src/inbound/monitor.ts b/extensions/whatsapp/src/inbound/monitor.ts index f38bcc95bb6b..c19e1d778dc4 100644 --- a/extensions/whatsapp/src/inbound/monitor.ts +++ b/extensions/whatsapp/src/inbound/monitor.ts @@ -26,10 +26,7 @@ import { } from "./message-delivery.js"; import { createWebSendApi } from "./send-api.js"; import { createWhatsAppAttachedSocketSession } from "./socket-session.js"; -import type { - AdmittedWebInboundCallbackMessage, - WebInboundMessageInput, -} from "./types.js"; +import type { AdmittedWebInboundCallbackMessage, WebInboundMessageInput } from "./types.js"; function logWhatsAppVerbose(enabled: boolean | undefined, message: string) { if (enabled) { diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index c5595ebd9282..0e3a8b1b42f5 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -204,8 +204,7 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // +2: shared ingress error factory through channel-outbound and channel-message. // +2: shared ingress retention defaults through channel-outbound and channel-message. // +1: collision-safe MCP server-name assignment for native harness catalogs. - // +3: prepared channel inbound envelope, projection, and reply-policy contracts. - 4777, + 4774, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( @@ -243,8 +242,7 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // +1: inbound media-fact metadata projection for plugin-owned channel ingestion. // +2: shared ingress error factory through channel-outbound and channel-message. // +1: collision-safe MCP server-name assignment for native harness catalogs. - // +2: prepared channel inbound projection and reply-policy resolution. - 2884, + 2882, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/channels/message/index.ts b/src/channels/message/index.ts index 10d8a06537af..0b329f470693 100644 --- a/src/channels/message/index.ts +++ b/src/channels/message/index.ts @@ -43,7 +43,6 @@ export { createReplyPrefixContext, createReplyPrefixOptions, createTypingCallbacks, - resolveChannelInboundReplyPolicy, resolveChannelSourceReplyDeliveryMode, } from "./reply-pipeline.js"; export type { ChannelIngressDrain } from "./ingress-drain.js"; diff --git a/src/channels/message/reply-pipeline.ts b/src/channels/message/reply-pipeline.ts index 359ae358ca89..1ea204b6756e 100644 --- a/src/channels/message/reply-pipeline.ts +++ b/src/channels/message/reply-pipeline.ts @@ -45,43 +45,6 @@ export function resolveChannelSourceReplyDeliveryMode(params: { return resolveSourceReplyDeliveryMode(params); } -type ChannelInboundReplyPolicy = { - sourceReplyDeliveryMode?: SourceReplyDeliveryMode; - disableBlockStreaming?: boolean; - suppressTyping: boolean; -}; - -/** Resolves product reply policy from portable turn facts and channel capabilities. */ -export function resolveChannelInboundReplyPolicy(params: { - cfg: OpenClawConfig; - ctx: SourceReplyDeliveryModeContext & { - ChatType?: string; - WasMentioned?: boolean; - }; - blockStreamingEnabled?: boolean; -}): ChannelInboundReplyPolicy { - const isRoom = params.ctx.ChatType === "group" || params.ctx.ChatType === "channel"; - const sourceReplyDeliveryMode = isRoom - ? resolveChannelSourceReplyDeliveryMode({ - cfg: params.cfg, - ctx: params.ctx, - }) - : undefined; - const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only"; - return { - sourceReplyDeliveryMode, - disableBlockStreaming: sourceRepliesAreToolOnly - ? true - : typeof params.blockStreamingEnabled === "boolean" - ? !params.blockStreamingEnabled - : undefined, - suppressTyping: - sourceRepliesAreToolOnly && - params.ctx.ChatType === "group" && - params.ctx.WasMentioned !== true, - }; -} - /** Reply pipeline options shared by core channel turns and plugin SDK callers. */ export type ChannelReplyPipeline = ReplyPrefixOptions & { /** Resolves a response prefix against the pipeline's live selected-model context. */ diff --git a/src/plugin-sdk/channel-inbound.test.ts b/src/plugin-sdk/channel-inbound.test.ts index 940574c78f2a..b9ee05231ae9 100644 --- a/src/plugin-sdk/channel-inbound.test.ts +++ b/src/plugin-sdk/channel-inbound.test.ts @@ -5,8 +5,6 @@ import { describe, expect, expectTypeOf, it } from "vitest"; import { buildChannelInboundEventContext, type BuildChannelInboundEventContextParams, - type PreparedChannelInbound, - projectPreparedChannelInbound, type PluginHookChannelSenderContext, } from "./channel-inbound.js"; @@ -73,97 +71,4 @@ describe("channel-inbound public helpers", () => { expect(ctx.ChannelContext?.sender?.testUnionId).toBe("union-1"); }); - - it("builds a portable prepared inbound without channel-native types", () => { - const inbound = { - channel: "example", - accountId: "work", - event: { - id: "event-1", - fullId: "example:event-1", - timestamp: 1_710_000_000, - }, - from: "example:user:u1", - sender: { - id: "u1", - name: "Alice", - }, - conversation: { - kind: "group", - id: "room-1", - label: "Example Room", - }, - route: { - agentId: "main", - accountId: "work", - routeSessionKey: "agent:main:example:group:room-1", - }, - reply: { - to: "example:room:room-1", - replyToId: "quoted-1", - }, - message: { - body: "agent body", - bodyForAgent: "agent body", - rawBody: "raw body", - commandBody: "/status", - }, - command: { - kind: "text-slash", - body: "/status", - authorization: { - kind: "denied", - reason: "sender_not_allowed", - }, - }, - media: [ - { - path: "/tmp/example.jpg", - contentType: "image/jpeg", - kind: "image", - }, - ], - context: { - senderE164: "+15550001111", - }, - } satisfies PreparedChannelInbound; - - const projected = projectPreparedChannelInbound({ - inbound, - control: { messageReceivedHooks: "core" }, - }); - expect(projected.input).toEqual({ - id: "event-1", - timestamp: 1_710_000_000, - rawText: "raw body", - textForAgent: "agent body", - textForCommands: "/status", - raw: inbound, - }); - expect(inbound.command.authorization).toEqual({ - kind: "denied", - reason: "sender_not_allowed", - }); - - const ctx = projected.context; - expect(ctx).toMatchObject({ - MessageSid: "event-1", - MessageSidFull: "example:event-1", - BodyForAgent: "agent body", - RawBody: "raw body", - CommandBody: "/status", - ReplyToId: "quoted-1", - CommandAuthorized: false, - ConversationLabel: "Example Room", - GroupSubject: "Example Room", - SenderE164: "+15550001111", - media: [ - { - path: "/tmp/example.jpg", - contentType: "image/jpeg", - kind: "image", - }, - ], - }); - }); }); diff --git a/src/plugin-sdk/channel-inbound.ts b/src/plugin-sdk/channel-inbound.ts index c21d66a38e9e..db4419a47b00 100644 --- a/src/plugin-sdk/channel-inbound.ts +++ b/src/plugin-sdk/channel-inbound.ts @@ -114,11 +114,6 @@ export type { FinalizeChannelInboundContextParams, FinalizeChannelInboundContextResult, }; -export { - projectPreparedChannelInbound, - type PreparedChannelInbound, -} from "../channels/inbound-event/prepared.js"; -export { resolveChannelInboundReplyPolicy } from "../channels/message/index.js"; /** * Deprecated turn-context input alias that still accepts the old `inboundTurnKind` name. *