File tree Expand file tree Collapse file tree
extensions/discord/src/monitor Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -12,6 +12,8 @@ type DiscordInboundJobRuntimeField =
1212 | "guildHistories"
1313 | "client"
1414 | "threadBindings"
15+ // Function-backed feedback stays runtime-only; payload must remain
16+ // materializable data so queued jobs cannot accidentally serialize it.
1517 | "replyTypingFeedback"
1618 | "discordRestFetch" ;
1719
@@ -27,6 +29,8 @@ export type DiscordInboundJob = {
2729} ;
2830
2931export function resolveDiscordInboundJobQueueKey ( ctx : DiscordMessagePreflightContext ) : string {
32+ // This key is both the run-queue serialization key and the typing prestart
33+ // dedupe key, so keep it aligned with the eventual session route.
3034 const sessionKey = ctx . route . sessionKey ?. trim ( ) ;
3135 if ( sessionKey ) {
3236 return sessionKey ;
Original file line number Diff line number Diff line change @@ -442,6 +442,8 @@ async function processDiscordMessageInner(
442442 const typingChannelId = deliverTarget . startsWith ( "channel:" )
443443 ? deliverTarget . slice ( "channel:" . length )
444444 : messageChannelId ;
445+ // Deliver target can move into a thread after preflight accepted the message.
446+ // The typing owner follows the final target before reply dispatch starts.
445447 const typingFeedback =
446448 replyTypingFeedback ??
447449 createDiscordReplyTypingFeedback ( {
Original file line number Diff line number Diff line change @@ -21,6 +21,8 @@ export type DiscordAcceptedTypingPrestartDecision = {
2121export function resolveDiscordSourceReplyDeliveryMode (
2222 ctx : DiscordMessagePreflightContext ,
2323) : SourceReplyDeliveryMode {
24+ // Keep prestart policy keyed to the same source-reply mode as dispatch.
25+ // Otherwise message-tool-only group replies would wait behind "message" mode.
2426 return resolveChannelMessageSourceReplyDeliveryMode ( {
2527 cfg : ctx . cfg ,
2628 ctx : {
@@ -51,13 +53,17 @@ export function resolveDiscordAcceptedTypingPrestart(
5153 }
5254 const configuredTypingMode = ctx . cfg . session ?. typingMode ?? ctx . cfg . agents ?. defaults ?. typingMode ;
5355 if ( configuredTypingMode !== undefined ) {
56+ // Explicit operator config wins over Discord heuristics.
57+ // Non-instant modes intentionally defer to the normal reply pipeline.
5458 return {
5559 sourceReplyDeliveryMode,
5660 shouldPrestart : configuredTypingMode === "instant" ,
5761 reason : configuredTypingMode === "instant" ? "configured-instant" : "configured-not-instant" ,
5862 } ;
5963 }
6064 if ( sourceReplyDeliveryMode === "message_tool_only" ) {
65+ // Message-tool-only replies have no visible default response path.
66+ // Prestart preserves user feedback while the tool-delivered reply waits.
6167 return { sourceReplyDeliveryMode, shouldPrestart : true , reason : "tool-only" } ;
6268 }
6369 if ( ! ctx . isGuildMessage && ! ctx . isGroupDm ) {
Original file line number Diff line number Diff line change @@ -133,6 +133,8 @@ export function createDiscordMessageHandler(
133133 "group-mentions" ;
134134 const preflightDiscordMessageImpl = params . testing ?. preflightDiscordMessage ;
135135 const replayGuard = createDiscordInboundReplayGuard ( ) ;
136+ // The map owns pre-dispatch typing leases, not queued work itself.
137+ // Each lease is released by the feedback cleanup hook installed below.
136138 const prestartedTypingFeedback = new Map < string , PrestartedTypingFeedbackEntry > ( ) ;
137139 const messageRunQueue = createDiscordMessageRunQueue ( {
138140 runtime : params . runtime ,
Original file line number Diff line number Diff line change @@ -107,6 +107,8 @@ export function createDiscordMessageRunQueue(
107107 let lifecycleActive = ! params . abortSignal ?. aborted ;
108108
109109 const cleanupSkippedQueuedMessages = ( ) => {
110+ // These callbacks represent jobs accepted into the queue but not started.
111+ // Running jobs remove their callback before processDiscordMessage owns cleanup.
110112 if ( ! lifecycleActive && skippedCleanup . size === 0 ) {
111113 return ;
112114 }
@@ -135,6 +137,8 @@ export function createDiscordMessageRunQueue(
135137 }
136138 skippedCleanup . add ( cleanupSkipped ) ;
137139 runQueue . enqueue ( job . queueKey , async ( { lifecycleSignal } ) => {
140+ // Once the task starts, normal process/commit handling owns cleanup.
141+ // Leaving it in skippedCleanup would double-release replay/typing state.
138142 skippedCleanup . delete ( cleanupSkipped ) ;
139143 await processDiscordQueuedMessage ( {
140144 job,
Original file line number Diff line number Diff line change @@ -7,6 +7,8 @@ import { sendTyping } from "./typing.js";
77
88export const DISCORD_REPLY_TYPING_MAX_DURATION_MS = 20 * 60_000 ;
99
10+ // Discord can keep long tool-heavy replies alive, but not forever.
11+ // The dispatch restart path refreshes this TTL after queue wait time.
1012export type DiscordReplyTypingFeedback = ReturnType < typeof createTypingCallbacks > & {
1113 updateChannelId : ( channelId : string ) => void ;
1214 getChannelId : ( ) => string ;
@@ -51,6 +53,8 @@ export function createDiscordReplyTypingFeedback(params: {
5153 } ;
5254 let callbacks = createCallbacks ( ) ;
5355 return {
56+ // Expose one stable owner while allowing the inner typing controller to
57+ // rotate between prequeue feedback and the actual dispatch lifecycle.
5458 onReplyStart : ( ) => callbacks . onReplyStart ( ) ,
5559 onIdle : ( ) => callbacks . onIdle ?.( ) ,
5660 onCleanup : ( ) => callbacks . onCleanup ?.( ) ,
You can’t perform that action at this time.
0 commit comments