fix: preserve subagent thread routing (#1241)

Thanks @gnarco.

Co-authored-by: gnarco <gnarco@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-20 17:22:07 +00:00
parent ae1c6f4313
commit 02ca148583
32 changed files with 195 additions and 32 deletions

View File

@@ -210,6 +210,8 @@ export async function runAgentTurnWithFallback(params: {
sessionKey: params.sessionKey,
messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
agentAccountId: params.sessionCtx.AccountId,
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
// Provider threading context for tool auto-injection
...buildThreadingToolContext({
sessionCtx: params.sessionCtx,

View File

@@ -106,6 +106,8 @@ export async function runMemoryFlushIfNeeded(params: {
sessionKey: params.sessionKey,
messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
agentAccountId: params.sessionCtx.AccountId,
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
// Provider threading context for tool auto-injection
...buildThreadingToolContext({
sessionCtx: params.sessionCtx,

View File

@@ -145,6 +145,8 @@ export function createFollowupRunner(params: {
sessionKey: queued.run.sessionKey,
messageProvider: queued.run.messageProvider,
agentAccountId: queued.run.agentAccountId,
messageTo: queued.originatingTo,
messageThreadId: queued.originatingThreadId,
sessionFile: queued.run.sessionFile,
workspaceDir: queued.run.workspaceDir,
config: queued.run.config,

View File

@@ -168,6 +168,8 @@ export async function handleInlineActions(params: {
agentSessionKey: sessionKey,
agentChannel: channel,
agentAccountId: (ctx as { AccountId?: string }).AccountId,
agentTo: ctx.OriginatingTo ?? ctx.To,
agentThreadId: ctx.MessageThreadId ?? undefined,
agentDir,
workspaceDir,
config: cfg,

View File

@@ -255,6 +255,22 @@ describe("routeReply", () => {
);
});
it("uses threadId as threadTs for Slack when replyToId is missing", async () => {
mocks.sendMessageSlack.mockClear();
await routeReply({
payload: { text: "hi" },
channel: "slack",
to: "channel:C123",
threadId: "1710000000.9999",
cfg: {} as never,
});
expect(mocks.sendMessageSlack).toHaveBeenCalledWith(
"channel:C123",
"hi",
expect.objectContaining({ threadTs: "1710000000.9999" }),
);
});
it("sends multiple mediaUrls (caption only on first)", async () => {
mocks.sendMessageSlack.mockClear();
await routeReply({

View File

@@ -100,6 +100,11 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
return { ok: false, error: "Reply routing aborted" };
}
const resolvedReplyToId =
replyToId ??
(channelId === "slack" && threadId != null && threadId !== "" ? String(threadId) : undefined);
const resolvedThreadId = channelId === "slack" ? null : (threadId ?? null);
try {
// Provider docking: this is an execution boundary (we're about to send).
// Keep the module cheap to import by loading outbound plumbing lazily.
@@ -110,8 +115,8 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
to,
accountId: accountId ?? undefined,
payloads: [normalized],
replyToId: replyToId ?? null,
threadId: threadId ?? null,
replyToId: resolvedReplyToId ?? null,
threadId: resolvedThreadId,
abortSignal,
mirror: params.sessionKey
? {

View File

@@ -221,16 +221,20 @@ export async function initSessionState(params: {
const lastChannelRaw = (ctx.OriginatingChannel as string | undefined) || baseEntry?.lastChannel;
const lastToRaw = (ctx.OriginatingTo as string | undefined) || ctx.To || baseEntry?.lastTo;
const lastAccountIdRaw = (ctx.AccountId as string | undefined) || baseEntry?.lastAccountId;
const lastThreadIdRaw =
(ctx.MessageThreadId as string | number | undefined) || baseEntry?.lastThreadId;
const deliveryFields = normalizeSessionDeliveryFields({
deliveryContext: {
channel: lastChannelRaw,
to: lastToRaw,
accountId: lastAccountIdRaw,
threadId: lastThreadIdRaw,
},
});
const lastChannel = deliveryFields.lastChannel ?? lastChannelRaw;
const lastTo = deliveryFields.lastTo ?? lastToRaw;
const lastAccountId = deliveryFields.lastAccountId ?? lastAccountIdRaw;
const lastThreadId = deliveryFields.lastThreadId ?? lastThreadIdRaw;
sessionEntry = {
...baseEntry,
sessionId,
@@ -261,6 +265,7 @@ export async function initSessionState(params: {
lastChannel,
lastTo,
lastAccountId,
lastThreadId,
};
const metaPatch = deriveSessionMetaPatch({
ctx: sessionCtxForState,