fix(discord): defer component interactions to prevent timeout (#16287)

* fix(discord): defer component interactions to prevent timeout

Discord requires interaction responses within 3 seconds. Button clicks
were routed through the LLM pipeline before responding, exceeding this
window and showing 'This interaction failed' to users.

Now immediately defers the interaction, then processes the agent
response asynchronously.

Fixes #16262

* fix: harden deferred interaction replies and silent chat finals (#16287) (thanks @robbyczgw-cla)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Robby
2026-02-14 17:38:01 +01:00
committed by GitHub
parent 69f809dca3
commit 078642b308
5 changed files with 109 additions and 16 deletions

View File

@@ -56,6 +56,16 @@ export class ChatLog extends Container {
this.addChild(new AssistantMessageComponent(text));
}
dropAssistant(runId?: string) {
const effectiveRunId = this.resolveRunId(runId);
const existing = this.streamingRuns.get(effectiveRunId);
if (!existing) {
return;
}
this.removeChild(existing);
this.streamingRuns.delete(effectiveRunId);
}
startTool(toolCallId: string, toolName: string, args: unknown) {
const existing = this.toolById.get(toolCallId);
if (existing) {

View File

@@ -6,7 +6,12 @@ import { createEventHandlers } from "./tui-event-handlers.js";
type MockChatLog = Pick<
ChatLog,
"startTool" | "updateToolResult" | "addSystem" | "updateAssistant" | "finalizeAssistant"
| "startTool"
| "updateToolResult"
| "addSystem"
| "updateAssistant"
| "finalizeAssistant"
| "dropAssistant"
>;
type MockTui = Pick<TUI, "requestRender">;
@@ -41,6 +46,7 @@ describe("tui-event-handlers: handleAgentEvent", () => {
addSystem: vi.fn(),
updateAssistant: vi.fn(),
finalizeAssistant: vi.fn(),
dropAssistant: vi.fn(),
};
const tui: MockTui = { requestRender: vi.fn() };
const setActivityStatus = vi.fn();
@@ -357,4 +363,33 @@ describe("tui-event-handlers: handleAgentEvent", () => {
expect(loadHistory).toHaveBeenCalledTimes(1);
});
it("drops streaming assistant when chat final has no message", () => {
const state = makeState({ activeChatRunId: null });
const { chatLog, tui, setActivityStatus } = makeContext(state);
const { handleChatEvent } = createEventHandlers({
chatLog,
tui,
state,
setActivityStatus,
});
handleChatEvent({
runId: "run-silent",
sessionKey: state.currentSessionKey,
state: "delta",
message: { content: "hello" },
});
chatLog.dropAssistant.mockClear();
chatLog.finalizeAssistant.mockClear();
handleChatEvent({
runId: "run-silent",
sessionKey: state.currentSessionKey,
state: "final",
});
expect(chatLog.dropAssistant).toHaveBeenCalledWith("run-silent");
expect(chatLog.finalizeAssistant).not.toHaveBeenCalled();
});
});

View File

@@ -109,6 +109,20 @@ export function createEventHandlers(context: EventHandlerContext) {
setActivityStatus("streaming");
}
if (evt.state === "final") {
if (!evt.message) {
if (isLocalRunId?.(evt.runId)) {
forgetLocalRunId?.(evt.runId);
} else {
void loadHistory?.();
}
chatLog.dropAssistant(evt.runId);
noteFinalizedRun(evt.runId);
state.activeChatRunId = null;
setActivityStatus("idle");
void refreshSessionInfo?.();
tui.requestRender();
return;
}
if (isCommandMessage(evt.message)) {
if (isLocalRunId?.(evt.runId)) {
forgetLocalRunId?.(evt.runId);