Browse Source

feat(cache-monitor): warn when a session never hits the provider cache

The existing bust warning only arms after a session reports cached tokens
at least once (everReportedCache), so a session whose prefix changes on
every request from turn one — the field signature of the v2.2.5
checkpoint-board regression — stayed silent while re-paying full input
each turn.

Add a second warning for sessions that report zero cached tokens on every
sizeable request past both a consecutive-request and a cumulative
uncached-input threshold. OpenCode coalesces missing provider telemetry to
zeros, so a cache-less provider is indistinguishable from a busted
session; the dual threshold keeps modest cache-less sessions silent and
the wording hedged. Fires once per session, observation only.
Alvin Unreal 2 weeks ago
parent
commit
ca10a59c3d
3 changed files with 129 additions and 7 deletions
  1. 17 6
      docs/cache-verification.md
  2. 57 1
      src/hooks/cache-monitor/index.test.ts
  3. 55 0
      src/hooks/cache-monitor/index.ts

+ 17 - 6
docs/cache-verification.md

@@ -45,12 +45,23 @@ the Prompt Cache Safety section in `AGENTS.md` for the authoring rules.
 ## Runtime cache monitoring
 
 `src/hooks/cache-monitor/` observes `message.updated` events and the
-provider-reported `tokens.cache.read` / `tokens.cache.write` counters. When a
-session that previously hit the cache reports zero cache-read tokens on a
-sizeable request, it logs a `[cache-monitor] possible prompt-cache bust`
-warning (once per bust streak) to the plugin log. Providers that never report
-cache telemetry produce no warnings. This is the field safety net for
-provider-side behavior no offline test can model.
+provider-reported `tokens.cache.read` / `tokens.cache.write` counters. It
+logs two warning shapes (observation only, to the plugin log):
+
+- `possible prompt-cache bust` — a session that previously hit the cache
+  reports zero cache-read tokens on a sizeable request (once per bust
+  streak). The signature of a mid-session prompt-prefix change.
+- `never hit the provider cache` — a session reports zero cached tokens on
+  every sizeable request from its first turn, past both a consecutive-request
+  and a cumulative-uncached-input threshold (once per session). The signature
+  of a prefix that changes on *every* request — how the v2.2.5
+  checkpoint-board regression looked in the field. OpenCode coalesces missing
+  provider telemetry to zeros, so a cache-less provider is indistinguishable
+  from this; the thresholds and hedged wording keep that ambiguity from
+  becoming noise, and modest sessions on cache-less providers stay silent.
+
+This is the field safety net for provider-side behavior no offline test can
+model.
 
 ## Prerequisites
 

+ 57 - 1
src/hooks/cache-monitor/index.test.ts

@@ -100,9 +100,12 @@ describe('createCacheMonitorHook', () => {
     expect(warnings).toHaveLength(2);
   });
 
-  test('stays silent for providers that never report cache tokens', async () => {
+  test('stays silent for modest sessions that never report cache tokens', async () => {
     const { hook, warnings } = createHarness();
 
+    // Cache-less providers are indistinguishable from busted sessions
+    // (OpenCode coalesces missing telemetry to zeros); below the cumulative
+    // input threshold the monitor must give them the benefit of the doubt.
     for (const id of ['c1', 'c2', 'c3']) {
       await hook.event(
         assistantMessageEvent({ messageID: id, input: 20000, cacheRead: 0 }),
@@ -112,6 +115,59 @@ describe('createCacheMonitorHook', () => {
     expect(warnings).toHaveLength(0);
   });
 
+  test('warns once for a large session that never hits the cache', async () => {
+    const { hook, warnings } = createHarness();
+
+    // The v2.2.5 checkpoint-board signature: consecutive ~146K-input
+    // requests, zero cache reads from the very first turn.
+    for (const id of ['g1', 'g2']) {
+      await hook.event(
+        assistantMessageEvent({ messageID: id, input: 146000, cacheRead: 0 }),
+      );
+    }
+    expect(warnings).toHaveLength(0);
+
+    await hook.event(
+      assistantMessageEvent({ messageID: 'g3', input: 146000, cacheRead: 0 }),
+    );
+    expect(warnings).toHaveLength(1);
+    expect(warnings[0].message).toContain('never hit the provider cache');
+    expect(warnings[0].data).toMatchObject({
+      sessionID: 'ses_monitor',
+      consecutiveUncachedRequests: 3,
+      uncachedInputTokens: 438000,
+    });
+
+    // Once per session, even as the streak keeps growing.
+    await hook.event(
+      assistantMessageEvent({ messageID: 'g4', input: 146000, cacheRead: 0 }),
+    );
+    expect(warnings).toHaveLength(1);
+  });
+
+  test('any reported cache activity disarms the never-cached warning', async () => {
+    const { hook, warnings } = createHarness();
+
+    // An Anthropic-style first request reports a cache write; later misses
+    // are the everReportedCache bust signature, not the never-cached one.
+    await hook.event(
+      assistantMessageEvent({
+        messageID: 'h1',
+        input: 146000,
+        cacheRead: 0,
+        cacheWrite: 140000,
+      }),
+    );
+    for (const id of ['h2', 'h3', 'h4']) {
+      await hook.event(
+        assistantMessageEvent({ messageID: id, input: 146000, cacheRead: 0 }),
+      );
+    }
+
+    expect(warnings).toHaveLength(1);
+    expect(warnings[0].message).toContain('prompt-cache bust');
+  });
+
   test('stays silent on the first request and on tiny prompts', async () => {
     const { hook, warnings } = createHarness();
 

+ 55 - 0
src/hooks/cache-monitor/index.ts

@@ -25,11 +25,30 @@ const MIN_INPUT_TOKENS_FOR_WARNING = 2048;
 const MAX_TRACKED_SESSIONS = 256;
 const MAX_TRACKED_MESSAGES_PER_SESSION = 512;
 
+/**
+ * A session busted from its very first request never trips the
+ * `everReportedCache` warning below — that was the field signature of the
+ * v2.2.5 checkpoint board regression, where every request re-paid full
+ * input from turn one and the monitor stayed silent.
+ *
+ * OpenCode coalesces missing provider cache telemetry to zeros, so explicit
+ * zeros cannot distinguish "prefix changes every request" from "provider
+ * has no prompt cache". Both thresholds must be met before warning — at
+ * least this many consecutive sizeable zero-cache requests AND this much
+ * cumulative uncached input — so the warning only fires where a working
+ * cache would have saved a large amount, and the wording stays hedged.
+ */
+const NEVER_CACHED_STREAK_FOR_WARNING = 3;
+const NEVER_CACHED_INPUT_TOKENS_FOR_WARNING = 100_000;
+
 interface SessionCacheState {
   completedRequests: number;
   everReportedCache: boolean;
   lastCacheRead: number;
   warnedSinceLastHit: boolean;
+  neverCachedStreak: number;
+  neverCachedInputTokens: number;
+  neverCachedWarned: boolean;
   processedMessageIDs: Set<string>;
 }
 
@@ -118,6 +137,9 @@ export function createCacheMonitorHook(options: CacheMonitorOptions = {}) {
       everReportedCache: false,
       lastCacheRead: 0,
       warnedSinceLastHit: false,
+      neverCachedStreak: 0,
+      neverCachedInputTokens: 0,
+      neverCachedWarned: false,
       processedMessageIDs: new Set(),
     };
     sessions.set(sessionID, state);
@@ -152,6 +174,39 @@ export function createCacheMonitorHook(options: CacheMonitorOptions = {}) {
       );
     }
 
+    // A session that never serves a single cached token, over enough
+    // sizeable requests that a working cache would have saved a large
+    // amount, is busted from turn one — it never arms the
+    // everReportedCache warning above. Small requests neither extend nor
+    // reset the streak: they sit under provider minimum-prefix thresholds
+    // and legitimately miss.
+    if (!state.everReportedCache) {
+      if (
+        message.cacheRead === 0 &&
+        message.cacheWrite === 0 &&
+        message.inputTokens >= MIN_INPUT_TOKENS_FOR_WARNING
+      ) {
+        state.neverCachedStreak += 1;
+        state.neverCachedInputTokens += message.inputTokens;
+      }
+      if (
+        !state.neverCachedWarned &&
+        state.neverCachedStreak >= NEVER_CACHED_STREAK_FOR_WARNING &&
+        state.neverCachedInputTokens >= NEVER_CACHED_INPUT_TOKENS_FOR_WARNING
+      ) {
+        state.neverCachedWarned = true;
+        logger(
+          '[cache-monitor] session has never hit the provider cache: every sizeable request reported 0 cache-read tokens. If this provider supports prompt caching, the prompt prefix is likely changing on every request; if not, this session is re-paying full input each turn — see docs/cache-verification.md.',
+          {
+            sessionID: message.sessionID,
+            requestNumber: state.completedRequests,
+            consecutiveUncachedRequests: state.neverCachedStreak,
+            uncachedInputTokens: state.neverCachedInputTokens,
+          },
+        );
+      }
+    }
+
     if (message.cacheRead > 0) state.warnedSinceLastHit = false;
     state.everReportedCache =
       state.everReportedCache ||