fix(slack): clear assistant thread status after replies
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
|
||||
export type ReplyDispatchKind = "tool" | "block" | "final";
|
||||
|
||||
@@ -22,6 +23,20 @@ export type ReplyDispatcherOptions = {
|
||||
onError?: ReplyDispatchErrorHandler;
|
||||
};
|
||||
|
||||
type ReplyDispatcherWithTypingOptions = Omit<
|
||||
ReplyDispatcherOptions,
|
||||
"onIdle"
|
||||
> & {
|
||||
onReplyStart?: () => Promise<void> | void;
|
||||
onIdle?: () => void;
|
||||
};
|
||||
|
||||
type ReplyDispatcherWithTypingResult = {
|
||||
dispatcher: ReplyDispatcher;
|
||||
replyOptions: Pick<GetReplyOptions, "onReplyStart" | "onTypingController">;
|
||||
markDispatchIdle: () => void;
|
||||
};
|
||||
|
||||
export type ReplyDispatcher = {
|
||||
sendToolResult: (payload: ReplyPayload) => boolean;
|
||||
sendBlockReply: (payload: ReplyPayload) => boolean;
|
||||
@@ -107,3 +122,31 @@ export function createReplyDispatcher(
|
||||
getQueuedCounts: () => ({ ...queuedCounts }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createReplyDispatcherWithTyping(
|
||||
options: ReplyDispatcherWithTypingOptions,
|
||||
): ReplyDispatcherWithTypingResult {
|
||||
const { onReplyStart, onIdle, ...dispatcherOptions } = options;
|
||||
let typingController: TypingController | undefined;
|
||||
const dispatcher = createReplyDispatcher({
|
||||
...dispatcherOptions,
|
||||
onIdle: () => {
|
||||
typingController?.markDispatchIdle();
|
||||
onIdle?.();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
dispatcher,
|
||||
replyOptions: {
|
||||
onReplyStart,
|
||||
onTypingController: (typing) => {
|
||||
typingController = typing;
|
||||
},
|
||||
},
|
||||
markDispatchIdle: () => {
|
||||
typingController?.markDispatchIdle();
|
||||
onIdle?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,8 +33,10 @@ import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
} from "../auto-reply/reply/mentions.js";
|
||||
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import type { TypingController } from "../auto-reply/reply/typing.js";
|
||||
import {
|
||||
createReplyDispatcher,
|
||||
createReplyDispatcherWithTyping,
|
||||
} from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import type { ReplyToMode } from "../config/config.js";
|
||||
@@ -797,8 +799,8 @@ export function createDiscordMessageHandler(params: {
|
||||
}
|
||||
|
||||
let didSendReply = false;
|
||||
let typingController: TypingController | undefined;
|
||||
const dispatcher = createReplyDispatcher({
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
createReplyDispatcherWithTyping({
|
||||
responsePrefix: cfg.messages?.responsePrefix,
|
||||
deliver: async (payload) => {
|
||||
await deliverDiscordReply({
|
||||
@@ -812,28 +814,21 @@ export function createDiscordMessageHandler(params: {
|
||||
});
|
||||
didSendReply = true;
|
||||
},
|
||||
onIdle: () => {
|
||||
typingController?.markDispatchIdle();
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
danger(`discord ${info.kind} reply failed: ${String(err)}`),
|
||||
);
|
||||
},
|
||||
onReplyStart: () => sendTyping(message),
|
||||
});
|
||||
|
||||
const { queuedFinal, counts } = await dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions: {
|
||||
onReplyStart: () => sendTyping(message),
|
||||
onTypingController: (typing) => {
|
||||
typingController = typing;
|
||||
},
|
||||
},
|
||||
replyOptions,
|
||||
});
|
||||
typingController?.markDispatchIdle();
|
||||
markDispatchIdle();
|
||||
if (!queuedFinal) {
|
||||
if (
|
||||
isGuildMessage &&
|
||||
|
||||
@@ -189,12 +189,20 @@ describe("monitorSlackProvider tool results", () => {
|
||||
const client = getSlackClient() as {
|
||||
assistant?: { threads?: { setStatus?: ReturnType<typeof vi.fn> } };
|
||||
};
|
||||
expect(client.assistant?.threads?.setStatus).toHaveBeenCalledWith({
|
||||
const setStatus = client.assistant?.threads?.setStatus;
|
||||
expect(setStatus).toHaveBeenCalledTimes(2);
|
||||
expect(setStatus).toHaveBeenNthCalledWith(1, {
|
||||
token: "bot-token",
|
||||
channel_id: "C1",
|
||||
thread_ts: "123",
|
||||
status: "is typing...",
|
||||
});
|
||||
expect(setStatus).toHaveBeenNthCalledWith(2, {
|
||||
token: "bot-token",
|
||||
channel_id: "C1",
|
||||
thread_ts: "123",
|
||||
status: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts channel messages when mentionPatterns match", async () => {
|
||||
|
||||
@@ -19,8 +19,7 @@ import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
} from "../auto-reply/reply/mentions.js";
|
||||
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import type { TypingController } from "../auto-reply/reply/typing.js";
|
||||
import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
@@ -860,15 +859,17 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
// Only thread replies if the incoming message was in a thread.
|
||||
const incomingThreadTs = message.thread_ts;
|
||||
const statusThreadTs = message.thread_ts ?? message.ts;
|
||||
let didSetStatus = false;
|
||||
const onReplyStart = async () => {
|
||||
didSetStatus = true;
|
||||
await setSlackThreadStatus({
|
||||
channelId: message.channel,
|
||||
threadTs: statusThreadTs,
|
||||
status: "is typing...",
|
||||
});
|
||||
};
|
||||
let typingController: TypingController | undefined;
|
||||
const dispatcher = createReplyDispatcher({
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
createReplyDispatcherWithTyping({
|
||||
responsePrefix: cfg.messages?.responsePrefix,
|
||||
deliver: async (payload) => {
|
||||
await deliverReplies({
|
||||
@@ -880,41 +881,36 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
threadTs: incomingThreadTs,
|
||||
});
|
||||
},
|
||||
onIdle: () => {
|
||||
typingController?.markDispatchIdle();
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
danger(`slack ${info.kind} reply failed: ${String(err)}`),
|
||||
);
|
||||
if (didSetStatus) {
|
||||
void setSlackThreadStatus({
|
||||
channelId: message.channel,
|
||||
threadTs: statusThreadTs,
|
||||
status: "",
|
||||
});
|
||||
}
|
||||
},
|
||||
onReplyStart,
|
||||
});
|
||||
|
||||
const { queuedFinal, counts } = await dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions: {
|
||||
onReplyStart,
|
||||
onTypingController: (typing) => {
|
||||
typingController = typing;
|
||||
},
|
||||
},
|
||||
replyOptions,
|
||||
});
|
||||
typingController?.markDispatchIdle();
|
||||
if (!queuedFinal) {
|
||||
markDispatchIdle();
|
||||
if (didSetStatus) {
|
||||
await setSlackThreadStatus({
|
||||
channelId: message.channel,
|
||||
threadTs: statusThreadTs,
|
||||
status: "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!queuedFinal) return;
|
||||
if (shouldLogVerbose()) {
|
||||
const finalCount = counts.final;
|
||||
logVerbose(
|
||||
|
||||
@@ -19,8 +19,7 @@ import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
} from "../auto-reply/reply/mentions.js";
|
||||
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import type { TypingController } from "../auto-reply/reply/typing.js";
|
||||
import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import type { ReplyToMode } from "../config/config.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
@@ -451,8 +450,8 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
let typingController: TypingController | undefined;
|
||||
const dispatcher = createReplyDispatcher({
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
createReplyDispatcherWithTyping({
|
||||
responsePrefix: cfg.messages?.responsePrefix,
|
||||
deliver: async (payload) => {
|
||||
await deliverReplies({
|
||||
@@ -465,28 +464,21 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
textLimit,
|
||||
});
|
||||
},
|
||||
onIdle: () => {
|
||||
typingController?.markDispatchIdle();
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
danger(`telegram ${info.kind} reply failed: ${String(err)}`),
|
||||
);
|
||||
},
|
||||
onReplyStart: sendTyping,
|
||||
});
|
||||
|
||||
const { queuedFinal } = await dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions: {
|
||||
onReplyStart: sendTyping,
|
||||
onTypingController: (typing) => {
|
||||
typingController = typing;
|
||||
},
|
||||
},
|
||||
replyOptions,
|
||||
});
|
||||
typingController?.markDispatchIdle();
|
||||
markDispatchIdle();
|
||||
if (!queuedFinal) return;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
buildMentionRegexes,
|
||||
normalizeMentionText,
|
||||
} from "../auto-reply/reply/mentions.js";
|
||||
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import type { TypingController } from "../auto-reply/reply/typing.js";
|
||||
import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
import { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
@@ -1161,8 +1160,8 @@ export async function monitorWebProvider(
|
||||
const textLimit = resolveTextChunkLimit(cfg, "whatsapp");
|
||||
let didLogHeartbeatStrip = false;
|
||||
let didSendReply = false;
|
||||
let typingController: TypingController | undefined;
|
||||
const dispatcher = createReplyDispatcher({
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
createReplyDispatcherWithTyping({
|
||||
responsePrefix: cfg.messages?.responsePrefix,
|
||||
onHeartbeatStrip: () => {
|
||||
if (!didLogHeartbeatStrip) {
|
||||
@@ -1212,9 +1211,6 @@ export async function monitorWebProvider(
|
||||
}
|
||||
}
|
||||
},
|
||||
onIdle: () => {
|
||||
typingController?.markDispatchIdle();
|
||||
},
|
||||
onError: (err, info) => {
|
||||
const label =
|
||||
info.kind === "tool"
|
||||
@@ -1226,6 +1222,7 @@ export async function monitorWebProvider(
|
||||
`Failed sending web ${label} to ${msg.from ?? conversationId}: ${formatError(err)}`,
|
||||
);
|
||||
},
|
||||
onReplyStart: msg.sendComposing,
|
||||
});
|
||||
|
||||
const { queuedFinal } = await dispatchReplyFromConfig({
|
||||
@@ -1258,14 +1255,9 @@ export async function monitorWebProvider(
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyResolver,
|
||||
replyOptions: {
|
||||
onReplyStart: msg.sendComposing,
|
||||
onTypingController: (typing) => {
|
||||
typingController = typing;
|
||||
},
|
||||
},
|
||||
replyOptions,
|
||||
});
|
||||
typingController?.markDispatchIdle();
|
||||
markDispatchIdle();
|
||||
if (!queuedFinal) {
|
||||
if (shouldClearGroupHistory && didSendReply) {
|
||||
groupHistories.set(route.sessionKey, []);
|
||||
|
||||
Reference in New Issue
Block a user