fix(discord): clarify deploy abort logs

This commit is contained in:
Peter Steinberger
2026-05-01 07:15:31 +01:00
parent 7340c0322f
commit d23c8a8eba
5 changed files with 133 additions and 5 deletions

View File

@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
- Discord: retry queued REST 429s against learned bucket/global cooldowns and reacquire fresh voice upload URLs after CDN upload rate limits, so outbound sends recover without reusing stale single-use upload URLs. Thanks @discord.
- TTS/providers: keep bundled speech-provider compat fallback available when plugins are globally disabled, so cold gateway and CLI startup can still resolve fallback speech providers instead of leaving explicit TTS provider selection with no registered providers. Refs #75265. Thanks @sliekens.
- Discord: collapse repeated native slash-command deploy rate-limit startup logs into one non-fatal warning while keeping per-request REST timing in verbose output. Thanks @discord.
- Discord: report native slash-command deploy aborts as REST timeouts with method, path, timeout budget, and observed duration, so startup logs explain slow Discord API calls instead of showing a generic aborted operation. Thanks @discord.
- Security/logging: redact payment credential field names such as card number, CVC/CVV, shared payment token, and payment credential across default log and tool-payload redaction patterns so wallet-style MCP tools do not expose raw payment credentials in UI events or transcripts. Thanks @stainlu.
- Providers/OpenAI Codex: preserve existing wrapped Codex streams during OpenAI attribution so PI OAuth bearer injection reaches ChatGPT/Codex Responses, and strip native Codex-only unsupported payload fields without touching custom compatible endpoints. (#75111) Thanks @keshavbotagent.
- Agents/tool-result guard: use the resolved runtime context token budget for non-context-engine tool-result overflow checks, so long tool-heavy sessions no longer compact early when `contextTokens` is larger than native `contextWindow`. Fixes #74917. Thanks @kAIborg24.

View File

@@ -13,6 +13,10 @@ export type DiscordDeployErrorLike = {
scope?: unknown;
rawBody?: unknown;
deployRequestBody?: unknown;
deployRestMethod?: unknown;
deployRestPath?: unknown;
deployRequestMs?: unknown;
deployTimeoutMs?: unknown;
};
export type DiscordDeployRateLimitDetails = {
@@ -32,6 +36,27 @@ export function attachDiscordDeployRequestBody(err: unknown, body: unknown) {
}
}
export function attachDiscordDeployRestContext(
err: unknown,
context: {
method: string;
path: string;
requestMs: number;
timeoutMs?: number;
},
) {
if (!err || typeof err !== "object") {
return;
}
const deployErr = err as DiscordDeployErrorLike;
deployErr.deployRestMethod = context.method;
deployErr.deployRestPath = context.path;
deployErr.deployRequestMs = context.requestMs;
if (typeof context.timeoutMs === "number" && Number.isFinite(context.timeoutMs)) {
deployErr.deployTimeoutMs = context.timeoutMs;
}
}
function stringifyDiscordDeployField(value: unknown): string {
if (typeof value === "string") {
return JSON.stringify(value);
@@ -82,6 +107,78 @@ function readFiniteNumber(value: unknown): number | undefined {
return undefined;
}
function formatDurationMs(ms: number): string {
return formatDurationSeconds(ms, { decimals: ms >= 1000 ? 1 : 0 });
}
function isAbortLikeError(err: unknown): boolean {
if (!err || typeof err !== "object") {
return false;
}
const name = "name" in err && typeof err.name === "string" ? err.name : undefined;
const message = formatErrorMessage(err);
return (
name === "AbortError" ||
message === "This operation was aborted" ||
message === "The operation was aborted" ||
/\boperation was aborted\b/i.test(message)
);
}
function formatDiscordDeployRestOperation(err: DiscordDeployErrorLike): string {
const method =
typeof err.deployRestMethod === "string" && err.deployRestMethod.trim().length > 0
? err.deployRestMethod.toUpperCase()
: undefined;
const path =
typeof err.deployRestPath === "string" && err.deployRestPath.trim().length > 0
? err.deployRestPath
: undefined;
if (method && path) {
return `${method} ${path}`;
}
if (method) {
return method;
}
if (path) {
return path;
}
return "request";
}
export function formatDiscordDeployErrorMessage(err: unknown): string {
if (!isAbortLikeError(err)) {
return formatErrorMessage(err);
}
const deployErr =
err && typeof err === "object"
? (err as DiscordDeployErrorLike)
: ({} as DiscordDeployErrorLike);
const requestMs = readFiniteNumber(deployErr.deployRequestMs);
const timeoutMs = readFiniteNumber(deployErr.deployTimeoutMs);
const operation = formatDiscordDeployRestOperation(deployErr);
const hasRestContext =
requestMs !== undefined ||
timeoutMs !== undefined ||
deployErr.deployRestMethod !== undefined ||
deployErr.deployRestPath !== undefined;
if (!hasRestContext) {
return "Discord REST request was aborted";
}
const timing: string[] = [];
if (timeoutMs !== undefined) {
timing.push(`timeout=${formatDurationMs(timeoutMs)}`);
}
if (requestMs !== undefined) {
timing.push(`observed=${formatDurationMs(requestMs)}`);
}
const timingText = timing.length > 0 ? ` (${timing.join(", ")})` : "";
if (timeoutMs !== undefined && requestMs !== undefined && requestMs >= timeoutMs) {
return `Discord REST ${operation} timed out${timingText}`;
}
return `Discord REST ${operation} was aborted${timingText}`;
}
export function resolveDiscordDeployRateLimitDetails(
err: unknown,
): DiscordDeployRateLimitDetails | undefined {

View File

@@ -2,8 +2,10 @@ import { formatDurationSeconds, warn, type RuntimeEnv } from "openclaw/plugin-sd
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { Client, overwriteApplicationCommands, type RequestClient } from "../internal/discord.js";
import {
attachDiscordDeployRestContext,
attachDiscordDeployRequestBody,
formatDiscordDeployErrorDetails,
formatDiscordDeployErrorMessage,
formatDiscordDeployRateLimitDetails,
formatDiscordDeployRateLimitWarning,
isDiscordDeployDailyCreateLimit,
@@ -27,6 +29,7 @@ function wrapDeployRestMethod(params: {
runtime: RuntimeEnv;
accountId: string;
startupStartedAt: number;
timeoutMs?: number;
shouldLogVerbose: () => boolean;
}) {
return async (path: string, data?: never, query?: never) => {
@@ -51,20 +54,27 @@ function wrapDeployRestMethod(params: {
}
return result;
} catch (err) {
const requestMs = Date.now() - startedAt;
attachDiscordDeployRequestBody(err, body);
attachDiscordDeployRestContext(err, {
method: params.method,
path,
requestMs,
timeoutMs: params.timeoutMs,
});
const rateLimitDetails = formatDiscordDeployRateLimitDetails(err);
if (rateLimitDetails) {
if (params.shouldLogVerbose()) {
params.runtime.log?.(
warn(
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:rate-limited ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt}${rateLimitDetails}`,
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:rate-limited ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${requestMs}${rateLimitDetails}`,
),
);
}
} else {
const details = formatDiscordDeployErrorDetails(err);
params.runtime.error?.(
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:error ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt} error=${formatErrorMessage(err)}${details}`,
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:error ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${requestMs} error=${formatDiscordDeployErrorMessage(err)}${details}`,
);
}
throw err;
@@ -87,12 +97,14 @@ function installDeployRestLogging(params: {
delete: params.rest.delete.bind(params.rest),
};
for (const method of Object.keys(original) as RestMethodName[]) {
const timeout = (params.rest as { options?: { timeout?: unknown } }).options?.timeout;
params.rest[method] = wrapDeployRestMethod({
method,
original,
runtime: params.runtime,
accountId: params.accountId,
startupStartedAt: params.startupStartedAt,
timeoutMs: typeof timeout === "number" ? timeout : undefined,
shouldLogVerbose: params.shouldLogVerbose,
}) as RequestClient[typeof method];
}
@@ -168,7 +180,7 @@ export async function deployDiscordCommands(params: {
params.runtime.log?.(
warn(
rateLimitWarning ??
`discord: native slash command deploy warning (not message send): ${formatErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`,
`discord: native slash command deploy warning (not message send): ${formatDiscordDeployErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`,
),
);
} finally {

View File

@@ -792,11 +792,25 @@ describe("monitorDiscordProvider", () => {
.mock.calls.some(
(call) =>
String(call[0]).includes("native slash command deploy warning (not message send):") &&
String(call[0]).includes("This operation was aborted"),
String(call[0]).includes("Discord REST request was aborted"),
),
).toBe(true);
});
it("formats native command deploy aborts with REST timeout context", () => {
const error = Object.assign(new Error("This operation was aborted"), {
name: "AbortError",
deployRestMethod: "patch",
deployRestPath: "/applications/app-1/commands/cmd-1",
deployRequestMs: 24_657,
deployTimeoutMs: 15_000,
});
expect(providerTesting.formatDiscordDeployErrorMessage(error)).toBe(
"Discord REST PATCH /applications/app-1/commands/cmd-1 timed out (timeout=15s, observed=24.7s)",
);
});
it("logs repeated native command deploy rate limits as one concise warning", async () => {
const runtime = baseRuntime();
const rateLimitError = createRateLimitError(

View File

@@ -47,7 +47,10 @@ import {
type GetPluginCommandSpecs,
} from "./provider.commands.js";
import { logDiscordResolvedConfig } from "./provider.config-log.js";
import { formatDiscordDeployErrorDetails } from "./provider.deploy-errors.js";
import {
formatDiscordDeployErrorDetails,
formatDiscordDeployErrorMessage,
} from "./provider.deploy-errors.js";
import {
clearDiscordNativeCommands,
runDiscordCommandDeployInBackground,
@@ -644,6 +647,7 @@ export const __testing = {
resolveDiscordRestFetch,
resolveThreadBindingsEnabled: resolveThreadBindingsEnabledForTesting,
formatDiscordDeployErrorDetails,
formatDiscordDeployErrorMessage,
setFetchDiscordApplicationId(mock?: typeof fetchDiscordApplicationId) {
fetchDiscordApplicationIdForTesting = mock;
},