Skip to content

Commit 54648a9

Browse files
committed
refactor: centralize followup origin routing helpers
1 parent 9b53102 commit 54648a9

9 files changed

Lines changed: 183 additions & 79 deletions

src/auto-reply/reply/agent-runner-payloads.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js";
66
import type { ReplyPayload } from "../types.js";
77
import { formatBunFetchSocketError, isBunFetchSocketError } from "./agent-runner-utils.js";
88
import { createBlockReplyPayloadKey, type BlockReplyPipeline } from "./block-reply-pipeline.js";
9+
import {
10+
resolveOriginAccountId,
11+
resolveOriginMessageProvider,
12+
resolveOriginMessageTo,
13+
} from "./origin-routing.js";
914
import { normalizeReplyPayloadDirectives } from "./reply-delivery.js";
1015
import {
1116
applyReplyThreading,
@@ -87,10 +92,17 @@ export function buildReplyPayloads(params: {
8792
const messagingToolSentTexts = params.messagingToolSentTexts ?? [];
8893
const messagingToolSentTargets = params.messagingToolSentTargets ?? [];
8994
const suppressMessagingToolReplies = shouldSuppressMessagingToolReplies({
90-
messageProvider: params.originatingChannel ?? params.messageProvider,
95+
messageProvider: resolveOriginMessageProvider({
96+
originatingChannel: params.originatingChannel,
97+
provider: params.messageProvider,
98+
}),
9199
messagingToolSentTargets,
92-
originatingTo: params.originatingTo,
93-
accountId: params.accountId,
100+
originatingTo: resolveOriginMessageTo({
101+
originatingTo: params.originatingTo,
102+
}),
103+
accountId: resolveOriginAccountId({
104+
originatingAccountId: params.accountId,
105+
}),
94106
});
95107
// Only dedupe against messaging tool sends for the same origin target.
96108
// Cross-target sends (for example posting to another channel) must not

src/auto-reply/reply/agent-runner-utils.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { isReasoningTagProvider } from "../../utils/provider-utils.js";
99
import { estimateUsageCost, formatTokenCount, formatUsd } from "../../utils/usage-format.js";
1010
import type { TemplateContext } from "../templating.js";
1111
import type { ReplyPayload } from "../types.js";
12+
import { resolveOriginMessageProvider, resolveOriginMessageTo } from "./origin-routing.js";
1213
import type { FollowupRun } from "./queue.js";
1314

1415
const BUN_FETCH_SOCKET_ERROR_RE = /socket connection was closed unexpectedly/i;
@@ -196,12 +197,15 @@ export function buildEmbeddedContextFromTemplate(params: {
196197
sessionId: params.run.sessionId,
197198
sessionKey: params.run.sessionKey,
198199
agentId: params.run.agentId,
199-
messageProvider:
200-
params.sessionCtx.OriginatingChannel?.trim().toLowerCase() ||
201-
params.sessionCtx.Provider?.trim().toLowerCase() ||
202-
undefined,
200+
messageProvider: resolveOriginMessageProvider({
201+
originatingChannel: params.sessionCtx.OriginatingChannel,
202+
provider: params.sessionCtx.Provider,
203+
}),
203204
agentAccountId: params.sessionCtx.AccountId,
204-
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
205+
messageTo: resolveOriginMessageTo({
206+
originatingTo: params.sessionCtx.OriginatingTo,
207+
to: params.sessionCtx.To,
208+
}),
205209
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
206210
// Provider threading context for tool auto-injection
207211
...buildThreadingToolContext({

src/auto-reply/reply/agent-runner.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { appendUsageLine, formatResponseUsageLine } from "./agent-runner-utils.j
4343
import { createAudioAsVoiceBuffer, createBlockReplyPipeline } from "./block-reply-pipeline.js";
4444
import { resolveBlockStreamingCoalescing } from "./block-streaming.js";
4545
import { createFollowupRunner } from "./followup-runner.js";
46+
import { resolveOriginMessageProvider, resolveOriginMessageTo } from "./origin-routing.js";
4647
import {
4748
auditPostCompactionReads,
4849
extractReadPaths,
@@ -179,11 +180,10 @@ export async function runReplyAgent(params: {
179180
const pendingToolTasks = new Set<Promise<void>>();
180181
const blockReplyTimeoutMs = opts?.blockReplyTimeoutMs ?? BLOCK_REPLY_SEND_TIMEOUT_MS;
181182

182-
const replyToChannel =
183-
sessionCtx.OriginatingChannel ??
184-
((sessionCtx.Surface ?? sessionCtx.Provider)?.toLowerCase() as
185-
| OriginatingChannelType
186-
| undefined);
183+
const replyToChannel = resolveOriginMessageProvider({
184+
originatingChannel: sessionCtx.OriginatingChannel,
185+
provider: sessionCtx.Surface ?? sessionCtx.Provider,
186+
}) as OriginatingChannelType | undefined;
187187
const replyToMode = resolveReplyToMode(
188188
followupRun.run.config,
189189
replyToChannel,
@@ -515,7 +515,10 @@ export async function runReplyAgent(params: {
515515
messagingToolSentMediaUrls: runResult.messagingToolSentMediaUrls,
516516
messagingToolSentTargets: runResult.messagingToolSentTargets,
517517
originatingChannel: sessionCtx.OriginatingChannel,
518-
originatingTo: sessionCtx.OriginatingTo ?? sessionCtx.To,
518+
originatingTo: resolveOriginMessageTo({
519+
originatingTo: sessionCtx.OriginatingTo,
520+
to: sessionCtx.To,
521+
}),
519522
accountId: sessionCtx.AccountId,
520523
});
521524
const { replyPayloads } = payloadResult;

src/auto-reply/reply/followup-runner.test.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ const baseQueuedRun = (messageProvider = "whatsapp"): FollowupRun =>
6363
},
6464
}) as FollowupRun;
6565

66+
function createQueuedRun(
67+
overrides: Partial<FollowupRun> & { run?: Partial<FollowupRun["run"]> } = {},
68+
): FollowupRun {
69+
const base = baseQueuedRun();
70+
return {
71+
...base,
72+
...overrides,
73+
run: {
74+
...base.run,
75+
...overrides.run,
76+
},
77+
};
78+
}
79+
6680
function mockCompactionRun(params: {
6781
willRetry: boolean;
6882
result: {
@@ -114,32 +128,11 @@ describe("createFollowupRunner compaction", () => {
114128
defaultModel: "anthropic/claude-opus-4-5",
115129
});
116130

117-
const queued = {
118-
prompt: "hello",
119-
summaryLine: "hello",
120-
enqueuedAt: Date.now(),
131+
const queued = createQueuedRun({
121132
run: {
122-
sessionId: "session",
123-
sessionKey: "main",
124-
messageProvider: "whatsapp",
125-
sessionFile: "/tmp/session.jsonl",
126-
workspaceDir: "/tmp",
127-
config: {},
128-
skillsSnapshot: {},
129-
provider: "anthropic",
130-
model: "claude",
131-
thinkLevel: "low",
132133
verboseLevel: "on",
133-
elevatedLevel: "off",
134-
bashElevated: {
135-
enabled: false,
136-
allowed: false,
137-
defaultLevel: "off",
138-
},
139-
timeoutMs: 1_000,
140-
blockReplyBreak: "message_end",
141134
},
142-
} as FollowupRun;
135+
});
143136

144137
await runner(queued);
145138

@@ -411,7 +404,7 @@ describe("createFollowupRunner agentDir forwarding", () => {
411404
defaultModel: "anthropic/claude-opus-4-5",
412405
});
413406
const agentDir = path.join("/tmp", "agent-dir");
414-
const queued = baseQueuedRun();
407+
const queued = createQueuedRun();
415408
await runner({
416409
...queued,
417410
run: {

src/auto-reply/reply/followup-runner.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import type { OriginatingChannelType } from "../templating.js";
1414
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
1515
import type { GetReplyOptions, ReplyPayload } from "../types.js";
1616
import { resolveRunAuthProfile } from "./agent-runner-utils.js";
17+
import {
18+
resolveOriginAccountId,
19+
resolveOriginMessageProvider,
20+
resolveOriginMessageTo,
21+
} from "./origin-routing.js";
1722
import type { FollowupRun } from "./queue.js";
1823
import {
1924
applyReplyThreading,
@@ -231,9 +236,10 @@ export function createFollowupRunner(params: {
231236
}
232237
return [{ ...payload, text: stripped.text }];
233238
});
234-
const replyToChannel =
235-
queued.originatingChannel ??
236-
(queued.run.messageProvider?.toLowerCase() as OriginatingChannelType | undefined);
239+
const replyToChannel = resolveOriginMessageProvider({
240+
originatingChannel: queued.originatingChannel,
241+
provider: queued.run.messageProvider,
242+
}) as OriginatingChannelType | undefined;
237243
const replyToMode = resolveReplyToMode(
238244
queued.run.config,
239245
replyToChannel,
@@ -256,10 +262,18 @@ export function createFollowupRunner(params: {
256262
sentMediaUrls: runResult.messagingToolSentMediaUrls ?? [],
257263
});
258264
const suppressMessagingToolReplies = shouldSuppressMessagingToolReplies({
259-
messageProvider: queued.originatingChannel ?? queued.run.messageProvider,
265+
messageProvider: resolveOriginMessageProvider({
266+
originatingChannel: queued.originatingChannel,
267+
provider: queued.run.messageProvider,
268+
}),
260269
messagingToolSentTargets: runResult.messagingToolSentTargets,
261-
originatingTo: queued.originatingTo,
262-
accountId: queued.originatingAccountId ?? queued.run.agentAccountId,
270+
originatingTo: resolveOriginMessageTo({
271+
originatingTo: queued.originatingTo,
272+
}),
273+
accountId: resolveOriginAccountId({
274+
originatingAccountId: queued.originatingAccountId,
275+
accountId: queued.run.agentAccountId,
276+
}),
263277
});
264278
const finalPayloads = suppressMessagingToolReplies ? [] : mediaFilteredPayloads;
265279

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import type { InlineDirectives } from "./directive-handling.js";
4040
import { buildGroupChatContext, buildGroupIntro } from "./groups.js";
4141
import { buildInboundMetaSystemPrompt, buildInboundUserContextPrefix } from "./inbound-meta.js";
4242
import type { createModelSelectionState } from "./model-selection.js";
43+
import { resolveOriginMessageProvider } from "./origin-routing.js";
4344
import { resolveQueueSettings } from "./queue.js";
4445
import { routeReply } from "./route-reply.js";
4546
import { BARE_SESSION_RESET_PROMPT } from "./session-reset-prompt.js";
@@ -460,10 +461,10 @@ export async function runPreparedReply(
460461
agentDir,
461462
sessionId: sessionIdFinal,
462463
sessionKey,
463-
messageProvider:
464-
sessionCtx.OriginatingChannel?.trim().toLowerCase() ||
465-
sessionCtx.Provider?.trim().toLowerCase() ||
466-
undefined,
464+
messageProvider: resolveOriginMessageProvider({
465+
originatingChannel: sessionCtx.OriginatingChannel,
466+
provider: sessionCtx.Provider,
467+
}),
467468
agentAccountId: sessionCtx.AccountId,
468469
groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined,
469470
groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
resolveOriginAccountId,
4+
resolveOriginMessageProvider,
5+
resolveOriginMessageTo,
6+
} from "./origin-routing.js";
7+
8+
describe("origin-routing helpers", () => {
9+
it("prefers originating channel over provider for message provider", () => {
10+
const provider = resolveOriginMessageProvider({
11+
originatingChannel: "Telegram",
12+
provider: "heartbeat",
13+
});
14+
15+
expect(provider).toBe("telegram");
16+
});
17+
18+
it("falls back to provider when originating channel is missing", () => {
19+
const provider = resolveOriginMessageProvider({
20+
provider: " Slack ",
21+
});
22+
23+
expect(provider).toBe("slack");
24+
});
25+
26+
it("prefers originating destination over fallback destination", () => {
27+
const to = resolveOriginMessageTo({
28+
originatingTo: "channel:C1",
29+
to: "channel:C2",
30+
});
31+
32+
expect(to).toBe("channel:C1");
33+
});
34+
35+
it("prefers originating account over fallback account", () => {
36+
const accountId = resolveOriginAccountId({
37+
originatingAccountId: "work",
38+
accountId: "personal",
39+
});
40+
41+
expect(accountId).toBe("work");
42+
});
43+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { OriginatingChannelType } from "../templating.js";
2+
3+
function normalizeProviderValue(value?: string): string | undefined {
4+
const normalized = value?.trim().toLowerCase();
5+
return normalized || undefined;
6+
}
7+
8+
export function resolveOriginMessageProvider(params: {
9+
originatingChannel?: OriginatingChannelType;
10+
provider?: string;
11+
}): string | undefined {
12+
return (
13+
normalizeProviderValue(params.originatingChannel) ?? normalizeProviderValue(params.provider)
14+
);
15+
}
16+
17+
export function resolveOriginMessageTo(params: {
18+
originatingTo?: string;
19+
to?: string;
20+
}): string | undefined {
21+
return params.originatingTo ?? params.to;
22+
}
23+
24+
export function resolveOriginAccountId(params: {
25+
originatingAccountId?: string;
26+
accountId?: string;
27+
}): string | undefined {
28+
return params.originatingAccountId ?? params.accountId;
29+
}

0 commit comments

Comments
 (0)