Skip to content

Commit 10dcd57

Browse files
committed
perf: keep queue and group parsing pure
1 parent 2cfd145 commit 10dcd57

9 files changed

Lines changed: 85 additions & 54 deletions

File tree

src/auto-reply/reply/get-reply-run.media-only.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ vi.mock("./inbound-meta.js", () => ({
8383
buildInboundUserContextPrefix: vi.fn().mockReturnValue(""),
8484
}));
8585

86-
vi.mock("./queue/settings.js", () => ({
86+
vi.mock("./queue/settings-runtime.js", () => ({
8787
resolveQueueSettings: vi.fn().mockReturnValue({ mode: "followup" }),
8888
}));
8989

@@ -345,7 +345,7 @@ describe("runPreparedReply media-only handling", () => {
345345
expect(getActiveReplyRunCount()).toBe(activeBefore);
346346
});
347347
it("waits for the previous active run to clear before registering a new reply operation", async () => {
348-
const queueSettings = await import("./queue/settings.js");
348+
const queueSettings = await import("./queue/settings-runtime.js");
349349
vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" });
350350

351351
const result = await runPreparedReply(
@@ -359,7 +359,7 @@ describe("runPreparedReply media-only handling", () => {
359359
expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce();
360360
});
361361
it("interrupts embedded-only active runs even without a reply operation", async () => {
362-
const queueSettings = await import("./queue/settings.js");
362+
const queueSettings = await import("./queue/settings-runtime.js");
363363
vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" });
364364
const embeddedAbort = vi.fn();
365365
const embeddedHandle = {
@@ -389,7 +389,7 @@ describe("runPreparedReply media-only handling", () => {
389389
it("rechecks same-session ownership after async prep before registering a new reply operation", async () => {
390390
const { resolveSessionAuthProfileOverride } =
391391
await import("../../agents/auth-profiles/session-override.js");
392-
const queueSettings = await import("./queue/settings.js");
392+
const queueSettings = await import("./queue/settings-runtime.js");
393393

394394
let resolveAuth!: () => void;
395395
const authPromise = new Promise<void>((resolve) => {
@@ -430,7 +430,7 @@ describe("runPreparedReply media-only handling", () => {
430430
it("re-resolves auth profile after waiting for a prior run", async () => {
431431
const { resolveSessionAuthProfileOverride } =
432432
await import("../../agents/auth-profiles/session-override.js");
433-
const queueSettings = await import("./queue/settings.js");
433+
const queueSettings = await import("./queue/settings-runtime.js");
434434
const sessionStore: Record<string, SessionEntry> = {
435435
"session-key": {
436436
sessionId: "session-auth-profile",
@@ -477,7 +477,7 @@ describe("runPreparedReply media-only handling", () => {
477477
it("re-resolves same-session ownership after session-id rotation during async prep", async () => {
478478
const { resolveSessionAuthProfileOverride } =
479479
await import("../../agents/auth-profiles/session-override.js");
480-
const queueSettings = await import("./queue/settings.js");
480+
const queueSettings = await import("./queue/settings-runtime.js");
481481

482482
let resolveAuth!: () => void;
483483
const authPromise = new Promise<void>((resolve) => {
@@ -532,7 +532,7 @@ describe("runPreparedReply media-only handling", () => {
532532
expect(call?.followupRun.run.sessionId).toBe("session-after-rotation");
533533
});
534534
it("continues when the original owner clears before an unrelated run appears", async () => {
535-
const queueSettings = await import("./queue/settings.js");
535+
const queueSettings = await import("./queue/settings-runtime.js");
536536
vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" });
537537
const previousRun = createReplyOperation({
538538
sessionId: "session-before-wait",
@@ -565,7 +565,7 @@ describe("runPreparedReply media-only handling", () => {
565565
nextRun.complete();
566566
});
567567
it("re-drains system events after waiting behind an active run", async () => {
568-
const queueSettings = await import("./queue/settings.js");
568+
const queueSettings = await import("./queue/settings-runtime.js");
569569
vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" });
570570
vi.mocked(drainFormattedSystemEvents)
571571
.mockResolvedValueOnce("System: [t] Initial event.")

src/auto-reply/reply/get-reply-run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import type { createModelSelectionState } from "./model-selection.js";
4242
import { resolveOriginMessageProvider } from "./origin-routing.js";
4343
import { buildReplyPromptBodies } from "./prompt-prelude.js";
4444
import { resolveActiveRunQueueAction } from "./queue-policy.js";
45-
import { resolveQueueSettings } from "./queue/settings.js";
45+
import { resolveQueueSettings } from "./queue/settings-runtime.js";
4646
import { buildBareSessionResetPrompt } from "./session-reset-prompt.js";
4747
import { drainFormattedSystemEvents } from "./session-system-events.js";
4848
import { resolveTypingMode } from "./typing-mode.js";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { normalizeOptionalString } from "../../shared/string-coerce.js";
2+
3+
export function extractSimpleExplicitGroupId(raw: string | undefined | null): string | undefined {
4+
const trimmed = normalizeOptionalString(raw) ?? "";
5+
if (!trimmed) {
6+
return undefined;
7+
}
8+
const parts = trimmed.split(":").filter(Boolean);
9+
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
10+
const joined = parts.slice(2).join(":");
11+
return joined.replace(/:topic:.*$/, "") || undefined;
12+
}
13+
if (parts.length >= 2 && (parts[0] === "group" || parts[0] === "channel")) {
14+
const joined = parts.slice(1).join(":");
15+
return joined.replace(/:topic:.*$/, "") || undefined;
16+
}
17+
if (parts.length >= 2 && parts[0] === "whatsapp") {
18+
const joined = parts
19+
.slice(1)
20+
.join(":")
21+
.replace(/:topic:.*$/, "");
22+
if (/@g\.us$/i.test(joined)) {
23+
return joined || undefined;
24+
}
25+
}
26+
return undefined;
27+
}
Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,50 @@
11
import { describe, expect, it } from "vitest";
2-
import { extractExplicitGroupId } from "./group-id.js";
2+
import { extractSimpleExplicitGroupId } from "./group-id-simple.js";
33

4-
describe("extractExplicitGroupId", () => {
4+
describe("extractSimpleExplicitGroupId", () => {
55
it("returns undefined for empty/null input", () => {
6-
expect(extractExplicitGroupId(undefined)).toBeUndefined();
7-
expect(extractExplicitGroupId(null)).toBeUndefined();
8-
expect(extractExplicitGroupId("")).toBeUndefined();
9-
expect(extractExplicitGroupId(" ")).toBeUndefined();
6+
expect(extractSimpleExplicitGroupId(undefined)).toBeUndefined();
7+
expect(extractSimpleExplicitGroupId(null)).toBeUndefined();
8+
expect(extractSimpleExplicitGroupId("")).toBeUndefined();
9+
expect(extractSimpleExplicitGroupId(" ")).toBeUndefined();
1010
});
1111

1212
it("extracts group ID from telegram group format", () => {
13-
expect(extractExplicitGroupId("telegram:group:-1003776849159")).toBe("-1003776849159");
13+
expect(extractSimpleExplicitGroupId("telegram:group:-1003776849159")).toBe("-1003776849159");
1414
});
1515

1616
it("extracts group ID from telegram forum topic format, stripping topic suffix", () => {
17-
expect(extractExplicitGroupId("telegram:group:-1003776849159:topic:1264")).toBe(
17+
expect(extractSimpleExplicitGroupId("telegram:group:-1003776849159:topic:1264")).toBe(
1818
"-1003776849159",
1919
);
2020
});
2121

2222
it("extracts group ID from channel format", () => {
23-
expect(extractExplicitGroupId("telegram:channel:-1001234567890")).toBe("-1001234567890");
23+
expect(extractSimpleExplicitGroupId("telegram:channel:-1001234567890")).toBe("-1001234567890");
2424
});
2525

2626
it("extracts group ID from channel format with topic", () => {
27-
expect(extractExplicitGroupId("telegram:channel:-1001234567890:topic:42")).toBe(
27+
expect(extractSimpleExplicitGroupId("telegram:channel:-1001234567890:topic:42")).toBe(
2828
"-1001234567890",
2929
);
3030
});
3131

3232
it("extracts group ID from bare group: prefix", () => {
33-
expect(extractExplicitGroupId("group:-1003776849159")).toBe("-1003776849159");
33+
expect(extractSimpleExplicitGroupId("group:-1003776849159")).toBe("-1003776849159");
3434
});
3535

3636
it("extracts group ID from bare group: prefix with topic", () => {
37-
expect(extractExplicitGroupId("group:-1003776849159:topic:999")).toBe("-1003776849159");
37+
expect(extractSimpleExplicitGroupId("group:-1003776849159:topic:999")).toBe("-1003776849159");
3838
});
3939

4040
it("extracts WhatsApp group ID", () => {
41-
expect(extractExplicitGroupId("whatsapp:120363123456789@g.us")).toBe("120363123456789@g.us");
41+
expect(extractSimpleExplicitGroupId("whatsapp:120363123456789@g.us")).toBe(
42+
"120363123456789@g.us",
43+
);
4244
});
4345

4446
it("returns undefined for unrecognized formats", () => {
45-
expect(extractExplicitGroupId("user:12345")).toBeUndefined();
46-
expect(extractExplicitGroupId("just-a-string")).toBeUndefined();
47+
expect(extractSimpleExplicitGroupId("user:12345")).toBeUndefined();
48+
expect(extractSimpleExplicitGroupId("just-a-string")).toBeUndefined();
4749
});
4850
});

src/auto-reply/reply/group-id.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,22 @@ import {
44
normalizeOptionalLowercaseString,
55
normalizeOptionalString,
66
} from "../../shared/string-coerce.js";
7+
import { extractSimpleExplicitGroupId } from "./group-id-simple.js";
8+
9+
export { extractSimpleExplicitGroupId };
710

811
export function extractExplicitGroupId(raw: string | undefined | null): string | undefined {
912
const trimmed = normalizeOptionalString(raw) ?? "";
1013
if (!trimmed) {
1114
return undefined;
1215
}
13-
const parts = trimmed.split(":").filter(Boolean);
14-
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
15-
const joined = parts.slice(2).join(":");
16-
return joined.replace(/:topic:.*$/, "") || undefined;
17-
}
18-
if (parts.length >= 2 && (parts[0] === "group" || parts[0] === "channel")) {
19-
const joined = parts.slice(1).join(":");
20-
return joined.replace(/:topic:.*$/, "") || undefined;
21-
}
22-
if (parts.length >= 2 && parts[0] === "whatsapp") {
23-
const joined = parts
24-
.slice(1)
25-
.join(":")
26-
.replace(/:topic:.*$/, "");
27-
if (/@g\.us$/i.test(joined)) {
28-
return joined || undefined;
29-
}
16+
const simple = extractSimpleExplicitGroupId(trimmed);
17+
if (simple) {
18+
return simple;
3019
}
20+
const firstPart = trimmed.split(":").find(Boolean);
3121
const channelId =
32-
normalizeChannelId(parts[0] ?? "") ?? normalizeOptionalLowercaseString(parts[0]);
22+
normalizeChannelId(firstPart ?? "") ?? normalizeOptionalLowercaseString(firstPart);
3323
const messaging = channelId
3424
? (getLoadedChannelPlugin(channelId)?.messaging ??
3525
getBundledChannelPlugin(channelId)?.messaging)

src/auto-reply/reply/queue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export {
77
getFollowupQueueDepth,
88
resetRecentQueuedMessageIdDedupe,
99
} from "./queue/enqueue.js";
10-
export { resolveQueueSettings } from "./queue/settings.js";
10+
export { resolveQueueSettings } from "./queue/settings-runtime.js";
1111
export { clearFollowupQueue, refreshQueuedFollowupSession } from "./queue/state.js";
1212
export type {
1313
FollowupRun,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { getChannelPlugin } from "../../../channels/plugins/index.js";
2+
import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js";
3+
import { resolveQueueSettings as resolveQueueSettingsCore } from "./settings.js";
4+
import type { QueueSettings, ResolveQueueSettingsParams } from "./types.js";
5+
6+
function resolvePluginDebounce(channelKey: string | undefined): number | undefined {
7+
if (!channelKey) {
8+
return undefined;
9+
}
10+
const plugin = getChannelPlugin(channelKey);
11+
const value = plugin?.defaults?.queue?.debounceMs;
12+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined;
13+
}
14+
15+
export function resolveQueueSettings(params: ResolveQueueSettingsParams): QueueSettings {
16+
const channelKey = normalizeOptionalLowercaseString(params.channel);
17+
return resolveQueueSettingsCore({
18+
...params,
19+
pluginDebounceMs: params.pluginDebounceMs ?? resolvePluginDebounce(channelKey),
20+
});
21+
}

src/auto-reply/reply/queue/settings.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { getChannelPlugin } from "../../../channels/plugins/index.js";
21
import type { InboundDebounceByProvider } from "../../../config/types.messages.js";
32
import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js";
43
import { normalizeQueueDropPolicy, normalizeQueueMode } from "./normalize.js";
@@ -21,15 +20,6 @@ function resolveChannelDebounce(
2120
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined;
2221
}
2322

24-
function resolvePluginDebounce(channelKey: string | undefined): number | undefined {
25-
if (!channelKey) {
26-
return undefined;
27-
}
28-
const plugin = getChannelPlugin(channelKey);
29-
const value = plugin?.defaults?.queue?.debounceMs;
30-
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined;
31-
}
32-
3323
export function resolveQueueSettings(params: ResolveQueueSettingsParams): QueueSettings {
3424
const channelKey = normalizeOptionalLowercaseString(params.channel);
3525
const queueCfg = params.cfg.messages?.queue;
@@ -47,7 +37,7 @@ export function resolveQueueSettings(params: ResolveQueueSettingsParams): QueueS
4737
params.inlineOptions?.debounceMs ??
4838
params.sessionEntry?.queueDebounceMs ??
4939
resolveChannelDebounce(queueCfg?.debounceMsByChannel, channelKey) ??
50-
resolvePluginDebounce(channelKey) ??
40+
params.pluginDebounceMs ??
5141
queueCfg?.debounceMs ??
5242
DEFAULT_QUEUE_DEBOUNCE_MS;
5343
const capRaw =

src/auto-reply/reply/queue/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,5 @@ export type ResolveQueueSettingsParams = {
9292
sessionEntry?: SessionEntry;
9393
inlineMode?: QueueMode;
9494
inlineOptions?: Partial<QueueSettings>;
95+
pluginDebounceMs?: number;
9596
};

0 commit comments

Comments
 (0)