fix: unify inbound dispatch pipeline
This commit is contained in:
parent
da26954dd0
commit
2e0a835e07
@ -10,6 +10,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Gateway/WebChat: route inbound messages through the unified dispatch pipeline so /new works consistently across WebChat/TUI and channels.
|
||||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||||
|
|||||||
@ -82,7 +82,8 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
// Track payloads sent directly (not via pipeline) during tool flush to avoid duplicates.
|
// Track payloads sent directly (not via pipeline) during tool flush to avoid duplicates.
|
||||||
const directlySentBlockKeys = new Set<string>();
|
const directlySentBlockKeys = new Set<string>();
|
||||||
|
|
||||||
const runId = crypto.randomUUID();
|
const runId = params.opts?.runId ?? crypto.randomUUID();
|
||||||
|
params.opts?.onAgentRunStart?.(runId);
|
||||||
if (params.sessionKey) {
|
if (params.sessionKey) {
|
||||||
registerAgentRunContext(runId, {
|
registerAgentRunContext(runId, {
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
@ -174,6 +175,7 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
|
extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
|
||||||
ownerNumbers: params.followupRun.run.ownerNumbers,
|
ownerNumbers: params.followupRun.run.ownerNumbers,
|
||||||
cliSessionId,
|
cliSessionId,
|
||||||
|
images: params.opts?.images,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
@ -248,6 +250,8 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
bashElevated: params.followupRun.run.bashElevated,
|
bashElevated: params.followupRun.run.bashElevated,
|
||||||
timeoutMs: params.followupRun.run.timeoutMs,
|
timeoutMs: params.followupRun.run.timeoutMs,
|
||||||
runId,
|
runId,
|
||||||
|
images: params.opts?.images,
|
||||||
|
abortSignal: params.opts?.abortSignal,
|
||||||
blockReplyBreak: params.resolvedBlockStreamingBreak,
|
blockReplyBreak: params.resolvedBlockStreamingBreak,
|
||||||
blockReplyChunking: params.blockReplyChunking,
|
blockReplyChunking: params.blockReplyChunking,
|
||||||
onPartialReply: allowPartialStream
|
onPartialReply: allowPartialStream
|
||||||
|
|||||||
@ -1,58 +1,44 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { FinalizedMsgContext } from "../templating.js";
|
import type { FinalizedMsgContext, MsgContext } from "../templating.js";
|
||||||
import type { GetReplyOptions } from "../types.js";
|
import type { GetReplyOptions } from "../types.js";
|
||||||
import type { DispatchFromConfigResult } from "./dispatch-from-config.js";
|
import type { DispatchInboundResult } from "../dispatch.js";
|
||||||
import { dispatchReplyFromConfig } from "./dispatch-from-config.js";
|
|
||||||
import {
|
import {
|
||||||
createReplyDispatcher,
|
dispatchInboundMessageWithBufferedDispatcher,
|
||||||
createReplyDispatcherWithTyping,
|
dispatchInboundMessageWithDispatcher,
|
||||||
type ReplyDispatcherOptions,
|
} from "../dispatch.js";
|
||||||
type ReplyDispatcherWithTypingOptions,
|
import type {
|
||||||
|
ReplyDispatcherOptions,
|
||||||
|
ReplyDispatcherWithTypingOptions,
|
||||||
} from "./reply-dispatcher.js";
|
} from "./reply-dispatcher.js";
|
||||||
|
|
||||||
export async function dispatchReplyWithBufferedBlockDispatcher(params: {
|
export async function dispatchReplyWithBufferedBlockDispatcher(params: {
|
||||||
ctx: FinalizedMsgContext;
|
ctx: MsgContext | FinalizedMsgContext;
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
dispatcherOptions: ReplyDispatcherWithTypingOptions;
|
dispatcherOptions: ReplyDispatcherWithTypingOptions;
|
||||||
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
||||||
replyResolver?: typeof import("../reply.js").getReplyFromConfig;
|
replyResolver?: typeof import("../reply.js").getReplyFromConfig;
|
||||||
}): Promise<DispatchFromConfigResult> {
|
}): Promise<DispatchInboundResult> {
|
||||||
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping(
|
return await dispatchInboundMessageWithBufferedDispatcher({
|
||||||
params.dispatcherOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await dispatchReplyFromConfig({
|
|
||||||
ctx: params.ctx,
|
ctx: params.ctx,
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
dispatcher,
|
dispatcherOptions: params.dispatcherOptions,
|
||||||
replyResolver: params.replyResolver,
|
replyResolver: params.replyResolver,
|
||||||
replyOptions: {
|
replyOptions: params.replyOptions,
|
||||||
...params.replyOptions,
|
|
||||||
...replyOptions,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
markDispatchIdle();
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function dispatchReplyWithDispatcher(params: {
|
export async function dispatchReplyWithDispatcher(params: {
|
||||||
ctx: FinalizedMsgContext;
|
ctx: MsgContext | FinalizedMsgContext;
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
dispatcherOptions: ReplyDispatcherOptions;
|
dispatcherOptions: ReplyDispatcherOptions;
|
||||||
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
||||||
replyResolver?: typeof import("../reply.js").getReplyFromConfig;
|
replyResolver?: typeof import("../reply.js").getReplyFromConfig;
|
||||||
}): Promise<DispatchFromConfigResult> {
|
}): Promise<DispatchInboundResult> {
|
||||||
const dispatcher = createReplyDispatcher(params.dispatcherOptions);
|
return await dispatchInboundMessageWithDispatcher({
|
||||||
|
|
||||||
const result = await dispatchReplyFromConfig({
|
|
||||||
ctx: params.ctx,
|
ctx: params.ctx,
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
dispatcher,
|
dispatcherOptions: params.dispatcherOptions,
|
||||||
replyResolver: params.replyResolver,
|
replyResolver: params.replyResolver,
|
||||||
replyOptions: params.replyOptions,
|
replyOptions: params.replyOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
await dispatcher.waitForIdle();
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
import type { TypingController } from "./reply/typing.js";
|
import type { TypingController } from "./reply/typing.js";
|
||||||
|
|
||||||
export type BlockReplyContext = {
|
export type BlockReplyContext = {
|
||||||
@ -13,6 +14,14 @@ export type ModelSelectedContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type GetReplyOptions = {
|
export type GetReplyOptions = {
|
||||||
|
/** Override run id for agent events (defaults to random UUID). */
|
||||||
|
runId?: string;
|
||||||
|
/** Abort signal for the underlying agent run. */
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
/** Optional inbound images (used for webchat attachments). */
|
||||||
|
images?: ImageContent[];
|
||||||
|
/** Notifies when an agent run actually starts (useful for webchat command handling). */
|
||||||
|
onAgentRunStart?: (runId: string) => void;
|
||||||
onReplyStart?: () => Promise<void> | void;
|
onReplyStart?: () => Promise<void> | void;
|
||||||
onTypingController?: (typing: TypingController) => void;
|
onTypingController?: (typing: TypingController) => void;
|
||||||
isHeartbeat?: boolean;
|
isHeartbeat?: boolean;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
|
||||||
|
|
||||||
const dispatchMock = vi.fn();
|
const dispatchMock = vi.fn();
|
||||||
|
|
||||||
@ -20,15 +21,34 @@ vi.mock("@buape/carbon", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../auto-reply/reply/dispatch-from-config.js", () => ({
|
vi.mock("../auto-reply/dispatch.js", async (importOriginal) => {
|
||||||
dispatchReplyFromConfig: (...args: unknown[]) => dispatchMock(...args),
|
const actual = await importOriginal<typeof import("../auto-reply/dispatch.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
dispatchInboundMessage: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
dispatchInboundMessageWithDispatcher: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
dispatchInboundMessageWithBufferedDispatcher: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dispatchMock.mockReset().mockImplementation(async ({ dispatcher }) => {
|
dispatchMock.mockReset().mockImplementation(async (params) => {
|
||||||
dispatcher.sendToolResult({ text: "tool update" });
|
if ("dispatcher" in params && params.dispatcher) {
|
||||||
dispatcher.sendFinalReply({ text: "final reply" });
|
params.dispatcher.sendToolResult({ text: "tool update" });
|
||||||
return { queuedFinal: true, counts: { tool: 1, block: 0, final: 1 } };
|
params.dispatcher.sendFinalReply({ text: "final reply" });
|
||||||
|
return { queuedFinal: true, counts: { tool: 1, block: 0, final: 1 } };
|
||||||
|
}
|
||||||
|
if ("dispatcherOptions" in params && params.dispatcherOptions) {
|
||||||
|
const { dispatcher, markDispatchIdle } = createReplyDispatcherWithTyping(
|
||||||
|
params.dispatcherOptions,
|
||||||
|
);
|
||||||
|
dispatcher.sendToolResult({ text: "tool update" });
|
||||||
|
dispatcher.sendFinalReply({ text: "final reply" });
|
||||||
|
await dispatcher.waitForIdle();
|
||||||
|
markDispatchIdle();
|
||||||
|
return { queuedFinal: true, counts: dispatcher.getQueuedCounts() };
|
||||||
|
}
|
||||||
|
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -18,9 +18,15 @@ vi.mock("./send.js", () => ({
|
|||||||
reactMock(...args);
|
reactMock(...args);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock("../auto-reply/reply/dispatch-from-config.js", () => ({
|
vi.mock("../auto-reply/dispatch.js", async (importOriginal) => {
|
||||||
dispatchReplyFromConfig: (...args: unknown[]) => dispatchMock(...args),
|
const actual = await importOriginal<typeof import("../auto-reply/dispatch.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
dispatchInboundMessage: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
dispatchInboundMessageWithDispatcher: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
dispatchInboundMessageWithBufferedDispatcher: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
};
|
||||||
|
});
|
||||||
vi.mock("../pairing/pairing-store.js", () => ({
|
vi.mock("../pairing/pairing-store.js", () => ({
|
||||||
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
|
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
|
||||||
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
||||||
@ -41,7 +47,7 @@ beforeEach(() => {
|
|||||||
updateLastRouteMock.mockReset();
|
updateLastRouteMock.mockReset();
|
||||||
dispatchMock.mockReset().mockImplementation(async ({ dispatcher }) => {
|
dispatchMock.mockReset().mockImplementation(async ({ dispatcher }) => {
|
||||||
dispatcher.sendFinalReply({ text: "hi" });
|
dispatcher.sendFinalReply({ text: "hi" });
|
||||||
return { queuedFinal: true, counts: { final: 1 } };
|
return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } };
|
||||||
});
|
});
|
||||||
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
||||||
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||||
|
|||||||
@ -18,9 +18,15 @@ vi.mock("./send.js", () => ({
|
|||||||
reactMock(...args);
|
reactMock(...args);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock("../auto-reply/reply/dispatch-from-config.js", () => ({
|
vi.mock("../auto-reply/dispatch.js", async (importOriginal) => {
|
||||||
dispatchReplyFromConfig: (...args: unknown[]) => dispatchMock(...args),
|
const actual = await importOriginal<typeof import("../auto-reply/dispatch.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
dispatchInboundMessage: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
dispatchInboundMessageWithDispatcher: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
dispatchInboundMessageWithBufferedDispatcher: (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
};
|
||||||
|
});
|
||||||
vi.mock("../pairing/pairing-store.js", () => ({
|
vi.mock("../pairing/pairing-store.js", () => ({
|
||||||
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
|
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
|
||||||
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
||||||
@ -40,7 +46,7 @@ beforeEach(() => {
|
|||||||
updateLastRouteMock.mockReset();
|
updateLastRouteMock.mockReset();
|
||||||
dispatchMock.mockReset().mockImplementation(async ({ dispatcher }) => {
|
dispatchMock.mockReset().mockImplementation(async ({ dispatcher }) => {
|
||||||
dispatcher.sendFinalReply({ text: "hi" });
|
dispatcher.sendFinalReply({ text: "hi" });
|
||||||
return { queuedFinal: true, counts: { final: 1 } };
|
return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } };
|
||||||
});
|
});
|
||||||
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
||||||
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||||
|
|||||||
@ -9,17 +9,24 @@ import { expectInboundContextContract } from "../../../test/helpers/inbound-cont
|
|||||||
|
|
||||||
let capturedCtx: MsgContext | undefined;
|
let capturedCtx: MsgContext | undefined;
|
||||||
|
|
||||||
vi.mock("../../auto-reply/reply/dispatch-from-config.js", () => ({
|
vi.mock("../../auto-reply/dispatch.js", async (importOriginal) => {
|
||||||
dispatchReplyFromConfig: vi.fn(async (params: { ctx: MsgContext }) => {
|
const actual = await importOriginal<typeof import("../../auto-reply/dispatch.js")>();
|
||||||
|
const dispatchInboundMessage = vi.fn(async (params: { ctx: MsgContext }) => {
|
||||||
capturedCtx = params.ctx;
|
capturedCtx = params.ctx;
|
||||||
return { queuedFinal: false, counts: { tool: 0, block: 0 } };
|
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
||||||
}),
|
});
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
dispatchInboundMessage,
|
||||||
|
dispatchInboundMessageWithDispatcher: dispatchInboundMessage,
|
||||||
|
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import { processDiscordMessage } from "./message-handler.process.js";
|
import { processDiscordMessage } from "./message-handler.process.js";
|
||||||
|
|
||||||
describe("discord processDiscordMessage inbound contract", () => {
|
describe("discord processDiscordMessage inbound contract", () => {
|
||||||
it("passes a finalized MsgContext to dispatchReplyFromConfig", async () => {
|
it("passes a finalized MsgContext to dispatchInboundMessage", async () => {
|
||||||
capturedCtx = undefined;
|
capturedCtx = undefined;
|
||||||
|
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-discord-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-discord-"));
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
formatThreadStarterEnvelope,
|
formatThreadStarterEnvelope,
|
||||||
resolveEnvelopeFormatOptions,
|
resolveEnvelopeFormatOptions,
|
||||||
} from "../../auto-reply/envelope.js";
|
} from "../../auto-reply/envelope.js";
|
||||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
|
||||||
import {
|
import {
|
||||||
buildPendingHistoryContextFromMap,
|
buildPendingHistoryContextFromMap,
|
||||||
clearHistoryEntries,
|
clearHistoryEntries,
|
||||||
@ -358,7 +358,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
onReplyStart: () => sendTyping({ client, channelId: typingChannelId }),
|
onReplyStart: () => sendTyping({ client, channelId: typingChannelId }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { queuedFinal, counts } = await dispatchReplyFromConfig({
|
const { queuedFinal, counts } = await dispatchInboundMessage({
|
||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
cfg,
|
cfg,
|
||||||
dispatcher,
|
dispatcher,
|
||||||
|
|||||||
@ -2,30 +2,18 @@ import { randomUUID } from "node:crypto";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { resolveSessionAgentId, resolveAgentWorkspaceDir } from "../../agents/agent-scope.js";
|
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||||
|
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../agents/identity.js";
|
||||||
import { resolveThinkingDefault } from "../../agents/model-selection.js";
|
import { resolveThinkingDefault } from "../../agents/model-selection.js";
|
||||||
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
||||||
import { ensureAgentWorkspace } from "../../agents/workspace.js";
|
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
|
||||||
import { isControlCommandMessage } from "../../auto-reply/command-detection.js";
|
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||||
import { normalizeCommandBody } from "../../auto-reply/commands-registry.js";
|
|
||||||
import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../../auto-reply/envelope.js";
|
|
||||||
import { buildCommandContext, handleCommands } from "../../auto-reply/reply/commands.js";
|
|
||||||
import { parseInlineDirectives } from "../../auto-reply/reply/directive-handling.js";
|
|
||||||
import { defaultGroupActivation } from "../../auto-reply/reply/groups.js";
|
|
||||||
import { resolveContextTokens } from "../../auto-reply/reply/model-selection.js";
|
|
||||||
import { resolveElevatedPermissions } from "../../auto-reply/reply/reply-elevated.js";
|
|
||||||
import {
|
import {
|
||||||
normalizeElevatedLevel,
|
extractShortModelName,
|
||||||
normalizeReasoningLevel,
|
type ResponsePrefixContext,
|
||||||
normalizeThinkLevel,
|
} from "../../auto-reply/reply/response-prefix-template.js";
|
||||||
normalizeVerboseLevel,
|
|
||||||
} from "../../auto-reply/thinking.js";
|
|
||||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||||
import { agentCommand } from "../../commands/agent.js";
|
|
||||||
import { mergeSessionEntry, updateSessionStore } from "../../config/sessions.js";
|
|
||||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
|
||||||
import { isAcpSessionKey } from "../../routing/session-key.js";
|
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
|
||||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||||
import {
|
import {
|
||||||
@ -53,7 +41,144 @@ import {
|
|||||||
} from "../session-utils.js";
|
} from "../session-utils.js";
|
||||||
import { stripEnvelopeFromMessages } from "../chat-sanitize.js";
|
import { stripEnvelopeFromMessages } from "../chat-sanitize.js";
|
||||||
import { formatForLog } from "../ws-log.js";
|
import { formatForLog } from "../ws-log.js";
|
||||||
import type { GatewayRequestHandlers } from "./types.js";
|
import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js";
|
||||||
|
|
||||||
|
type TranscriptAppendResult = {
|
||||||
|
ok: boolean;
|
||||||
|
messageId?: string;
|
||||||
|
message?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveTranscriptPath(params: {
|
||||||
|
sessionId: string;
|
||||||
|
storePath: string | undefined;
|
||||||
|
sessionFile?: string;
|
||||||
|
}): string | null {
|
||||||
|
const { sessionId, storePath, sessionFile } = params;
|
||||||
|
if (sessionFile) return sessionFile;
|
||||||
|
if (!storePath) return null;
|
||||||
|
return path.join(path.dirname(storePath), `${sessionId}.jsonl`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureTranscriptFile(params: { transcriptPath: string; sessionId: string }): {
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
} {
|
||||||
|
if (fs.existsSync(params.transcriptPath)) return { ok: true };
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(params.transcriptPath), { recursive: true });
|
||||||
|
const header = {
|
||||||
|
type: "session",
|
||||||
|
version: CURRENT_SESSION_VERSION,
|
||||||
|
id: params.sessionId,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
cwd: process.cwd(),
|
||||||
|
};
|
||||||
|
fs.writeFileSync(params.transcriptPath, `${JSON.stringify(header)}\n`, "utf-8");
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendAssistantTranscriptMessage(params: {
|
||||||
|
message: string;
|
||||||
|
label?: string;
|
||||||
|
sessionId: string;
|
||||||
|
storePath: string | undefined;
|
||||||
|
sessionFile?: string;
|
||||||
|
createIfMissing?: boolean;
|
||||||
|
}): TranscriptAppendResult {
|
||||||
|
const transcriptPath = resolveTranscriptPath({
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
storePath: params.storePath,
|
||||||
|
sessionFile: params.sessionFile,
|
||||||
|
});
|
||||||
|
if (!transcriptPath) {
|
||||||
|
return { ok: false, error: "transcript path not resolved" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(transcriptPath)) {
|
||||||
|
if (!params.createIfMissing) {
|
||||||
|
return { ok: false, error: "transcript file not found" };
|
||||||
|
}
|
||||||
|
const ensured = ensureTranscriptFile({
|
||||||
|
transcriptPath,
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
});
|
||||||
|
if (!ensured.ok) {
|
||||||
|
return { ok: false, error: ensured.error ?? "failed to create transcript file" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const messageId = randomUUID().slice(0, 8);
|
||||||
|
const labelPrefix = params.label ? `[${params.label}]\n\n` : "";
|
||||||
|
const messageBody: Record<string, unknown> = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: `${labelPrefix}${params.message}` }],
|
||||||
|
timestamp: now,
|
||||||
|
stopReason: "injected",
|
||||||
|
usage: { input: 0, output: 0, totalTokens: 0 },
|
||||||
|
};
|
||||||
|
const transcriptEntry = {
|
||||||
|
type: "message",
|
||||||
|
id: messageId,
|
||||||
|
timestamp: new Date(now).toISOString(),
|
||||||
|
message: messageBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(transcriptPath, `${JSON.stringify(transcriptEntry)}\n`, "utf-8");
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, messageId, message: transcriptEntry.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextChatSeq(context: { agentRunSeq: Map<string, number> }, runId: string) {
|
||||||
|
const next = (context.agentRunSeq.get(runId) ?? 0) + 1;
|
||||||
|
context.agentRunSeq.set(runId, next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastChatFinal(params: {
|
||||||
|
context: Pick<GatewayRequestContext, "broadcast" | "nodeSendToSession" | "agentRunSeq">;
|
||||||
|
runId: string;
|
||||||
|
sessionKey: string;
|
||||||
|
message?: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
const seq = nextChatSeq({ agentRunSeq: params.context.agentRunSeq }, params.runId);
|
||||||
|
const payload = {
|
||||||
|
runId: params.runId,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
seq,
|
||||||
|
state: "final" as const,
|
||||||
|
message: params.message,
|
||||||
|
};
|
||||||
|
params.context.broadcast("chat", payload);
|
||||||
|
params.context.nodeSendToSession(params.sessionKey, "chat", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastChatError(params: {
|
||||||
|
context: Pick<GatewayRequestContext, "broadcast" | "nodeSendToSession" | "agentRunSeq">;
|
||||||
|
runId: string;
|
||||||
|
sessionKey: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}) {
|
||||||
|
const seq = nextChatSeq({ agentRunSeq: params.context.agentRunSeq }, params.runId);
|
||||||
|
const payload = {
|
||||||
|
runId: params.runId,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
seq,
|
||||||
|
state: "error" as const,
|
||||||
|
errorMessage: params.errorMessage,
|
||||||
|
};
|
||||||
|
params.context.broadcast("chat", payload);
|
||||||
|
params.context.nodeSendToSession(params.sessionKey, "chat", payload);
|
||||||
|
}
|
||||||
|
|
||||||
export const chatHandlers: GatewayRequestHandlers = {
|
export const chatHandlers: GatewayRequestHandlers = {
|
||||||
"chat.history": async ({ params, respond, context }) => {
|
"chat.history": async ({ params, respond, context }) => {
|
||||||
@ -168,7 +293,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
runIds: res.aborted ? [runId] : [],
|
runIds: res.aborted ? [runId] : [],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
"chat.send": async ({ params, respond, context }) => {
|
"chat.send": async ({ params, respond, context, client }) => {
|
||||||
if (!validateChatSendParams(params)) {
|
if (!validateChatSendParams(params)) {
|
||||||
respond(
|
respond(
|
||||||
false,
|
false,
|
||||||
@ -228,20 +353,13 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { cfg, storePath, entry, canonicalKey, store } = loadSessionEntry(p.sessionKey);
|
const { cfg, entry } = loadSessionEntry(p.sessionKey);
|
||||||
const timeoutMs = resolveAgentTimeoutMs({
|
const timeoutMs = resolveAgentTimeoutMs({
|
||||||
cfg,
|
cfg,
|
||||||
overrideMs: p.timeoutMs,
|
overrideMs: p.timeoutMs,
|
||||||
});
|
});
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const sessionId = entry?.sessionId ?? randomUUID();
|
|
||||||
const sessionEntry = mergeSessionEntry(entry, {
|
|
||||||
sessionId,
|
|
||||||
updatedAt: now,
|
|
||||||
});
|
|
||||||
store[canonicalKey] = sessionEntry;
|
|
||||||
const clientRunId = p.idempotencyKey;
|
const clientRunId = p.idempotencyKey;
|
||||||
registerAgentRunContext(clientRunId, { sessionKey: p.sessionKey });
|
|
||||||
|
|
||||||
const sendPolicy = resolveSendPolicy({
|
const sendPolicy = resolveSendPolicy({
|
||||||
cfg,
|
cfg,
|
||||||
@ -298,21 +416,11 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
context.chatAbortControllers.set(clientRunId, {
|
context.chatAbortControllers.set(clientRunId, {
|
||||||
controller: abortController,
|
controller: abortController,
|
||||||
sessionId,
|
sessionId: entry?.sessionId ?? clientRunId,
|
||||||
sessionKey: p.sessionKey,
|
sessionKey: p.sessionKey,
|
||||||
startedAtMs: now,
|
startedAtMs: now,
|
||||||
expiresAtMs: resolveChatRunExpiresAtMs({ now, timeoutMs }),
|
expiresAtMs: resolveChatRunExpiresAtMs({ now, timeoutMs }),
|
||||||
});
|
});
|
||||||
context.addChatRun(clientRunId, {
|
|
||||||
sessionKey: p.sessionKey,
|
|
||||||
clientRunId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (storePath) {
|
|
||||||
await updateSessionStore(storePath, (store) => {
|
|
||||||
store[canonicalKey] = sessionEntry;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ackPayload = {
|
const ackPayload = {
|
||||||
runId: clientRunId,
|
runId: clientRunId,
|
||||||
@ -320,170 +428,116 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
};
|
};
|
||||||
respond(true, ackPayload, undefined, { runId: clientRunId });
|
respond(true, ackPayload, undefined, { runId: clientRunId });
|
||||||
|
|
||||||
if (isControlCommandMessage(parsedMessage, cfg)) {
|
const trimmedMessage = parsedMessage.trim();
|
||||||
try {
|
const injectThinking = Boolean(
|
||||||
const isFastTestEnv = process.env.CLAWDBOT_TEST_FAST === "1";
|
p.thinking && trimmedMessage && !trimmedMessage.startsWith("/"),
|
||||||
const agentId = resolveSessionAgentId({ sessionKey: p.sessionKey, config: cfg });
|
);
|
||||||
const agentCfg = cfg.agents?.defaults;
|
const commandBody = injectThinking ? `/think ${p.thinking} ${parsedMessage}` : parsedMessage;
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
const clientInfo = client?.connect?.client;
|
||||||
const workspace = await ensureAgentWorkspace({
|
const ctx: MsgContext = {
|
||||||
dir: workspaceDir,
|
Body: parsedMessage,
|
||||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap && !isFastTestEnv,
|
BodyForAgent: parsedMessage,
|
||||||
});
|
BodyForCommands: commandBody,
|
||||||
const ctx: MsgContext = {
|
RawBody: parsedMessage,
|
||||||
Body: parsedMessage,
|
CommandBody: commandBody,
|
||||||
CommandBody: parsedMessage,
|
SessionKey: p.sessionKey,
|
||||||
BodyForCommands: parsedMessage,
|
Provider: INTERNAL_MESSAGE_CHANNEL,
|
||||||
CommandSource: "text",
|
Surface: INTERNAL_MESSAGE_CHANNEL,
|
||||||
CommandAuthorized: true,
|
OriginatingChannel: INTERNAL_MESSAGE_CHANNEL,
|
||||||
Provider: INTERNAL_MESSAGE_CHANNEL,
|
ChatType: "direct",
|
||||||
Surface: "tui",
|
CommandAuthorized: true,
|
||||||
From: p.sessionKey,
|
MessageSid: clientRunId,
|
||||||
To: INTERNAL_MESSAGE_CHANNEL,
|
SenderId: clientInfo?.id,
|
||||||
SessionKey: p.sessionKey,
|
SenderName: clientInfo?.displayName,
|
||||||
ChatType: "direct",
|
SenderUsername: clientInfo?.displayName,
|
||||||
};
|
};
|
||||||
const command = buildCommandContext({
|
|
||||||
ctx,
|
const agentId = resolveSessionAgentId({
|
||||||
cfg,
|
sessionKey: p.sessionKey,
|
||||||
agentId,
|
config: cfg,
|
||||||
sessionKey: p.sessionKey,
|
});
|
||||||
isGroup: false,
|
let prefixContext: ResponsePrefixContext = {
|
||||||
triggerBodyNormalized: normalizeCommandBody(parsedMessage),
|
identityName: resolveIdentityName(cfg, agentId),
|
||||||
commandAuthorized: true,
|
};
|
||||||
});
|
const finalReplyParts: string[] = [];
|
||||||
const directives = parseInlineDirectives(parsedMessage);
|
const dispatcher = createReplyDispatcher({
|
||||||
const { provider, model } = resolveSessionModelRef(cfg, sessionEntry);
|
responsePrefix: resolveEffectiveMessagesConfig(cfg, agentId).responsePrefix,
|
||||||
const contextTokens = resolveContextTokens({ agentCfg, model });
|
responsePrefixContextProvider: () => prefixContext,
|
||||||
const resolveDefaultThinkingLevel = async () => {
|
onError: (err) => {
|
||||||
const configured = agentCfg?.thinkingDefault;
|
context.logGateway.warn(`webchat dispatch failed: ${formatForLog(err)}`);
|
||||||
if (configured) return configured;
|
},
|
||||||
const catalog = await context.loadGatewayModelCatalog();
|
deliver: async (payload, info) => {
|
||||||
return resolveThinkingDefault({ cfg, provider, model, catalog });
|
if (info.kind !== "final") return;
|
||||||
};
|
const text = payload.text?.trim() ?? "";
|
||||||
const resolvedThinkLevel =
|
if (!text) return;
|
||||||
normalizeThinkLevel(sessionEntry?.thinkingLevel ?? agentCfg?.thinkingDefault) ??
|
finalReplyParts.push(text);
|
||||||
(await resolveDefaultThinkingLevel());
|
},
|
||||||
const resolvedVerboseLevel =
|
});
|
||||||
normalizeVerboseLevel(sessionEntry?.verboseLevel ?? agentCfg?.verboseDefault) ?? "off";
|
|
||||||
const resolvedReasoningLevel =
|
let agentRunStarted = false;
|
||||||
normalizeReasoningLevel(sessionEntry?.reasoningLevel) ?? "off";
|
void dispatchInboundMessage({
|
||||||
const resolvedElevatedLevel = normalizeElevatedLevel(
|
ctx,
|
||||||
sessionEntry?.elevatedLevel ?? agentCfg?.elevatedDefault,
|
cfg,
|
||||||
);
|
dispatcher,
|
||||||
const elevated = resolveElevatedPermissions({
|
replyOptions: {
|
||||||
cfg,
|
runId: clientRunId,
|
||||||
agentId,
|
abortSignal: abortController.signal,
|
||||||
ctx,
|
images: parsedImages.length > 0 ? parsedImages : undefined,
|
||||||
provider: INTERNAL_MESSAGE_CHANNEL,
|
disableBlockStreaming: true,
|
||||||
});
|
onAgentRunStart: () => {
|
||||||
const commandResult = await handleCommands({
|
agentRunStarted = true;
|
||||||
ctx,
|
},
|
||||||
cfg,
|
onModelSelected: (ctx) => {
|
||||||
command,
|
prefixContext.provider = ctx.provider;
|
||||||
agentId,
|
prefixContext.model = extractShortModelName(ctx.model);
|
||||||
directives,
|
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
|
||||||
elevated,
|
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
|
||||||
sessionEntry,
|
},
|
||||||
previousSessionEntry: entry,
|
},
|
||||||
sessionStore: store,
|
})
|
||||||
sessionKey: p.sessionKey,
|
.then(() => {
|
||||||
storePath,
|
if (!agentRunStarted) {
|
||||||
sessionScope: (cfg.session?.scope ?? "per-sender") as "per-sender" | "global",
|
const combinedReply = finalReplyParts
|
||||||
workspaceDir: workspace.dir,
|
.map((part) => part.trim())
|
||||||
defaultGroupActivation: () => defaultGroupActivation(true),
|
.filter(Boolean)
|
||||||
resolvedThinkLevel,
|
.join("\n\n")
|
||||||
resolvedVerboseLevel,
|
.trim();
|
||||||
resolvedReasoningLevel,
|
let message: Record<string, unknown> | undefined;
|
||||||
resolvedElevatedLevel,
|
if (combinedReply) {
|
||||||
resolveDefaultThinkingLevel,
|
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(
|
||||||
provider,
|
p.sessionKey,
|
||||||
model,
|
);
|
||||||
contextTokens,
|
const sessionId = latestEntry?.sessionId ?? entry?.sessionId ?? clientRunId;
|
||||||
isGroup: false,
|
const appended = appendAssistantTranscriptMessage({
|
||||||
});
|
message: combinedReply,
|
||||||
if (!commandResult.shouldContinue) {
|
sessionId,
|
||||||
const text = commandResult.reply?.text ?? "";
|
storePath: latestStorePath,
|
||||||
const message = {
|
sessionFile: latestEntry?.sessionFile,
|
||||||
role: "assistant",
|
createIfMissing: true,
|
||||||
content: text.trim() ? [{ type: "text", text }] : [],
|
});
|
||||||
timestamp: Date.now(),
|
if (appended.ok) {
|
||||||
command: true,
|
message = appended.message;
|
||||||
};
|
} else {
|
||||||
const payload = {
|
context.logGateway.warn(
|
||||||
|
`webchat transcript append failed: ${appended.error ?? "unknown error"}`,
|
||||||
|
);
|
||||||
|
const now = Date.now();
|
||||||
|
message = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: combinedReply }],
|
||||||
|
timestamp: now,
|
||||||
|
stopReason: "injected",
|
||||||
|
usage: { input: 0, output: 0, totalTokens: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
broadcastChatFinal({
|
||||||
|
context,
|
||||||
runId: clientRunId,
|
runId: clientRunId,
|
||||||
sessionKey: p.sessionKey,
|
sessionKey: p.sessionKey,
|
||||||
seq: 0,
|
|
||||||
state: "final" as const,
|
|
||||||
message,
|
message,
|
||||||
};
|
|
||||||
context.broadcast("chat", payload);
|
|
||||||
context.nodeSendToSession(p.sessionKey, "chat", payload);
|
|
||||||
context.dedupe.set(`chat:${clientRunId}`, {
|
|
||||||
ts: Date.now(),
|
|
||||||
ok: true,
|
|
||||||
payload: { runId: clientRunId, status: "ok" as const },
|
|
||||||
});
|
});
|
||||||
context.chatAbortControllers.delete(clientRunId);
|
|
||||||
context.removeChatRun(clientRunId, clientRunId, p.sessionKey);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
const payload = {
|
|
||||||
runId: clientRunId,
|
|
||||||
sessionKey: p.sessionKey,
|
|
||||||
seq: 0,
|
|
||||||
state: "error" as const,
|
|
||||||
errorMessage: formatForLog(err),
|
|
||||||
};
|
|
||||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
|
||||||
context.broadcast("chat", payload);
|
|
||||||
context.nodeSendToSession(p.sessionKey, "chat", payload);
|
|
||||||
context.dedupe.set(`chat:${clientRunId}`, {
|
|
||||||
ts: Date.now(),
|
|
||||||
ok: false,
|
|
||||||
payload: {
|
|
||||||
runId: clientRunId,
|
|
||||||
status: "error" as const,
|
|
||||||
summary: String(err),
|
|
||||||
},
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
context.chatAbortControllers.delete(clientRunId);
|
|
||||||
context.removeChatRun(clientRunId, clientRunId, p.sessionKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
|
|
||||||
const envelopedMessage = formatInboundEnvelope({
|
|
||||||
channel: "WebChat",
|
|
||||||
from: p.sessionKey,
|
|
||||||
timestamp: now,
|
|
||||||
body: parsedMessage,
|
|
||||||
chatType: "direct",
|
|
||||||
previousTimestamp: entry?.updatedAt,
|
|
||||||
envelope: envelopeOptions,
|
|
||||||
});
|
|
||||||
const lane = isAcpSessionKey(p.sessionKey) ? p.sessionKey : undefined;
|
|
||||||
void agentCommand(
|
|
||||||
{
|
|
||||||
message: envelopedMessage,
|
|
||||||
images: parsedImages.length > 0 ? parsedImages : undefined,
|
|
||||||
sessionId,
|
|
||||||
sessionKey: p.sessionKey,
|
|
||||||
runId: clientRunId,
|
|
||||||
thinking: p.thinking,
|
|
||||||
deliver: p.deliver,
|
|
||||||
timeout: Math.ceil(timeoutMs / 1000).toString(),
|
|
||||||
messageChannel: INTERNAL_MESSAGE_CHANNEL,
|
|
||||||
abortSignal: abortController.signal,
|
|
||||||
lane,
|
|
||||||
},
|
|
||||||
defaultRuntime,
|
|
||||||
context.deps,
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
context.dedupe.set(`chat:${clientRunId}`, {
|
context.dedupe.set(`chat:${clientRunId}`, {
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -502,6 +556,12 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
},
|
},
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
broadcastChatError({
|
||||||
|
context,
|
||||||
|
runId: clientRunId,
|
||||||
|
sessionKey: p.sessionKey,
|
||||||
|
errorMessage: String(err),
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
context.chatAbortControllers.delete(clientRunId);
|
context.chatAbortControllers.delete(clientRunId);
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import path from "node:path";
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||||
import {
|
import {
|
||||||
agentCommand,
|
|
||||||
connectOk,
|
connectOk,
|
||||||
|
getReplyFromConfig,
|
||||||
installGatewayTestHooks,
|
installGatewayTestHooks,
|
||||||
onceMessage,
|
onceMessage,
|
||||||
rpcReq,
|
rpcReq,
|
||||||
@ -47,7 +47,7 @@ describe("gateway server chat", () => {
|
|||||||
async () => {
|
async () => {
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
const { server, ws } = await startServerWithClient();
|
const { server, ws } = await startServerWithClient();
|
||||||
const spy = vi.mocked(agentCommand);
|
const spy = vi.mocked(getReplyFromConfig);
|
||||||
const resetSpy = () => {
|
const resetSpy = () => {
|
||||||
spy.mockReset();
|
spy.mockReset();
|
||||||
spy.mockResolvedValue(undefined);
|
spy.mockResolvedValue(undefined);
|
||||||
@ -122,8 +122,9 @@ describe("gateway server chat", () => {
|
|||||||
let abortInFlight: Promise<unknown> | undefined;
|
let abortInFlight: Promise<unknown> | undefined;
|
||||||
try {
|
try {
|
||||||
const callsBefore = spy.mock.calls.length;
|
const callsBefore = spy.mock.calls.length;
|
||||||
spy.mockImplementationOnce(async (opts) => {
|
spy.mockImplementationOnce(async (_ctx, opts) => {
|
||||||
const signal = (opts as { abortSignal?: AbortSignal }).abortSignal;
|
opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-1");
|
||||||
|
const signal = opts?.abortSignal;
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
if (!signal) return resolve();
|
if (!signal) return resolve();
|
||||||
if (signal.aborted) return resolve();
|
if (signal.aborted) return resolve();
|
||||||
@ -155,7 +156,7 @@ describe("gateway server chat", () => {
|
|||||||
const tick = () => {
|
const tick = () => {
|
||||||
if (spy.mock.calls.length > callsBefore) return resolve();
|
if (spy.mock.calls.length > callsBefore) return resolve();
|
||||||
if (Date.now() > deadline)
|
if (Date.now() > deadline)
|
||||||
return reject(new Error("timeout waiting for agentCommand"));
|
return reject(new Error("timeout waiting for getReplyFromConfig"));
|
||||||
setTimeout(tick, 5);
|
setTimeout(tick, 5);
|
||||||
};
|
};
|
||||||
tick();
|
tick();
|
||||||
@ -177,8 +178,9 @@ describe("gateway server chat", () => {
|
|||||||
sessionStoreSaveDelayMs.value = 120;
|
sessionStoreSaveDelayMs.value = 120;
|
||||||
resetSpy();
|
resetSpy();
|
||||||
try {
|
try {
|
||||||
spy.mockImplementationOnce(async (opts) => {
|
spy.mockImplementationOnce(async (_ctx, opts) => {
|
||||||
const signal = (opts as { abortSignal?: AbortSignal }).abortSignal;
|
opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-save-1");
|
||||||
|
const signal = opts?.abortSignal;
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
if (!signal) return resolve();
|
if (!signal) return resolve();
|
||||||
if (signal.aborted) return resolve();
|
if (signal.aborted) return resolve();
|
||||||
@ -215,8 +217,9 @@ describe("gateway server chat", () => {
|
|||||||
await writeStore({ main: { sessionId: "sess-main", updatedAt: Date.now() } });
|
await writeStore({ main: { sessionId: "sess-main", updatedAt: Date.now() } });
|
||||||
resetSpy();
|
resetSpy();
|
||||||
const callsBeforeStop = spy.mock.calls.length;
|
const callsBeforeStop = spy.mock.calls.length;
|
||||||
spy.mockImplementationOnce(async (opts) => {
|
spy.mockImplementationOnce(async (_ctx, opts) => {
|
||||||
const signal = (opts as { abortSignal?: AbortSignal }).abortSignal;
|
opts?.onAgentRunStart?.(opts.runId ?? "idem-stop-1");
|
||||||
|
const signal = opts?.abortSignal;
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
if (!signal) return resolve();
|
if (!signal) return resolve();
|
||||||
if (signal.aborted) return resolve();
|
if (signal.aborted) return resolve();
|
||||||
@ -261,7 +264,8 @@ describe("gateway server chat", () => {
|
|||||||
const runDone = new Promise<void>((resolve) => {
|
const runDone = new Promise<void>((resolve) => {
|
||||||
resolveRun = resolve;
|
resolveRun = resolve;
|
||||||
});
|
});
|
||||||
spy.mockImplementationOnce(async () => {
|
spy.mockImplementationOnce(async (_ctx, opts) => {
|
||||||
|
opts?.onAgentRunStart?.(opts.runId ?? "idem-status-1");
|
||||||
await runDone;
|
await runDone;
|
||||||
});
|
});
|
||||||
const started = await rpcReq<{ runId?: string; status?: string }>(ws, "chat.send", {
|
const started = await rpcReq<{ runId?: string; status?: string }>(ws, "chat.send", {
|
||||||
@ -294,8 +298,9 @@ describe("gateway server chat", () => {
|
|||||||
}
|
}
|
||||||
expect(completed).toBe(true);
|
expect(completed).toBe(true);
|
||||||
resetSpy();
|
resetSpy();
|
||||||
spy.mockImplementationOnce(async (opts) => {
|
spy.mockImplementationOnce(async (_ctx, opts) => {
|
||||||
const signal = (opts as { abortSignal?: AbortSignal }).abortSignal;
|
opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-all-1");
|
||||||
|
const signal = opts?.abortSignal;
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
if (!signal) return resolve();
|
if (!signal) return resolve();
|
||||||
if (signal.aborted) return resolve();
|
if (signal.aborted) return resolve();
|
||||||
@ -359,9 +364,9 @@ describe("gateway server chat", () => {
|
|||||||
const agentStartedP = new Promise<void>((resolve) => {
|
const agentStartedP = new Promise<void>((resolve) => {
|
||||||
agentStartedResolve = resolve;
|
agentStartedResolve = resolve;
|
||||||
});
|
});
|
||||||
spy.mockImplementationOnce(async (opts) => {
|
spy.mockImplementationOnce(async (_ctx, opts) => {
|
||||||
agentStartedResolve?.();
|
agentStartedResolve?.();
|
||||||
const signal = (opts as { abortSignal?: AbortSignal }).abortSignal;
|
const signal = opts?.abortSignal;
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
if (!signal) return resolve();
|
if (!signal) return resolve();
|
||||||
if (signal.aborted) return resolve();
|
if (signal.aborted) return resolve();
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import { WebSocket } from "ws";
|
|||||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||||
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
||||||
import {
|
import {
|
||||||
agentCommand,
|
|
||||||
connectOk,
|
connectOk,
|
||||||
|
getReplyFromConfig,
|
||||||
installGatewayTestHooks,
|
installGatewayTestHooks,
|
||||||
onceMessage,
|
onceMessage,
|
||||||
rpcReq,
|
rpcReq,
|
||||||
@ -71,7 +71,7 @@ describe("gateway server chat", () => {
|
|||||||
webchatWs.close();
|
webchatWs.close();
|
||||||
webchatWs = undefined;
|
webchatWs = undefined;
|
||||||
|
|
||||||
const spy = vi.mocked(agentCommand);
|
const spy = vi.mocked(getReplyFromConfig);
|
||||||
spy.mockClear();
|
spy.mockClear();
|
||||||
testState.agentConfig = { timeoutSeconds: 123 };
|
testState.agentConfig = { timeoutSeconds: 123 };
|
||||||
const callsBeforeTimeout = spy.mock.calls.length;
|
const callsBeforeTimeout = spy.mock.calls.length;
|
||||||
@ -83,8 +83,8 @@ describe("gateway server chat", () => {
|
|||||||
expect(timeoutRes.ok).toBe(true);
|
expect(timeoutRes.ok).toBe(true);
|
||||||
|
|
||||||
await waitFor(() => spy.mock.calls.length > callsBeforeTimeout);
|
await waitFor(() => spy.mock.calls.length > callsBeforeTimeout);
|
||||||
const timeoutCall = spy.mock.calls.at(-1)?.[0] as { timeout?: string } | undefined;
|
const timeoutCall = spy.mock.calls.at(-1)?.[1] as { runId?: string } | undefined;
|
||||||
expect(timeoutCall?.timeout).toBe("123");
|
expect(timeoutCall?.runId).toBe("idem-timeout-1");
|
||||||
testState.agentConfig = undefined;
|
testState.agentConfig = undefined;
|
||||||
|
|
||||||
spy.mockClear();
|
spy.mockClear();
|
||||||
@ -97,8 +97,8 @@ describe("gateway server chat", () => {
|
|||||||
expect(sessionRes.ok).toBe(true);
|
expect(sessionRes.ok).toBe(true);
|
||||||
|
|
||||||
await waitFor(() => spy.mock.calls.length > callsBeforeSession);
|
await waitFor(() => spy.mock.calls.length > callsBeforeSession);
|
||||||
const sessionCall = spy.mock.calls.at(-1)?.[0] as { sessionKey?: string } | undefined;
|
const sessionCall = spy.mock.calls.at(-1)?.[0] as { SessionKey?: string } | undefined;
|
||||||
expect(sessionCall?.sessionKey).toBe("agent:main:subagent:abc");
|
expect(sessionCall?.SessionKey).toBe("agent:main:subagent:abc");
|
||||||
|
|
||||||
const sendPolicyDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const sendPolicyDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
tempDirs.push(sendPolicyDir);
|
tempDirs.push(sendPolicyDir);
|
||||||
@ -203,10 +203,10 @@ describe("gateway server chat", () => {
|
|||||||
expect(imgRes.payload?.runId).toBeDefined();
|
expect(imgRes.payload?.runId).toBeDefined();
|
||||||
|
|
||||||
await waitFor(() => spy.mock.calls.length > callsBeforeImage, 8000);
|
await waitFor(() => spy.mock.calls.length > callsBeforeImage, 8000);
|
||||||
const imgCall = spy.mock.calls.at(-1)?.[0] as
|
const imgOpts = spy.mock.calls.at(-1)?.[1] as
|
||||||
| { images?: Array<{ type: string; data: string; mimeType: string }> }
|
| { images?: Array<{ type: string; data: string; mimeType: string }> }
|
||||||
| undefined;
|
| undefined;
|
||||||
expect(imgCall?.images).toEqual([{ type: "image", data: pngB64, mimeType: "image/png" }]);
|
expect(imgOpts?.images).toEqual([{ type: "image", data: pngB64, mimeType: "image/png" }]);
|
||||||
|
|
||||||
const historyDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
const historyDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||||
tempDirs.push(historyDir);
|
tempDirs.push(historyDir);
|
||||||
|
|||||||
@ -166,6 +166,7 @@ const hoisted = vi.hoisted(() => ({
|
|||||||
waitCalls: [] as string[],
|
waitCalls: [] as string[],
|
||||||
waitResults: new Map<string, boolean>(),
|
waitResults: new Map<string, boolean>(),
|
||||||
},
|
},
|
||||||
|
getReplyFromConfig: vi.fn().mockResolvedValue(undefined),
|
||||||
sendWhatsAppMock: vi.fn().mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }),
|
sendWhatsAppMock: vi.fn().mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -197,6 +198,7 @@ export const testTailnetIPv4 = hoisted.testTailnetIPv4;
|
|||||||
export const piSdkMock = hoisted.piSdkMock;
|
export const piSdkMock = hoisted.piSdkMock;
|
||||||
export const cronIsolatedRun = hoisted.cronIsolatedRun;
|
export const cronIsolatedRun = hoisted.cronIsolatedRun;
|
||||||
export const agentCommand = hoisted.agentCommand;
|
export const agentCommand = hoisted.agentCommand;
|
||||||
|
export const getReplyFromConfig = hoisted.getReplyFromConfig;
|
||||||
|
|
||||||
export const testState = {
|
export const testState = {
|
||||||
agentConfig: undefined as Record<string, unknown> | undefined,
|
agentConfig: undefined as Record<string, unknown> | undefined,
|
||||||
@ -540,6 +542,9 @@ vi.mock("../channels/web/index.js", async () => {
|
|||||||
vi.mock("../commands/agent.js", () => ({
|
vi.mock("../commands/agent.js", () => ({
|
||||||
agentCommand,
|
agentCommand,
|
||||||
}));
|
}));
|
||||||
|
vi.mock("../auto-reply/reply.js", () => ({
|
||||||
|
getReplyFromConfig,
|
||||||
|
}));
|
||||||
vi.mock("../cli/deps.js", async () => {
|
vi.mock("../cli/deps.js", async () => {
|
||||||
const actual = await vi.importActual<typeof import("../cli/deps.js")>("../cli/deps.js");
|
const actual = await vi.importActual<typeof import("../cli/deps.js")>("../cli/deps.js");
|
||||||
const base = actual.createDefaultDeps();
|
const base = actual.createDefaultDeps();
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
createInboundDebouncer,
|
createInboundDebouncer,
|
||||||
resolveInboundDebounceMs,
|
resolveInboundDebounceMs,
|
||||||
} from "../../auto-reply/inbound-debounce.js";
|
} from "../../auto-reply/inbound-debounce.js";
|
||||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
|
||||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||||
import {
|
import {
|
||||||
buildPendingHistoryContextFromMap,
|
buildPendingHistoryContextFromMap,
|
||||||
@ -565,7 +565,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { queuedFinal } = await dispatchReplyFromConfig({
|
const { queuedFinal } = await dispatchInboundMessage({
|
||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
cfg,
|
cfg,
|
||||||
dispatcher,
|
dispatcher,
|
||||||
|
|||||||
@ -12,21 +12,21 @@ describe("signal event handler sender prefix", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dispatchMock.mockReset().mockImplementation(async ({ dispatcher, ctx }) => {
|
dispatchMock.mockReset().mockImplementation(async ({ dispatcher, ctx }) => {
|
||||||
dispatcher.sendFinalReply({ text: "ok" });
|
dispatcher.sendFinalReply({ text: "ok" });
|
||||||
return { queuedFinal: true, counts: { final: 1 }, ctx };
|
return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 }, ctx };
|
||||||
});
|
});
|
||||||
readAllowFromMock.mockReset().mockResolvedValue([]);
|
readAllowFromMock.mockReset().mockResolvedValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefixes group bodies with sender label", async () => {
|
it("prefixes group bodies with sender label", async () => {
|
||||||
let capturedBody = "";
|
let capturedBody = "";
|
||||||
const dispatchModule = await import("../auto-reply/reply/dispatch-from-config.js");
|
const dispatchModule = await import("../auto-reply/dispatch.js");
|
||||||
vi.spyOn(dispatchModule, "dispatchReplyFromConfig").mockImplementation(
|
vi.spyOn(dispatchModule, "dispatchInboundMessage").mockImplementation(
|
||||||
async (...args: unknown[]) => dispatchMock(...args),
|
async (...args: unknown[]) => dispatchMock(...args),
|
||||||
);
|
);
|
||||||
dispatchMock.mockImplementationOnce(async ({ dispatcher, ctx }) => {
|
dispatchMock.mockImplementationOnce(async ({ dispatcher, ctx }) => {
|
||||||
capturedBody = ctx.Body ?? "";
|
capturedBody = ctx.Body ?? "";
|
||||||
dispatcher.sendFinalReply({ text: "ok" });
|
dispatcher.sendFinalReply({ text: "ok" });
|
||||||
return { queuedFinal: true, counts: { final: 1 } };
|
return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } };
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createSignalEventHandler } = await import("./monitor/event-handler.js");
|
const { createSignalEventHandler } = await import("./monitor/event-handler.js");
|
||||||
|
|||||||
@ -9,14 +9,21 @@ vi.mock("./send.js", () => ({
|
|||||||
sendReadReceiptSignal: (...args: unknown[]) => sendReadReceiptMock(...args),
|
sendReadReceiptSignal: (...args: unknown[]) => sendReadReceiptMock(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../auto-reply/reply/dispatch-from-config.js", () => ({
|
vi.mock("../auto-reply/dispatch.js", async (importOriginal) => {
|
||||||
dispatchReplyFromConfig: vi.fn(
|
const actual = await importOriginal<typeof import("../auto-reply/dispatch.js")>();
|
||||||
|
const dispatchInboundMessage = vi.fn(
|
||||||
async (params: { replyOptions?: { onReplyStart?: () => void } }) => {
|
async (params: { replyOptions?: { onReplyStart?: () => void } }) => {
|
||||||
await Promise.resolve(params.replyOptions?.onReplyStart?.());
|
await Promise.resolve(params.replyOptions?.onReplyStart?.());
|
||||||
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
dispatchInboundMessage,
|
||||||
|
dispatchInboundMessageWithDispatcher: dispatchInboundMessage,
|
||||||
|
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../pairing/pairing-store.js", () => ({
|
vi.mock("../pairing/pairing-store.js", () => ({
|
||||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||||
@ -25,11 +32,13 @@ vi.mock("../pairing/pairing-store.js", () => ({
|
|||||||
|
|
||||||
describe("signal event handler typing + read receipts", () => {
|
describe("signal event handler typing + read receipts", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
sendTypingMock.mockReset().mockResolvedValue(true);
|
sendTypingMock.mockReset().mockResolvedValue(true);
|
||||||
sendReadReceiptMock.mockReset().mockResolvedValue(true);
|
sendReadReceiptMock.mockReset().mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends typing + read receipt for allowed DMs", async () => {
|
it("sends typing + read receipt for allowed DMs", async () => {
|
||||||
|
vi.resetModules();
|
||||||
const { createSignalEventHandler } = await import("./monitor/event-handler.js");
|
const { createSignalEventHandler } = await import("./monitor/event-handler.js");
|
||||||
const handler = createSignalEventHandler({
|
const handler = createSignalEventHandler({
|
||||||
runtime: { log: () => {}, error: () => {} } as any,
|
runtime: { log: () => {}, error: () => {} } as any,
|
||||||
|
|||||||
@ -5,17 +5,24 @@ import { expectInboundContextContract } from "../../../test/helpers/inbound-cont
|
|||||||
|
|
||||||
let capturedCtx: MsgContext | undefined;
|
let capturedCtx: MsgContext | undefined;
|
||||||
|
|
||||||
vi.mock("../../auto-reply/reply/dispatch-from-config.js", () => ({
|
vi.mock("../../auto-reply/dispatch.js", async (importOriginal) => {
|
||||||
dispatchReplyFromConfig: vi.fn(async (params: { ctx: MsgContext }) => {
|
const actual = await importOriginal<typeof import("../../auto-reply/dispatch.js")>();
|
||||||
|
const dispatchInboundMessage = vi.fn(async (params: { ctx: MsgContext }) => {
|
||||||
capturedCtx = params.ctx;
|
capturedCtx = params.ctx;
|
||||||
return { queuedFinal: false, counts: { tool: 0, block: 0 } };
|
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
||||||
}),
|
});
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
dispatchInboundMessage,
|
||||||
|
dispatchInboundMessageWithDispatcher: dispatchInboundMessage,
|
||||||
|
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import { createSignalEventHandler } from "./event-handler.js";
|
import { createSignalEventHandler } from "./event-handler.js";
|
||||||
|
|
||||||
describe("signal createSignalEventHandler inbound contract", () => {
|
describe("signal createSignalEventHandler inbound contract", () => {
|
||||||
it("passes a finalized MsgContext to dispatchReplyFromConfig", async () => {
|
it("passes a finalized MsgContext to dispatchInboundMessage", async () => {
|
||||||
capturedCtx = undefined;
|
capturedCtx = undefined;
|
||||||
|
|
||||||
const handler = createSignalEventHandler({
|
const handler = createSignalEventHandler({
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
createInboundDebouncer,
|
createInboundDebouncer,
|
||||||
resolveInboundDebounceMs,
|
resolveInboundDebounceMs,
|
||||||
} from "../../auto-reply/inbound-debounce.js";
|
} from "../../auto-reply/inbound-debounce.js";
|
||||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
|
||||||
import {
|
import {
|
||||||
buildPendingHistoryContextFromMap,
|
buildPendingHistoryContextFromMap,
|
||||||
clearHistoryEntries,
|
clearHistoryEntries,
|
||||||
@ -225,7 +225,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
|||||||
onReplyStart,
|
onReplyStart,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { queuedFinal } = await dispatchReplyFromConfig({
|
const { queuedFinal } = await dispatchInboundMessage({
|
||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
cfg: deps.cfg,
|
cfg: deps.cfg,
|
||||||
dispatcher,
|
dispatcher,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
extractShortModelName,
|
extractShortModelName,
|
||||||
type ResponsePrefixContext,
|
type ResponsePrefixContext,
|
||||||
} from "../../../auto-reply/reply/response-prefix-template.js";
|
} from "../../../auto-reply/reply/response-prefix-template.js";
|
||||||
import { dispatchReplyFromConfig } from "../../../auto-reply/reply/dispatch-from-config.js";
|
import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js";
|
||||||
import { clearHistoryEntries } from "../../../auto-reply/reply/history.js";
|
import { clearHistoryEntries } from "../../../auto-reply/reply/history.js";
|
||||||
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
|
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
|
||||||
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
|
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
|
||||||
@ -104,7 +104,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
|||||||
onReplyStart,
|
onReplyStart,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { queuedFinal, counts } = await dispatchReplyFromConfig({
|
const { queuedFinal, counts } = await dispatchInboundMessage({
|
||||||
ctx: prepared.ctxPayload,
|
ctx: prepared.ctxPayload,
|
||||||
cfg,
|
cfg,
|
||||||
dispatcher,
|
dispatcher,
|
||||||
|
|||||||
@ -4,6 +4,10 @@ import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelo
|
|||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -23,6 +27,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -22,6 +26,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -22,6 +26,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -22,6 +26,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -6,6 +6,9 @@ let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
|||||||
let getTelegramSequentialKey: typeof import("./bot.js").getTelegramSequentialKey;
|
let getTelegramSequentialKey: typeof import("./bot.js").getTelegramSequentialKey;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-throttler-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -25,6 +28,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -22,6 +26,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -22,6 +26,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -22,6 +26,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -6,6 +6,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-reply-threading-${Math.random()
|
||||||
|
.toString(16)
|
||||||
|
.slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
const { loadWebMedia } = vi.hoisted(() => ({
|
const { loadWebMedia } = vi.hoisted(() => ({
|
||||||
loadWebMedia: vi.fn(),
|
loadWebMedia: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -25,6 +31,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
@ -21,6 +21,10 @@ vi.mock("../auto-reply/skill-commands.js", () => ({
|
|||||||
listSkillCommandsForAgents,
|
listSkillCommandsForAgents,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
|
sessionStorePath: `/tmp/clawdbot-telegram-bot-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
}));
|
||||||
|
|
||||||
function resolveSkillCommands(config: Parameters<typeof listNativeCommandSpecsForConfig>[0]) {
|
function resolveSkillCommands(config: Parameters<typeof listNativeCommandSpecsForConfig>[0]) {
|
||||||
return listSkillCommandsForAgents({ cfg: config });
|
return listSkillCommandsForAgents({ cfg: config });
|
||||||
}
|
}
|
||||||
@ -44,6 +48,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
const { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({
|
||||||
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
readTelegramAllowFromStore: vi.fn(async () => [] as string[]),
|
||||||
upsertTelegramPairingRequest: vi.fn(async () => ({
|
upsertTelegramPairingRequest: vi.fn(async () => ({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user