Skip to content

Commit a96f445

Browse files
fix: resolve ratio base from active session model, not agent config
Audited every site that consumes the new `*Share` fields and confirmed the context-window base is always the SESSION-active model: - `pi-embedded-runner/run/attempt.ts` reads `params.model.contextWindow`, where `params.model` is `effectiveModel` from `resolveEffectiveRuntimeModel` using the per-run `provider`/`modelId` (which already honors session model overrides via `resolveHookModelSelection` and persisted session overrides). - `pi-embedded-runner/compact.ts` reads `model?.contextWindow` from the model resolved by `resolveModelAsync(provider, modelId, …)` where provider/modelId flow from `params.provider`/`params.model` (session-scoped) through `resolveEmbeddedCompactionTarget`, with the documented `compaction.model` override taking precedence only when explicitly configured. - `auto-reply/reply/memory-flush.ts` resolves the window via `resolveMemoryFlushContextWindowTokens` from `followupRun.run.model` — the per-run model captured at queue time, which is session-scoped. No code path computes the share against a statically baked agent-default model, so no fix to the resolution logic is required. Documentation, however, was implicit. This commit: - Extends JSDoc on each `*Share` field in `types.agent-defaults.ts` to state explicitly that the base is the context window of the model active for the CURRENT SESSION, falling back to the agent's default model, then to DEFAULT_CONTEXT_TOKENS. - Adds a regression test in `reply-state.test.ts` proving that two sessions of the same agent (one inheriting the agent default glm-4.7@200k, one overriding to kimi-k2@1M) compute different `reserveTokensFloor` and `softThresholdTokens` from the same share config — locking in the session-scoped semantics so a future refactor cannot silently regress to agent-scoped resolution. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent f22fd96 commit a96f445

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

src/auto-reply/reply/reply-state.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,73 @@ describe("applyMemoryFlushSharesToPlan", () => {
516516
expect(small.reserveTokensFloor).toBe(800);
517517
expect(small.softThresholdTokens).toBe(160);
518518
});
519+
520+
// Regression: a single agent runs sessions with different models. The share
521+
// must be evaluated against the SESSION-active model's context window, not
522+
// a value statically derived from the agent's default model. Two sessions
523+
// of the same agent with different active models must yield different
524+
// resolved budgets.
525+
it("share resolves against the SESSION-active model, not the agent default model", () => {
526+
// Same agent config: both share fields set, no absolute overrides.
527+
const cfg = {
528+
models: {
529+
providers: {
530+
glm: { models: [{ id: "glm-4.7", contextWindow: 200_000 }] },
531+
moonshot: { models: [{ id: "kimi-k2", contextWindow: 1_000_000 }] },
532+
},
533+
},
534+
agents: {
535+
defaults: {
536+
// Agent-level default model (would be used as a fallback when the
537+
// session does not override).
538+
model: { primary: "glm/glm-4.7" },
539+
compaction: {
540+
reserveTokensFloorShare: 0.1,
541+
memoryFlush: { softThresholdTokensShare: 0.02 },
542+
},
543+
},
544+
},
545+
} as never;
546+
547+
// Session S1 inherits the agent default (glm-4.7, 200k window).
548+
const sessionS1Window = resolveMemoryFlushContextWindowTokens({
549+
cfg,
550+
provider: "glm",
551+
modelId: "glm-4.7",
552+
});
553+
expect(sessionS1Window).toBe(200_000);
554+
555+
const planS1 = applyMemoryFlushSharesToPlan({
556+
plan: basePlan,
557+
cfg,
558+
contextWindowTokens: sessionS1Window,
559+
});
560+
expect(planS1.reserveTokensFloor).toBe(20_000); // 200k × 0.1
561+
expect(planS1.softThresholdTokens).toBe(4_000); // 200k × 0.02
562+
563+
// Session S2 of the SAME agent overrides to kimi-k2 (1M window). The
564+
// share must scale to the active session model, NOT stay locked at the
565+
// agent default's 200k.
566+
const sessionS2Window = resolveMemoryFlushContextWindowTokens({
567+
cfg,
568+
provider: "moonshot",
569+
modelId: "kimi-k2",
570+
});
571+
expect(sessionS2Window).toBe(1_000_000);
572+
573+
const planS2 = applyMemoryFlushSharesToPlan({
574+
plan: basePlan,
575+
cfg,
576+
contextWindowTokens: sessionS2Window,
577+
});
578+
expect(planS2.reserveTokensFloor).toBe(100_000); // 1M × 0.1
579+
expect(planS2.softThresholdTokens).toBe(20_000); // 1M × 0.02
580+
581+
// Sanity: budgets really did differ between the two sessions even though
582+
// the agent config is identical — proving the ratio is session-scoped.
583+
expect(planS2.reserveTokensFloor).not.toBe(planS1.reserveTokensFloor);
584+
expect(planS2.softThresholdTokens).not.toBe(planS1.softThresholdTokens);
585+
});
519586
});
520587

521588
describe("incrementCompactionCount", () => {

src/config/types.agent-defaults.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,12 @@ export type AgentCompactionConfig = {
388388
* Fraction of the model's contextWindowTokens used as the reserve-tokens target
389389
* (0.01-0.9). Wins over `reserveTokens` when both are set, which lets a single
390390
* config scale correctly across heterogeneous context windows (e.g. 200k vs 1M).
391+
*
392+
* The base window is the context window of the model active for the CURRENT
393+
* SESSION (resolved per-run from session model overrides), falling back to the
394+
* agent's configured default model, then to DEFAULT_CONTEXT_TOKENS. This means
395+
* a single agent can host sessions on differently-sized models and the share
396+
* scales per session, not per agent config.
391397
*/
392398
reserveTokensShare?: number;
393399
/** Pi keepRecentTokens budget used for cut-point selection. */
@@ -396,6 +402,10 @@ export type AgentCompactionConfig = {
396402
* Fraction of the model's contextWindowTokens used as the keepRecentTokens budget
397403
* (0.01-0.9). Wins over `keepRecentTokens` when both are set so recent-turn
398404
* preservation scales with the window instead of being fixed in absolute tokens.
405+
*
406+
* The base window is the context window of the model active for the CURRENT
407+
* SESSION (per-run), falling back to the agent's default model, then to
408+
* DEFAULT_CONTEXT_TOKENS.
399409
*/
400410
keepRecentTokensShare?: number;
401411
/** Minimum reserve tokens enforced for Pi compaction (0 disables the floor). */
@@ -404,6 +414,10 @@ export type AgentCompactionConfig = {
404414
* Fraction of the model's contextWindowTokens used as the reserve-tokens floor
405415
* (0.01-0.9). Wins over `reserveTokensFloor` when both are set; the resolved
406416
* floor is still applied as an absolute minimum on the final reserve tokens.
417+
*
418+
* The base window is the context window of the model active for the CURRENT
419+
* SESSION (per-run), falling back to the agent's default model, then to
420+
* DEFAULT_CONTEXT_TOKENS.
407421
*/
408422
reserveTokensFloorShare?: number;
409423
/** Max share of context window for history during safeguard pruning (0.1–0.9, default 0.5). */
@@ -462,6 +476,10 @@ export type AgentCompactionMemoryFlushConfig = {
462476
* Fraction of the model's contextWindowTokens used as the memory-flush soft threshold
463477
* (0.01-0.9). Wins over `softThresholdTokens` when both are set, so the trigger point
464478
* scales with the actual context window rather than being fixed in absolute tokens.
479+
*
480+
* The base window is the context window of the model active for the CURRENT
481+
* SESSION (resolved per-run from session model overrides), falling back to the
482+
* agent's default model, then to DEFAULT_CONTEXT_TOKENS.
465483
*/
466484
softThresholdTokensShare?: number;
467485
/**

0 commit comments

Comments
 (0)