openclaw - ✅(Solved) Fix bug(telegram): deleteMessageTelegram throws on benign 400s — should fail-soft like reactMessageTelegram [4 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#73726Fetched 2026-04-29 06:15:54
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
cross-referenced ×4commented ×1

deleteMessageTelegram (in extensions/telegram/src/send.ts) lets all errors from api.deleteMessage propagate, including benign Telegram Bot API 400s that occur in normal operation:

  • Bad Request: message to delete not found — message was already removed by a user
  • Bad Request: message can't be deleted — message is older than the 48h bot-delete window, or never existed (e.g. a stale msg_id from a tracking file)
  • MESSAGE_ID_INVALID / MESSAGE_DELETE_FORBIDDEN — analogous edge cases

Because the error escapes through createTelegramRequestWithDiag, it gets logged at ERROR level in the openclaw runtime log every time it happens. For agents that maintain "delete the previous reminder before sending a new one" idioms, this puts recurring noise into operator dashboards / digest emails for what is operationally a no-op.

Error Message

[telegram] deleteMessage failed: Call to 'deleteMessage' failed! (400: Bad Request: message can't be deleted) GrammyError: Call to 'deleteMessage' failed! (400: Bad Request: message can't be deleted)

Root Cause

Because the error escapes through createTelegramRequestWithDiag, it gets logged at ERROR level in the openclaw runtime log every time it happens. For agents that maintain "delete the previous reminder before sending a new one" idioms, this puts recurring noise into operator dashboards / digest emails for what is operationally a no-op.

Fix Action

Fixed

PR fix notes

PR #73734: fix(telegram): fail-soft on benign deleteMessage 400s (#73726)

Description (problem / solution / changelog)

What

Fixes #73726. deleteMessageTelegram (in extensions/telegram/src/send.ts) let every error from api.deleteMessage propagate, including the benign Telegram Bot API 400s that occur in normal operation:

  • Bad Request: message to delete not found — already removed by user
  • Bad Request: message can't be deleted — older than the 48h bot-delete window or stale tracking-file msg_id
  • MESSAGE_ID_INVALID / MESSAGE_DELETE_FORBIDDEN — analogous edge cases

These propagated through createTelegramRequestWithDiag and got logged at ERROR level on every recurring "delete the previous reminder before sending a new one" idiom. Operator dashboards / digest emails were noisy for what is operationally a no-op (the message is gone, which is the desired state).

Fix

Mirror the established reactMessageTelegram REACTION_INVALID pattern in the same file (send.ts:1045-1053): catch the benign 400s, log at verbose level, and return { ok: false, warning }.

try {
  await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage");
} catch (err: unknown) {
  const msg = formatErrorMessage(err);
  if (
    /message to delete not found/i.test(msg) ||
    /message can'?t be deleted/i.test(msg) ||
    /MESSAGE_ID_INVALID/i.test(msg) ||
    /MESSAGE_DELETE_FORBIDDEN/i.test(msg)
  ) {
    logVerbose(...);
    return { ok: false, warning: msg };
  }
  throw err;
}

The message delete action runtime caller (action-runtime.ts:481) is updated to surface the warning in the JSON result while still treating the operation as successful ({ok: true, deleted: false, warning: ...} instead of {ok: true, deleted: true}). Real failures (auth, network, etc.) still propagate.

Pre-implement audit

  1. Existing-helper check (vincentkoc #57341). Pattern reused verbatim from reactMessageTelegram (lines 1011-1055 in the same file). The shape — try/catch + regex on formatErrorMessage(err) + {ok: false, warning} return — is the established Telegram fail-soft idiom. ✅
  2. Shared-helper caller check (steipete #60623). deleteMessageTelegram is exported from runtime-api.ts and called from action-runtime.ts:481. Updated the in-tree caller; the return-type widening from Promise<{ok: true}> to Promise<{ok: true} | {ok: false, warning}> is backward-compatible because {ok: true} is a subset of the union — external callers that only check ok keep behaving identically. ✅
  3. Broader-fix rival scan (steipete #68270). Zero rival PRs reference #73726. ✅

Verified locally

npx oxlint extensions/telegram/src/send.ts extensions/telegram/src/send.test.ts extensions/telegram/src/action-runtime.ts
# Found 0 warnings and 0 errors.

npx vitest run extensions/telegram/src/send.test.ts
# Tests  91 passed (91)

npx vitest run extensions/telegram/src/action-runtime.test.ts
# Tests  53 passed (53)

Tests cover:

  • Happy path (ok: true on successful delete)
  • All 4 benign 400 messages (parameterized via it.each)
  • Non-benign error rethrow (e.g. 401 Unauthorized) still bubbles up

lobster-biscuit: 73726-telegram-delete-fail-soft

Sign-Off:

  • I have read and agree to the OpenClaw Contributor License Agreement.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/telegram/src/action-runtime.ts (modified, +16/-5)
  • extensions/telegram/src/send.test.ts (modified, +63/-0)
  • extensions/telegram/src/send.ts (modified, +23/-2)

PR #73654: fix(cli): make crestodian exit non-zero on no-TTY (#73646)

Description (problem / solution / changelog)

What

Fixes #73646. pnpm openclaw crestodian < /dev/null (and node ./dist/index.js crestodian < /dev/null) currently:

  1. Detects no TTY
  2. Prints Crestodian needs an interactive TTY. Use --message for one command. to stderr
  3. Returns from runCrestodian cleanly — no thrown error, no process.exitCode set
  4. The CLI wrapper runCommandWithRuntime only sets runtime.exit(1) in its catch path, so the silent return falls through and Node exits 0

Shell scripts and CI flows reading $? see success and proceed as if Crestodian ran. The two in-tree sibling subcommands for the same condition (models auth login, secrets configure) both throw new Error("…") and exit non-zero. The bare-root crestodian path at cli/run-main.ts:399-404 also already sets process.exitCode = 1 directly. Crestodian-as-subcommand is the lone outlier.

Fix

Replace the silent runtime.error(...) + return in src/crestodian/crestodian.ts:93-96 with a throw new Error(...). The CLI wrapper's catch then runs runtime.error + exit(1) and the process exits non-zero. Behavior matches the bare-root path and the models auth login / secrets configure precedents.

if (!interactive || !inputIsTty || !outputIsTty) {
  throw new Error("Crestodian needs an interactive TTY. Use --message for one command.");
}

Verified locally

npx oxlint src/crestodian/crestodian.ts src/crestodian/crestodian.test.ts
# Found 0 warnings and 0 errors.

npx vitest run src/crestodian/crestodian.test.ts
# Tests  4 passed (4)

The new test pins the regression: runCrestodian({ input: { isTTY: false }, output: { isTTY: false } }) rejects with /needs an interactive TTY/ so the CLI wrapper's catch fires.

Pre-implement audit

  1. Existing-helper check. Error message + pattern reused verbatim from sibling subcommands (models auth login, secrets configure); setup-token uses the identical throw new Error("… requires an interactive TTY.") shape. ✅
  2. Shared-helper caller check. runCrestodian is called from 4 sites — cli/run-main.ts:423,433, cli/program/register.onboard.ts:182, cli/program/register.crestodian.ts:30 — all wrapped in runCommandWithRuntime (or runCommandWithRuntime-style catch in run-main.ts). All four catch paths surface the thrown error and exit non-zero. ✅
  3. Broader-fix rival scan. Zero rival PRs reference #73646. ✅

Note

#73562 (plugins uninstall < /dev/null exits 13) is a separate bug filed by the same reporter — wrong exit code there, but at least non-zero. Not addressed here.

lobster-biscuit: 73646-crestodian-no-tty-exit

Sign-Off:

  • I have read and agree to the OpenClaw Contributor License Agreement.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/crestodian/crestodian.test.ts (modified, +21/-0)
  • src/crestodian/crestodian.ts (modified, +6/-2)

PR #73735: fix(telegram): fail-soft on benign deleteMessage 400s (#73726)

Description (problem / solution / changelog)

What

Fixes #73726. deleteMessageTelegram (in extensions/telegram/src/send.ts) let every error from api.deleteMessage propagate, including the benign Telegram Bot API 400s that occur in normal operation:

  • Bad Request: message to delete not found — already removed by user
  • Bad Request: message can't be deleted — older than the 48h bot-delete window or stale tracking-file msg_id
  • MESSAGE_ID_INVALID / MESSAGE_DELETE_FORBIDDEN — analogous edge cases

These propagated through createTelegramRequestWithDiag and got logged at ERROR level on every recurring "delete the previous reminder before sending a new one" idiom. Operator dashboards / digest emails were noisy for what is operationally a no-op (the message is gone, which is the desired state).

Fix

Mirror the established reactMessageTelegram REACTION_INVALID pattern in the same file (send.ts:1045-1053): catch the benign 400s, log at verbose level, and return { ok: false, warning }.

try {
  await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage");
} catch (err: unknown) {
  const msg = formatErrorMessage(err);
  if (
    /message to delete not found/i.test(msg) ||
    /message can'?t be deleted/i.test(msg) ||
    /MESSAGE_ID_INVALID/i.test(msg) ||
    /MESSAGE_DELETE_FORBIDDEN/i.test(msg)
  ) {
    logVerbose(...);
    return { ok: false, warning: msg };
  }
  throw err;
}

The message delete action runtime caller (action-runtime.ts:481) is updated to surface the warning in the JSON result while still treating the operation as successful ({ok: true, deleted: false, warning: ...} instead of {ok: true, deleted: true}). Real failures (auth, network, etc.) still propagate.

Pre-implement audit

  1. Existing-helper check (vincentkoc #57341). Pattern reused verbatim from reactMessageTelegram (lines 1011-1055 in the same file). The shape — try/catch + regex on formatErrorMessage(err) + {ok: false, warning} return — is the established Telegram fail-soft idiom. ✅
  2. Shared-helper caller check (steipete #60623). deleteMessageTelegram is exported from runtime-api.ts and called from action-runtime.ts:481. Updated the in-tree caller; the return-type widening from Promise<{ok: true}> to Promise<{ok: true} | {ok: false, warning}> is backward-compatible because {ok: true} is a subset of the union — external callers that only check ok keep behaving identically. ✅
  3. Broader-fix rival scan (steipete #68270). Zero rival PRs reference #73726. ✅

Verified locally

npx oxlint extensions/telegram/src/send.ts extensions/telegram/src/send.test.ts extensions/telegram/src/action-runtime.ts
# Found 0 warnings and 0 errors.

npx vitest run extensions/telegram/src/send.test.ts
# Tests  91 passed (91)

npx vitest run extensions/telegram/src/action-runtime.test.ts
# Tests  53 passed (53)

Tests cover:

  • Happy path (ok: true on successful delete)
  • All 4 benign 400 messages (parameterized via it.each)
  • Non-benign error rethrow (e.g. 401 Unauthorized) still bubbles up

lobster-biscuit: 73726-telegram-delete-fail-soft

Sign-Off:

  • I have read and agree to the OpenClaw Contributor License Agreement.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/telegram/src/action-runtime.ts (modified, +16/-5)
  • extensions/telegram/src/send.test.ts (modified, +63/-0)
  • extensions/telegram/src/send.ts (modified, +23/-2)

PR #73927: [AI-assisted] fix(telegram): soft-fail benign delete errors

Description (problem / solution / changelog)

Summary

  • Treat benign Telegram delete 400s as non-fatal delete results instead of hard tool errors.
  • Surface soft delete warnings through the Telegram action runtime so agents do not retry no-op deletes.
  • Add regression coverage for benign delete failures and non-benign hard failures.

Fixes #73726.

Testing

  • node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-telegram.config.ts extensions/telegram/src/send.test.ts extensions/telegram/src/action-runtime.test.ts — 144 passed
  • node_modules/.bin/oxfmt.CMD --check extensions/telegram/src/send.ts extensions/telegram/src/action-runtime.ts extensions/telegram/src/send.test.ts extensions/telegram/src/action-runtime.test.ts
  • git diff --check -- extensions/telegram/src/send.ts extensions/telegram/src/action-runtime.ts extensions/telegram/src/send.test.ts extensions/telegram/src/action-runtime.test.ts
  • node node_modules/@typescript/native-preview/bin/tsgo.js -p tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo

AI-assisted by OpenAI Codex.

Changed files

  • extensions/telegram/src/action-runtime.test.ts (modified, +28/-1)
  • extensions/telegram/src/action-runtime.ts (modified, +17/-5)
  • extensions/telegram/src/send.test.ts (modified, +42/-0)
  • extensions/telegram/src/send.ts (modified, +16/-2)

Code Example

# Try to delete a never-existed message ID:
openclaw message delete --channel telegram --target <chat_id> --message-id 1 \
    --account default

---

[telegram] deleteMessage failed: Call to 'deleteMessage' failed!
  (400: Bad Request: message can't be deleted)
GrammyError: Call to 'deleteMessage' failed!
  (400: Bad Request: message can't be deleted)

---

// extensions/telegram/src/send.ts (around line 1067 on main)

export async function deleteMessageTelegram(
  chatIdInput: string | number,
  messageIdInput: string | number,
  opts: TelegramDeleteOpts,
): Promise<{ ok: true } | { ok: false; warning: string }> {
  const { cfg, account, api } = resolveTelegramApiContext(opts);
  const rawTarget = String(chatIdInput);
  const chatId = await resolveAndPersistChatId({
    cfg, api,
    lookupTarget: rawTarget,
    persistTarget: rawTarget,
    verbose: opts.verbose,
  });
  const messageId = normalizeMessageId(messageIdInput);
  const requestWithDiag = createTelegramRequestWithDiag({
    cfg, account,
    retry: opts.retry,
    verbose: opts.verbose,
    shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
  });
  try {
    await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage");
  } catch (err: unknown) {
    const msg = formatErrorMessage(err);
    if (/message to delete not found|message can't be deleted|MESSAGE_ID_INVALID|MESSAGE_DELETE_FORBIDDEN/i.test(msg)) {
      logVerbose(`[telegram] Delete skipped: message ${messageId} in chat ${chatId} already gone (${msg})`);
      return { ok: false as const, warning: `Message ${messageId} not deletable: ${msg}` };
    }
    throw err;
  }
  logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
  return { ok: true };
}

---

const result = await telegramActionRuntime.deleteMessageTelegram(chatId ?? "", messageId ?? 0, {
  cfg, token, accountId: accountId ?? undefined,
});
return jsonResult(
  result.ok
    ? { ok: true, deleted: true }
    : { ok: false, deleted: false, warning: result.warning },
);
RAW_BUFFERClick to expand / collapse

Summary

deleteMessageTelegram (in extensions/telegram/src/send.ts) lets all errors from api.deleteMessage propagate, including benign Telegram Bot API 400s that occur in normal operation:

  • Bad Request: message to delete not found — message was already removed by a user
  • Bad Request: message can't be deleted — message is older than the 48h bot-delete window, or never existed (e.g. a stale msg_id from a tracking file)
  • MESSAGE_ID_INVALID / MESSAGE_DELETE_FORBIDDEN — analogous edge cases

Because the error escapes through createTelegramRequestWithDiag, it gets logged at ERROR level in the openclaw runtime log every time it happens. For agents that maintain "delete the previous reminder before sending a new one" idioms, this puts recurring noise into operator dashboards / digest emails for what is operationally a no-op.

Why this is a bug

There's already direct precedent in the same file: reactMessageTelegram (lines ~1015-1056 of send.ts) wraps its requestWithDiag call in a try/catch and fails-soft on the analogous benign 400 (REACTION_INVALID), returning { ok: false, warning: ... } instead of throwing.

deleteMessageTelegram should follow the same pattern — Telegram telling us "the thing you wanted to delete is already gone" is a successful outcome from the caller's perspective.

Reproduction

# Try to delete a never-existed message ID:
openclaw message delete --channel telegram --target <chat_id> --message-id 1 \
    --account default

Output (CLI exits 0, but error is logged):

[telegram] deleteMessage failed: Call to 'deleteMessage' failed!
  (400: Bad Request: message can't be deleted)
GrammyError: Call to 'deleteMessage' failed!
  (400: Bad Request: message can't be deleted)

Same surface for message to delete not found when a user has manually deleted a tracked message before the bot's cleanup pass.

Proposed fix

Mirror the reactMessageTelegram pattern in deleteMessageTelegram:

// extensions/telegram/src/send.ts (around line 1067 on main)

export async function deleteMessageTelegram(
  chatIdInput: string | number,
  messageIdInput: string | number,
  opts: TelegramDeleteOpts,
): Promise<{ ok: true } | { ok: false; warning: string }> {
  const { cfg, account, api } = resolveTelegramApiContext(opts);
  const rawTarget = String(chatIdInput);
  const chatId = await resolveAndPersistChatId({
    cfg, api,
    lookupTarget: rawTarget,
    persistTarget: rawTarget,
    verbose: opts.verbose,
  });
  const messageId = normalizeMessageId(messageIdInput);
  const requestWithDiag = createTelegramRequestWithDiag({
    cfg, account,
    retry: opts.retry,
    verbose: opts.verbose,
    shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
  });
  try {
    await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage");
  } catch (err: unknown) {
    const msg = formatErrorMessage(err);
    if (/message to delete not found|message can't be deleted|MESSAGE_ID_INVALID|MESSAGE_DELETE_FORBIDDEN/i.test(msg)) {
      logVerbose(`[telegram] Delete skipped: message ${messageId} in chat ${chatId} already gone (${msg})`);
      return { ok: false as const, warning: `Message ${messageId} not deletable: ${msg}` };
    }
    throw err;
  }
  logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
  return { ok: true };
}

Caller impact

Only one downstream caller in extensions/telegram/src/action-runtime.ts (~line 481). It currently ignores the return value and unconditionally returns { ok: true, deleted: true }. After the type change it should surface the warning, e.g.:

const result = await telegramActionRuntime.deleteMessageTelegram(chatId ?? "", messageId ?? 0, {
  cfg, token, accountId: accountId ?? undefined,
});
return jsonResult(
  result.ok
    ? { ok: true, deleted: true }
    : { ok: false, deleted: false, warning: result.warning },
);

Test coverage

Mirror the REACTION_INVALID test in send.proxy.test.ts for the four benign strings.

Adjacent prior art

  • #37514 (closed) classifies Telegram 400s as permanent in delivery recovery — same family of 400-is-terminal reasoning, but doesn't cover this code path.

Environment

  • openclaw 2026.4.24 (cbcfdf6)
  • Node 22.22.0
  • Bot uses standard grammY transport

extent analysis

TL;DR

The proposed fix involves modifying the deleteMessageTelegram function to catch and handle benign Telegram Bot API 400 errors, returning a warning instead of throwing an error.

Guidance

  • Implement the proposed fix by wrapping the api.deleteMessage call in a try-catch block and checking the error message for specific benign error strings.
  • Update the downstream caller in extensions/telegram/src/action-runtime.ts to surface the warning returned by deleteMessageTelegram.
  • Add test coverage for the four benign error strings in send.proxy.test.ts.
  • Verify that the fix works by running the reproduction command and checking that the error is no longer logged at the ERROR level.

Example

The proposed fix code snippet is already provided in the issue body, which demonstrates how to modify the deleteMessageTelegram function to handle benign errors.

Notes

The fix assumes that the benign error strings are correctly identified and that the warning returned by deleteMessageTelegram is properly surfaced by the downstream caller. Additional testing may be necessary to ensure that the fix works as expected in all scenarios.

Recommendation

Apply the proposed workaround by implementing the modified deleteMessageTelegram function and updating the downstream caller, as this will prevent benign errors from being logged at the ERROR level and provide a more accurate representation of the bot's operation.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix bug(telegram): deleteMessageTelegram throws on benign 400s — should fail-soft like reactMessageTelegram [4 pull requests, 1 comments, 2 participants]