Skip to content

Commit efdaed8

Browse files
authored
Merge branch 'main' into nova/sdk-app-client-p0
2 parents 397a9e4 + 2d1523e commit efdaed8

42 files changed

Lines changed: 1227 additions & 249 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ Docs: https://docs.openclaw.ai
3131

3232
### Fixes
3333

34+
- Bonjour/Gateway: cap flapping advertiser restarts in a sliding window, so mDNS probing/name-conflict loops disable discovery instead of churning indefinitely on constrained hosts. Refs #74209 and #74242. Thanks @ndj888 and @Sanjays2402.
35+
- Plugins/runtime-deps: verify staged package entry files before reusing mirrored runtime roots, so browser-control repairs incomplete `ajv`/MCP SDK installs after update instead of failing after restart on a missing `ajv/dist/ajv.js`. Refs #74630. Thanks @spickeringlr.
36+
- Heartbeat: resolve `responsePrefix` template variables with the selected provider, model, and thinking context before delivering alerts or suppressing prefixed `HEARTBEAT_OK` replies. Fixes #43064; repairs #43065; supersedes #46858. Thanks @yweiii and @JunJD.
3437
- Channels/Feishu: retry file-typed iOS video resource downloads as `media` after a Feishu/Lark HTTP 502 and preserve the original 502 when the fallback also fails. Fixes #49855; carries forward #50164 and #73986. Thanks @alex-xuweilong.
3538
- Providers/Amazon Bedrock: expose the full Claude Opus 4.7 thinking profile (`xhigh`, `adaptive`, and `max`) for Bedrock model refs, while keeping Opus/Sonnet 4.6 on adaptive-by-default, so `/think` menus and validation match the Anthropic transport behavior. Fixes #74701. Thanks @prasad-yashdeep, @sparkleHazard, @Sanjays2402, and @hclsys.
3639
- Plugins/tokenjuice: compile the bundled plugin against tokenjuice 0.7.0's published OpenClaw host types instead of a local compatibility shim, so package contract drift fails in OpenClaw validation before release. Thanks @vincentkoc.
@@ -44,6 +47,9 @@ Docs: https://docs.openclaw.ai
4447
- Agents/Codex: flush accepted debounced steering messages before normal app-server turn cleanup, so inbound follow-ups acknowledged as queued are not dropped when the turn completes before the debounce fires. Thanks @vincentkoc.
4548
- Slack/interactive replies: keep rendered buttons and selects within Slack Block Kit value and count limits, and align command argument select values with Slack's option limit, so overlong agent-authored choices no longer make Slack reject the whole block payload. Thanks @slackapi.
4649
- Slack/interactive replies: drop overlong Block Kit button URLs while preserving valid callback values, so malformed link buttons no longer make Slack reject the whole interactive reply. Thanks @slackapi.
50+
- Slack/commands: truncate native command argument-menu confirmation text to Slack's dialog limit, so long plugin arg names no longer make fallback buttons render invalid Block Kit payloads. Thanks @slackapi.
51+
- Slack/exec approvals: cap native approval metadata context to Slack's element and text limits, so large approval details no longer make Slack reject the approval card. Thanks @slackapi.
52+
- Channels/WhatsApp: require Baileys outbound message ids before marking auto-replies delivered, so transcript text and ack reactions no longer make failed group replies look sent. Fixes #49225. Thanks @TinyTb.
4753
- CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash.
4854
- Channels/Voice call: keep pre-auth webhook in-flight limiting active when socket remote address metadata is missing, so slow-body requests from stripped-IP proxy paths still share the fallback bucket. (#74453) Thanks @davidangularme.
4955
- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
ae28566c922ce79527943b069abc199de28e3898ec08eea12c4ff6050795f276 plugin-sdk-api-baseline.json
2-
79446b23832949553b23e7cf92be37b81c69d123fc09bed6f8fc04bd98e9257d plugin-sdk-api-baseline.jsonl
1+
d26a70c9ea3bd277135a1712556f07195fb464b5cd846d04f18c2166c319a73d plugin-sdk-api-baseline.json
2+
9fe2cb122fb3de17eaaf54c7768f268aa689063cf9091bd4b0be9422550a70a8 plugin-sdk-api-baseline.jsonl

docs/channels/whatsapp.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,15 @@ Behavior notes:
569569

570570
</Accordion>
571571

572+
<Accordion title="Reply appears in transcript but not in WhatsApp">
573+
Transcript rows record what the agent generated. WhatsApp delivery is checked separately: OpenClaw only treats an auto-reply as sent after Baileys returns an outbound message id for at least one visible text or media send.
574+
575+
Ack reactions are independent pre-reply receipts. A successful reaction does not prove that the later text or media reply was accepted by WhatsApp.
576+
577+
Check gateway logs for `auto-reply delivery failed` or `auto-reply was not accepted by WhatsApp provider`.
578+
579+
</Accordion>
580+
572581
<Accordion title="Group messages unexpectedly ignored">
573582
Check in this order:
574583

extensions/bonjour/src/advertiser.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,49 @@ describe("gateway bonjour advertiser", () => {
727727
expect(shutdown).toHaveBeenCalledTimes(1);
728728
});
729729

730+
it("disables bonjour when the advertiser flaps within a sliding window", async () => {
731+
enableAdvertiserUnitMode();
732+
vi.useFakeTimers();
733+
734+
const stateRef = { value: "announced" };
735+
const destroy = vi.fn().mockResolvedValue(undefined);
736+
const advertise = vi.fn().mockResolvedValue(undefined);
737+
mockCiaoService({ advertise, destroy, stateRef });
738+
739+
const started = await startAdvertiser({
740+
gatewayPort: 18789,
741+
sshPort: 2222,
742+
});
743+
744+
for (let cycle = 0; cycle < 12; cycle += 1) {
745+
stateRef.value = "announced";
746+
await vi.advanceTimersByTimeAsync(5_000);
747+
stateRef.value = "probing";
748+
await vi.advanceTimersByTimeAsync(25_000);
749+
if (
750+
logger.warn.mock.calls.some(
751+
(call) => typeof call[0] === "string" && call[0].includes("disabling advertiser after"),
752+
)
753+
) {
754+
break;
755+
}
756+
}
757+
758+
const disableLog = logger.warn.mock.calls.find(
759+
(call) => typeof call[0] === "string" && call[0].includes("disabling advertiser after"),
760+
);
761+
expect(disableLog).toBeDefined();
762+
expect(String(disableLog?.[0])).toMatch(/restarts within \d+ minutes/);
763+
764+
const advertiseCallsAtDisable = advertise.mock.calls.length;
765+
const createServiceCallsAtDisable = createService.mock.calls.length;
766+
await vi.advanceTimersByTimeAsync(5 * 60_000);
767+
expect(advertise).toHaveBeenCalledTimes(advertiseCallsAtDisable);
768+
expect(createService).toHaveBeenCalledTimes(createServiceCallsAtDisable);
769+
770+
await started.stop();
771+
});
772+
730773
it("normalizes hostnames with domains for service names", async () => {
731774
// Allow advertiser to run in unit tests.
732775
delete process.env.VITEST;

extensions/bonjour/src/advertiser.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ const REPAIR_DEBOUNCE_MS = 30_000;
8888
// See https://github.com/openclaw/openclaw/issues/72481
8989
const STUCK_ANNOUNCING_MS = 20_000;
9090
const MAX_CONSECUTIVE_RESTARTS = 3;
91+
// A flapping advertiser can briefly reach "announced" between probing
92+
// failures, which resets the consecutive counter. Bound total restarts too.
93+
const RESTART_WINDOW_MS = 30 * 60_000;
94+
const MAX_RESTARTS_IN_WINDOW = 5;
9195
const BONJOUR_ANNOUNCED_STATE = "announced";
9296
const CIAO_SELF_PROBE_RETRY_FRAGMENT =
9397
"failed probing with reason: Error: Can't probe for a service which is announced already.";
@@ -563,6 +567,7 @@ export async function startGatewayBonjourAdvertiser(
563567
let recreatePromise: Promise<void> | null = null;
564568
let disabled = false;
565569
let consecutiveRestarts = 0;
570+
const restartTimestamps: number[] = [];
566571
let cycle: BonjourCycle | null = createCycle();
567572
const stateTracker = new Map<string, ServiceStateTracker>();
568573

@@ -590,10 +595,25 @@ export async function startGatewayBonjourAdvertiser(
590595
}
591596
recreatePromise = (async () => {
592597
consecutiveRestarts += 1;
593-
if (consecutiveRestarts > MAX_CONSECUTIVE_RESTARTS) {
598+
const now = Date.now();
599+
while (
600+
restartTimestamps.length > 0 &&
601+
now - (restartTimestamps[0] ?? 0) > RESTART_WINDOW_MS
602+
) {
603+
restartTimestamps.shift();
604+
}
605+
restartTimestamps.push(now);
606+
const tooManyConsecutive = consecutiveRestarts > MAX_CONSECUTIVE_RESTARTS;
607+
const tooManyInWindow = restartTimestamps.length >= MAX_RESTARTS_IN_WINDOW;
608+
if (tooManyConsecutive || tooManyInWindow) {
594609
disabled = true;
610+
const detail = tooManyConsecutive
611+
? `${MAX_CONSECUTIVE_RESTARTS} failed restarts`
612+
: `${MAX_RESTARTS_IN_WINDOW} restarts within ${Math.round(
613+
RESTART_WINDOW_MS / 60_000,
614+
)} minutes`;
595615
logger.warn(
596-
`bonjour: disabling advertiser after ${MAX_CONSECUTIVE_RESTARTS} failed restarts (${reason}); set discovery.mdns.mode="off" or OPENCLAW_DISABLE_BONJOUR=1 to disable mDNS discovery`,
616+
`bonjour: disabling advertiser after ${detail} (${reason}); set discovery.mdns.mode="off" or OPENCLAW_DISABLE_BONJOUR=1 to disable mDNS discovery`,
597617
);
598618
const previous = cycle;
599619
cycle = null;

extensions/imessage/src/monitor/monitor-provider.ts

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,9 @@ import {
1212
} from "openclaw/plugin-sdk/conversation-runtime";
1313
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
1414
import { normalizeScpRemoteHost } from "openclaw/plugin-sdk/host-runtime";
15-
import {
16-
hasFinalInboundReplyDispatch,
17-
runPreparedInboundReplyTurn,
18-
} from "openclaw/plugin-sdk/inbound-reply-dispatch";
15+
import { runPreparedInboundReplyTurn } from "openclaw/plugin-sdk/inbound-reply-dispatch";
1916
import { isInboundPathAllowed, kindFromMime } from "openclaw/plugin-sdk/media-runtime";
20-
import {
21-
clearHistoryEntriesIfEnabled,
22-
DEFAULT_GROUP_HISTORY_LIMIT,
23-
type HistoryEntry,
24-
} from "openclaw/plugin-sdk/reply-history";
17+
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
2518
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
2619
import { dispatchInboundMessage } from "openclaw/plugin-sdk/reply-runtime";
2720
import { createReplyDispatcher } from "openclaw/plugin-sdk/reply-runtime";
@@ -442,7 +435,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
442435
},
443436
});
444437

445-
const { dispatchResult } = await runPreparedInboundReplyTurn({
438+
await runPreparedInboundReplyTurn({
446439
channel: "imessage",
447440
accountId: decision.route.accountId,
448441
routeSessionKey: decision.route.sessionKey,
@@ -475,6 +468,12 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
475468
logVerbose(`imessage: failed updating session meta: ${String(err)}`);
476469
},
477470
},
471+
history: {
472+
isGroup: decision.isGroup,
473+
historyKey: decision.historyKey,
474+
historyMap: groupHistories,
475+
limit: historyLimit,
476+
},
478477
onPreDispatchFailure: () => settleReplyDispatcher({ dispatcher }),
479478
runDispatch: () =>
480479
dispatchInboundMessage({
@@ -490,23 +489,6 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
490489
},
491490
}),
492491
});
493-
if (!hasFinalInboundReplyDispatch(dispatchResult)) {
494-
if (decision.isGroup && decision.historyKey) {
495-
clearHistoryEntriesIfEnabled({
496-
historyMap: groupHistories,
497-
historyKey: decision.historyKey,
498-
limit: historyLimit,
499-
});
500-
}
501-
return;
502-
}
503-
if (decision.isGroup && decision.historyKey) {
504-
clearHistoryEntriesIfEnabled({
505-
historyMap: groupHistories,
506-
historyKey: decision.historyKey,
507-
limit: historyLimit,
508-
});
509-
}
510492
}
511493

512494
const handleMessage = async (raw: unknown) => {

extensions/line/src/monitor.ts

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
import type { webhook } from "@line/bot-sdk";
22
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
33
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
4-
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
5-
import {
6-
hasFinalInboundReplyDispatch,
7-
runPreparedInboundReplyTurn,
8-
} from "openclaw/plugin-sdk/inbound-reply-dispatch";
9-
import {
10-
dispatchReplyWithBufferedBlockDispatcher,
11-
chunkMarkdownText,
12-
} from "openclaw/plugin-sdk/reply-runtime";
4+
import { hasFinalInboundReplyDispatch } from "openclaw/plugin-sdk/inbound-reply-dispatch";
5+
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
136
import {
147
danger,
158
logVerbose,
@@ -32,6 +25,7 @@ import { deliverLineAutoReply } from "./auto-reply-delivery.js";
3225
import { createLineBot } from "./bot.js";
3326
import { processLineMessage } from "./markdown-to-line.js";
3427
import { sendLineReplyChunks } from "./reply-chunks.js";
28+
import { getLineRuntime } from "./runtime.js";
3529
import {
3630
createFlexMessage,
3731
createImageMessage,
@@ -236,21 +230,36 @@ export async function monitorLineProvider(
236230
accountId: route.accountId,
237231
});
238232

239-
const { dispatchResult } = await runPreparedInboundReplyTurn({
233+
const core = getLineRuntime();
234+
const { dispatchResult } = await core.channel.turn.run({
240235
channel: "line",
241236
accountId: route.accountId,
242-
routeSessionKey: route.sessionKey,
243-
storePath: ctx.turn.storePath,
244-
ctxPayload,
245-
recordInboundSession,
246-
record: ctx.turn.record,
247-
runDispatch: () =>
248-
dispatchReplyWithBufferedBlockDispatcher({
249-
ctx: ctxPayload,
237+
raw: ctx,
238+
adapter: {
239+
ingest: () => ({
240+
id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
241+
rawText: ctxPayload.RawBody ?? ctxPayload.BodyForAgent ?? "",
242+
}),
243+
resolveTurn: () => ({
250244
cfg: config,
245+
channel: "line",
246+
accountId: route.accountId,
247+
agentId: route.agentId,
248+
routeSessionKey: route.sessionKey,
249+
storePath: ctx.turn.storePath,
250+
ctxPayload,
251+
recordInboundSession: core.channel.session.recordInboundSession,
252+
dispatchReplyWithBufferedBlockDispatcher:
253+
core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
254+
record: ctx.turn.record,
251255
dispatcherOptions: {
252256
...replyPipeline,
253-
deliver: async (payload, _info) => {
257+
},
258+
replyOptions: {
259+
onModelSelected,
260+
},
261+
delivery: {
262+
deliver: async (payload) => {
254263
const lineData = (payload.channelData?.line as LineChannelData | undefined) ?? {};
255264

256265
if (ctx.userId && !ctx.isGroup) {
@@ -304,10 +313,8 @@ export async function monitorLineProvider(
304313
runtime.error?.(danger(`line ${info.kind} reply failed: ${String(err)}`));
305314
},
306315
},
307-
replyOptions: {
308-
onModelSelected,
309-
},
310316
}),
317+
},
311318
});
312319
if (!hasFinalInboundReplyDispatch(dispatchResult)) {
313320
logVerbose(`line: no response generated for message from ${ctxPayload.From}`);

extensions/signal/src/monitor/event-handler.ts

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,10 @@ import {
2525
toInternalMessageReceivedContext,
2626
triggerInternalHook,
2727
} from "openclaw/plugin-sdk/hook-runtime";
28-
import {
29-
hasFinalInboundReplyDispatch,
30-
runPreparedInboundReplyTurn,
31-
} from "openclaw/plugin-sdk/inbound-reply-dispatch";
28+
import { runPreparedInboundReplyTurn } from "openclaw/plugin-sdk/inbound-reply-dispatch";
3229
import { kindFromMime } from "openclaw/plugin-sdk/media-runtime";
3330
import {
3431
buildPendingHistoryContextFromMap,
35-
clearHistoryEntriesIfEnabled,
3632
recordPendingHistoryEntryIfEnabled,
3733
} from "openclaw/plugin-sdk/reply-history";
3834
import { dispatchInboundMessage } from "openclaw/plugin-sdk/reply-runtime";
@@ -292,7 +288,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
292288
},
293289
});
294290

295-
const { dispatchResult } = await runPreparedInboundReplyTurn({
291+
await runPreparedInboundReplyTurn({
296292
channel: "signal",
297293
accountId: route.accountId,
298294
routeSessionKey: route.sessionKey,
@@ -331,6 +327,12 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
331327
logVerbose(`signal: failed updating session meta: ${String(err)}`);
332328
},
333329
},
330+
history: {
331+
isGroup: entry.isGroup,
332+
historyKey,
333+
historyMap: deps.groupHistories,
334+
limit: deps.historyLimit,
335+
},
334336
onPreDispatchFailure: () =>
335337
settleReplyDispatcher({
336338
dispatcher,
@@ -354,23 +356,6 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
354356
}
355357
},
356358
});
357-
if (!hasFinalInboundReplyDispatch(dispatchResult)) {
358-
if (entry.isGroup && historyKey) {
359-
clearHistoryEntriesIfEnabled({
360-
historyMap: deps.groupHistories,
361-
historyKey,
362-
limit: deps.historyLimit,
363-
});
364-
}
365-
return;
366-
}
367-
if (entry.isGroup && historyKey) {
368-
clearHistoryEntriesIfEnabled({
369-
historyMap: deps.groupHistories,
370-
historyKey,
371-
limit: deps.historyLimit,
372-
});
373-
}
374359
}
375360

376361
const { debouncer: inboundDebouncer } = createChannelInboundDebouncer<SignalInboundEntry>({

0 commit comments

Comments
 (0)