v2 phase 5: pending questions with interactive cards

End-to-end ask_user_question flow:
- Agent MCP tool writes question card to messages_out
- Host delivery creates pending_questions row, delivers as Discord Card with buttons
- Local webhook server receives Gateway INTERACTION_CREATE events
- Acknowledges interaction + updates card to show selected answer
- Routes response back to session DB as system message
- MCP tool poll picks up response and returns to agent

Key fixes:
- Poll loop now skips system messages (reserved for MCP tool responses)
- Gateway listener uses webhookUrl forwarding mode for interaction support
- Button custom_id encodes questionId + option text for self-contained routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-09 03:26:16 +03:00
parent c348fabf22
commit c31bb02c06
7 changed files with 233 additions and 6 deletions

View File

@@ -38,8 +38,16 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
let sessionId: string | undefined;
let resumeAt: string | undefined;
let pollCount = 0;
while (true) {
const messages = getPendingMessages();
// Skip system messages — they're responses for MCP tools (e.g., ask_user_question)
const messages = getPendingMessages().filter((m) => m.kind !== 'system');
pollCount++;
// Periodic heartbeat so we know the loop is alive
if (pollCount % 30 === 0) {
log(`Poll heartbeat (${pollCount} iterations, ${messages.length} pending)`);
}
if (messages.length === 0) {
await sleep(POLL_INTERVAL_MS);
@@ -210,7 +218,8 @@ async function processQuery(query: AgentQuery, routing: RoutingContext, config:
const pollHandle = setInterval(() => {
if (done) return;
const newMessages = getPendingMessages();
// Skip system messages — they're responses for MCP tools (e.g., ask_user_question)
const newMessages = getPendingMessages().filter((m) => m.kind !== 'system');
if (newMessages.length > 0) {
const newIds = newMessages.map((m) => m.id);
markProcessing(newIds);