mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
fix: handle discord auto-thread race (#7517) (thanks @rohanpatriot)
This commit is contained in:
@@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Sessions/Agents: pass `agentId` through status and usage transcript-resolution paths (auto-reply, gateway usage APIs, and session cost/log loaders) so non-default agents can resolve absolute session files without path-validation failures. (#15103) Thanks @jalehman.
|
||||
- Signal/Install: auto-install `signal-cli` via Homebrew on non-x64 Linux architectures, avoiding x86_64 native binary `Exec format error` failures on arm64/arm hosts. (#15443) Thanks @jogvan-k.
|
||||
- Discord: avoid misrouting numeric guild allowlist entries to `/channels/<guildId>` by prefixing guild-only inputs with `guild:` during resolution. (#12326) Thanks @headswim.
|
||||
- Discord: reuse an auto-created thread when multiple agents race to create it, so replies stay in the shared thread. (#7517) Thanks @rohanpatriot.
|
||||
- Config: preserve `${VAR}` env references when writing config files so `openclaw config set/apply/patch` does not persist secrets to disk. Thanks @thewilloftheshadow.
|
||||
- Process/Exec: avoid shell execution for `.exe` commands on Windows so env overrides work reliably in `runCommandWithTimeout`. Thanks @thewilloftheshadow.
|
||||
- Web tools/web_fetch: prefer `text/markdown` responses for Cloudflare Markdown for Agents, add `cf-markdown` extraction for markdown bodies, and redact fetched URLs in `x-markdown-tokens` debug logs to avoid leaking raw paths/query params. (#15376) Thanks @Yaxuan42.
|
||||
|
||||
@@ -115,16 +115,14 @@ describe("resolveDiscordReplyDeliveryPlan", () => {
|
||||
|
||||
describe("maybeCreateDiscordAutoThread", () => {
|
||||
it("returns existing thread ID when creation fails due to race condition", async () => {
|
||||
// First call succeeds (simulating another agent creating the thread)
|
||||
let callCount = 0;
|
||||
let getCalls = 0;
|
||||
const client = {
|
||||
rest: {
|
||||
post: async () => {
|
||||
callCount++;
|
||||
throw new Error("A thread has already been created on this message");
|
||||
},
|
||||
get: async () => {
|
||||
// Return message with existing thread (simulating race condition resolution)
|
||||
getCalls += 1;
|
||||
return { thread: { id: "existing-thread" } };
|
||||
},
|
||||
},
|
||||
@@ -146,16 +144,18 @@ describe("maybeCreateDiscordAutoThread", () => {
|
||||
});
|
||||
|
||||
expect(result).toBe("existing-thread");
|
||||
expect(getCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("returns undefined when creation fails and no existing thread found", async () => {
|
||||
it("returns undefined when creation fails with non-race error", async () => {
|
||||
let getCalls = 0;
|
||||
const client = {
|
||||
rest: {
|
||||
post: async () => {
|
||||
throw new Error("Some other error");
|
||||
},
|
||||
get: async () => {
|
||||
// Message has no thread
|
||||
getCalls += 1;
|
||||
return { thread: null };
|
||||
},
|
||||
},
|
||||
@@ -177,6 +177,7 @@ describe("maybeCreateDiscordAutoThread", () => {
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(getCalls).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -226,6 +226,57 @@ export function sanitizeDiscordThreadName(rawName: string, fallbackId: string):
|
||||
return truncateUtf16Safe(base, 100) || `Thread ${fallbackId}`;
|
||||
}
|
||||
|
||||
const DISCORD_THREAD_ALREADY_EXISTS_HINTS = [
|
||||
"thread has already been created on this message",
|
||||
"thread already exists",
|
||||
];
|
||||
|
||||
function extractDiscordErrorMessage(err: unknown): string {
|
||||
if (!err) {
|
||||
return "";
|
||||
}
|
||||
if (typeof err === "string") {
|
||||
return err;
|
||||
}
|
||||
if (err instanceof Error && typeof err.message === "string") {
|
||||
return err.message;
|
||||
}
|
||||
if (typeof err === "object" && err !== null && "message" in err) {
|
||||
const message = (err as { message?: unknown }).message;
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
if (typeof err === "number" || typeof err === "boolean") {
|
||||
return err.toString();
|
||||
}
|
||||
if (typeof err === "bigint") {
|
||||
return err.toString();
|
||||
}
|
||||
if (typeof err === "symbol") {
|
||||
return err.description ?? "symbol";
|
||||
}
|
||||
if (typeof err === "function") {
|
||||
return err.name ? `function ${err.name}` : "function";
|
||||
}
|
||||
if (typeof err === "object") {
|
||||
try {
|
||||
return JSON.stringify(err) ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isDiscordThreadAlreadyExistsError(err: unknown): boolean {
|
||||
const message = extractDiscordErrorMessage(err).trim().toLowerCase();
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
return DISCORD_THREAD_ALREADY_EXISTS_HINTS.some((hint) => message.includes(hint));
|
||||
}
|
||||
|
||||
type DiscordReplyDeliveryPlan = {
|
||||
deliverTarget: string;
|
||||
replyTarget: string;
|
||||
@@ -360,6 +411,9 @@ export async function maybeCreateDiscordAutoThread(params: {
|
||||
logVerbose(
|
||||
`discord: autoThread creation failed for ${params.message.channelId}/${params.message.id}: ${String(err)}`,
|
||||
);
|
||||
if (!isDiscordThreadAlreadyExistsError(err)) {
|
||||
return undefined;
|
||||
}
|
||||
// Race condition: another agent may have already created a thread on this
|
||||
// message. Re-fetch the message to check for an existing thread.
|
||||
try {
|
||||
@@ -373,8 +427,10 @@ export async function maybeCreateDiscordAutoThread(params: {
|
||||
);
|
||||
return existingThreadId;
|
||||
}
|
||||
} catch {
|
||||
// If the refetch also fails, fall through to return undefined.
|
||||
} catch (refetchErr) {
|
||||
logVerbose(
|
||||
`discord: autoThread refetch failed for ${params.message.channelId}/${params.message.id}: ${String(refetchErr)}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user