fix(sessions): preserve resolved context in cold projections (#126422)

* fix(sessions): preserve resolved context in cold projections

* fix(sessions): preserve verified context provenance

* test(sessions): complete context metadata fixture

* fix(sessions): version verified context resolution
This commit is contained in:
Jason (Json)
2026-08-19 16:34:20 -06:00
committed by GitHub
parent 7e0b599ca4
commit beb466df3c
6 changed files with 264 additions and 29 deletions
@@ -6,10 +6,11 @@ import type { FollowupExecutionResult } from "./followup-turn-execution.js";
const mocks = vi.hoisted(() => ({
persistRunSessionUsage: vi.fn(async (_params: unknown) => undefined),
refreshQueuedFollowupSession: vi.fn(),
resolveContextTokensForModel: vi.fn<() => number | undefined>(() => 200_000),
}));
vi.mock("../../agents/context.js", () => ({
resolveContextTokensForModel: () => 200_000,
resolveContextTokensForModel: () => mocks.resolveContextTokensForModel(),
}));
vi.mock("../../agents/fast-mode.js", () => ({
@@ -166,6 +167,7 @@ function createParams(
describe("accountFollowupTurn", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.resolveContextTokensForModel.mockReturnValue(200_000);
});
it("forwards typed runtime context provenance to session persistence", async () => {
@@ -194,6 +196,83 @@ describe("accountFollowupTurn", () => {
);
});
it("treats a source-less current-run context window as runtime provenance", async () => {
const params = createParams();
const result = params.execution.execution.outcome;
if (result.kind !== "settled") {
throw new Error("expected settled test execution");
}
result.result.meta.agentMeta = {
sessionId: "session-1",
provider: "openai",
model: "gpt-4o",
agentHarnessId: "legacy-runtime",
contextTokens: 512_000,
};
await accountFollowupTurn(params);
expect(mocks.persistRunSessionUsage).toHaveBeenCalledWith(
expect.objectContaining({
agentHarnessId: "legacy-runtime",
contextTokensUsed: 512_000,
contextTokensSource: "runtime",
}),
);
});
it("marks a successful current model lookup with versioned resolved provenance", async () => {
const params = createParams();
await accountFollowupTurn(params);
expect(mocks.persistRunSessionUsage).toHaveBeenCalledWith(
expect.objectContaining({
contextTokensUsed: 200_000,
contextTokensSource: "resolved-v1",
}),
);
});
it("does not label a prior context fallback as a current resolution after a model switch", async () => {
mocks.resolveContextTokensForModel.mockReturnValueOnce(undefined);
const params = createParams();
const session = params.turn.session as unknown as {
current: () => SessionEntry;
adopt: (entry: SessionEntry) => void;
};
session.adopt({
...session.current(),
modelProvider: "anthropic",
model: "claude",
agentHarnessId: "openclaw",
contextTokens: 272_000,
contextTokensSource: "resolved",
});
const result = params.execution.execution.outcome;
if (result.kind !== "settled") {
throw new Error("expected settled test execution");
}
result.result.meta.agentMeta = {
sessionId: "session-1",
provider: "openai",
model: "gpt-4o",
agentHarnessId: "codex",
};
await accountFollowupTurn(params);
expect(mocks.persistRunSessionUsage).toHaveBeenCalledWith(
expect.objectContaining({
providerUsed: "openai",
modelUsed: "gpt-4o",
agentHarnessId: "codex",
contextTokensUsed: 272_000,
contextTokensSource: undefined,
}),
);
});
it.each([
{
name: "source-less legacy user pin",
@@ -230,17 +230,27 @@ export async function accountAgentTurn(context: AgentTurnAccountingContext) {
runResult.meta.agentMeta.contextTokens > 0
? Math.floor(runResult.meta.agentMeta.contextTokens)
: undefined;
const resolvedContextTokens =
runtimeContextTokens === undefined
? resolveContextTokensForModel({
cfg,
provider: providerUsed,
model: modelUsed,
allowAsyncLoad: false,
})
: undefined;
const contextTokensUsed =
runtimeContextTokens ??
resolveContextTokensForModel({
cfg,
provider: providerUsed,
model: modelUsed,
fallbackContextTokens: activeSessionEntry?.contextTokens ?? DEFAULT_CONTEXT_TOKENS,
allowAsyncLoad: false,
}) ??
resolvedContextTokens ??
activeSessionEntry?.contextTokens ??
DEFAULT_CONTEXT_TOKENS;
const contextTokensSource = runResult.meta?.agentMeta?.contextTokensSource ?? "resolved";
const contextTokensSource =
runResult.meta?.agentMeta?.contextTokensSource ??
(runtimeContextTokens !== undefined
? "runtime"
: resolvedContextTokens !== undefined
? "resolved-v1"
: undefined);
await persistRunSessionUsage({
storePath,
@@ -124,6 +124,54 @@ describe("resolveProjectedSessionContextTokens", () => {
).toBe(1_000_000);
});
it("falls back to the matching persisted resolution while current resolution is unavailable", () => {
expect(
resolveProjectedSessionContextTokens({
entry: { ...matchingRuntimeEntry, contextTokensSource: "resolved-v1" },
...currentSelection,
resolvedContextTokens: undefined,
}),
).toBe(272_000);
});
it("rejects a legacy resolved row because its producer may have reused a fallback", () => {
expect(
resolveProjectedSessionContextTokens({
entry: { ...matchingRuntimeEntry, contextTokensSource: "resolved" },
...currentSelection,
resolvedContextTokens: undefined,
}),
).toBeUndefined();
});
it("does not resurrect a removed runtime-configured cap while resolution is unavailable", () => {
expect(
resolveProjectedSessionContextTokens({
entry: { ...matchingRuntimeEntry, contextTokensSource: "runtime-configured" },
...currentSelection,
resolvedContextTokens: undefined,
}),
).toBeUndefined();
});
it.each([
{ name: "provider", patch: { modelProvider: "openrouter" } },
{ name: "model", patch: { model: "gpt-5.5" } },
{ name: "harness", patch: { agentHarnessId: "openclaw" } },
])("rejects a persisted resolution owned by a different $name", ({ patch }) => {
expect(
resolveProjectedSessionContextTokens({
entry: {
...matchingRuntimeEntry,
contextTokensSource: "resolved-v1",
...patch,
},
...currentSelection,
resolvedContextTokens: undefined,
}),
).toBeUndefined();
});
it("preserves a locked native window ahead of current configuration", () => {
expect(
resolveProjectedSessionContextTokens({
+48 -18
View File
@@ -15,6 +15,46 @@ function resolvePositiveContextTokens(value: number | null | undefined): number
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
}
function isExactProducerSelection(params: {
entry: SessionContextTokenOwner | undefined;
provider: string | null | undefined;
model: string | null | undefined;
agentHarnessId: string | null | undefined;
}): boolean {
const entryProvider = normalizeLowercaseStringOrEmpty(params.entry?.modelProvider);
const entryModel = normalizeLowercaseStringOrEmpty(params.entry?.model);
const entryHarness = normalizeLowercaseStringOrEmpty(params.entry?.agentHarnessId);
const currentProvider = normalizeLowercaseStringOrEmpty(params.provider);
const currentModel = normalizeLowercaseStringOrEmpty(params.model);
const currentHarness = normalizeLowercaseStringOrEmpty(params.agentHarnessId);
return Boolean(
entryProvider &&
entryModel &&
entryHarness &&
currentProvider &&
currentModel &&
currentHarness &&
entryProvider === currentProvider &&
entryModel === currentModel &&
entryHarness === currentHarness,
);
}
/** Returns a persisted effective resolution only for its exact producing selection. */
function resolveMatchingPersistedResolution(params: {
entry: SessionContextTokenOwner | undefined;
provider: string | null | undefined;
model: string | null | undefined;
agentHarnessId: string | null | undefined;
}): number | undefined {
if (params.entry?.contextTokensSource !== "resolved-v1") {
return undefined;
}
return isExactProducerSelection(params)
? resolvePositiveContextTokens(params.entry?.contextTokens)
: undefined;
}
/** Returns persisted telemetry only when it belongs to the current producing selection. */
export function resolveTrustedSessionContextTokens(params: {
entry: SessionContextTokenOwner | undefined;
@@ -45,23 +85,7 @@ export function resolveTrustedSessionContextTokens(params: {
if (params.entry?.contextTokensSource !== "runtime") {
return undefined;
}
const entryHarness = normalizeLowercaseStringOrEmpty(params.entry.agentHarnessId);
const currentHarness = normalizeLowercaseStringOrEmpty(params.agentHarnessId);
if (
!entryProvider ||
!entryModel ||
!entryHarness ||
!currentProvider ||
!currentModel ||
!currentHarness
) {
return undefined;
}
return entryProvider === currentProvider &&
entryModel === currentModel &&
entryHarness === currentHarness
? contextTokens
: undefined;
return isExactProducerSelection(params) ? contextTokens : undefined;
}
/** Projects the context window owned by the current session selection. */
@@ -76,14 +100,20 @@ export function resolveProjectedSessionContextTokens(params: {
const resolvedContextTokens = resolvePositiveContextTokens(params.resolvedContextTokens);
const authoredContextTokens = resolvePositiveContextTokens(params.authoredContextTokens);
const trustedContextTokens = resolveTrustedSessionContextTokens(params);
const persistedResolution =
resolvedContextTokens === undefined && authoredContextTokens === undefined
? resolveMatchingPersistedResolution(params)
: undefined;
// An authored effective cap owns the current selection. Otherwise current
// model capacity only constrains telemetry from that exact producer tuple.
// When synchronous model resolution is unavailable, preserve the last
// matching effective resolution instead of publishing an unknown window.
const currentContextTokens =
authoredContextTokens !== undefined
? resolvedContextTokens
: trustedContextTokens !== undefined && resolvedContextTokens !== undefined
? Math.min(trustedContextTokens, resolvedContextTokens)
: (trustedContextTokens ?? resolvedContextTokens);
: (trustedContextTokens ?? resolvedContextTokens ?? persistedResolution);
return params.entry?.modelSelectionLocked === true
? (trustedContextTokens ?? currentContextTokens)
: currentContextTokens;
+2 -2
View File
@@ -581,8 +581,8 @@ type SessionEntryCore = SessionRestartRecoveryState &
agentHarnessId?: string;
fallbackNotice?: FallbackNoticeState;
contextTokens?: number;
/** Origin of the persisted context window; absent on legacy/unproven rows. */
contextTokensSource?: "runtime" | "runtime-configured" | "resolved";
/** Origin of the persisted context window; `resolved` is legacy/unverified. */
contextTokensSource?: "runtime" | "runtime-configured" | "resolved" | "resolved-v1";
contextBudgetStatus?: SessionContextBudgetStatus;
compactionCount?: number;
compactionCheckpoints?: SessionCompactionCheckpoint[];
+68
View File
@@ -1588,6 +1588,74 @@ describe("gateway session utils", () => {
},
);
test.each([true, false])(
"projects a matching persisted resolved cap when catalog resolution is unavailable (lightweight=%s)",
(lightweightListRow) => {
const cfg = {
agents: {
defaults: {
model: { primary: "openai/gpt-5.6-sol" },
models: { "openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } } },
},
},
} as unknown as OpenClawConfig;
const entry = {
sessionId: "matching-resolved-cap",
modelProvider: "openai",
model: "gpt-5.6-sol",
agentHarnessId: "codex",
contextTokens: 272_000,
contextTokensSource: "resolved-v1",
} as SessionEntry;
const row = buildGatewaySessionRow({
cfg,
storePath: "",
store: { "agent:main:main": entry },
key: "agent:main:main",
entry,
lightweightListRow,
});
expect(row.agentRuntime?.id).toBe("codex");
expect(row.contextTokens).toBe(272_000);
},
);
test.each([true, false])(
"rejects an unresolved fallback even after persistence records the current tuple (lightweight=%s)",
(lightweightListRow) => {
const cfg = {
agents: {
defaults: {
model: { primary: "openai/gpt-5.6-sol" },
models: { "openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } } },
},
},
} as unknown as OpenClawConfig;
const entry = {
sessionId: "unresolved-fallback",
modelProvider: "openai",
model: "gpt-5.6-sol",
agentHarnessId: "codex",
contextTokens: 272_000,
contextTokensSource: undefined,
} as SessionEntry;
const row = buildGatewaySessionRow({
cfg,
storePath: "",
store: { "agent:main:main": entry },
key: "agent:main:main",
entry,
lightweightListRow,
});
expect(row.agentRuntime?.id).toBe("codex");
expect(row.contextTokens).toBeUndefined();
},
);
test.each(["xhigh", "max"] as const)(
"preserves catalog-less persisted %s in session change projections",
(thinkingLevel) => {