refactor: unify typing callbacks

This commit is contained in:
Peter Steinberger
2026-01-23 22:55:41 +00:00
parent d82ecaf9dc
commit 8252ae2da1
11 changed files with 114 additions and 63 deletions

27
src/channels/typing.ts Normal file
View File

@@ -0,0 +1,27 @@
export type TypingCallbacks = {
onReplyStart: () => Promise<void>;
onIdle?: () => void;
};
export function createTypingCallbacks(params: {
start: () => Promise<void>;
stop?: () => Promise<void>;
onStartError: (err: unknown) => void;
onStopError?: (err: unknown) => void;
}): TypingCallbacks {
const onReplyStart = async () => {
try {
await params.start();
} catch (err) {
params.onStartError(err);
}
};
const onIdle = params.stop
? () => {
void params.stop().catch((err) => (params.onStopError ?? params.onStartError)(err));
}
: undefined;
return { onReplyStart, onIdle };
}

View File

@@ -12,6 +12,7 @@ import {
removeAckReactionAfterReply,
shouldAckReaction as shouldAckReactionGate,
} from "../../channels/ack-reactions.js";
import { createTypingCallbacks } from "../../channels/typing.js";
import {
formatInboundEnvelope,
formatThreadStarterEnvelope,
@@ -350,7 +351,12 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
onError: (err, info) => {
runtime.error?.(danger(`discord ${info.kind} reply failed: ${String(err)}`));
},
onReplyStart: () => sendTyping({ client, channelId: typingChannelId }),
onReplyStart: createTypingCallbacks({
start: () => sendTyping({ client, channelId: typingChannelId }),
onStartError: (err) => {
logVerbose(`discord typing cue failed for channel ${typingChannelId}: ${String(err)}`);
},
}).onReplyStart,
});
const { queuedFinal, counts } = await dispatchInboundMessage({

View File

@@ -1,15 +1,9 @@
import type { Client } from "@buape/carbon";
import { logVerbose } from "../../globals.js";
export async function sendTyping(params: { client: Client; channelId: string }) {
try {
const channel = await params.client.fetchChannel(params.channelId);
if (!channel) return;
if ("triggerTyping" in channel && typeof channel.triggerTyping === "function") {
await channel.triggerTyping();
}
} catch (err) {
logVerbose(`discord typing cue failed for channel ${params.channelId}: ${String(err)}`);
const channel = await params.client.fetchChannel(params.channelId);
if (!channel) return;
if ("triggerTyping" in channel && typeof channel.triggerTyping === "function") {
await channel.triggerTyping();
}
}

View File

@@ -129,6 +129,7 @@ export {
shouldAckReaction,
shouldAckReactionForWhatsApp,
} from "../channels/ack-reactions.js";
export { createTypingCallbacks } from "../channels/typing.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
export type { NormalizedLocation } from "../channels/location.js";
export { formatLocationText, toLocationContext } from "../channels/location.js";

View File

@@ -25,6 +25,7 @@ import {
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
import { recordInboundSession } from "../../channels/session.js";
import { createTypingCallbacks } from "../../channels/typing.js";
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
@@ -182,18 +183,19 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
identityName: resolveIdentityName(deps.cfg, route.agentId),
};
const onReplyStart = async () => {
try {
const typingCallbacks = createTypingCallbacks({
start: async () => {
if (!ctxPayload.To) return;
await sendTypingSignal(ctxPayload.To, {
baseUrl: deps.baseUrl,
account: deps.account,
accountId: deps.accountId,
});
} catch (err) {
},
onStartError: (err) => {
logVerbose(`signal typing cue failed for ${ctxPayload.To}: ${String(err)}`);
}
};
},
});
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
responsePrefix: resolveEffectiveMessagesConfig(deps.cfg, route.agentId).responsePrefix,
@@ -214,7 +216,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
onError: (err, info) => {
deps.runtime.error?.(danger(`signal ${info.kind} reply failed: ${String(err)}`));
},
onReplyStart,
onReplyStart: typingCallbacks.onReplyStart,
});
const { queuedFinal } = await dispatchInboundMessage({

View File

@@ -10,6 +10,7 @@ import {
import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js";
import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.js";
import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js";
import { createTypingCallbacks } from "../../../channels/typing.js";
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
import { removeSlackReaction } from "../../actions.js";
@@ -43,14 +44,30 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
hasRepliedRef,
});
const onReplyStart = async () => {
didSetStatus = true;
await ctx.setSlackThreadStatus({
channelId: message.channel,
threadTs: statusThreadTs,
status: "is typing...",
});
};
const typingCallbacks = createTypingCallbacks({
start: async () => {
didSetStatus = true;
await ctx.setSlackThreadStatus({
channelId: message.channel,
threadTs: statusThreadTs,
status: "is typing...",
});
},
stop: async () => {
if (!didSetStatus) return;
await ctx.setSlackThreadStatus({
channelId: message.channel,
threadTs: statusThreadTs,
status: "",
});
},
onStartError: (err) => {
runtime.error?.(danger(`slack typing cue failed: ${String(err)}`));
},
onStopError: (err) => {
runtime.error?.(danger(`slack typing stop failed: ${String(err)}`));
},
});
// Create mutable context for response prefix template interpolation
let prefixContext: ResponsePrefixContext = {
@@ -76,15 +93,10 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
},
onError: (err, info) => {
runtime.error?.(danger(`slack ${info.kind} reply failed: ${String(err)}`));
if (didSetStatus) {
void ctx.setSlackThreadStatus({
channelId: message.channel,
threadTs: statusThreadTs,
status: "",
});
}
typingCallbacks.onIdle?.();
},
onReplyStart,
onReplyStart: typingCallbacks.onReplyStart,
onIdle: typingCallbacks.onIdle,
});
const { queuedFinal, counts } = await dispatchInboundMessage({
@@ -110,14 +122,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
});
markDispatchIdle();
if (didSetStatus) {
await ctx.setSlackThreadStatus({
channelId: message.channel,
threadTs: statusThreadTs,
status: "",
});
}
if (!queuedFinal) {
if (prepared.isRoomish) {
clearHistoryEntriesIfEnabled({

View File

@@ -156,11 +156,7 @@ export const buildTelegramMessageContext = async ({
}
const sendTyping = async () => {
try {
await bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId));
} catch (err) {
logVerbose(`telegram typing cue failed for chat ${chatId}: ${String(err)}`);
}
await bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId));
};
const sendRecordVoice = async () => {

View File

@@ -8,6 +8,7 @@ import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { removeAckReactionAfterReply } from "../channels/ack-reactions.js";
import { createTypingCallbacks } from "../channels/typing.js";
import { danger, logVerbose } from "../globals.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { deliverReplies } from "./bot/delivery.js";
@@ -158,7 +159,12 @@ export const dispatchTelegramMessage = async ({
onError: (err, info) => {
runtime.error?.(danger(`telegram ${info.kind} reply failed: ${String(err)}`));
},
onReplyStart: sendTyping,
onReplyStart: createTypingCallbacks({
start: sendTyping,
onStartError: (err) => {
logVerbose(`telegram typing cue failed for chat ${chatId}: ${String(err)}`);
},
}).onReplyStart,
},
replyOptions: {
skillFilter,