Skip to content

Commit f0057e4

Browse files
committed
test(compaction): cover production last-cycle source ttl windows
1 parent 18f315f commit f0057e4

2 files changed

Lines changed: 50 additions & 3 deletions

File tree

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ When a cycle's input tokens cross a threshold, the director compacts the inferen
202202
| `ollama` | never | Local inference has no remote cache to expire |
203203
| anything else | 10 min | Assumed OpenAI-style in-memory economics; tune per upstream |
204204

205+
Production identity for the `ollama` row is `sourceId` `ollama` / `ollama/<instance>` via `isOllamaProviderId` (`src/provider/ollama.ts`), not a bare model id: Ollama is `buildOpenAISource` (`provider: openai-compatible`, model `llama3` / `qwen3`). Slash-form `ollama/…` is a table key, not what the harness stamps.
206+
205207
- **Overflow recovery** — A `context_overflow` inference error would otherwise become a terminal error reply; the governor compacts and retries instead, bounded so a history the compactor cannot shrink does not loop forever. Overflow ignores hysteresis for the compact itself.
206208

207209
The TTL window is measured from the last `inference.done` (every inference rewrites the provider's prefix cache) with one fire per window per compact of any kind, and it never fires with a tool batch outstanding — stall pings can arrive mid-work. The consecutive-compact cap shared with the threshold path still bounds compact→infer→compact. Prompt-caching the summary call itself is an explicit non-goal: the fold carries no cache options.

‎src/agent/compaction.test.ts‎

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -793,13 +793,17 @@ describe("provider-aware idle recompress (CL-8745)", () => {
793793
function ttlInferenceDone(
794794
modelOrSource:
795795
| string
796-
| { sourceId: string; provider: string; model: string },
796+
| { sourceId?: string; provider?: string; model?: string },
797797
withTools: boolean,
798798
): Extract<ReactorInboundEvent, { type: "inference.done" }> {
799799
const source =
800800
typeof modelOrSource === "string"
801801
? { sourceId: "s", provider: "p", model: modelOrSource }
802-
: modelOrSource;
802+
: {
803+
sourceId: modelOrSource.sourceId ?? "s",
804+
provider: modelOrSource.provider ?? "p",
805+
model: modelOrSource.model ?? "m",
806+
};
803807
return {
804808
type: "inference.done",
805809
turn: {
@@ -845,7 +849,10 @@ describe("provider-aware idle recompress (CL-8745)", () => {
845849
() => nowMs,
846850
);
847851
governor.noteInferenceDone(
848-
ttlInferenceDone("anthropic/claude-opus-4-6", false),
852+
ttlInferenceDone(
853+
{ provider: "anthropic", model: "claude-opus-4-6" },
854+
false,
855+
),
849856
tenTurns,
850857
);
851858

@@ -866,6 +873,44 @@ describe("provider-aware idle recompress (CL-8745)", () => {
866873
expect(governor.resumeAfterCompact(emptyMessage())).toBe("meter");
867874
});
868875

876+
test("production LastCycleSource: anthropic fires at 5m, codex stays quiet until 10m", () => {
877+
// Harness stamps { sourceId, provider, model } with a bare model, not
878+
// slash-form "anthropic/claude-opus-4-6". Anthropic's 5-minute window
879+
// must come from provider, not a dummy model string; Codex must not
880+
// inherit that 5-minute fire from sourceId "codex/work".
881+
let nowMs = 15_000_000;
882+
const clock = () => nowMs;
883+
const anthropic = createCompactionGovernor(() => undefined, "", [], clock);
884+
const codex = createCompactionGovernor(() => undefined, "", [], clock);
885+
anthropic.noteInferenceDone(
886+
ttlInferenceDone(
887+
{ provider: "anthropic", model: "claude-opus-4-6" },
888+
false,
889+
),
890+
tenTurns,
891+
);
892+
codex.noteInferenceDone(
893+
ttlInferenceDone(
894+
{ sourceId: "codex/work", provider: "codex-responses" },
895+
false,
896+
),
897+
tenTurns,
898+
);
899+
900+
nowMs += 5 * MINUTE_MS + 1;
901+
expect(
902+
anthropic.interceptIdleContinuation(emptyMessage(), capabilities),
903+
).toEqual(ttlCompact);
904+
expect(
905+
codex.interceptIdleContinuation(emptyMessage(), capabilities),
906+
).toBeNull();
907+
908+
nowMs += 5 * MINUTE_MS;
909+
expect(
910+
codex.interceptIdleContinuation(emptyMessage(), capabilities),
911+
).toEqual(ttlCompact);
912+
});
913+
869914
test("follows provider economics: deepseek waits out its long window, ollama never fires", () => {
870915
let nowMs = 20_000_000;
871916
const clock = () => nowMs;

0 commit comments

Comments
 (0)