diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256
index a20226d35441..0bec19087892 100644
--- a/docs/.generated/plugin-sdk-api-baseline.sha256
+++ b/docs/.generated/plugin-sdk-api-baseline.sha256
@@ -1,2 +1,2 @@
-8bb175b965c6fab4512f110cd584ba601074885fb49375f2dd18e832882d61a5 plugin-sdk-api-baseline.json
-5a1548ecb0cf8dffb67591b41b9e1a3c04ee7fe0cb22ac2f3bcfa24e319f4f47 plugin-sdk-api-baseline.jsonl
+598c5f1bb39362de6b11b4cd3403551f99fb7c3bbf85d5e3dcf1b1408a8cec24 plugin-sdk-api-baseline.json
+d87a296d6d449c2e01b63eeb1e1743d1f11dcdf4091520328c54798ac18f149f plugin-sdk-api-baseline.jsonl
diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md
index 1eddc02694cc..888c5e364252 100644
--- a/docs/plugins/sdk-migration.md
+++ b/docs/plugins/sdk-migration.md
@@ -791,6 +791,18 @@ major release. Every entry maps the old API to its canonical replacement.
+
+ **Old**: return `{ ok, messageId, error }` through
+ `ChannelSendRawResult` and normalize it with
+ `createRawChannelSendResultAdapter(...)`.
+
+ **New**: return `OutboundDeliveryResult` fields and attach the channel with
+ `createAttachedChannelResultAdapter(...)`. Failed sends should throw instead
+ of returning an error string. The raw result type remains available until
+ the next plugin-SDK major release.
+
+
+
Two legacy type aliases still exported from `src/plugins/runtime/types.ts`:
diff --git a/extensions/telegram/src/inline-buttons.test.ts b/extensions/telegram/src/inline-buttons.test.ts
index 0dc0a82baced..dd446c4ce832 100644
--- a/extensions/telegram/src/inline-buttons.test.ts
+++ b/extensions/telegram/src/inline-buttons.test.ts
@@ -1,5 +1,5 @@
// Telegram tests cover inline buttons plugin behavior.
-import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
+import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it } from "vitest";
import { buildTelegramInteractiveButtons } from "./button-types.js";
import { describeTelegramInteractiveButtonBehavior } from "./button-types.test-helpers.js";
diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts
index 330b8bf8ebf7..e8e4b38960ea 100644
--- a/extensions/zalo/src/channel.ts
+++ b/extensions/zalo/src/channel.ts
@@ -14,15 +14,18 @@ import {
createChatChannelPlugin,
type ChannelPlugin,
} from "openclaw/plugin-sdk/channel-core";
-import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
+import {
+ defineChannelMessageAdapter,
+ type MessageReceipt,
+} from "openclaw/plugin-sdk/channel-outbound";
import {
buildOpenGroupPolicyRestrictSendersWarning,
buildOpenGroupPolicyWarning,
createOpenProviderGroupPolicyWarningCollector,
} from "openclaw/plugin-sdk/channel-policy";
import {
+ createAttachedChannelResultAdapter,
createEmptyChannelResult,
- createRawChannelSendResultAdapter,
} from "openclaw/plugin-sdk/channel-send-result";
import { buildTokenChannelStatusSummary } from "openclaw/plugin-sdk/channel-status";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
@@ -82,27 +85,32 @@ const zaloSetupWizard = createZaloSetupWizardProxy(
);
const zaloTextChunkLimit = 2000;
-const zaloRawSendResultAdapter = createRawChannelSendResultAdapter({
+async function sendZaloDelivery(ctx: {
+ cfg: OpenClawConfig;
+ to: string;
+ text: string;
+ accountId?: string | null;
+ mediaUrl?: string;
+}): Promise<{ messageId: string; receipt: MessageReceipt }> {
+ const result = await (
+ await loadZaloChannelRuntime()
+ ).sendZaloText({
+ to: ctx.to,
+ text: ctx.text,
+ accountId: ctx.accountId ?? undefined,
+ mediaUrl: ctx.mediaUrl,
+ cfg: ctx.cfg,
+ });
+ if (!result.ok) {
+ throw new Error(result.error ?? `Failed to send Zalo ${ctx.mediaUrl ? "media" : "message"}`);
+ }
+ return { messageId: result.messageId ?? "", receipt: result.receipt };
+}
+
+const zaloSendResultAdapter = createAttachedChannelResultAdapter({
channel: "zalo",
- sendText: async ({ to, text, accountId, cfg }) =>
- await (
- await loadZaloChannelRuntime()
- ).sendZaloText({
- to,
- text,
- accountId: accountId ?? undefined,
- cfg,
- }),
- sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) =>
- await (
- await loadZaloChannelRuntime()
- ).sendZaloText({
- to,
- text,
- accountId: accountId ?? undefined,
- mediaUrl,
- cfg,
- }),
+ sendText: sendZaloDelivery,
+ sendMedia: sendZaloDelivery,
});
export const zaloMessageAdapter = defineChannelMessageAdapter({
@@ -115,25 +123,8 @@ export const zaloMessageAdapter = defineChannelMessageAdapter({
},
},
send: {
- text: async ({ to, text, accountId, cfg }) =>
- await (
- await loadZaloChannelRuntime()
- ).sendZaloText({
- to,
- text,
- accountId: accountId ?? undefined,
- cfg,
- }),
- media: async ({ to, text, mediaUrl, accountId, cfg }) =>
- await (
- await loadZaloChannelRuntime()
- ).sendZaloText({
- to,
- text,
- accountId: accountId ?? undefined,
- mediaUrl,
- cfg,
- }),
+ text: sendZaloDelivery,
+ media: sendZaloDelivery,
},
});
@@ -303,11 +294,11 @@ export const zaloPlugin: ChannelPlugin =
ctx,
textChunkLimit: zaloTextChunkLimit,
chunker: chunkTextForOutbound,
- sendText: (nextCtx) => zaloRawSendResultAdapter.sendText!(nextCtx),
- sendMedia: (nextCtx) => zaloRawSendResultAdapter.sendMedia!(nextCtx),
+ sendText: (nextCtx) => zaloSendResultAdapter.sendText!(nextCtx),
+ sendMedia: (nextCtx) => zaloSendResultAdapter.sendMedia!(nextCtx),
emptyResult: createEmptyChannelResult("zalo"),
onResult: ctx.onDeliveryResult,
}),
- ...zaloRawSendResultAdapter,
+ ...zaloSendResultAdapter,
},
});
diff --git a/extensions/zalo/src/outbound-payload.contract.test.ts b/extensions/zalo/src/outbound-payload.contract.test.ts
index 0ab2a6cf7676..d48e8800e3b3 100644
--- a/extensions/zalo/src/outbound-payload.contract.test.ts
+++ b/extensions/zalo/src/outbound-payload.contract.test.ts
@@ -6,9 +6,16 @@ import {
} from "openclaw/plugin-sdk/channel-contract-testing";
import {
createMessageReceiptFromOutboundResults,
+ sendDurableMessageBatch,
verifyChannelMessageAdapterCapabilityProofs,
} from "openclaw/plugin-sdk/channel-outbound";
-import { describe, expect, it, vi } from "vitest";
+import {
+ createEmptyPluginRegistry,
+ createTestRegistry,
+ resetPluginRuntimeStateForTest,
+ setActivePluginRegistry,
+} from "openclaw/plugin-sdk/plugin-test-runtime";
+import { afterEach, describe, expect, it, vi } from "vitest";
import { zaloMessageAdapter, zaloPlugin } from "./channel.js";
const { sendZaloTextMock } = vi.hoisted(() => ({
@@ -47,9 +54,21 @@ function requireZaloMediaSender(): NonNullable {
return media;
}
+function requireZaloOutboundTextSender(): NonNullable {
+ const sendText = zaloPlugin.outbound?.sendText;
+ if (!sendText) {
+ throw new Error("Expected Zalo outbound text sender");
+ }
+ return sendText;
+}
+
function createZaloHarness(params: OutboundPayloadHarnessParams) {
const sendZalo = vi.fn();
- primeChannelOutboundSendMock(sendZalo, { ok: true, messageId: "zl-1" }, params.sendResults);
+ primeChannelOutboundSendMock(
+ sendZalo,
+ { ok: true, messageId: "zl-1" },
+ params.sendResults?.map((result) => ({ ok: true, ...result })),
+ );
sendZaloTextMock.mockReset().mockImplementation(
async (nextCtx: { to: string; text: string; mediaUrl?: string }) =>
await sendZalo(nextCtx.to, nextCtx.text, {
@@ -70,6 +89,11 @@ function createZaloHarness(params: OutboundPayloadHarnessParams) {
};
}
+afterEach(() => {
+ resetPluginRuntimeStateForTest();
+ setActivePluginRegistry(createEmptyPluginRegistry());
+});
+
describe("Zalo outbound payload contract", () => {
installChannelOutboundPayloadContractSuite({
channel: "zalo",
@@ -77,6 +101,52 @@ describe("Zalo outbound payload contract", () => {
createHarness: createZaloHarness,
});
+ it("throws legacy raw send failures at the typed delivery boundary", async () => {
+ sendZaloTextMock.mockReset().mockResolvedValue({
+ ok: false,
+ error: "Zalo rejected the message",
+ receipt: createMessageReceiptFromOutboundResults({ results: [], kind: "text" }),
+ });
+
+ await expect(
+ requireZaloOutboundTextSender()({ cfg: {}, to: "123456789", text: "hello" }),
+ ).rejects.toThrow("Zalo rejected the message");
+ });
+
+ it.each([
+ { kind: "text", payload: { text: "hello" } },
+ {
+ kind: "media",
+ payload: { text: "image", mediaUrl: "https://example.com/image.png" },
+ },
+ ] as const)(
+ "reports $kind message-adapter failures to durable delivery",
+ async ({ kind, payload }) => {
+ setActivePluginRegistry(
+ createTestRegistry([{ pluginId: "zalo", source: "test", plugin: zaloPlugin }]),
+ );
+ sendZaloTextMock.mockReset().mockResolvedValue({
+ ok: false,
+ error: `Zalo rejected the ${kind}`,
+ receipt: createMessageReceiptFromOutboundResults({ results: [], kind }),
+ });
+
+ const result = await sendDurableMessageBatch({
+ cfg: {},
+ channel: "zalo",
+ to: "123456789",
+ payloads: [payload],
+ skipQueue: true,
+ });
+
+ expect(result.status).toBe("failed");
+ if (result.status !== "failed") {
+ throw new Error(`Expected failed Zalo ${kind} delivery`);
+ }
+ expect(result.error).toMatchObject({ message: `Zalo rejected the ${kind}` });
+ },
+ );
+
it("declares message adapter durable text and media with receipt proofs", async () => {
sendZaloTextMock.mockReset().mockImplementation(async (ctx: { mediaUrl?: string }) =>
ctx.mediaUrl
diff --git a/scripts/check-pairing-account-scope.mjs b/scripts/check-pairing-account-scope.mjs
index e3370931be09..8fc875ee5497 100644
--- a/scripts/check-pairing-account-scope.mjs
+++ b/scripts/check-pairing-account-scope.mjs
@@ -55,14 +55,6 @@ function findViolations(content, filePath) {
reason: "readChannelAllowFromStore call must pass explicit accountId as 3rd arg",
});
}
- } else if (
- callName === "readLegacyChannelAllowFromStore" ||
- callName === "readLegacyChannelAllowFromStoreSync"
- ) {
- violations.push({
- line: toLine(sourceFile, node),
- reason: `${callName} is legacy-only; use account-scoped readChannelAllowFromStore* APIs`,
- });
} else if (callName === "upsertChannelPairingRequest") {
const firstArg = node.arguments[0];
if (!firstArg || !hasRequiredAccountIdProperty(firstArg)) {
diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs
index 23759606826d..e9b1c8c39639 100644
--- a/scripts/plugin-sdk-surface-report.mjs
+++ b/scripts/plugin-sdk-surface-report.mjs
@@ -104,7 +104,6 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
"channel-streaming": 49,
"approval-gateway-runtime": 1,
"approval-handler-runtime": 1,
- "approval-reaction-runtime": 1,
"approval-reply-runtime": 3,
"approval-runtime": 1,
"config-runtime": 123,
@@ -125,6 +124,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
"ssrf-runtime": 1,
"media-runtime": 2,
"text-runtime": 191,
+ "agent-core": 1,
"agent-runtime": 7,
"plugin-runtime": 13,
"channel-secret-runtime": 23,
@@ -158,6 +158,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
// landed on main (#105802) without entrypoint pins; not touched by this PR.
"channel-pairing": 1,
"conversation-runtime": 4,
+ "channel-send-result": 1,
"channel-policy": 8,
"channel-route": 5,
"session-store-runtime": 4,
@@ -203,20 +204,17 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
),
publicExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
- 10644,
+ 10640,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS",
- 5358,
+ 5354,
env,
),
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS",
- // 3279 + 5 deprecated pairing/conversation exports added on main by the
- // SQLite pairing migration (#105802) without a pin bump (its changed-path
- // set skipped this lane); sources are byte-identical to main here.
- 3284,
+ 3282,
env,
),
publicWildcardReexports: readPluginSdkSurfaceBudgetEnv(
diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts
index 623752622e36..142fd98c53d4 100644
--- a/src/pairing/pairing-store.ts
+++ b/src/pairing/pairing-store.ts
@@ -210,14 +210,6 @@ async function updateAllowFromStoreEntry(params: {
}, sqliteOptionsForEnv(env));
}
-/** @deprecated Reads the canonical default-account SQLite rows. */
-export async function readLegacyChannelAllowFromStore(
- channel: PairingChannel,
- env: NodeJS.ProcessEnv = process.env,
-): Promise {
- return readAllowFromState(channel, env, DEFAULT_ACCOUNT_ID);
-}
-
export async function readChannelAllowFromStore(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
@@ -226,14 +218,6 @@ export async function readChannelAllowFromStore(
return readAllowFromState(channel, env, accountId);
}
-/** @deprecated Reads the canonical default-account SQLite rows. */
-export function readLegacyChannelAllowFromStoreSync(
- channel: PairingChannel,
- env: NodeJS.ProcessEnv = process.env,
-): string[] {
- return readAllowFromState(channel, env, DEFAULT_ACCOUNT_ID);
-}
-
export function readChannelAllowFromStoreSync(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
@@ -242,9 +226,6 @@ export function readChannelAllowFromStoreSync(
return readAllowFromState(channel, env, accountId);
}
-/** @deprecated SQLite reads are uncached; retained for the shipped SDK test surface. */
-export function clearPairingAllowFromReadCacheForTest(): void {}
-
type AllowFromStoreEntryUpdateParams = {
channel: PairingChannel;
entry: string | number;
diff --git a/src/plugin-sdk/approval-reaction-runtime.test.ts b/src/plugin-sdk/approval-reaction-runtime.test.ts
index b431464d9a21..5ac455e44bf8 100644
--- a/src/plugin-sdk/approval-reaction-runtime.test.ts
+++ b/src/plugin-sdk/approval-reaction-runtime.test.ts
@@ -14,7 +14,6 @@ import {
listApprovalReactionBindings,
normalizeApprovalReactionEmoji,
resolveApprovalReactionDecision,
- resolveApprovalReactionTarget,
resolveTypedApprovalReactionTarget,
shouldSuppressLocalNativeExecApprovalPrompt,
} from "./approval-reaction-runtime.js";
@@ -145,23 +144,6 @@ describe("plugin-sdk/approval-reaction-runtime", () => {
});
});
- it("preserves deprecated id-based kind inference", () => {
- expect(
- resolveApprovalReactionTarget({
- target: {
- approvalId: "plugin:legacy-id",
- allowedDecisions: ["deny"],
- },
- reactionKey: "👎",
- }),
- ).toEqual({
- approvalId: "plugin:legacy-id",
- approvalKind: "plugin",
- decision: "deny",
- normalizedEmoji: "👎",
- });
- });
-
it("builds canonical exec reaction prompts without presentation controls", () => {
const payload = buildApprovalReactionPromptPayloadForRequest({
request: execRequest,
diff --git a/src/plugin-sdk/approval-reaction-runtime.ts b/src/plugin-sdk/approval-reaction-runtime.ts
index 1510647cf8fc..6b3f482071ed 100644
--- a/src/plugin-sdk/approval-reaction-runtime.ts
+++ b/src/plugin-sdk/approval-reaction-runtime.ts
@@ -242,20 +242,6 @@ function resolveApprovalReactionTargetInternal(params: {
};
}
-/**
- * Resolve a stored target while retaining shipped id-based ownership inference.
- * @deprecated Use resolveTypedApprovalReactionTarget with an explicit approvalKind.
- */
-export function resolveApprovalReactionTarget(params: {
- target: ApprovalReactionTargetRecord | null | undefined;
- reactionKey: string;
-}): ApprovalReactionTargetResolution | null {
- return resolveApprovalReactionTargetInternal({
- ...params,
- allowLegacyKindInference: true,
- });
-}
-
/** Resolve an explicitly typed target without deriving ownership from its id. */
export function resolveTypedApprovalReactionTarget(params: {
target:
diff --git a/src/plugin-sdk/channel-send-result.ts b/src/plugin-sdk/channel-send-result.ts
index c8957b0f0660..01929966477f 100644
--- a/src/plugin-sdk/channel-send-result.ts
+++ b/src/plugin-sdk/channel-send-result.ts
@@ -6,7 +6,11 @@ import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
export type { ChannelOutboundAdapter } from "../channels/plugins/outbound.types.js";
export type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
-/** Legacy raw send result shape accepted from channel SDK adapters. */
+/**
+ * Legacy raw send result shape accepted from channel SDK adapters.
+ * @deprecated Return `OutboundDeliveryResult` and use
+ * `createAttachedChannelResultAdapter`. Removal with the next plugin-SDK major.
+ */
export type ChannelSendRawResult = {
/** Whether the channel send operation succeeded. */
ok: boolean;