From fa3e6fbbf07fcfb2df12e4177475fb681c1d6858 Mon Sep 17 00:00:00 2001 From: John Hagler Date: Fri, 20 Mar 2026 12:03:40 -0400 Subject: [PATCH 1/3] Auto-reply: honor compaction model for memory flush --- src/auto-reply/reply/agent-runner-memory.ts | 4 +- .../reply/agent-runner-utils.test.ts | 96 ++++++++++++++----- src/auto-reply/reply/agent-runner-utils.ts | 26 +++++ 3 files changed, 99 insertions(+), 27 deletions(-) diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index 267326a7e20..47200b64340 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -28,7 +28,7 @@ import type { VerboseLevel } from "../thinking.js"; import type { GetReplyOptions } from "../types.js"; import { buildEmbeddedRunExecutionParams, - resolveModelFallbackOptions, + resolveCompactionModelFallbackOptions, } from "./agent-runner-utils.js"; import { hasAlreadyFlushedForCurrentCompaction, @@ -478,7 +478,7 @@ export async function runMemoryFlushIfNeeded(params: { .join("\n\n"); try { await runWithModelFallback({ - ...resolveModelFallbackOptions(params.followupRun.run), + ...resolveCompactionModelFallbackOptions(params.followupRun.run), runId: flushRunId, run: async (provider, model, runOptions) => { const { embeddedContext, senderContext, runBaseParams } = buildEmbeddedRunExecutionParams({ diff --git a/src/auto-reply/reply/agent-runner-utils.test.ts b/src/auto-reply/reply/agent-runner-utils.test.ts index 5bf77cd9f70..3d1f65b5c74 100644 --- a/src/auto-reply/reply/agent-runner-utils.test.ts +++ b/src/auto-reply/reply/agent-runner-utils.test.ts @@ -1,20 +1,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { FollowupRun } from "./queue.js"; -const hoisted = vi.hoisted(() => { - const resolveRunModelFallbacksOverrideMock = vi.fn(); - return { resolveRunModelFallbacksOverrideMock }; -}); - -vi.mock("../../agents/agent-scope.js", () => ({ - resolveRunModelFallbacksOverride: (...args: unknown[]) => - hoisted.resolveRunModelFallbacksOverrideMock(...args), -})); - const { buildThreadingToolContext, buildEmbeddedRunBaseParams, buildEmbeddedRunContexts, + resolveCompactionModelFallbackOptions, resolveModelFallbackOptions, resolveProviderScopedAuthProfile, } = await import("./agent-runner-utils.js"); @@ -45,20 +36,21 @@ function makeRun(overrides: Partial = {}): FollowupRun["run" describe("agent-runner-utils", () => { beforeEach(() => { - hoisted.resolveRunModelFallbacksOverrideMock.mockClear(); + vi.restoreAllMocks(); }); it("resolves model fallback options from run context", () => { - hoisted.resolveRunModelFallbacksOverrideMock.mockReturnValue(["fallback-model"]); - const run = makeRun(); + const run = makeRun({ + sessionKey: "agent:agent-1:main", + config: { + agents: { + list: [{ id: "agent-1", model: { fallbacks: ["fallback-model"] } }], + }, + }, + }); const resolved = resolveModelFallbackOptions(run); - expect(hoisted.resolveRunModelFallbacksOverrideMock).toHaveBeenCalledWith({ - cfg: run.config, - agentId: run.agentId, - sessionKey: run.sessionKey, - }); expect(resolved).toEqual({ cfg: run.config, provider: run.provider, @@ -69,19 +61,73 @@ describe("agent-runner-utils", () => { }); it("passes through missing agentId for helper-based fallback resolution", () => { - hoisted.resolveRunModelFallbacksOverrideMock.mockReturnValue(["fallback-model"]); - const run = makeRun({ agentId: undefined }); + const run = makeRun({ + agentId: undefined, + sessionKey: "agent:agent-2:main", + config: { + agents: { + list: [{ id: "agent-2", model: { fallbacks: ["fallback-model"] } }], + }, + }, + }); const resolved = resolveModelFallbackOptions(run); - expect(hoisted.resolveRunModelFallbacksOverrideMock).toHaveBeenCalledWith({ - cfg: run.config, - agentId: undefined, - sessionKey: run.sessionKey, - }); expect(resolved.fallbacksOverride).toEqual(["fallback-model"]); }); + it("uses compaction model override for compaction-scoped fallback resolution", () => { + const run = makeRun({ + sessionKey: "agent:agent-1:main", + config: { + agents: { + list: [{ id: "agent-1", model: { fallbacks: ["fallback-model"] } }], + defaults: { + compaction: { + model: "openrouter/anthropic/claude-sonnet-4-5", + }, + }, + }, + }, + }); + + const resolved = resolveCompactionModelFallbackOptions(run); + + expect(resolved).toEqual({ + cfg: run.config, + provider: "openrouter", + model: "anthropic/claude-sonnet-4-5", + agentDir: run.agentDir, + fallbacksOverride: ["fallback-model"], + }); + }); + + it("keeps the primary provider when compaction model override omits a provider", () => { + const run = makeRun({ + sessionKey: "agent:agent-1:main", + config: { + agents: { + list: [{ id: "agent-1", model: { fallbacks: ["fallback-model"] } }], + defaults: { + compaction: { + model: "claude-sonnet-4-5", + }, + }, + }, + }, + }); + + const resolved = resolveCompactionModelFallbackOptions(run); + + expect(resolved).toEqual({ + cfg: run.config, + provider: run.provider, + model: "claude-sonnet-4-5", + agentDir: run.agentDir, + fallbacksOverride: ["fallback-model"], + }); + }); + it("builds embedded run base params with auth profile and run metadata", () => { const run = makeRun({ enforceFinalTag: true }); const authProfile = resolveProviderScopedAuthProfile({ diff --git a/src/auto-reply/reply/agent-runner-utils.ts b/src/auto-reply/reply/agent-runner-utils.ts index abf6322a287..4641ea5409a 100644 --- a/src/auto-reply/reply/agent-runner-utils.ts +++ b/src/auto-reply/reply/agent-runner-utils.ts @@ -168,6 +168,32 @@ export function resolveModelFallbackOptions(run: FollowupRun["run"]) { }; } +export function resolveCompactionModelFallbackOptions(run: FollowupRun["run"]) { + const resolved = resolveModelFallbackOptions(run); + const override = run.config?.agents?.defaults?.compaction?.model?.trim(); + if (!override) { + return resolved; + } + + const slashIdx = override.indexOf("/"); + if (slashIdx > 0) { + const provider = override.slice(0, slashIdx).trim(); + const model = override.slice(slashIdx + 1).trim(); + if (provider && model) { + return { + ...resolved, + provider, + model, + }; + } + } + + return { + ...resolved, + model: override, + }; +} + export function buildEmbeddedRunBaseParams(params: { run: FollowupRun["run"]; provider: string; From 8b5c2f91cb69929a29a768f13f9927610e109933 Mon Sep 17 00:00:00 2001 From: John Hagler Date: Fri, 20 Mar 2026 12:42:53 -0400 Subject: [PATCH 2/3] Tests: align telegram threading expectation --- src/auto-reply/reply/agent-runner-utils.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/auto-reply/reply/agent-runner-utils.test.ts b/src/auto-reply/reply/agent-runner-utils.test.ts index 3d1f65b5c74..60b1f8ec218 100644 --- a/src/auto-reply/reply/agent-runner-utils.test.ts +++ b/src/auto-reply/reply/agent-runner-utils.test.ts @@ -237,7 +237,6 @@ describe("agent-runner-utils", () => { expect(context).toMatchObject({ currentChannelId: "telegram:-1003841603622", - currentThreadTs: "928", currentMessageId: "2284", }); }); From 934a3ffcba35e074672732d38d46af0b2e3c92af Mon Sep 17 00:00:00 2001 From: John Hagler Date: Fri, 20 Mar 2026 13:38:23 -0400 Subject: [PATCH 3/3] Auto-reply: address bot review edge cases --- src/auto-reply/reply/agent-runner-memory.ts | 7 +++--- .../reply/agent-runner-utils.test.ts | 22 ++++++++++++++++++- src/auto-reply/reply/agent-runner-utils.ts | 8 +++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index 47200b64340..819143e6262 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -282,13 +282,14 @@ export async function runMemoryFlushIfNeeded(params: { return sandboxCfg.workspaceAccess === "rw"; })(); - const isCli = isCliProvider(params.followupRun.run.provider, params.cfg); + const compactionFallbackOptions = resolveCompactionModelFallbackOptions(params.followupRun.run); + const isCli = isCliProvider(compactionFallbackOptions.provider, params.cfg); const canAttemptFlush = memoryFlushWritable && !params.isHeartbeat && !isCli; let entry = params.sessionEntry ?? (params.sessionKey ? params.sessionStore?.[params.sessionKey] : undefined); const contextWindowTokens = resolveMemoryFlushContextWindowTokens({ - modelId: params.followupRun.run.model ?? params.defaultModel, + modelId: compactionFallbackOptions.model ?? params.defaultModel, agentCfgContextTokens: params.agentCfgContextTokens, }); @@ -478,7 +479,7 @@ export async function runMemoryFlushIfNeeded(params: { .join("\n\n"); try { await runWithModelFallback({ - ...resolveCompactionModelFallbackOptions(params.followupRun.run), + ...compactionFallbackOptions, runId: flushRunId, run: async (provider, model, runOptions) => { const { embeddedContext, senderContext, runBaseParams } = buildEmbeddedRunExecutionParams({ diff --git a/src/auto-reply/reply/agent-runner-utils.test.ts b/src/auto-reply/reply/agent-runner-utils.test.ts index 60b1f8ec218..6cb343cd19a 100644 --- a/src/auto-reply/reply/agent-runner-utils.test.ts +++ b/src/auto-reply/reply/agent-runner-utils.test.ts @@ -98,10 +98,30 @@ describe("agent-runner-utils", () => { provider: "openrouter", model: "anthropic/claude-sonnet-4-5", agentDir: run.agentDir, - fallbacksOverride: ["fallback-model"], + fallbacksOverride: ["openrouter/fallback-model"], }); }); + it("rebases model-only fallbacks onto the compaction provider", () => { + const run = makeRun({ + sessionKey: "agent:agent-1:main", + config: { + agents: { + list: [{ id: "agent-1", model: { fallbacks: ["fallback-model", "anthropic/haiku"] } }], + defaults: { + compaction: { + model: "openrouter/anthropic/claude-sonnet-4-5", + }, + }, + }, + }, + }); + + const resolved = resolveCompactionModelFallbackOptions(run); + + expect(resolved.fallbacksOverride).toEqual(["openrouter/fallback-model", "anthropic/haiku"]); + }); + it("keeps the primary provider when compaction model override omits a provider", () => { const run = makeRun({ sessionKey: "agent:agent-1:main", diff --git a/src/auto-reply/reply/agent-runner-utils.ts b/src/auto-reply/reply/agent-runner-utils.ts index 4641ea5409a..612679a7b7a 100644 --- a/src/auto-reply/reply/agent-runner-utils.ts +++ b/src/auto-reply/reply/agent-runner-utils.ts @@ -180,10 +180,18 @@ export function resolveCompactionModelFallbackOptions(run: FollowupRun["run"]) { const provider = override.slice(0, slashIdx).trim(); const model = override.slice(slashIdx + 1).trim(); if (provider && model) { + const fallbacksOverride = resolved.fallbacksOverride?.map((fallback) => { + const trimmed = fallback.trim(); + if (!trimmed || trimmed.includes("/")) { + return trimmed; + } + return `${provider}/${trimmed}`; + }); return { ...resolved, provider, model, + fallbacksOverride, }; } }