diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d56bf8015f..e6776aa0b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai - Telegram: keep no-response DM turns quiet instead of rewriting them into visible silent-reply chatter. Fixes #78188. (#78228) Thanks @Beandon13. - Telegram: handle managed select button callbacks before the raw callback fallback while preserving delimiter-containing option values such as `env|prod`. (#79816) Thanks @moeedahmed. - OpenAI-compatible models: handle JSON chat-completion bodies returned to streaming requests, preserving reasoning fields and visible text instead of completing an empty agent turn. Fixes #77870. +- Discord/models: defer model picker component interactions before loading route, model, and preference data, preventing "This interaction failed" timeouts under gateway load. Fixes #77283. Thanks @colin-chang. - xAI: expose `/think low|medium|high` for reasoning-capable Grok models and keep `reasoning.effort` on native Responses payloads while preserving off-only behavior for non-reasoning routes. Fixes #79210. Thanks @colinmcintosh. - CLI/media: let explicit image description model refs use bundled static provider catalogs and generic model-backed image hooks, so `openclaw infer image describe --model zai/glm-4.6v` works like direct model runs and Anthropic auth probes avoid stale Claude 3 Haiku catalog entries. - Models/Anthropic: add `anthropic/claude-haiku-4-5` to Anthropic API-key agent allowlist defaults when an Anthropic default model is configured, so cron model overrides can select the current Haiku alias. Fixes #78000. diff --git a/extensions/discord/src/monitor/native-command-model-picker-interaction.ts b/extensions/discord/src/monitor/native-command-model-picker-interaction.ts index 13a02f516ed..195dec6b833 100644 --- a/extensions/discord/src/monitor/native-command-model-picker-interaction.ts +++ b/extensions/discord/src/monitor/native-command-model-picker-interaction.ts @@ -147,6 +147,17 @@ export async function handleDiscordModelPickerInteraction(params: { return; } + let deferredUpdate = interaction.acknowledged; + if (!deferredUpdate) { + const deferred = await params.safeInteractionCall("model picker defer", () => + interaction.acknowledge(), + ); + if (deferred === null) { + return; + } + deferredUpdate = true; + } + const route = await resolveDiscordModelPickerRoute({ interaction, cfg: ctx.cfg, @@ -175,7 +186,9 @@ export async function handleDiscordModelPickerInteraction(params: { limit: 5, }); const updatePicker = async (payload: MessagePayload) => - await params.safeInteractionCall("model picker update", () => interaction.update(payload)); + await params.safeInteractionCall("model picker update", () => + deferredUpdate ? interaction.editReply(payload) : interaction.update(payload), + ); const showNotice = async (message: string) => await updatePicker(buildDiscordModelPickerNoticePayload(message)); diff --git a/extensions/discord/src/monitor/native-command.model-picker.test.ts b/extensions/discord/src/monitor/native-command.model-picker.test.ts index ff8afba37f2..951b8adccef 100644 --- a/extensions/discord/src/monitor/native-command.model-picker.test.ts +++ b/extensions/discord/src/monitor/native-command.model-picker.test.ts @@ -44,7 +44,9 @@ type MockInteraction = { reply: ReturnType; followUp: ReturnType; update: ReturnType; + editReply: ReturnType; acknowledge: ReturnType; + acknowledged: boolean; client: object; }; @@ -96,7 +98,7 @@ function createModelPickerContext(): ModelPickerContext { function createInteraction(params?: { userId?: string; values?: string[] }): MockInteraction { const userId = params?.userId ?? "owner"; - return { + const interaction = { user: { id: userId, username: "tester", @@ -115,9 +117,16 @@ function createInteraction(params?: { userId?: string; values?: string[] }): Moc reply: vi.fn().mockResolvedValue({ ok: true }), followUp: vi.fn().mockResolvedValue({ ok: true }), update: vi.fn().mockResolvedValue({ ok: true }), - acknowledge: vi.fn().mockResolvedValue({ ok: true }), + editReply: vi.fn().mockResolvedValue({ ok: true }), + acknowledge: vi.fn(), + acknowledged: false, client: {}, }; + interaction.acknowledge.mockImplementation(async () => { + interaction.acknowledged = true; + return { ok: true }; + }); + return interaction; } function createDefaultModelPickerData(): ModelsProviderData { @@ -323,6 +332,28 @@ describe("Discord model picker interactions", () => { expect(loadSpy).not.toHaveBeenCalled(); }); + it("defers owner picker interactions before loading model data", async () => { + const context = createModelPickerContext(); + const pickerData = createDefaultModelPickerData(); + const loadSpy = vi + .spyOn(modelPickerModule, "loadDiscordModelPickerData") + .mockImplementation(async () => { + expect(interaction.acknowledge).toHaveBeenCalledTimes(1); + return pickerData; + }); + const select = createModelPickerFallbackSelect(context); + const interaction = createInteraction({ userId: "owner", values: ["gpt-4o"] }); + + await select.run( + interaction as unknown as PickerSelectInteraction, + createModelsViewSelectData(), + ); + + expect(loadSpy).toHaveBeenCalledTimes(1); + expect(interaction.editReply).toHaveBeenCalledTimes(1); + expect(interaction.update).not.toHaveBeenCalled(); + }); + it("requires submit click before routing selected model through /model pipeline", async () => { const context = createModelPickerContext(); const pickerData = createDefaultModelPickerData(); @@ -338,7 +369,7 @@ describe("Discord model picker interactions", () => { dispatchCommandInteraction: dispatchSpy, }); - expect(selectInteraction.update).toHaveBeenCalledTimes(1); + expect(selectInteraction.editReply).toHaveBeenCalledTimes(1); expect(dispatchSpy).not.toHaveBeenCalled(); const submitInteraction = await runSubmitButton({ @@ -347,7 +378,7 @@ describe("Discord model picker interactions", () => { dispatchCommandInteraction: dispatchSpy, }); - expect(submitInteraction.update).toHaveBeenCalledTimes(1); + expect(submitInteraction.editReply).toHaveBeenCalledTimes(1); expect(dispatchSpy).toHaveBeenCalledTimes(1); expectDispatchedModelSelection({ dispatchSpy, @@ -547,8 +578,8 @@ describe("Discord model picker interactions", () => { await button.run(interaction as unknown as PickerButtonInteraction, data); - expect(interaction.update).toHaveBeenCalledTimes(1); - const updatePayload = interaction.update.mock.calls[0]?.[0]; + expect(interaction.editReply).toHaveBeenCalledTimes(1); + const updatePayload = interaction.editReply.mock.calls[0]?.[0]; if (!updatePayload) { throw new Error("recents button did not emit an update payload"); } @@ -585,7 +616,7 @@ describe("Discord model picker interactions", () => { dispatchCommandInteraction: dispatchSpy, }); - expect(submitInteraction.update).toHaveBeenCalledTimes(1); + expect(submitInteraction.editReply).toHaveBeenCalledTimes(1); expect(dispatchSpy).toHaveBeenCalledTimes(1); expectDispatchedModelSelection({ dispatchSpy, model: "openai/gpt-4o" }); });