diff --git a/fleetd/src/main/java/dev/ltms/fleet/lead/LeadContextGauge.java b/fleetd/src/main/java/dev/ltms/fleet/lead/LeadContextGauge.java index 13c925a6..4a4925f6 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/lead/LeadContextGauge.java +++ b/fleetd/src/main/java/dev/ltms/fleet/lead/LeadContextGauge.java @@ -68,8 +68,10 @@ import java.util.function.LongSupplier; * (never the whole 52 MB a long-lived transcript reaches on the host this was measured on), and * {@link #DEFAULT_CACHE_TTL_MILLIS} bounds how often that bounded read actually happens — a burst * of {@code fleet_list} calls inside one TTL window reads the file once. One instance's cache is - * keyed by {@code (configDir, sessionId)}, so it is safe to share across every lead a single - * {@code fleet_list} call reports on. + * keyed by {@code (configDir, sessionId, highThreshold)}, so it is safe to share across every lead + * a single {@code fleet_list} call reports on, and a call that resolves a different effective + * window for the same lead never reads back a state computed against the other window's + * threshold. */ public final class LeadContextGauge { @@ -192,13 +194,14 @@ public final class LeadContextGauge { String base = (configDir == null || configDir.isBlank()) ? System.getProperty("user.home") + "/.claude" : configDir; - String cacheKey = base + '\u0000' + sessionId; + long highThreshold = highThreshold(effectiveWindowTokens); + String cacheKey = base + '\u0000' + sessionId + '\u0000' + highThreshold; long now = clock.getAsLong(); CacheEntry cached = cache.get(cacheKey); if (cached != null && now - cached.readAtMillis() < ttlMillis) { return cached.reading(); } - Reading fresh = readUncached(base, sessionId, highThreshold(effectiveWindowTokens)); + Reading fresh = readUncached(base, sessionId, highThreshold); cache.put(cacheKey, new CacheEntry(fresh, now)); return fresh; } diff --git a/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadConfigDirSourceWindowWiringTest.java b/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadConfigDirSourceWindowWiringTest.java new file mode 100644 index 00000000..75c09de1 --- /dev/null +++ b/fleetd/src/test/java/dev/ltms/fleet/FleetdLeadConfigDirSourceWindowWiringTest.java @@ -0,0 +1,63 @@ +package dev.ltms.fleet; + +import dev.ltms.fleet.config.FleetConfig; +import dev.ltms.fleet.mcp.FleetMcp; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Pins {@link Fleetd#leadConfigDirSource}'s own wiring of the window lookup into the returned + * {@link FleetMcp.LeadConfigDirSource}, not only the detached {@link Fleetd#leadContextWindowLookup} + * factory it delegates to. Calls the producer directly, with real {@link FleetConfig.Profile}/ + * {@link FleetConfig.Leader} fixtures, and asserts on {@code windowFor()} — the companion of + * {@link FleetdLeadConfigDirSourceWiringTest}, which pins the same factory's {@code configDirFor()}. + */ +class FleetdLeadConfigDirSourceWindowWiringTest { + + private static FleetConfig.Profile profileWithWindow(String name, Integer autoCompactWindow) { + return new FleetConfig.Profile(name, null, "claude-sonnet-5", null, null, null, + "tab", "fleet", "w #{n}", null, null, null, null, null, null, null, + null, null, true, null, null, null, null, null, autoCompactWindow, null); + } + + private static FleetConfig.Leader leadOnProfile(String profile) { + return new FleetConfig.Leader(profile, "lead: primary", 1, "lead:", 10, "claude", "claude-sonnet-5"); + } + + @Test + @DisplayName("the returned source resolves the lead's REAL configured effective window, not a hardcoded null") + void resolvesTheRealConfiguredWindow() { + Map profiles = Map.of("opus", profileWithWindow("opus", 250_000)); + Map leaders = Map.of("primary", leadOnProfile("opus")); + + FleetMcp.LeadConfigDirSource source = Fleetd.leadConfigDirSource(() -> profiles, leaders); + + assertEquals(250_000L, source.windowFor().apply("primary"), + "windowFor must delegate to the real leadContextWindowLookup, not a stub that always " + + "returns null"); + } + + @Test + @DisplayName("a lead on a profile with no window configured still resolves to null, not a crash") + void leadWithNoWindowConfiguredResolvesToNull() { + Map profiles = Map.of("opus", profileWithWindow("opus", null)); + Map leaders = Map.of("primary", leadOnProfile("opus")); + + FleetMcp.LeadConfigDirSource source = Fleetd.leadConfigDirSource(() -> profiles, leaders); + + assertNull(source.windowFor().apply("primary")); + } + + @Test + @DisplayName("an unrecognised lead name resolves to null, not a thrown exception") + void unrecognisedLeadNameResolvesToNull() { + FleetMcp.LeadConfigDirSource source = Fleetd.leadConfigDirSource(Map::of, Map.of()); + + assertNull(source.windowFor().apply("ghost-lead")); + } +} diff --git a/fleetd/src/test/java/dev/ltms/fleet/lead/LeadContextGaugeHighThresholdTest.java b/fleetd/src/test/java/dev/ltms/fleet/lead/LeadContextGaugeHighThresholdTest.java index 33dd5208..3fee37a7 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/lead/LeadContextGaugeHighThresholdTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/lead/LeadContextGaugeHighThresholdTest.java @@ -88,4 +88,23 @@ class LeadContextGaugeHighThresholdTest { legacyGauge.read(legacyConfigDir, SESSION_ID, "claude").state(), "the 3-arg read() (no window argument at all) must behave exactly like passing a null window"); } + + @Test + @DisplayName("a second read with a different window, within the TTL, reports against its own window, not the first call's cached state") + void aSecondReadWithADifferentWindowWithinTheTtlReportsAgainstItsOwnWindow(@TempDir Path tmp) throws IOException { + String configDir = writeTranscript(tmp, SESSION_ID, 90_000); + long[] now = {0L}; + LeadContextGauge gauge = new LeadContextGauge(() -> now[0], 5_000); + + LeadContextGauge.Reading first = gauge.read(configDir, SESSION_ID, "claude", 100_000L); + assertEquals(LeadContextGauge.State.HIGH, first.state(), + "90,000 tokens against a 100,000 window is HIGH"); + + now[0] += 1_000; // stays inside the 5,000ms TTL — the cache key must still vary with the window + LeadContextGauge.Reading second = gauge.read(configDir, SESSION_ID, "claude", 1_000_000L); + + assertEquals(LeadContextGauge.State.OK, second.state(), + "90,000 tokens against a 1,000,000 window must report OK regardless of the previous call's " + + "window, even while that call's cache entry is still within its TTL"); + } }