Skip to content

Commit 21c911c

Browse files
committed
docs(discord): explain typing lifecycle invariants
1 parent 11beb38 commit 21c911c

6 files changed

Lines changed: 22 additions & 0 deletions

File tree

extensions/discord/src/monitor/inbound-job.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff 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

2931
export 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;

extensions/discord/src/monitor/message-handler.process.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff 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({

extensions/discord/src/monitor/message-handler.reply-typing-policy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export type DiscordAcceptedTypingPrestartDecision = {
2121
export 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) {

extensions/discord/src/monitor/message-handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff 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,

extensions/discord/src/monitor/message-run-queue.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff 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,

extensions/discord/src/monitor/reply-typing-feedback.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { sendTyping } from "./typing.js";
77

88
export 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.
1012
export 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?.(),

0 commit comments

Comments
 (0)