Skip to content

Commit b6ae0d5

Browse files
authored
chore(i18n): re-sync locale metadata after rebase onto refreshed main locales
1 parent ad73fc0 commit b6ae0d5

18 files changed

Lines changed: 1166 additions & 517 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Docs: https://docs.openclaw.ai
2323
- **Remote browser reliability:** bound persistent Playwright tab enumeration by the existing remote CDP timeout budget and retire timed-out connection attempts so late completions cannot restore a stuck connection. (#80147, #58968) Thanks @HemantSudarshan and @KeaneYan.
2424
- **MCP OAuth response bounds:** reject body-less foreign error bodies without calling their inherently unbounded `text()` fallback, while preserving HTTP status and headers for safe SDK diagnostics. (#98143) Thanks @Pick-cat.
2525
- **Control UI approval prompts:** keep stale resolve failures and busy-state cleanup from leaking across newer approvals or Gateway reconnects. (#98394) Thanks @haruaiclone-droid.
26-
- **Agent empty replies:** surface a visible failure when a completed interactive turn has no deliverable reply, including queued follow-ups, while preserving explicit silence, pending continuations, and committed side effects. (#100456) Thanks @mushuiyu886.
26+
- **Agent empty replies:** surface a visible failure when a completed interactive turn has no deliverable reply, including queued follow-ups, while preserving explicit silence, pending continuations, and committed side effects, honoring queued send policies, and treating compaction notices as progress. (#100456) Thanks @mushuiyu886.
2727
- **Child process output safety:** prevent stdout/stderr pipe failures from crashing agent exec sessions, local TUI shell commands, and bounded process execution. (#100407, #100406, #100410) Thanks @cxbAsDev.
2828
- **Background refresh isolation:** keep remote skill-bin refreshes running when one node fails, and contain periodic subagent-sweeper failures without hiding errors from direct callers. (#100393, #100390) Thanks @cxbAsDev.
2929
- **Skill scan diagnostics:** report directory enumeration failures through the existing resource diagnostics instead of silently dropping affected skills. (#100380) Thanks @wendy-chsy.

src/agents/embedded-agent-messaging.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type MessagingToolSend = {
1313
threadSuppressed?: boolean;
1414
text?: string;
1515
mediaUrls?: string[];
16+
hasRichContent?: true;
1617
};
1718

1819
export type MessagingToolSourceReplyPayload = Pick<

src/agents/embedded-agent-runner/delivery-evidence.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
11
import { describe, expect, it } from "vitest";
2-
import { collectDeliveredMediaUrls } from "./delivery-evidence.js";
2+
import {
3+
collectDeliveredMediaUrls,
4+
hasVisibleOutboundDeliveryEvidence,
5+
} from "./delivery-evidence.js";
6+
7+
describe("visible messaging-tool delivery evidence", () => {
8+
it("keeps the coarse flag when detailed delivery metadata is unavailable", () => {
9+
expect(hasVisibleOutboundDeliveryEvidence({ didSendViaMessagingTool: true })).toBe(true);
10+
});
11+
12+
it("lets detailed metadata disprove a coarse send flag", () => {
13+
expect(
14+
hasVisibleOutboundDeliveryEvidence({
15+
didSendViaMessagingTool: true,
16+
messagingToolSentTexts: [" "],
17+
messagingToolSentMediaUrls: ["\t"],
18+
messagingToolSentTargets: [{ text: "\n" }],
19+
}),
20+
).toBe(false);
21+
});
22+
23+
it("keeps rich delivery evidence when accompanying text is blank", () => {
24+
expect(
25+
hasVisibleOutboundDeliveryEvidence({
26+
didSendViaMessagingTool: true,
27+
messagingToolSentTexts: [],
28+
messagingToolSentMediaUrls: [],
29+
messagingToolSentTargets: [{ text: " ", hasRichContent: true }],
30+
}),
31+
).toBe(true);
32+
});
33+
});
334

435
describe("collectDeliveredMediaUrls attachment recursion", () => {
536
it("collects media URLs across nested attachments", () => {

src/agents/embedded-agent-runner/delivery-evidence.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ type AgentDeliveryEvidence = {
4242
};
4343
};
4444

45+
type SourceReplyDeliveryEvidence = {
46+
didDeliverSourceReplyViaMessageTool?: unknown;
47+
messagingToolSourceReplyPayloads?: unknown;
48+
};
49+
4550
function hasNonEmptyString(value: unknown): value is string {
4651
return typeof value === "string" && value.trim().length > 0;
4752
}
@@ -54,6 +59,21 @@ function hasNonEmptyStringArray(value: unknown): boolean {
5459
return Array.isArray(value) && value.some(hasNonEmptyString);
5560
}
5661

62+
function hasVisibleMessagingToolTarget(value: unknown): boolean {
63+
if (!value || typeof value !== "object" || Array.isArray(value)) {
64+
return false;
65+
}
66+
const target = value as { text?: unknown; mediaUrls?: unknown; hasRichContent?: unknown };
67+
if ("text" in target || "mediaUrls" in target || "hasRichContent" in target) {
68+
return (
69+
hasNonEmptyString(target.text) ||
70+
hasNonEmptyStringArray(target.mediaUrls) ||
71+
target.hasRichContent === true
72+
);
73+
}
74+
return true;
75+
}
76+
5777
function hasVisibleAttachmentReference(value: unknown): boolean {
5878
if (!Array.isArray(value)) {
5979
return false;
@@ -227,6 +247,53 @@ export function hasCommittedMessagingToolDeliveryEvidence(
227247
);
228248
}
229249

250+
/** Returns whether messaging-tool metadata proves a user-visible committed delivery. */
251+
export function hasVisibleCommittedMessagingToolDeliveryEvidence(
252+
result: Pick<
253+
AgentDeliveryEvidence,
254+
"messagingToolSentTexts" | "messagingToolSentMediaUrls" | "messagingToolSentTargets"
255+
>,
256+
): boolean {
257+
return (
258+
hasNonEmptyStringArray(result.messagingToolSentTexts) ||
259+
hasNonEmptyStringArray(result.messagingToolSentMediaUrls) ||
260+
(Array.isArray(result.messagingToolSentTargets) &&
261+
result.messagingToolSentTargets.some(hasVisibleMessagingToolTarget))
262+
);
263+
}
264+
265+
function hasGranularMessagingToolDeliveryEvidence(result: AgentDeliveryEvidence): boolean {
266+
return (
267+
result.messagingToolSentTexts !== undefined ||
268+
result.messagingToolSentMediaUrls !== undefined ||
269+
result.messagingToolSentTargets !== undefined
270+
);
271+
}
272+
273+
/** Returns whether a source reply was visibly delivered through the message tool. */
274+
export function hasCommittedSourceReplyDeliveryEvidence(
275+
result: SourceReplyDeliveryEvidence,
276+
): boolean {
277+
return (
278+
result.didDeliverSourceReplyViaMessageTool === true ||
279+
hasVisibleAgentPayload({ payloads: result.messagingToolSourceReplyPayloads })
280+
);
281+
}
282+
283+
/** Returns whether outbound metadata proves a visible message, spawn, or cron side effect. */
284+
export function hasVisibleOutboundDeliveryEvidence(result: AgentDeliveryEvidence): boolean {
285+
return (
286+
hasVisibleCommittedMessagingToolDeliveryEvidence(result) ||
287+
// The coarse flag is the only evidence available for older callers. Once detailed
288+
// metadata exists, it owns visibility so blank sends cannot suppress recovery.
289+
(result.didSendViaMessagingTool === true &&
290+
!hasGranularMessagingToolDeliveryEvidence(result)) ||
291+
(Array.isArray(result.acceptedSessionSpawns) &&
292+
hasAcceptedSessionSpawn(result.acceptedSessionSpawns)) ||
293+
hasPositiveNumber(result.successfulCronAdds)
294+
);
295+
}
296+
230297
/** Returns whether committed outbound evidence makes replay unsafe. */
231298
export function hasCommittedOutboundDeliveryEvidence(result: AgentDeliveryEvidence): boolean {
232299
return (

src/agents/embedded-agent-subscribe.handlers.tools.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,48 @@ describe("handleToolExecutionEnd mutating failure recovery", () => {
11771177
]);
11781178
});
11791179

1180+
it("records rich-content delivery when visible text is blank", async () => {
1181+
const { ctx } = createTestContext();
1182+
const toolCallId = "tool-message-rich-content";
1183+
1184+
await handleToolExecutionStart(
1185+
ctx as never,
1186+
{
1187+
type: "tool_execution_start",
1188+
toolName: "message",
1189+
toolCallId,
1190+
args: {
1191+
action: "send",
1192+
provider: "telegram",
1193+
to: "chat-rich",
1194+
text: " ",
1195+
presentation: JSON.stringify({
1196+
blocks: [{ type: "buttons", buttons: [{ label: "OK", value: "ok" }] }],
1197+
}),
1198+
},
1199+
} as never,
1200+
);
1201+
await handleToolExecutionEnd(
1202+
ctx as never,
1203+
{
1204+
type: "tool_execution_end",
1205+
toolName: "message",
1206+
toolCallId,
1207+
isError: false,
1208+
result: { details: { messageId: "message-rich" } },
1209+
} as never,
1210+
);
1211+
1212+
expect(ctx.state.messagingToolSentTargets).toEqual([
1213+
expect.objectContaining({
1214+
tool: "message",
1215+
provider: "telegram",
1216+
to: "chat-rich",
1217+
hasRichContent: true,
1218+
}),
1219+
]);
1220+
});
1221+
11801222
it("records reply target evidence without treating it as terminal send evidence", async () => {
11811223
const { ctx } = createTestContext();
11821224
const toolCallId = "tool-message-reply-target";

src/agents/embedded-agent-subscribe.handlers.tools.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ import {
3030
emitAgentPatchSummaryEvent,
3131
} from "../infra/agent-events.js";
3232
import type { ExecApprovalDecision } from "../infra/exec-approvals.js";
33+
import {
34+
parseInteractiveParam,
35+
parseJsonMessageParam,
36+
} from "../infra/outbound/message-action-params.js";
37+
import { hasReplyPayloadContent } from "../interactive/payload.js";
3338
import type { PluginHookAfterToolCallEvent } from "../plugins/types.js";
3439
import { createLazyImportLoader } from "../shared/lazy-promise.js";
3540
import { truncateUtf16Safe } from "../utils.js";
@@ -558,6 +563,21 @@ function readMessagingText(record: Record<string, unknown>): string | undefined
558563
return undefined;
559564
}
560565

566+
function hasMessagingRichContent(record: Record<string, unknown>): boolean {
567+
const payload = {
568+
presentation: record.presentation,
569+
interactive: record.interactive,
570+
channelData: record.channelData,
571+
};
572+
try {
573+
parseJsonMessageParam(payload, "presentation");
574+
parseInteractiveParam(payload);
575+
} catch {
576+
return false;
577+
}
578+
return hasReplyPayloadContent(payload);
579+
}
580+
561581
function queuePendingToolMedia(
562582
ctx: ToolHandlerContext,
563583
mediaReply: { mediaUrls: string[]; audioAsVoice?: boolean; trustedLocalMedia?: boolean },
@@ -1268,6 +1288,7 @@ export async function handleToolExecutionEnd(
12681288
});
12691289
const messageText = isMessagingSend ? readMessagingText(startArgs) : undefined;
12701290
const argumentMediaUrls = isMessagingSend ? collectMessagingMediaUrlsFromRecord(startArgs) : [];
1291+
const hasRichContent = isMessagingSend && hasMessagingRichContent(startArgs);
12711292
const messageTarget = hasMessagingTargetEvidence
12721293
? extractMessagingToolSend(toolName, messagingArgs, {
12731294
config: ctx.params.config,
@@ -1300,6 +1321,7 @@ export async function handleToolExecutionEnd(
13001321
...confirmedTarget,
13011322
...(messageText ? { text: messageText } : {}),
13021323
...(committedMediaUrls.length > 0 ? { mediaUrls: committedMediaUrls.slice() } : {}),
1324+
...(hasRichContent ? { hasRichContent: true as const } : {}),
13031325
});
13041326
ctx.trimMessagingToolSent();
13051327
}

src/auto-reply/reply/agent-runner-execution.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { TemplateContext } from "../templating.js";
2525
import { SILENT_REPLY_TOKEN } from "../tokens.js";
2626
import type { GetReplyOptions, ReplyPayload } from "../types.js";
2727
import {
28+
buildEmptyInteractiveReplyPayload,
2829
buildContextOverflowRecoveryText,
2930
computeContextAwareReserveTokensFloor,
3031
MAX_LIVE_SWITCH_RETRIES,
@@ -58,6 +59,8 @@ const state = vi.hoisted(() => ({
5859

5960
const GENERIC_RUN_FAILURE_TEXT =
6061
"⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.";
62+
const EMPTY_INTERACTIVE_REPLY_TEXT =
63+
"I finished the turn, but it did not produce a visible reply. Please try again, or start a new session if this keeps happening.";
6164

6265
describe("resolveSessionRuntimeOverrideForProvider", () => {
6366
afterEach(() => {
@@ -649,6 +652,37 @@ describe("computeContextAwareReserveTokensFloor", () => {
649652
});
650653
});
651654

655+
describe("buildEmptyInteractiveReplyPayload", () => {
656+
const baseParams = {
657+
isInteractive: true,
658+
isMessageToolOnly: false,
659+
hasPendingContinuation: false,
660+
hasExplicitSilentReply: false,
661+
hasCommittedDelivery: false,
662+
sessionCtx: {
663+
Provider: "discord",
664+
Surface: "discord",
665+
ChatType: "group",
666+
},
667+
} as const;
668+
669+
it("preserves the default silent policy in group conversations", () => {
670+
const payload = buildEmptyInteractiveReplyPayload(baseParams);
671+
672+
expect(payload?.text).toBe(SILENT_REPLY_TOKEN);
673+
expect(payload?.isError).toBeUndefined();
674+
});
675+
676+
it("surfaces the fallback when group silence is explicitly disallowed", () => {
677+
expect(
678+
buildEmptyInteractiveReplyPayload({
679+
...baseParams,
680+
cfg: { agents: { defaults: { silentReply: { group: "disallow" } } } },
681+
}),
682+
).toMatchObject({ text: EMPTY_INTERACTIVE_REPLY_TEXT, isError: true });
683+
});
684+
});
685+
652686
describe("buildContextOverflowRecoveryText", () => {
653687
it("keeps the generic compaction-buffer hint without heartbeat model evidence", () => {
654688
const text = buildContextOverflowRecoveryText({

src/auto-reply/reply/agent-runner-execution.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,12 +1857,12 @@ async function runAgentTurnWithFallbackInternal(
18571857
const currentMessageId = params.sessionCtx.MessageSidFull ?? params.sessionCtx.MessageSid;
18581858
const notifyUserAboutCompaction = shouldNotifyUserAboutCompaction(runtimeConfig);
18591859
const deliverCompactionNoticePayload = async (noticePayload: ReplyPayload, label: string) => {
1860+
const deliver = params.opts?.onBlockReply ?? params.onCompactionNoticePayload;
1861+
if (!deliver) {
1862+
return;
1863+
}
18601864
try {
1861-
if (params.opts?.onBlockReply) {
1862-
await params.opts.onBlockReply(noticePayload);
1863-
return;
1864-
}
1865-
await params.onCompactionNoticePayload?.(noticePayload);
1865+
await deliver(noticePayload);
18661866
} catch (err) {
18671867
// Non-critical notice delivery failure should not bubble out of the
18681868
// fire-and-forget event handler.
@@ -2193,6 +2193,8 @@ async function runAgentTurnWithFallbackInternal(
21932193
applyReplyToMode: params.applyReplyToMode,
21942194
normalizeMediaPaths: replyMediaContext.normalizePayload,
21952195
typingSignals: params.typingSignals,
2196+
reasoningPayloadsEnabled: params.opts?.reasoningPayloadsEnabled,
2197+
commentaryPayloadsEnabled: params.opts?.commentaryPayloadsEnabled,
21962198
blockStreamingEnabled: params.blockStreamingEnabled,
21972199
blockReplyPipeline,
21982200
directlySentBlockKeys,

0 commit comments

Comments
 (0)