Skip to content

Commit 3552736

Browse files
authored
Merge branch 'main' into vincentkoc-code/telegram-media-max-config
2 parents c6be9d4 + f9d86b9 commit 3552736

File tree

6 files changed

+63
-1
lines changed

6 files changed

+63
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ Docs: https://docs.openclaw.ai
203203
- Skills/openai-image-gen CLI validation: validate `--background` and `--style` inputs early, normalize supported values, and warn when those flags are ignored for incompatible models. (#36762) Thanks @shuofengzhang and @vincentkoc.
204204
- Skills/openai-image-gen output formats: validate `--output-format` values early, normalize aliases like `jpg -> jpeg`, and warn when the flag is ignored for incompatible models. (#36648) Thanks @shuofengzhang and @vincentkoc.
205205
- WhatsApp media upload caps: make outbound media sends and auto-replies honor `channels.whatsapp.mediaMaxMb` with per-account overrides so inbound and outbound limits use the same channel config. Thanks @vincentkoc.
206+
- Windows/Plugin install: when OpenClaw runs on Windows via Bun and `npm-cli.js` is not colocated with the runtime binary, fall back to `npm.cmd`/`npx.cmd` through the existing `cmd.exe` wrapper so `openclaw plugins install` no longer fails with `spawn EINVAL`. (#38056) Thanks @0xlin2023.
207+
- Telegram/send retry classification: retry grammY `Network request ... failed after N attempts` envelopes in send flows without reclassifying plain `Network request ... failed!` wrappers as transient, restoring the intended retry path while keeping broad send-context message matching tight. (#38056) Thanks @0xlin2023.
206208

207209
## 2026.3.2
208210

src/process/exec.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ChildProcess } from "node:child_process";
22
import { EventEmitter } from "node:events";
3+
import fs from "node:fs";
34
import process from "node:process";
45
import { describe, expect, it, vi } from "vitest";
56
import { attachChildProcessBridge } from "./child-process-bridge.js";
@@ -77,6 +78,20 @@ describe("runCommandWithTimeout", () => {
7778
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
7879
},
7980
);
81+
82+
it.runIf(process.platform === "win32")(
83+
"falls back to npm.cmd when npm-cli.js is unavailable",
84+
async () => {
85+
const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false);
86+
try {
87+
const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 10_000 });
88+
expect(result.code).toBe(0);
89+
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
90+
} finally {
91+
existsSpy.mockRestore();
92+
}
93+
},
94+
);
8095
});
8196

8297
describe("attachChildProcessBridge", () => {

src/process/exec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ function resolveNpmArgvForWindows(argv: string[]): string[] | null {
5858
const nodeDir = path.dirname(process.execPath);
5959
const cliPath = path.join(nodeDir, "node_modules", "npm", "bin", cliName);
6060
if (!fs.existsSync(cliPath)) {
61-
return null;
61+
// Bun-based runs don't ship npm-cli.js next to process.execPath.
62+
// Fall back to npm.cmd/npx.cmd so we still route through cmd wrapper
63+
// (avoids direct .cmd spawn EINVAL on patched Node).
64+
const command = argv[0] ?? "";
65+
const ext = path.extname(command).toLowerCase();
66+
const shimmedCommand = ext ? command : `${command}.cmd`;
67+
return [shimmedCommand, ...argv.slice(1)];
6268
}
6369
return [process.execPath, cliPath, ...argv.slice(1)];
6470
}

src/telegram/network-errors.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ describe("isRecoverableTelegramNetworkError", () => {
4949
expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "polling" })).toBe(true);
5050
});
5151

52+
it("treats grammY failed-after envelope errors as recoverable in send context", () => {
53+
expect(
54+
isRecoverableTelegramNetworkError(
55+
new Error("Network request for 'sendMessage' failed after 2 attempts."),
56+
{ context: "send" },
57+
),
58+
).toBe(true);
59+
});
60+
5261
it("returns false for unrelated errors", () => {
5362
expect(isRecoverableTelegramNetworkError(new Error("invalid token"))).toBe(false);
5463
});

src/telegram/network-errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const RECOVERABLE_ERROR_NAMES = new Set([
3333
]);
3434

3535
const ALWAYS_RECOVERABLE_MESSAGES = new Set(["fetch failed", "typeerror: fetch failed"]);
36+
const GRAMMY_NETWORK_REQUEST_FAILED_AFTER_RE =
37+
/^network request(?:\s+for\s+["']?[^"']+["']?)?\s+failed\s+after\b.*[!.]?$/i;
3638

3739
const RECOVERABLE_MESSAGE_SNIPPETS = [
3840
"undici",
@@ -106,6 +108,9 @@ export function isRecoverableTelegramNetworkError(
106108
if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) {
107109
return true;
108110
}
111+
if (message && GRAMMY_NETWORK_REQUEST_FAILED_AFTER_RE.test(message)) {
112+
return true;
113+
}
109114
if (allowMessageMatch && message) {
110115
if (RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) {
111116
return true;

src/telegram/send.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,31 @@ describe("sendMessageTelegram", () => {
779779
expect(sendMessage).toHaveBeenCalledTimes(1);
780780
});
781781

782+
it("retries when grammY network envelope message includes failed-after wording", async () => {
783+
const chatId = "123";
784+
const sendMessage = vi
785+
.fn()
786+
.mockRejectedValueOnce(
787+
new Error("Network request for 'sendMessage' failed after 1 attempts."),
788+
)
789+
.mockResolvedValueOnce({
790+
message_id: 7,
791+
chat: { id: chatId },
792+
});
793+
const api = { sendMessage } as unknown as {
794+
sendMessage: typeof sendMessage;
795+
};
796+
797+
const result = await sendMessageTelegram(chatId, "hi", {
798+
token: "tok",
799+
api,
800+
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
801+
});
802+
803+
expect(sendMessage).toHaveBeenCalledTimes(2);
804+
expect(result).toEqual({ messageId: "7", chatId });
805+
});
806+
782807
it("sends GIF media as animation", async () => {
783808
const chatId = "123";
784809
const sendAnimation = vi.fn().mockResolvedValue({

0 commit comments

Comments
 (0)