Skip to content

Commit 75a665d

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents f7f1dd8 + 0ce5e35 commit 75a665d

48 files changed

Lines changed: 1835 additions & 178 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
88

99
- Plugins/tasks: add a detached runtime registration contract so plugin executors can own detached task lifecycle and cancellation without reaching into core task internals. (#68915) Thanks @mbelinky.
1010
- Terminal/logging: optimize `sanitizeForLog()` by replacing the iterative control-character stripping loop with a single regex pass while preserving the existing ANSI-first sanitization behavior. (#67205) Thanks @bulutmuf.
11+
- QA/CI: make `openclaw qa suite` and `openclaw qa telegram` fail by default when scenarios fail, add `--allow-failures` for artifact-only runs, and tighten live-lane defaults for CI automation. (#69122) Thanks @joshavant.
1112

1213
### Fixes
1314

@@ -27,6 +28,12 @@ Docs: https://docs.openclaw.ai
2728
- Matrix/commands: recognize slash commands that are prefixed with the bot's Matrix mention, so room messages like `@bot:server /new` trigger the command path without requiring custom mention regexes. (#68570) Thanks @nightq and @johnlanni.
2829
- Agents/subagents: include requested role and runtime timing on subagent failure payloads so parent agents can correlate failed or timed-out child work. (#68726) Thanks @BKF-Gitty.
2930
- Gateway/sessions: reject stale agent-scoped sessions after an agent is removed from config while preserving legacy default-agent main-session aliases. (#65986) Thanks @bittoby.
31+
- Cron/isolated-agent: preserve explicit `delivery.mode: "none"` message targets for isolated runs without inheriting implicit `last` routing, so agent-initiated Telegram sends keep their authored destination while bare `mode:none` jobs stay targetless. (#69153) Thanks @obviyus.
32+
- Cron/isolated-agent: keep `delivery.mode: "none"` account-only or thread-only configs from inheriting a stale implicit recipient, so isolated runs only resolve message routing when the job authored an explicit `to` target. (#69163) Thanks @obviyus.
33+
- Gateway/TUI: retry session history while the local gateway is still finishing startup, so `openclaw tui` reconnects no longer fail on transient `chat.history unavailable during gateway startup` errors. (#69164) Thanks @shakkernerd.
34+
- BlueBubbles/reactions: fall back to `love` when an agent reacts with an emoji outside the iMessage tapback set (`love`/`like`/`dislike`/`laugh`/`emphasize`/`question`), so wider-vocabulary model reactions like `👀` still produce a visible tapback instead of failing the whole reaction request. Configured ack reactions still validate strictly via the new `normalizeBlueBubblesReactionInputStrict` path. (#64693) Thanks @zqchris.
35+
- BlueBubbles: prefer iMessage over SMS when both chats exist for the same handle, honor explicit `sms:` targets, and never silently downgrade iMessage-available recipients. (#61781) Thanks @rmartin.
36+
- Telegram/setup: require numeric `allowFrom` user IDs during setup instead of offering unsupported `@username` DM resolution, and point operators to `from.id`/`getUpdates` for discovery. (#69191) Thanks @obviyus.
3037

3138
## 2026.4.19-beta.2
3239

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
20db3f5afb93db334ad7456d26303c81b2a3eeaa5c1f8846a459eec72be20b96 plugin-sdk-api-baseline.json
2-
d02926e9facb3321a1018804d4c0370d9627963bee5e478942dda469e529c20b plugin-sdk-api-baseline.jsonl
1+
40d6f3ba88037ba0ef7d51743f28cc996b9951137fbe65553473e71b054c6510 plugin-sdk-api-baseline.json
2+
869e0b705e48001a98d85c574ad1e6ec8aef11393cc5f13b936f6004f58213dd plugin-sdk-api-baseline.jsonl

docs/channels/telegram.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Token resolution order is account-aware. In practice, config values win over env
115115

116116
`channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized.
117117
`dmPolicy: "allowlist"` with empty `allowFrom` blocks all DMs and is rejected by config validation.
118-
Onboarding accepts `@username` input and resolves it to numeric IDs.
118+
Setup asks for numeric user IDs only.
119119
If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token).
120120
If you previously relied on pairing-store allowlist files, `openclaw doctor --fix` can recover entries into `channels.telegram.allowFrom` in allowlist flows (for example when `dmPolicy: "allowlist"` has no explicit IDs yet).
121121

docs/concepts/qa-e2e-automation.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ disposable server. It requires `OPENCLAW_QA_TELEGRAM_GROUP_ID`,
8080
private group. The SUT bot must have a Telegram username, and bot-to-bot
8181
observation works best when both bots have Bot-to-Bot Communication Mode
8282
enabled in `@BotFather`.
83+
The command exits non-zero when any scenario fails. Use `--allow-failures` when
84+
you want artifacts without a failing exit code.
8385

8486
Live transport lanes now share one smaller contract instead of each inventing
8587
their own scenario list shape:
@@ -107,9 +109,11 @@ inside the guest, runs `qa suite`, then copies the normal QA report and
107109
summary back into `.artifacts/qa-e2e/...` on the host.
108110
It reuses the same scenario-selection behavior as `qa suite` on the host.
109111
Host and Multipass suite runs execute multiple selected scenarios in parallel
110-
with isolated gateway workers by default, up to 64 workers or the selected
111-
scenario count. Use `--concurrency <count>` to tune the worker count, or
112-
`--concurrency 1` for serial execution.
112+
with isolated gateway workers by default. `qa-channel` defaults to concurrency
113+
4, capped by the selected scenario count. Use `--concurrency <count>` to tune
114+
the worker count, or `--concurrency 1` for serial execution.
115+
The command exits non-zero when any scenario fails. Use `--allow-failures` when
116+
you want artifacts without a failing exit code.
113117
Live runs forward the supported QA auth inputs that are practical for the
114118
guest: env-based provider keys, the QA live provider config path, and
115119
`CODEX_HOME` when present. Keep `--output-dir` under the repo root so the guest

docs/help/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@ for usage/billing and raise limits as needed.
767767
<Accordion title="Telegram: what goes in allowFrom?">
768768
`channels.telegram.allowFrom` is **the human sender's Telegram user ID** (numeric). It is not the bot username.
769769

770-
Onboarding accepts `@username` input and resolves it to a numeric ID, but OpenClaw authorization uses numeric IDs only.
770+
Setup asks for numeric user IDs only. If you already have legacy `@username` entries in config, `openclaw doctor --fix` can try to resolve them.
771771

772772
Safer (no third-party bot):
773773

docs/help/testing.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ These commands sit beside the main test suites when you need QA-lab realism:
4949
- `pnpm openclaw qa suite`
5050
- Runs repo-backed QA scenarios directly on the host.
5151
- Runs multiple selected scenarios in parallel by default with isolated
52-
gateway workers, up to 64 workers or the selected scenario count. Use
53-
`--concurrency <count>` to tune the worker count, or `--concurrency 1` for
54-
the older serial lane.
52+
gateway workers. `qa-channel` defaults to concurrency 4 (bounded by the
53+
selected scenario count). Use `--concurrency <count>` to tune the worker
54+
count, or `--concurrency 1` for the older serial lane.
55+
- Exits non-zero when any scenario fails. Use `--allow-failures` when you
56+
want artifacts without a failing exit code.
5557
- Supports provider modes `live-frontier`, `mock-openai`, and `aimock`.
5658
`aimock` starts a local AIMock-backed provider server for experimental
5759
fixture and protocol-mock coverage without replacing the scenario-aware
@@ -86,6 +88,8 @@ These commands sit beside the main test suites when you need QA-lab realism:
8688
- Runs the Telegram live QA lane against a real private group using the driver and SUT bot tokens from env.
8789
- Requires `OPENCLAW_QA_TELEGRAM_GROUP_ID`, `OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN`, and `OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN`. The group id must be the numeric Telegram chat id.
8890
- Supports `--credential-source convex` for shared pooled credentials. Use env mode by default, or set `OPENCLAW_QA_CREDENTIAL_SOURCE=convex` to opt into pooled leases.
91+
- Exits non-zero when any scenario fails. Use `--allow-failures` when you
92+
want artifacts without a failing exit code.
8993
- Requires two distinct bots in the same private group, with the SUT bot exposing a Telegram username.
9094
- For stable bot-to-bot observation, enable Bot-to-Bot Communication Mode in `@BotFather` for both bots and ensure the driver bot can observe group bot traffic.
9195
- Writes a Telegram QA report, summary, and observed-messages artifact under `.artifacts/qa-e2e/...`.
@@ -118,7 +122,7 @@ Required env vars:
118122
- `OPENCLAW_QA_CONVEX_SECRET_CI` for `ci`
119123
- Credential role selection:
120124
- CLI: `--credential-role maintainer|ci`
121-
- Env default: `OPENCLAW_QA_CREDENTIAL_ROLE` (defaults to `maintainer`)
125+
- Env default: `OPENCLAW_QA_CREDENTIAL_ROLE` (defaults to `ci` in CI, `maintainer` otherwise)
122126

123127
Optional env vars:
124128

extensions/bluebubbles/src/monitor-processing.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ import type {
7070
} from "./monitor-shared.js";
7171
import { enrichBlueBubblesParticipantsWithContactNames } from "./participant-contact-names.js";
7272
import { isBlueBubblesPrivateApiEnabled } from "./probe.js";
73-
import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./reactions.js";
73+
import {
74+
normalizeBlueBubblesReactionInputStrict,
75+
sendBlueBubblesReaction,
76+
} from "./reactions.js";
7477
import type { OpenClawConfig } from "./runtime-api.js";
7578
import { normalizeSecretInputString } from "./secret-input.js";
7679
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
@@ -393,7 +396,7 @@ function resolveBlueBubblesAckReaction(params: {
393396
return null;
394397
}
395398
try {
396-
normalizeBlueBubblesReactionInput(raw);
399+
normalizeBlueBubblesReactionInputStrict(raw);
397400
return raw;
398401
} catch {
399402
const key = normalizeLowercaseStringOrEmpty(raw);

extensions/bluebubbles/src/reactions.test.ts

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, expect, it, vi } from "vitest";
2-
import { sendBlueBubblesReaction } from "./reactions.js";
2+
import {
3+
normalizeBlueBubblesReactionInput,
4+
normalizeBlueBubblesReactionInputStrict,
5+
sendBlueBubblesReaction,
6+
} from "./reactions.js";
37
import { installBlueBubblesFetchTestHooks } from "./test-harness.js";
48

59
vi.mock("./accounts.js", async () => {
@@ -106,18 +110,24 @@ describe("reactions", () => {
106110
).rejects.toThrow("password is required");
107111
});
108112

109-
it("throws for unsupported reaction type", async () => {
110-
await expect(
111-
sendBlueBubblesReaction({
112-
chatGuid: "chat-123",
113-
messageGuid: "msg-123",
114-
emoji: "unsupported",
115-
opts: {
116-
serverUrl: "http://localhost:1234",
117-
password: "test",
118-
},
119-
}),
120-
).rejects.toThrow("Unsupported BlueBubbles reaction");
113+
it("falls back to love for unsupported reaction type", async () => {
114+
mockFetch.mockResolvedValueOnce({
115+
ok: true,
116+
text: () => Promise.resolve(""),
117+
});
118+
119+
await sendBlueBubblesReaction({
120+
chatGuid: "chat-123",
121+
messageGuid: "msg-123",
122+
emoji: "👀",
123+
opts: {
124+
serverUrl: "http://localhost:1234",
125+
password: "test",
126+
},
127+
});
128+
129+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
130+
expect(body.reaction).toBe("love");
121131
});
122132

123133
describe("reaction type normalization", () => {
@@ -236,6 +246,27 @@ describe("reactions", () => {
236246
await expectRemovedReaction("-love");
237247
});
238248

249+
it("falls back to removing love for unsupported removal reactions", async () => {
250+
mockFetch.mockResolvedValueOnce({
251+
ok: true,
252+
text: () => Promise.resolve(""),
253+
});
254+
255+
await sendBlueBubblesReaction({
256+
chatGuid: "chat-123",
257+
messageGuid: "msg-123",
258+
emoji: "👀",
259+
remove: true,
260+
opts: {
261+
serverUrl: "http://localhost:1234",
262+
password: "test",
263+
},
264+
});
265+
266+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
267+
expect(body.reaction).toBe("-love");
268+
});
269+
239270
it("uses custom partIndex when provided", async () => {
240271
mockFetch.mockResolvedValueOnce({
241272
ok: true,
@@ -335,4 +366,54 @@ describe("reactions", () => {
335366
});
336367
});
337368
});
369+
370+
describe("normalizeBlueBubblesReactionInputStrict", () => {
371+
it("maps supported emoji to canonical type", () => {
372+
expect(normalizeBlueBubblesReactionInputStrict("👍")).toBe("like");
373+
expect(normalizeBlueBubblesReactionInputStrict("❤️")).toBe("love");
374+
expect(normalizeBlueBubblesReactionInputStrict("😂")).toBe("laugh");
375+
});
376+
377+
it("throws on unsupported input so validators can detect misconfiguration", () => {
378+
expect(() => normalizeBlueBubblesReactionInputStrict("👀")).toThrow(
379+
/Unsupported BlueBubbles reaction/,
380+
);
381+
expect(() => normalizeBlueBubblesReactionInputStrict("🎉")).toThrow(
382+
/Unsupported BlueBubbles reaction/,
383+
);
384+
});
385+
386+
it("throws on empty input", () => {
387+
expect(() => normalizeBlueBubblesReactionInputStrict("")).toThrow(
388+
/requires an emoji or name/,
389+
);
390+
expect(() => normalizeBlueBubblesReactionInputStrict(" ")).toThrow(
391+
/requires an emoji or name/,
392+
);
393+
});
394+
});
395+
396+
describe("normalizeBlueBubblesReactionInput (lenient)", () => {
397+
it("maps supported emoji to canonical type", () => {
398+
expect(normalizeBlueBubblesReactionInput("👍")).toBe("like");
399+
expect(normalizeBlueBubblesReactionInput("❤️")).toBe("love");
400+
});
401+
402+
it("falls back to love when input is unsupported by iMessage tapback", () => {
403+
expect(normalizeBlueBubblesReactionInput("👀")).toBe("love");
404+
expect(normalizeBlueBubblesReactionInput("🎉")).toBe("love");
405+
});
406+
407+
it("falls back to -love on unsupported remove", () => {
408+
expect(normalizeBlueBubblesReactionInput("👀", true)).toBe("-love");
409+
});
410+
411+
it("still throws on empty input (strict error bubbles up unchanged)", () => {
412+
// Empty input is a contract error from the caller, not a decorative
413+
// emoji the model picked; we intentionally do not mask it.
414+
expect(() => normalizeBlueBubblesReactionInput("")).toThrow(
415+
/requires an emoji or name/,
416+
);
417+
});
418+
});
338419
});

extensions/bluebubbles/src/reactions.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,18 @@ const REACTION_EMOJIS = new Map<string, string>([
111111
["?", "question"],
112112
]);
113113

114-
export function normalizeBlueBubblesReactionInput(emoji: string, remove?: boolean): string {
114+
const UNSUPPORTED_REACTION_ERROR = "UnsupportedBlueBubblesReaction";
115+
116+
/**
117+
* Strict normalizer: throws when the input does not map to a supported
118+
* BlueBubbles reaction type. Use this for validator-style callers that
119+
* need to detect unsupported input (e.g. config sanity checks) rather
120+
* than gracefully substituting a fallback.
121+
*/
122+
export function normalizeBlueBubblesReactionInputStrict(
123+
emoji: string,
124+
remove?: boolean,
125+
): string {
115126
const trimmed = emoji.trim();
116127
if (!trimmed) {
117128
throw new Error("BlueBubbles reaction requires an emoji or name.");
@@ -123,11 +134,38 @@ export function normalizeBlueBubblesReactionInput(emoji: string, remove?: boolea
123134
const aliased = REACTION_ALIASES.get(raw) ?? raw;
124135
const mapped = REACTION_EMOJIS.get(trimmed) ?? REACTION_EMOJIS.get(raw) ?? aliased;
125136
if (!REACTION_TYPES.has(mapped)) {
126-
throw new Error(`Unsupported BlueBubbles reaction: ${trimmed}`);
137+
const error = new Error(`Unsupported BlueBubbles reaction: ${trimmed}`);
138+
error.name = UNSUPPORTED_REACTION_ERROR;
139+
throw error;
127140
}
128141
return remove ? `-${mapped}` : mapped;
129142
}
130143

144+
/**
145+
* Lenient normalizer: when the input does not map to a supported
146+
* BlueBubbles reaction type (iMessage tapback only supports
147+
* love/like/dislike/laugh/emphasize/question), fall back to `love`
148+
* so agents that react with a wider emoji vocabulary (e.g. 👀 to
149+
* ack "seen, working on it") still produce a visible tapback instead
150+
* of failing the whole reaction request.
151+
*
152+
* Contract errors (empty input) continue to bubble up so callers
153+
* still catch misuse.
154+
*
155+
* Use this for model-facing paths. Callers that need to detect
156+
* unsupported input should use {@link normalizeBlueBubblesReactionInputStrict}.
157+
*/
158+
export function normalizeBlueBubblesReactionInput(emoji: string, remove?: boolean): string {
159+
try {
160+
return normalizeBlueBubblesReactionInputStrict(emoji, remove);
161+
} catch (error) {
162+
if (error instanceof Error && error.name === UNSUPPORTED_REACTION_ERROR) {
163+
return remove ? "-love" : "love";
164+
}
165+
throw error;
166+
}
167+
}
168+
131169
export async function sendBlueBubblesReaction(params: {
132170
chatGuid: string;
133171
messageGuid: string;

0 commit comments

Comments
 (0)