From fc8ab82aab8ecf0ee0e2dae5ea25bb011371c16e Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 1 Apr 2026 16:20:58 +0100 Subject: [PATCH] refactor: trim cron session startup imports --- src/agents/live-model-switch-error.ts | 22 ++ src/agents/live-model-switch.ts | 23 +- src/agents/model-fallback.test.ts | 2 +- src/agents/model-fallback.ts | 2 +- src/agents/pi-embedded-runner/run.ts | 2 +- .../reply/agent-runner-execution.test.ts | 2 +- .../reply/agent-runner-execution.ts | 2 +- .../run.live-session-model-switch.test.ts | 2 +- .../isolated-agent/run.skill-filter.test.ts | 35 +++ src/cron/isolated-agent/run.test-harness.ts | 283 ++++++++++++------ src/cron/isolated-agent/run.ts | 96 +++--- 11 files changed, 319 insertions(+), 152 deletions(-) create mode 100644 src/agents/live-model-switch-error.ts diff --git a/src/agents/live-model-switch-error.ts b/src/agents/live-model-switch-error.ts new file mode 100644 index 00000000000..070dd172cff --- /dev/null +++ b/src/agents/live-model-switch-error.ts @@ -0,0 +1,22 @@ +export type LiveSessionModelSelection = { + provider: string; + model: string; + authProfileId?: string; + authProfileIdSource?: "auto" | "user"; +}; + +export class LiveSessionModelSwitchError extends Error { + provider: string; + model: string; + authProfileId?: string; + authProfileIdSource?: "auto" | "user"; + + constructor(selection: LiveSessionModelSelection) { + super(`Live session model switch requested: ${selection.provider}/${selection.model}`); + this.name = "LiveSessionModelSwitchError"; + this.provider = selection.provider; + this.model = selection.model; + this.authProfileId = selection.authProfileId; + this.authProfileIdSource = selection.authProfileIdSource; + } +} diff --git a/src/agents/live-model-switch.ts b/src/agents/live-model-switch.ts index 0f65a93a87e..6b06edc3171 100644 --- a/src/agents/live-model-switch.ts +++ b/src/agents/live-model-switch.ts @@ -1,4 +1,7 @@ -import { loadSessionStore, resolveStorePath, type SessionEntry } from "../config/sessions.js"; +import { resolveStorePath } from "../config/sessions/paths.js"; +import { loadSessionStore } from "../config/sessions/store.js"; +import type { SessionEntry } from "../config/sessions/types.js"; +import { LiveSessionModelSwitchError } from "./live-model-switch-error.js"; import { resolveDefaultModelForAgent, resolvePersistedModelRef } from "./model-selection.js"; import { consumeEmbeddedRunModelSwitch, @@ -6,25 +9,9 @@ import { type EmbeddedRunModelSwitchRequest, } from "./pi-embedded-runner/runs.js"; import { abortEmbeddedPiRun } from "./pi-embedded.js"; - +export { LiveSessionModelSwitchError } from "./live-model-switch-error.js"; export type LiveSessionModelSelection = EmbeddedRunModelSwitchRequest; -export class LiveSessionModelSwitchError extends Error { - provider: string; - model: string; - authProfileId?: string; - authProfileIdSource?: "auto" | "user"; - - constructor(selection: LiveSessionModelSelection) { - super(`Live session model switch requested: ${selection.provider}/${selection.model}`); - this.name = "LiveSessionModelSwitchError"; - this.provider = selection.provider; - this.model = selection.model; - this.authProfileId = selection.authProfileId; - this.authProfileIdSource = selection.authProfileIdSource; - } -} - export function resolveLiveSessionModelSelection(params: { cfg?: { session?: { store?: string } } | undefined; sessionKey?: string; diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 3b11d698050..23e8a77c02b 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -9,7 +9,7 @@ import type { AuthProfileStore } from "./auth-profiles.js"; import { saveAuthProfileStore } from "./auth-profiles.js"; import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js"; import { isAnthropicBillingError } from "./live-auth-keys.js"; -import { LiveSessionModelSwitchError } from "./live-model-switch.js"; +import { LiveSessionModelSwitchError } from "./live-model-switch-error.js"; import { runWithImageModelFallback, runWithModelFallback } from "./model-fallback.js"; import { makeModelFallbackCfg } from "./test-helpers/model-fallback-config-fixture.js"; diff --git a/src/agents/model-fallback.ts b/src/agents/model-fallback.ts index f13e768a590..a140f195655 100644 --- a/src/agents/model-fallback.ts +++ b/src/agents/model-fallback.ts @@ -26,7 +26,7 @@ import { shouldPreserveTransientCooldownProbeSlot, shouldUseTransientCooldownProbeSlot, } from "./failover-policy.js"; -import { LiveSessionModelSwitchError } from "./live-model-switch.js"; +import { LiveSessionModelSwitchError } from "./live-model-switch-error.js"; import { logModelFallbackDecision } from "./model-fallback-observation.js"; import type { FallbackAttempt, ModelCandidate } from "./model-fallback.types.js"; import { diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 9e92007af4b..733f1e4c1e0 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -25,9 +25,9 @@ import { FailoverError, resolveFailoverStatus, } from "../failover-error.js"; +import { LiveSessionModelSwitchError } from "../live-model-switch-error.js"; import { hasDifferentLiveSessionModelSelection, - LiveSessionModelSwitchError, consumeLiveSessionModelSwitch, } from "../live-model-switch.js"; import { diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index 801c805e334..63e042cf605 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { LiveSessionModelSwitchError } from "../../agents/live-model-switch.js"; +import { LiveSessionModelSwitchError } from "../../agents/live-model-switch-error.js"; import type { TemplateContext } from "../templating.js"; import type { GetReplyOptions } from "../types.js"; import { MAX_LIVE_SWITCH_RETRIES } from "./agent-runner-execution.js"; diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 13d162665ed..17a3f42fbae 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -7,7 +7,7 @@ import { import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js"; import { runCliAgent } from "../../agents/cli-runner.js"; import { getCliSessionBinding } from "../../agents/cli-session.js"; -import { LiveSessionModelSwitchError } from "../../agents/live-model-switch.js"; +import { LiveSessionModelSwitchError } from "../../agents/live-model-switch-error.js"; import { runWithModelFallback, isFallbackSummaryError } from "../../agents/model-fallback.js"; import { isCliProvider } from "../../agents/model-selection.js"; import { diff --git a/src/cron/isolated-agent/run.live-session-model-switch.test.ts b/src/cron/isolated-agent/run.live-session-model-switch.test.ts index 9d92ce87a9c..bddbda4d8e7 100644 --- a/src/cron/isolated-agent/run.live-session-model-switch.test.ts +++ b/src/cron/isolated-agent/run.live-session-model-switch.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { LiveSessionModelSwitchError } from "../../agents/live-model-switch.js"; +import { LiveSessionModelSwitchError } from "../../agents/live-model-switch-error.js"; import { clearFastTestEnv, loadRunCronIsolatedAgentTurn, diff --git a/src/cron/isolated-agent/run.skill-filter.test.ts b/src/cron/isolated-agent/run.skill-filter.test.ts index 54c28d6bc2e..178b8545cc7 100644 --- a/src/cron/isolated-agent/run.skill-filter.test.ts +++ b/src/cron/isolated-agent/run.skill-filter.test.ts @@ -8,8 +8,11 @@ import { buildWorkspaceSkillSnapshotMock, getCliSessionIdMock, isCliProviderMock, + lookupCachedContextTokensMock, loadRunCronIsolatedAgentTurn, logWarnMock, + makeCronSession, + makeCronSessionEntry, resolveAgentConfigMock, resolveAgentSkillsFilterMock, resolveAllowedModelRefMock, @@ -320,4 +323,36 @@ describe("runCronIsolatedAgentTurn — skill filter", () => { ); }); }); + + describe("context token fallback", () => { + it("preserves existing session contextTokens when no cached model window is loaded", async () => { + const session = makeCronSession({ + sessionEntry: makeCronSessionEntry({ + contextTokens: 222_000, + }), + }); + resolveCronSessionMock.mockReturnValue(session); + lookupCachedContextTokensMock.mockReturnValue(undefined); + + const result = await runSkillFilterCase(); + + expect(result.status).toBe("ok"); + expect(session.sessionEntry.contextTokens).toBe(222_000); + }); + + it("prefers cached model contextTokens over the previous session value", async () => { + const session = makeCronSession({ + sessionEntry: makeCronSessionEntry({ + contextTokens: 222_000, + }), + }); + resolveCronSessionMock.mockReturnValue(session); + lookupCachedContextTokensMock.mockReturnValue(512_000); + + const result = await runSkillFilterCase(); + + expect(result.status).toBe("ok"); + expect(session.sessionEntry.contextTokens).toBe(512_000); + }); + }); }); diff --git a/src/cron/isolated-agent/run.test-harness.ts b/src/cron/isolated-agent/run.test-harness.ts index 0a000a89332..d28fcdda6ae 100644 --- a/src/cron/isolated-agent/run.test-harness.ts +++ b/src/cron/isolated-agent/run.test-harness.ts @@ -1,5 +1,4 @@ import { vi, type Mock } from "vitest"; -import { LiveSessionModelSwitchError } from "../../agents/live-model-switch.js"; type CronSessionEntry = { sessionId: string; @@ -24,6 +23,18 @@ function createMock(): Mock { return vi.fn(); } +function normalizeModelSelectionForTest(value: unknown): string | undefined { + if (typeof value === "string") { + const trimmed = value.trim(); + return trimmed || undefined; + } + if (!value || typeof value !== "object") { + return undefined; + } + const primary = (value as { primary?: unknown }).primary; + return typeof primary === "string" && primary.trim() ? primary.trim() : undefined; +} + export const buildWorkspaceSkillSnapshotMock = createMock(); export const resolveAgentConfigMock = createMock(); export const resolveAgentModelFallbacksOverrideMock = createMock(); @@ -37,6 +48,7 @@ export const resolveThinkingDefaultMock = createMock(); export const runWithModelFallbackMock = createMock(); export const runEmbeddedPiAgentMock = createMock(); export const runCliAgentMock = createMock(); +export const lookupCachedContextTokensMock = createMock(); export const getCliSessionIdMock = createMock(); export const updateSessionStoreMock = createMock(); export const resolveCronSessionMock = createMock(); @@ -48,39 +60,94 @@ export const resolveCronPayloadOutcomeMock = createMock(); export const resolveCronDeliveryPlanMock = createMock(); export const resolveDeliveryTargetMock = createMock(); export const resolveSessionAuthProfileOverrideMock = createMock(); -const resolveAgentDirMock = vi.fn().mockReturnValue("/tmp/agent-dir"); -const resolveAgentWorkspaceDirMock = vi.fn().mockReturnValue("/tmp/workspace"); -const resolveDefaultAgentIdMock = vi.fn().mockReturnValue("default"); -const getSkillsSnapshotVersionMock = vi.fn().mockReturnValue(42); -const ensureAgentWorkspaceMock = vi.fn().mockResolvedValue({ dir: "/tmp/workspace" }); -const loadModelCatalogMock = vi.fn().mockResolvedValue({ models: [] }); -const normalizeModelSelectionMock = vi.fn((value: unknown) => - typeof value === "string" ? value.trim() || undefined : undefined, -); -const lookupContextTokensMock = vi.fn().mockReturnValue(128000); -const resolveCronStyleNowMock = vi.fn().mockReturnValue({ - formattedTime: "2026-02-10 12:00", - timeLine: "Current time: 2026-02-10 12:00 UTC", + +vi.mock("../../agents/agent-scope.js", () => ({ + resolveAgentConfig: resolveAgentConfigMock, + resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent-dir"), + resolveAgentModelFallbacksOverride: resolveAgentModelFallbacksOverrideMock, + resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/workspace"), + resolveDefaultAgentId: vi.fn().mockReturnValue("default"), + resolveAgentSkillsFilter: resolveAgentSkillsFilterMock, +})); + +vi.mock("../../agents/skills.js", () => ({ + buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock, +})); + +vi.mock("../../agents/skills/refresh.js", () => ({ + getSkillsSnapshotVersion: vi.fn().mockReturnValue(42), +})); + +vi.mock("../../agents/workspace.js", () => ({ + DEFAULT_IDENTITY_FILENAME: "IDENTITY.md", + ensureAgentWorkspace: vi.fn().mockResolvedValue({ dir: "/tmp/workspace" }), +})); + +vi.mock("../../agents/model-catalog.js", () => ({ + loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }), +})); + +vi.mock("../../agents/model-selection.js", () => ({ + getModelRefStatus: getModelRefStatusMock, + isCliProvider: isCliProviderMock, + normalizeModelSelection: normalizeModelSelectionForTest, + resolveAllowedModelRef: resolveAllowedModelRefMock, + resolveConfiguredModelRef: resolveConfiguredModelRefMock, + resolveHooksGmailModel: resolveHooksGmailModelMock, + resolveThinkingDefault: resolveThinkingDefaultMock, +})); + +vi.mock("../../agents/model-fallback.js", () => ({ + runWithModelFallback: runWithModelFallbackMock, +})); + +vi.mock("../../agents/auth-profiles/session-override.js", () => ({ + resolveSessionAuthProfileOverride: resolveSessionAuthProfileOverrideMock, +})); + +vi.mock("../../agents/live-model-switch-error.js", async (importOriginal) => { + return await importOriginal(); }); -const resolveAgentTimeoutMsMock = vi.fn().mockReturnValue(60_000); -const deriveSessionTotalTokensMock = vi.fn().mockReturnValue(30); -const hasNonzeroUsageMock = vi.fn().mockReturnValue(false); -const normalizeThinkLevelMock = vi.fn().mockReturnValue(undefined); -const normalizeVerboseLevelMock = vi.fn().mockReturnValue("off"); -const supportsXHighThinkingMock = vi.fn().mockReturnValue(false); -const resolveSessionTranscriptPathMock = vi.fn().mockReturnValue("/tmp/transcript.jsonl"); -const setSessionRuntimeModelMock = vi.fn(); -const registerAgentRunContextMock = vi.fn(); -const buildSafeExternalPromptMock = vi.fn().mockReturnValue("safe prompt"); -const detectSuspiciousPatternsMock = vi.fn().mockReturnValue([]); -const isExternalHookSessionMock = vi.fn().mockReturnValue(false); -const mapHookExternalContentSourceMock = vi.fn().mockReturnValue("unknown"); -const resolveHookExternalContentSourceMock = vi.fn().mockReturnValue(undefined); -const estimateUsageCostMock = vi.fn().mockReturnValue(undefined); -const resolveModelCostConfigMock = vi.fn().mockReturnValue(undefined); -const resolveBootstrapWarningSignaturesSeenMock = vi.fn().mockReturnValue([]); -const resolveFastModeStateMock = vi.fn().mockReturnValue({ enabled: false }); -const resolveNestedAgentLaneMock = vi.fn((lane: string | undefined) => lane); + +vi.mock("../../agents/pi-embedded.js", () => ({ + runEmbeddedPiAgent: runEmbeddedPiAgentMock, +})); + +vi.mock("../../agents/context-cache.js", () => ({ + lookupCachedContextTokens: lookupCachedContextTokensMock, +})); + +vi.mock("../../agents/date-time.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + formatUserTime: vi.fn().mockReturnValue("2026-02-10 12:00"), + resolveUserTimeFormat: vi.fn().mockReturnValue("24h"), + resolveUserTimezone: vi.fn().mockReturnValue("UTC"), + }; +}); + +vi.mock("../../agents/timeout.js", () => ({ + resolveAgentTimeoutMs: vi.fn().mockReturnValue(60_000), +})); + +vi.mock("../../agents/usage.js", () => ({ + deriveSessionTotalTokens: vi.fn().mockReturnValue(30), + hasNonzeroUsage: vi.fn().mockReturnValue(false), +})); + +vi.mock("../../agents/subagent-announce.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + runSubagentAnnounceFlow: vi.fn().mockResolvedValue(true), + }; +}); + +vi.mock("../../agents/subagent-registry.js", () => ({ + countActiveDescendantRuns: countActiveDescendantRunsMock, + listDescendantRunsForRequester: listDescendantRunsForRequesterMock, +})); vi.mock("../../agents/cli-runner.runtime.js", () => ({ runCliAgent: runCliAgentMock, @@ -88,60 +155,96 @@ vi.mock("../../agents/cli-runner.runtime.js", () => ({ setCliSessionId: vi.fn(), })); -vi.mock("./run.runtime.js", () => ({ - DEFAULT_CONTEXT_TOKENS: 128000, - DEFAULT_MODEL: "gpt-4", - DEFAULT_PROVIDER: "openai", - LiveSessionModelSwitchError, - buildSafeExternalPrompt: buildSafeExternalPromptMock, - buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock, - countActiveDescendantRuns: countActiveDescendantRunsMock, - deriveSessionTotalTokens: deriveSessionTotalTokensMock, - detectSuspiciousPatterns: detectSuspiciousPatternsMock, - ensureAgentWorkspace: ensureAgentWorkspaceMock, - estimateUsageCost: estimateUsageCostMock, - listDescendantRunsForRequester: listDescendantRunsForRequesterMock, - getModelRefStatus: getModelRefStatusMock, - getRemoteSkillEligibility: vi.fn().mockReturnValue({}), - getSkillsSnapshotVersion: getSkillsSnapshotVersionMock, - hasNonzeroUsage: hasNonzeroUsageMock, - isCliProvider: isCliProviderMock, - isExternalHookSession: isExternalHookSessionMock, - loadModelCatalog: loadModelCatalogMock, - logWarn: (...args: unknown[]) => logWarnMock(...args), - lookupContextTokens: lookupContextTokensMock, - mapHookExternalContentSource: mapHookExternalContentSourceMock, - normalizeAgentId: vi.fn((id: string) => id), - normalizeModelSelection: normalizeModelSelectionMock, - normalizeThinkLevel: normalizeThinkLevelMock, - normalizeVerboseLevel: normalizeVerboseLevelMock, - registerAgentRunContext: registerAgentRunContextMock, - resolveAgentConfig: resolveAgentConfigMock, - resolveAgentDir: resolveAgentDirMock, - resolveAgentModelFallbacksOverride: resolveAgentModelFallbacksOverrideMock, - resolveAgentSkillsFilter: resolveAgentSkillsFilterMock, - resolveAgentTimeoutMs: resolveAgentTimeoutMsMock, - resolveAgentWorkspaceDir: resolveAgentWorkspaceDirMock, - resolveAllowedModelRef: resolveAllowedModelRefMock, - resolveBootstrapWarningSignaturesSeen: resolveBootstrapWarningSignaturesSeenMock, - resolveConfiguredModelRef: resolveConfiguredModelRefMock, - resolveCronStyleNow: resolveCronStyleNowMock, - resolveDefaultAgentId: resolveDefaultAgentIdMock, - resolveFastModeState: resolveFastModeStateMock, - resolveHookExternalContentSource: resolveHookExternalContentSourceMock, - resolveHooksGmailModel: resolveHooksGmailModelMock, - resolveModelCostConfig: resolveModelCostConfigMock, - resolveNestedAgentLane: resolveNestedAgentLaneMock, - resolveSessionAuthProfileOverride: resolveSessionAuthProfileOverrideMock, - resolveSessionTranscriptPath: resolveSessionTranscriptPathMock, - resolveThinkingDefault: resolveThinkingDefaultMock, - runEmbeddedPiAgent: runEmbeddedPiAgentMock, - runWithModelFallback: runWithModelFallbackMock, - setSessionRuntimeModel: setSessionRuntimeModelMock, - supportsXHighThinking: supportsXHighThinkingMock, +vi.mock("../../auto-reply/thinking.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + normalizeThinkLevel: vi.fn().mockReturnValue(undefined), + normalizeVerboseLevel: vi.fn().mockReturnValue("off"), + supportsXHighThinking: vi.fn().mockReturnValue(false), + }; +}); + +vi.mock("../../cli/outbound-send-deps.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createOutboundSendDeps: vi.fn().mockReturnValue({}), + }; +}); + +vi.mock("../../config/sessions/paths.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveSessionTranscriptPath: vi.fn().mockReturnValue("/tmp/transcript.jsonl"), + }; +}); + +vi.mock("../../config/sessions/store.runtime.js", () => ({ updateSessionStore: updateSessionStoreMock, })); +vi.mock("../../config/sessions/types.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + setSessionRuntimeModel: vi.fn(), + }; +}); + +vi.mock("../../routing/session-key.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + buildAgentMainSessionKey: vi.fn().mockReturnValue("agent:default:cron:test"), + normalizeAgentId: vi.fn((id: string) => id), + }; +}); + +vi.mock("../../infra/agent-events.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + registerAgentRunContext: vi.fn(), + }; +}); + +vi.mock("../../infra/outbound/deliver.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + deliverOutboundPayloads: vi.fn().mockResolvedValue(undefined), + }; +}); + +vi.mock("../../infra/skills-remote.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getRemoteSkillEligibility: vi.fn().mockReturnValue({}), + }; +}); + +vi.mock("../../logger.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + logWarn: (...args: unknown[]) => logWarnMock(...args), + }; +}); + +vi.mock("../../security/external-content.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + buildSafeExternalPrompt: vi.fn().mockReturnValue("safe prompt"), + detectSuspiciousPatterns: vi.fn().mockReturnValue([]), + getHookType: vi.fn().mockReturnValue("unknown"), + isExternalHookSession: vi.fn().mockReturnValue(false), + }; +}); + vi.mock("../delivery.js", () => ({ resolveCronDeliveryPlan: resolveCronDeliveryPlanMock, })); @@ -164,6 +267,16 @@ vi.mock("./session.js", () => ({ resolveCronSession: resolveCronSessionMock, })); +vi.mock("../../agents/defaults.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + DEFAULT_CONTEXT_TOKENS: 128000, + DEFAULT_MODEL: "gpt-4", + DEFAULT_PROVIDER: "openai", + }; +}); + export function makeCronSessionEntry(overrides?: Record): CronSessionEntry { return { sessionId: "test-session-id", @@ -235,6 +348,8 @@ export function resetRunCronIsolatedAgentTurnHarness(): void { runEmbeddedPiAgentMock.mockResolvedValue(makeDefaultEmbeddedResult()); runCliAgentMock.mockReset(); + lookupCachedContextTokensMock.mockReset(); + lookupCachedContextTokensMock.mockReturnValue(undefined); getCliSessionIdMock.mockReturnValue(undefined); updateSessionStoreMock.mockReset(); diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index a726fa14a74..c017a2800e6 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -1,5 +1,50 @@ +import { + resolveAgentConfig, + resolveAgentDir, + resolveAgentModelFallbacksOverride, + resolveAgentWorkspaceDir, + resolveDefaultAgentId, +} from "../../agents/agent-scope.js"; +import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js"; +import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js"; +import { lookupCachedContextTokens } from "../../agents/context-cache.js"; +import { resolveCronStyleNow } from "../../agents/current-time.js"; +import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js"; +import { resolveFastModeState } from "../../agents/fast-mode.js"; +import { resolveNestedAgentLane } from "../../agents/lanes.js"; +import { LiveSessionModelSwitchError } from "../../agents/live-model-switch-error.js"; +import { loadModelCatalog } from "../../agents/model-catalog.js"; +import { runWithModelFallback } from "../../agents/model-fallback.js"; +import { isCliProvider, resolveThinkingDefault } from "../../agents/model-selection.js"; +import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; +import { + countActiveDescendantRuns, + listDescendantRunsForRequester, +} from "../../agents/subagent-registry.js"; +import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; +import { deriveSessionTotalTokens, hasNonzeroUsage } from "../../agents/usage.js"; +import { ensureAgentWorkspace } from "../../agents/workspace.js"; +import { + normalizeThinkLevel, + normalizeVerboseLevel, + supportsXHighThinking, +} from "../../auto-reply/thinking.js"; import type { CliDeps } from "../../cli/outbound-send-deps.js"; import type { OpenClawConfig } from "../../config/config.js"; +import { resolveSessionTranscriptPath } from "../../config/sessions/paths.js"; +import { updateSessionStore } from "../../config/sessions/store.runtime.js"; +import { setSessionRuntimeModel } from "../../config/sessions/types.js"; +import { registerAgentRunContext } from "../../infra/agent-events.js"; +import { logWarn } from "../../logger.js"; +import { normalizeAgentId } from "../../routing/session-key.js"; +import { + buildSafeExternalPrompt, + detectSuspiciousPatterns, + mapHookExternalContentSource, + isExternalHookSession, + resolveHookExternalContentSource, +} from "../../security/external-content.js"; +import { estimateUsageCost, resolveModelCostConfig } from "../../utils/usage-format.js"; import { resolveCronDeliveryPlan } from "../delivery.js"; import type { CronJob, CronRunOutcome, CronRunTelemetry } from "../types.js"; import { @@ -15,48 +60,6 @@ import { } from "./helpers.js"; import { resolveCronModelSelection } from "./model-selection.js"; import { buildCronAgentDefaultsConfig } from "./run-config.js"; -import { - DEFAULT_CONTEXT_TOKENS, - LiveSessionModelSwitchError, - buildSafeExternalPrompt, - countActiveDescendantRuns, - deriveSessionTotalTokens, - detectSuspiciousPatterns, - ensureAgentWorkspace, - estimateUsageCost, - hasNonzeroUsage, - isCliProvider, - isExternalHookSession, - listDescendantRunsForRequester, - loadModelCatalog, - logWarn, - lookupContextTokens, - mapHookExternalContentSource, - normalizeAgentId, - normalizeThinkLevel, - normalizeVerboseLevel, - registerAgentRunContext, - resolveAgentConfig, - resolveAgentDir, - resolveAgentModelFallbacksOverride, - resolveAgentTimeoutMs, - resolveAgentWorkspaceDir, - resolveBootstrapWarningSignaturesSeen, - resolveCronStyleNow, - resolveDefaultAgentId, - resolveFastModeState, - resolveHookExternalContentSource, - resolveModelCostConfig, - resolveNestedAgentLane, - resolveSessionAuthProfileOverride, - resolveSessionTranscriptPath, - resolveThinkingDefault, - runEmbeddedPiAgent, - runWithModelFallback, - setSessionRuntimeModel, - supportsXHighThinking, - updateSessionStore, -} from "./run.runtime.js"; import { resolveCronAgentSessionKey } from "./session-key.js"; import { resolveCronSession } from "./session.js"; import { resolveCronSkillsSnapshot } from "./skills-snapshot.js"; @@ -163,6 +166,10 @@ function appendCronDeliveryInstruction(params: { return `${params.commandBody}\n\nReturn your summary as plain text; it will be delivered automatically. If the task explicitly calls for messaging a specific external recipient, note who/where it should go instead of sending it yourself.`.trim(); } +function resolvePositiveContextTokens(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined; +} + async function loadCliRunnerRuntime() { return await import("../../agents/cli-runner.runtime.js"); } @@ -702,8 +709,9 @@ export async function runCronIsolatedAgentTurn(params: { const providerUsed = finalRunResult.meta?.agentMeta?.provider ?? fallbackProvider ?? liveSelection.provider; const contextTokens = - agentCfg?.contextTokens ?? - lookupContextTokens(modelUsed, { allowAsyncLoad: false }) ?? + resolvePositiveContextTokens(agentCfg?.contextTokens) ?? + lookupCachedContextTokens(modelUsed) ?? + resolvePositiveContextTokens(cronSession.sessionEntry.contextTokens) ?? DEFAULT_CONTEXT_TOKENS; setSessionRuntimeModel(cronSession.sessionEntry, {