fix: avoid repeated discord thread starter context

This commit is contained in:
Peter Steinberger
2026-05-02 11:00:48 +01:00
parent d25019b416
commit e0a267afc6
3 changed files with 57 additions and 2 deletions

View File

@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
- Discord: preserve attachment and sticker filenames when saving inbound media, so agents can see human-readable file names instead of only UUID-based paths. Fixes #59744. Thanks @xela92 and @rockcent.
- Discord: preserve non-ASCII channel names in session display labels while keeping allowlist matching on the existing ASCII slug contract. Thanks @swjeong9.
- Discord/PluralKit: canonicalize proxied webhook turns to the original Discord message id for inbound dedupe, while preserving the proxy message id for reply routing. Thanks @acgh213.
- Discord: only inject thread starter context on the first turn of the effective thread session, so follow-up thread replies do not repeat the starter block. Fixes #41355; supersedes #44447 and #44449. Thanks @p3nchan.
- Gateway/diagnostics: include a bounded redacted startup error message in stability bundles, so crash-loop reports identify the failing plugin or contract without exposing secrets. Refs #75797. Thanks @ymebosma.
- Gateway/pricing: abort in-flight model pricing catalog fetches when Gateway shutdown stops the refresh loop, and avoid post-stop cache writes or refresh timers. Fixes #72208. Thanks @rzcq.
- Codex/app-server: make startup retry cleanup ownership-aware so concurrent Codex lanes cannot close another lane's freshly restarted shared app-server client. Thanks @vincentkoc.

View File

@@ -296,6 +296,15 @@ export async function buildDiscordMessageProcessContext(params: {
}))
: undefined;
const originatingTo = autoThreadContext?.OriginatingTo ?? dmConversationTarget ?? replyTarget;
const effectiveSessionKey =
boundSessionKey ?? autoThreadContext?.SessionKey ?? threadKeys.sessionKey;
const effectivePreviousTimestamp =
effectiveSessionKey === route.sessionKey
? previousTimestamp
: readSessionUpdatedAt({
storePath,
sessionKey: effectiveSessionKey,
});
const ctxPayload = finalizeInboundContext({
Body: combinedBody,
@@ -306,7 +315,7 @@ export async function buildDiscordMessageProcessContext(params: {
...(preflightAudioTranscript !== undefined ? { Transcript: preflightAudioTranscript } : {}),
From: effectiveFrom,
To: effectiveTo,
SessionKey: boundSessionKey ?? autoThreadContext?.SessionKey ?? threadKeys.sessionKey,
SessionKey: effectiveSessionKey,
AccountId: route.accountId,
ChatType: isDirectMessage ? "direct" : "channel",
ConversationLabel: fromLabel,
@@ -335,7 +344,7 @@ export async function buildDiscordMessageProcessContext(params: {
ModelParentSessionKey:
autoThreadContext?.ModelParentSessionKey ?? modelParentSessionKey ?? undefined,
MessageThreadId: threadChannel?.id ?? autoThreadContext?.createdThreadId ?? undefined,
ThreadStarterBody: threadStarterBody,
ThreadStarterBody: !effectivePreviousTimestamp ? threadStarterBody : undefined,
ThreadLabel: threadLabel,
Timestamp: resolveTimestampMs(message.timestamp),
...mediaPayload,

View File

@@ -364,6 +364,7 @@ function getLastDispatchCtx():
OriginatingTo?: string;
ParentSessionKey?: string;
SessionKey?: string;
ThreadStarterBody?: string;
To?: string;
Transcript?: string;
}
@@ -384,6 +385,7 @@ function getLastDispatchCtx():
OriginatingTo?: string;
ParentSessionKey?: string;
SessionKey?: string;
ThreadStarterBody?: string;
To?: string;
Transcript?: string;
};
@@ -1053,6 +1055,49 @@ describe("processDiscordMessage session routing", () => {
});
expect(getLastDispatchCtx()?.ParentSessionKey).toBeUndefined();
});
it("omits thread starter context when the effective thread session already exists", async () => {
const threadSessionKey = "agent:main:discord:channel:thread-1";
readSessionUpdatedAt.mockImplementation((params?: unknown) => {
const sessionKey = (params as { sessionKey?: string } | undefined)?.sessionKey;
return sessionKey === threadSessionKey ? 1_700_000_000_000 : undefined;
});
const rest = {
get: vi.fn(async () => ({
content: "original thread starter",
embeds: [],
author: { id: "U2", username: "bob", discriminator: "0" },
timestamp: new Date().toISOString(),
})),
};
const ctx = await createBaseContext({
baseSessionKey: threadSessionKey,
route: BASE_CHANNEL_ROUTE,
messageChannelId: "thread-1",
message: {
id: "m1",
channelId: "thread-1",
content: "follow-up",
timestamp: new Date().toISOString(),
attachments: [],
},
messageText: "follow-up",
baseText: "follow-up",
threadChannel: { id: "thread-1", name: "child-thread" },
threadParentId: "parent-1",
client: { rest },
channelConfig: { allowed: true, users: ["U2"] },
});
await runProcessDiscordMessage(ctx);
expect(rest.get).toHaveBeenCalled();
expect(getLastDispatchCtx()).toMatchObject({
SessionKey: threadSessionKey,
MessageThreadId: "thread-1",
});
expect(getLastDispatchCtx()?.ThreadStarterBody).toBeUndefined();
});
});
describe("processDiscordMessage draft streaming", () => {