Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 656588f597 | |||
| e33377b2ca | |||
| f288cee2bb | |||
| 9417de1123 | |||
| 03d92be751 | |||
| 136bec8e28 | |||
| ae94d511d7 | |||
| 436b026696 | |||
| 905fa3a454 |
@@ -1075,7 +1075,33 @@ public final class Fleetd {
|
||||
*/
|
||||
static FleetMcp.LeadConfigDirSource leadConfigDirSource(Supplier<Map<String, FleetConfig.Profile>> profiles,
|
||||
Map<String, FleetConfig.Leader> leaders) {
|
||||
return new FleetMcp.LeadConfigDirSource(leadConfigDirLookup(profiles, leaders));
|
||||
return new FleetMcp.LeadConfigDirSource(leadConfigDirLookup(profiles, leaders),
|
||||
leadContextWindowLookup(profiles, leaders));
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-lead-name factory for the effective auto-compact window {@link LeadContextGauge} scales
|
||||
* its HIGH threshold against — the same {@code fleet.leaders.<name>.profile} link {@link
|
||||
* #leadConfigDirLookup} already follows, one step further to that profile's own {@link
|
||||
* FleetConfig.Profile#effectiveAutoCompactWindow()}. A lead entry that names no
|
||||
* {@code profile:}, or whose named profile is not configured, or whose profile resolves no
|
||||
* window at all, returns {@code null} — {@link LeadContextGauge} then falls back to its own
|
||||
* fixed HIGH threshold.
|
||||
*/
|
||||
static Function<String, Long> leadContextWindowLookup(Supplier<Map<String, FleetConfig.Profile>> profiles,
|
||||
Map<String, FleetConfig.Leader> leaders) {
|
||||
return leadName -> {
|
||||
FleetConfig.Leader lead = leaders.get(leadName);
|
||||
if (lead == null || lead.profile() == null || lead.profile().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
FleetConfig.Profile leadProfile = profiles.get().get(lead.profile());
|
||||
if (leadProfile == null) {
|
||||
return null;
|
||||
}
|
||||
Integer window = leadProfile.effectiveAutoCompactWindow();
|
||||
return window == null ? null : window.longValue();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1097,22 +1123,30 @@ public final class Fleetd {
|
||||
* @param liveLeadTerminals terminal id → lead name for every CURRENTLY recognised lead
|
||||
* @param configDirForLeadName lead name → {@code configDir}, normally {@link
|
||||
* #leadConfigDirLookup}'s return
|
||||
* @param windowForLeadName lead name → that lead's profile's effective auto-compact window,
|
||||
* normally {@link #leadContextWindowLookup}'s return, and passed
|
||||
* through to {@link LeadContextGauge#read} so the heartbeat's own
|
||||
* HIGH reading scales with that lead's real window, or {@code null}
|
||||
* when it cannot be resolved — either way {@link LeadContextGauge}
|
||||
* falls back to its own fixed HIGH threshold
|
||||
*/
|
||||
static Function<String, LeadContextGauge.Reading> leadContextLookup(LeadContextGauge gauge, AgentControl agents,
|
||||
Supplier<Map<String, String>> liveLeadTerminals, Function<String, String> configDirForLeadName) {
|
||||
Supplier<Map<String, String>> liveLeadTerminals, Function<String, String> configDirForLeadName,
|
||||
Function<String, Long> windowForLeadName) {
|
||||
return terminal -> {
|
||||
String leadName = liveLeadTerminals.get().get(terminal);
|
||||
if (leadName == null) {
|
||||
return LeadContextGauge.Reading.unknown();
|
||||
}
|
||||
String configDir = configDirForLeadName.apply(leadName);
|
||||
Long effectiveWindow = windowForLeadName.apply(leadName);
|
||||
Agent live;
|
||||
try {
|
||||
live = agents.get(terminal);
|
||||
} catch (RuntimeException e) {
|
||||
return LeadContextGauge.Reading.unknown();
|
||||
}
|
||||
return gauge.read(configDir, live.sessionId(), live.agentType());
|
||||
return gauge.read(configDir, live.sessionId(), live.agentType(), effectiveWindow);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1124,9 +1158,10 @@ public final class Fleetd {
|
||||
* (see {@code FleetdLeadConfigDirSourceWiringTest}'s javadoc for the measured gap this shape closes).
|
||||
*/
|
||||
static LeadHeartbeatLoop.LeadContextSource leadContextSource(LeadContextGauge gauge, AgentControl agents,
|
||||
Supplier<Map<String, String>> liveLeadTerminals, Function<String, String> configDirForLeadName) {
|
||||
Supplier<Map<String, String>> liveLeadTerminals, Function<String, String> configDirForLeadName,
|
||||
Function<String, Long> windowForLeadName) {
|
||||
return new LeadHeartbeatLoop.LeadContextSource(
|
||||
leadContextLookup(gauge, agents, liveLeadTerminals, configDirForLeadName));
|
||||
leadContextLookup(gauge, agents, liveLeadTerminals, configDirForLeadName, windowForLeadName));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -405,7 +405,8 @@ final class FleetdAssembly {
|
||||
TimeUnit.SECONDS.toNanos(hb.idleAfterSeconds()), hb.backoffMs(), hb.quietNudgeCap(),
|
||||
metrics,
|
||||
Fleetd.leadContextSource(leadContextGauge, router.leadAgents(), leads,
|
||||
Fleetd.leadConfigDirLookup(() -> config.get().profiles(), leaders)),
|
||||
Fleetd.leadConfigDirLookup(() -> config.get().profiles(), leaders),
|
||||
Fleetd.leadContextWindowLookup(() -> config.get().profiles(), leaders)),
|
||||
Boolean.TRUE.equals(hb.contextHighNudge()), requireOperatorConfirm);
|
||||
heartbeat.start();
|
||||
} else {
|
||||
|
||||
@@ -798,6 +798,25 @@ public record FleetConfig(
|
||||
return isSubscription() ? SUBSCRIPTION_CREDENTIAL_ID : profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* The auto-compaction window a launched Claude Code session actually runs on: {@code env:
|
||||
* CLAUDE_CODE_AUTO_COMPACT_WINDOW} when it parses as an integer, since that environment
|
||||
* variable wins over the {@code --autocompact} flag {@link #autoCompactWindow} produces (see
|
||||
* {@code ClaudeCodeArguments}); {@link #autoCompactWindow} otherwise. {@code null} when
|
||||
* neither resolves to a usable number.
|
||||
*/
|
||||
public Integer effectiveAutoCompactWindow() {
|
||||
String envValue = env.get(CLAUDE_CODE_AUTO_COMPACT_WINDOW_ENV);
|
||||
if (envValue == null) {
|
||||
return autoCompactWindow;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(envValue.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return autoCompactWindow;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when this profile's workers are granted a forge token to open their own PR (CB-302). */
|
||||
public boolean hasGitToken() {
|
||||
return gitTokenEnv != null && !gitTokenEnv.isBlank();
|
||||
@@ -2184,6 +2203,12 @@ public record FleetConfig(
|
||||
static final int AUTO_COMPACT_WINDOW_MIN = 100_000;
|
||||
/** Highest {@code autoCompactWindow} Claude Code's {@code --autocompact <tokens>} flag accepts. */
|
||||
static final int AUTO_COMPACT_WINDOW_MAX = 1_000_000;
|
||||
/**
|
||||
* The {@code env:} key a launched Claude Code session reads for its auto-compaction window,
|
||||
* ahead of the {@code --autocompact} launch flag {@code autoCompactWindow} produces (see
|
||||
* {@link Profile#effectiveAutoCompactWindow()}).
|
||||
*/
|
||||
static final String CLAUDE_CODE_AUTO_COMPACT_WINDOW_ENV = "CLAUDE_CODE_AUTO_COMPACT_WINDOW";
|
||||
|
||||
/**
|
||||
* Reject a profile whose {@code autoCompactWindow:} is set but outside the token band Claude
|
||||
@@ -2265,13 +2290,13 @@ public record FleetConfig(
|
||||
if (!(entry.getValue() instanceof Map<?, ?> profile)
|
||||
|| !(profile.get("autoCompactWindow") instanceof Number window)
|
||||
|| !(profile.get("env") instanceof Map<?, ?> env)
|
||||
|| !env.containsKey("CLAUDE_CODE_AUTO_COMPACT_WINDOW")) {
|
||||
|| !env.containsKey(CLAUDE_CODE_AUTO_COMPACT_WINDOW_ENV)) {
|
||||
continue;
|
||||
}
|
||||
Object kind = profile.get("kind");
|
||||
boolean claudeCode = kind == null || String.valueOf(kind).isBlank()
|
||||
|| Profile.KIND_CLAUDE_CODE.equalsIgnoreCase(String.valueOf(kind));
|
||||
Object envValue = env.get("CLAUDE_CODE_AUTO_COMPACT_WINDOW");
|
||||
Object envValue = env.get(CLAUDE_CODE_AUTO_COMPACT_WINDOW_ENV);
|
||||
if (claudeCode && !String.valueOf(window).equals(String.valueOf(envValue))) {
|
||||
String name = String.valueOf(entry.getKey());
|
||||
names.add(name);
|
||||
@@ -2663,9 +2688,10 @@ public record FleetConfig(
|
||||
* tab labels — every member gets one rendered into its tab. Choose a lead {@code tabPrefix} that
|
||||
* a member template matches and the daemon starts labelling its own members as leads, promoting
|
||||
* the entire fleet to {@link dev.ltms.fleet.auth.Role#PRIMARY} with no message and no diff.
|
||||
* The member-space exclusion in {@link dev.ltms.fleet.herdr.LeadTabScanner} already blocks the
|
||||
* realistic path, but defence that depends on one workspace label holding is not defence enough
|
||||
* for a privilege boundary.
|
||||
* {@link #validatePanePlacementAgainstLeadTabs()} is the check that stops a pane-placed member
|
||||
* from landing inside a lead's tab in the first place; this check is a second, independent
|
||||
* guard that catches the hazard even when every profile places members correctly, by refusing
|
||||
* a label that a scan would still misread as a lead.
|
||||
*
|
||||
* <p>CB-557 shrank this check rather than removing it. The default template is
|
||||
* {@code "{role}: {profile} #{n}"} and {@code {role}} comes from a closed enum, so a
|
||||
@@ -2712,6 +2738,45 @@ public record FleetConfig(
|
||||
+ "lead tabs cannot be confused.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a profile that places its members by {@code "pane"} while any {@code fleet.leaders}
|
||||
* entry names a {@code tab}. A pane-placed member lands inside the focused tab rather than its
|
||||
* own, so it can land inside a lead's own labelled tab. {@link
|
||||
* dev.ltms.fleet.herdr.LeadTabScanner} identifies a lead purely by that tab's label — it does
|
||||
* not exclude the member space — so a member that ends up there would be read back as the lead
|
||||
* and granted spawn/stop/send on the whole fleet.
|
||||
*
|
||||
* <p>Only a leader with a non-blank {@code tab} is in scope: one with no {@code tab} feeds
|
||||
* nothing into {@link dev.ltms.fleet.herdr.LeadTabScanner}, so it creates no hazard here.
|
||||
*
|
||||
* @throws IllegalStateException when any {@code profiles:} entry is pane-placed while any
|
||||
* {@code fleet.leaders} entry names a non-blank {@code tab}
|
||||
*/
|
||||
public void validatePanePlacementAgainstLeadTabs() {
|
||||
if (fleet == null || fleet.leaders().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean anyLeaderHasTab = fleet.leaders().values().stream()
|
||||
.anyMatch(leader -> leader != null && leader.tab() != null && !leader.tab().isBlank());
|
||||
if (!anyLeaderHasTab) {
|
||||
return;
|
||||
}
|
||||
List<String> bad = new ArrayList<>();
|
||||
profiles().entrySet().stream()
|
||||
.filter(e -> !e.getValue().tabPlacement())
|
||||
.map(Map.Entry::getKey)
|
||||
.sorted()
|
||||
.forEach(bad::add);
|
||||
if (bad.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("refusing to start: profile(s) " + bad
|
||||
+ " use placement: pane while fleet.leaders names a tab. A pane-placed member can "
|
||||
+ "land inside a lead's labelled tab and be read back as the lead, granted "
|
||||
+ "spawn/stop/send on the whole fleet. Set placement: tab for each named profile, "
|
||||
+ "or remove the tab from every fleet.leaders entry.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a present {@code leadRollover:} block with no (or a blank) {@code handoverPath}
|
||||
* (fleetd #480). There is no sane non-null default for an operator-specific file path, unlike
|
||||
|
||||
@@ -37,8 +37,11 @@ import java.util.function.Supplier;
|
||||
* <p><strong>Direction of trust.</strong> The label names the lead; it never <em>grants</em>
|
||||
* anything a pane could take for itself. Three properties keep that honest:
|
||||
* <ol>
|
||||
* <li>Worker spaces are excluded wholesale ({@code excludedWorkspaceLabels}), so a worker cannot
|
||||
* become a lead by being placed — as a split, say — inside a matching tab.</li>
|
||||
* <li>{@code excludedWorkspaceLabels} can filter a workspace out of the scan, but this class does
|
||||
* not by itself stop a worker from landing inside a matching tab — a caller may pass an empty
|
||||
* set, and the daemon does. The guard against that is {@code
|
||||
* FleetConfig.validatePanePlacementAgainstLeadTabs}: it refuses, at startup, any profile that
|
||||
* places members by pane while a lead names a tab.</li>
|
||||
* <li>A worker cannot rename a tab: {@code tab.rename} is reachable only through
|
||||
* {@link WorkspaceControl}, which no {@code fleet_*} tool exposes. The label is writable by
|
||||
* the human at the terminal and by nobody the bridge is defending against.</li>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -97,14 +99,19 @@ public final class LeadContextGauge {
|
||||
static final long DEFAULT_CACHE_TTL_MILLIS = 5_000;
|
||||
|
||||
/**
|
||||
* Live tokens at or above this count report {@link State#HIGH}. On the host this was measured
|
||||
* on, auto-compaction actually fires around 267,000–270,000 tokens, but the point of a HIGH
|
||||
* state is to warn before that happens, not at it — 200,000 is the standard Claude context
|
||||
* window size and a sensible built-in default: no config key is required to pick it, and a
|
||||
* lead crossing it is already deep enough into its window that a compaction is foreseeable.
|
||||
* Fallback HIGH threshold used when a caller resolves no effective auto-compact window for the
|
||||
* lead being read (see {@link #read(String, String, String, Long)}) — the built-in default so
|
||||
* no config key is required to get a warning at all.
|
||||
*/
|
||||
static final long HIGH_THRESHOLD_TOKENS = 200_000;
|
||||
|
||||
/**
|
||||
* The fraction of a resolved effective auto-compact window that HIGH warns at, so the warning
|
||||
* margin scales with the window instead of only ever meaning something against the fixed
|
||||
* {@link #HIGH_THRESHOLD_TOKENS} fallback.
|
||||
*/
|
||||
static final double HIGH_THRESHOLD_FRACTION = 2.0 / 3.0;
|
||||
|
||||
/** The only peer kind this reader understands ({@code Agent.agentType()}'s wire value). */
|
||||
private static final String CLAUDE_AGENT_TYPE = "claude";
|
||||
|
||||
@@ -167,6 +174,17 @@ public final class LeadContextGauge {
|
||||
* transcript format
|
||||
*/
|
||||
public Reading read(String configDir, String sessionId, String agentType) {
|
||||
return read(configDir, sessionId, agentType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param effectiveWindowTokens the caller's resolved effective auto-compact window for this
|
||||
* lead's own profile, or {@code null} when it cannot be resolved.
|
||||
* HIGH fires at {@link #HIGH_THRESHOLD_FRACTION} of this value;
|
||||
* {@code null} (or a non-positive value) falls back to the fixed
|
||||
* {@link #HIGH_THRESHOLD_TOKENS}
|
||||
*/
|
||||
public Reading read(String configDir, String sessionId, String agentType, Long effectiveWindowTokens) {
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return Reading.unknown();
|
||||
}
|
||||
@@ -176,18 +194,27 @@ 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);
|
||||
Reading fresh = readUncached(base, sessionId, highThreshold);
|
||||
cache.put(cacheKey, new CacheEntry(fresh, now));
|
||||
return fresh;
|
||||
}
|
||||
|
||||
private Reading readUncached(String base, String sessionId) {
|
||||
/** {@link #HIGH_THRESHOLD_FRACTION} of {@code effectiveWindowTokens}, or the fixed fallback. */
|
||||
private static long highThreshold(Long effectiveWindowTokens) {
|
||||
if (effectiveWindowTokens == null || effectiveWindowTokens <= 0) {
|
||||
return HIGH_THRESHOLD_TOKENS;
|
||||
}
|
||||
return (long) (effectiveWindowTokens * HIGH_THRESHOLD_FRACTION);
|
||||
}
|
||||
|
||||
private Reading readUncached(String base, String sessionId, long highThreshold) {
|
||||
diskReads.incrementAndGet();
|
||||
Path file = findTranscript(base, sessionId);
|
||||
if (file == null) {
|
||||
@@ -199,7 +226,7 @@ public final class LeadContextGauge {
|
||||
} catch (IOException e) {
|
||||
return Reading.unknown();
|
||||
}
|
||||
return parse(tail);
|
||||
return parse(tail, highThreshold);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +285,7 @@ public final class LeadContextGauge {
|
||||
* read raced — see the "torn final line" section of the class javadoc) is skipped, not fatal.
|
||||
* Only when none of the remaining lines parse does this report {@link State#UNKNOWN}.
|
||||
*/
|
||||
private Reading parse(TailRead tail) {
|
||||
private Reading parse(TailRead tail, long highThreshold) {
|
||||
String text = new String(tail.bytes(), StandardCharsets.UTF_8);
|
||||
List<String> lines = new ArrayList<>(List.of(text.split("\n", -1)));
|
||||
if (!lines.isEmpty() && lines.get(lines.size() - 1).isEmpty()) {
|
||||
@@ -299,7 +326,7 @@ public final class LeadContextGauge {
|
||||
if (tokens == null) {
|
||||
return new Reading(State.UNKNOWN, null, compactions);
|
||||
}
|
||||
State state = tokens >= HIGH_THRESHOLD_TOKENS ? State.HIGH : State.OK;
|
||||
State state = tokens >= highThreshold ? State.HIGH : State.OK;
|
||||
return new Reading(state, tokens, compactions);
|
||||
}
|
||||
|
||||
|
||||
@@ -201,8 +201,8 @@ public final class LeadLauncher {
|
||||
/**
|
||||
* How many live leads exist per configured name, and which of that name's labelled tabs are
|
||||
* <em>not</em> live: a running agent in a tab labelled with that lead's exact {@code tab}
|
||||
* (CB-579). Member workspaces are excluded, exactly as the scanner excludes them: a member must
|
||||
* not be counted as a lead because it happens to sit in a matching tab.
|
||||
* (CB-579). A member sitting in the same shared workspace is not counted as a lead because its
|
||||
* tab carries a different label, not because any workspace is excluded from this count.
|
||||
*
|
||||
* <p>There used to be a second path here — a running agent on the terminal a
|
||||
* {@code fleet.leaders.<name>.terminal} pin named, for a lead opened and pinned by hand. That
|
||||
|
||||
@@ -284,10 +284,15 @@ public final class FleetMcp {
|
||||
* no profile, or that profile sets no {@code configDir} override — either
|
||||
* way {@link LeadContextGauge} then falls back to its own built-in default,
|
||||
* exactly as before this ticket
|
||||
* @param windowFor lead name → that lead's profile's effective auto-compact window (see
|
||||
* {@code dev.ltms.fleet.config.FleetConfig.Profile
|
||||
* #effectiveAutoCompactWindow()}), or {@code null} when it cannot be
|
||||
* resolved — either way {@link LeadContextGauge} falls back to its own
|
||||
* fixed HIGH threshold
|
||||
*/
|
||||
public record LeadConfigDirSource(Function<String, String> configDirFor) {
|
||||
/** Inert source — every lead reads {@link LeadContextGauge}'s built-in default {@code configDir}. */
|
||||
public static LeadConfigDirSource none() { return new LeadConfigDirSource(_ -> null); }
|
||||
public record LeadConfigDirSource(Function<String, String> configDirFor, Function<String, Long> windowFor) {
|
||||
/** Inert source — every lead reads {@link LeadContextGauge}'s built-in defaults. */
|
||||
public static LeadConfigDirSource none() { return new LeadConfigDirSource(_ -> null, _ -> null); }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2153,7 +2158,8 @@ public final class FleetMcp {
|
||||
m.put("self", true);
|
||||
}
|
||||
String configDir = leadConfigDirs.configDirFor().apply(name);
|
||||
m.put("context", contextView(contextGauge, live, configDir));
|
||||
Long effectiveWindow = leadConfigDirs.windowFor().apply(name);
|
||||
m.put("context", contextView(contextGauge, live, configDir, effectiveWindow));
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -2163,12 +2169,15 @@ public final class FleetMcp {
|
||||
* {@code fleet.leaders.<name>.profile} → that profile's own {@code configDir:} — or {@code null}
|
||||
* when the lead's entry names no profile, or that profile sets no override, in which case
|
||||
* {@link LeadContextGauge#read} falls back to its own built-in default
|
||||
* ({@code <user.home>/.claude}).
|
||||
* ({@code <user.home>/.claude}). {@code effectiveWindowTokens} is the same lead's resolved
|
||||
* auto-compact window, or {@code null} when it cannot be resolved, in which case the gauge
|
||||
* falls back to its own fixed HIGH threshold instead.
|
||||
*/
|
||||
private static Map<String, Object> contextView(LeadContextGauge contextGauge, Agent live, String configDir) {
|
||||
private static Map<String, Object> contextView(LeadContextGauge contextGauge, Agent live, String configDir,
|
||||
Long effectiveWindowTokens) {
|
||||
String sessionId = live == null ? null : live.sessionId();
|
||||
String agentType = live == null ? null : live.agentType();
|
||||
LeadContextGauge.Reading reading = contextGauge.read(configDir, sessionId, agentType);
|
||||
LeadContextGauge.Reading reading = contextGauge.read(configDir, sessionId, agentType, effectiveWindowTokens);
|
||||
Map<String, Object> c = new LinkedHashMap<>();
|
||||
c.put("state", reading.state().name().toLowerCase());
|
||||
if (reading.tokens() != null) {
|
||||
|
||||
@@ -461,9 +461,9 @@ public final class LeadHeartbeatLoop {
|
||||
.append(reading.compactions()).append(' ').append(compactionWord).append(" so far.");
|
||||
} else {
|
||||
// A HIGH reading always carries a non-null token count today: LeadContextGauge only
|
||||
// reaches HIGH by comparing a number against HIGH_THRESHOLD_TOKENS. That invariant
|
||||
// lives in another class and nothing asserts it, so this branch does not rely on it —
|
||||
// it drops the token clause rather than printing "null tokens".
|
||||
// reaches HIGH by comparing a number against a threshold. That invariant lives in
|
||||
// another class and nothing asserts it, so this branch does not rely on it — it drops
|
||||
// the token clause rather than printing "null tokens".
|
||||
sb.append(" (").append(reading.compactions()).append(' ').append(compactionWord)
|
||||
.append(" so far).");
|
||||
}
|
||||
|
||||
@@ -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<String, FleetConfig.Profile> profiles = Map.of("opus", profileWithWindow("opus", 250_000));
|
||||
Map<String, FleetConfig.Leader> 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<String, FleetConfig.Profile> profiles = Map.of("opus", profileWithWindow("opus", null));
|
||||
Map<String, FleetConfig.Leader> 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"));
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ class FleetdLeadContextLookupTest {
|
||||
@DisplayName("an unrecognised terminal resolves to UNKNOWN, not a thrown exception")
|
||||
void unrecognisedTerminalResolvesToUnknown() {
|
||||
Function<String, LeadContextGauge.Reading> lookup = Fleetd.leadContextLookup(
|
||||
new LeadContextGauge(), throwingAgentControl(), Map::of, name -> null);
|
||||
new LeadContextGauge(), throwingAgentControl(), Map::of, name -> null, name -> null);
|
||||
|
||||
LeadContextGauge.Reading reading = assertDoesNotThrow(() -> lookup.apply("ghost-terminal"));
|
||||
|
||||
@@ -112,7 +112,7 @@ class FleetdLeadContextLookupTest {
|
||||
void agentsGetThrowingDegradesToUnknown() {
|
||||
Map<String, String> liveLeadTerminals = Map.of(LEAD_TERMINAL, LEAD_NAME);
|
||||
Function<String, LeadContextGauge.Reading> lookup = Fleetd.leadContextLookup(
|
||||
new LeadContextGauge(), throwingAgentControl(), () -> liveLeadTerminals, name -> null);
|
||||
new LeadContextGauge(), throwingAgentControl(), () -> liveLeadTerminals, name -> null, name -> null);
|
||||
|
||||
LeadContextGauge.Reading reading = assertDoesNotThrow(() -> lookup.apply(LEAD_TERMINAL));
|
||||
|
||||
@@ -128,7 +128,7 @@ class FleetdLeadContextLookupTest {
|
||||
AgentControl agents = agentControlStub(SESSION_ID, "claude", "idle");
|
||||
Function<String, LeadContextGauge.Reading> lookup = Fleetd.leadContextLookup(
|
||||
new LeadContextGauge(), agents, () -> liveLeadTerminals,
|
||||
name -> LEAD_NAME.equals(name) ? tmp.toString() : null);
|
||||
name -> LEAD_NAME.equals(name) ? tmp.toString() : null, name -> null);
|
||||
|
||||
LeadContextGauge.Reading reading = lookup.apply(LEAD_TERMINAL);
|
||||
|
||||
@@ -146,7 +146,7 @@ class FleetdLeadContextLookupTest {
|
||||
AgentControl agents = agentControlStub(SESSION_ID, "opencode", "idle");
|
||||
Function<String, LeadContextGauge.Reading> lookup = Fleetd.leadContextLookup(
|
||||
new LeadContextGauge(), agents, () -> liveLeadTerminals,
|
||||
name -> LEAD_NAME.equals(name) ? tmp.toString() : null);
|
||||
name -> LEAD_NAME.equals(name) ? tmp.toString() : null, name -> null);
|
||||
|
||||
LeadContextGauge.Reading reading = lookup.apply(LEAD_TERMINAL);
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package dev.ltms.fleet;
|
||||
|
||||
import dev.ltms.fleet.config.ConfigRef;
|
||||
import dev.ltms.fleet.config.FleetConfig;
|
||||
import dev.ltms.fleet.guard.SubscriptionGuard;
|
||||
import dev.ltms.fleet.herdr.FakeHerdr;
|
||||
import dev.ltms.fleet.herdr.HerdrClient;
|
||||
import dev.ltms.fleet.lead.LeadContextGauge;
|
||||
import dev.ltms.fleet.msg.LeadHeartbeatLoop;
|
||||
import dev.ltms.fleet.msg.ReplyInbox;
|
||||
import io.javalin.Javalin;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* fleetd #659: {@code FleetdAssembly.java} wires {@code Fleetd.leadContextSource}'s window-lookup
|
||||
* argument with {@code Fleetd.leadContextWindowLookup(() -> config.get().profiles(), leaders)} —
|
||||
* but nothing called the real assembled {@link LeadHeartbeatLoop} far enough to prove that
|
||||
* argument is the one the live heartbeat reads through. Measured: swapping that one call-site
|
||||
* argument for {@code _ -> null} compiles with 0 errors and leaves the full suite green.
|
||||
*
|
||||
* <p>This test drives the REAL {@link LeadHeartbeatLoop} the real {@link
|
||||
* FleetdAssembly#assembleAndStart} builds, reached through {@link FleetdRuntime#heartbeat()}, and
|
||||
* reads its private {@code contextSource} field via reflection — the loop exposes no public
|
||||
* accessor for it, the same reason {@link FleetdLeadConfigDirSourceAssemblyTest} reflects on
|
||||
* {@code FleetMcp.leadConfigDirs}. The configured profile's {@code autoCompactWindow: 100000}
|
||||
* resolves a HIGH threshold of {@code 66666} ({@link LeadContextGauge}'s {@code 2/3} fraction) —
|
||||
* far below the fixed {@code 200000} fallback a lost window argument would silently revert to.
|
||||
* {@code 90000} live tokens sits between the two: HIGH under the real window, OK under the
|
||||
* fallback — a property the fallback can never produce by accident.
|
||||
*/
|
||||
class FleetdLeadContextSourceWindowAssemblyTest {
|
||||
|
||||
private static final String LEAD_NAME = "opus";
|
||||
private static final String LEAD_TAB = "lead: opus";
|
||||
private static final String LEAD_PROFILE = "sonnet";
|
||||
/** {@code FakeHerdr}'s own default {@code agent.list} entry: terminal {@code term_a}, session {@code sess-1111}. */
|
||||
private static final String LEAD_TERMINAL = "term_a";
|
||||
private static final String LEAD_SESSION_ID = "sess-1111";
|
||||
|
||||
private static final class RecordingResourcePorts implements ResourcePorts {
|
||||
|
||||
final FakeHerdr herdr = new FakeHerdr();
|
||||
final SentinelReplyInbox replyInbox = new SentinelReplyInbox();
|
||||
|
||||
@Override
|
||||
public Map<String, String> environment() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HerdrClient connectHerdr(Path socketPath) {
|
||||
return herdr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fleetd.AmqpOpener replyInboxOpener() {
|
||||
return (uri, prefetch) -> replyInbox;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fleetd.LeadMailboxOpener leadMailboxOpener() {
|
||||
return (uri, selfCoordId, prefetch) -> {
|
||||
throw new UnsupportedOperationException(
|
||||
"leadMailboxOpener must not be called — no coordinator: block is configured");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongSupplier nanoClock() {
|
||||
return System::nanoTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongSupplier wallClockNanos() {
|
||||
return System::nanoTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledExecutorService newScheduler(String purpose) {
|
||||
return Executors.newSingleThreadScheduledExecutor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addShutdownHook(Runnable hook) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startHttp(Javalin app, String host, int port) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable herdrPollWait() {
|
||||
// Never invoked: this test's FakeHerdr answers immediately, so awaitHerdr never polls.
|
||||
return () -> {
|
||||
throw new UnsupportedOperationException("herdrPollWait must not be called — herdr is healthy");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SentinelReplyInbox implements ReplyInbox, AutoCloseable {
|
||||
@Override
|
||||
public void own(String target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release(String target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(String target, String msgId, String content) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InboxMessage> peek(String target) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ack(String target, String msgId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
|
||||
private static String usageLine(long tokens) {
|
||||
return "{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"usage\":{"
|
||||
+ "\"input_tokens\":" + tokens + ",\"cache_read_input_tokens\":0,\"cache_creation_input_tokens\":0}}}";
|
||||
}
|
||||
|
||||
/** Lays out {@code <configDir>/projects/<anySlug>/<sessionId>.jsonl} carrying one usage record. */
|
||||
private static void writeTranscript(Path configDir, String sessionId, long tokens) throws IOException {
|
||||
Path projectDir = configDir.resolve("projects").resolve("some-project-slug");
|
||||
Files.createDirectories(projectDir);
|
||||
Files.writeString(projectDir.resolve(sessionId + ".jsonl"), usageLine(tokens) + "\n", StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static FleetConfig writeConfig(Path dir, String configDir) throws Exception {
|
||||
Path f = dir.resolve("fleetd.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
host: 127.0.0.1
|
||||
port: 8765
|
||||
idleSleepGuard:
|
||||
enabled: false
|
||||
broker:
|
||||
uri: "amqp://fake-test-broker/vh"
|
||||
leadHeartbeat:
|
||||
idleAfterSeconds: 600
|
||||
backoffMs: 15000
|
||||
quietNudgeCap: 5
|
||||
fleet:
|
||||
leaders:
|
||||
%s:
|
||||
tab: "%s"
|
||||
profile: %s
|
||||
profiles:
|
||||
%s:
|
||||
subscription: true
|
||||
argv: ["ccs", "sonnet"]
|
||||
configDir: "%s"
|
||||
autoCompactWindow: 100000
|
||||
""".formatted(LEAD_NAME, LEAD_TAB, LEAD_PROFILE, LEAD_PROFILE, configDir));
|
||||
return FleetConfig.load(f);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static LeadHeartbeatLoop.LeadContextSource contextSourceOf(LeadHeartbeatLoop heartbeat) throws Exception {
|
||||
Field field = LeadHeartbeatLoop.class.getDeclaredField("contextSource");
|
||||
field.setAccessible(true);
|
||||
return (LeadHeartbeatLoop.LeadContextSource) field.get(heartbeat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("[BEHAVIOURAL] the real assembled heartbeat loop resolves HIGH against the lead's "
|
||||
+ "REAL configured window, not the fixed 200000 fallback a lost window argument reverts to")
|
||||
void assembledHeartbeatContextSourceResolvesTheRealConfiguredWindow(@TempDir Path dir) throws Exception {
|
||||
FleetConfig cfg = writeConfig(dir, dir.toString());
|
||||
writeTranscript(dir, LEAD_SESSION_ID, 90_000);
|
||||
ConfigRef config = new ConfigRef(dir.resolve("fleetd.yaml"), cfg);
|
||||
SubscriptionGuard guard = new SubscriptionGuard(cfg.guard().hostSet());
|
||||
RecordingResourcePorts ports = new RecordingResourcePorts();
|
||||
// Label FakeHerdr's own default pane's tab (term_a / w2:p7 / w2:t7, already carrying a live
|
||||
// agent on session sess-1111) to match fleet.leaders.opus.tab exactly, so LeadTabScanner
|
||||
// recognises it as the live "opus" lead without a second auto-launched pane.
|
||||
ports.herdr.withTab("w2", "w2:t7", LEAD_TAB).agentSessionId(LEAD_SESSION_ID);
|
||||
|
||||
FleetdRuntime runtime = FleetdAssembly.assembleAndStart(new AssemblyInputs(cfg, config, guard), ports);
|
||||
try {
|
||||
LeadHeartbeatLoop.LeadContextSource source = contextSourceOf(runtime.heartbeat());
|
||||
|
||||
LeadContextGauge.Reading reading = source.readingFor().apply(LEAD_TERMINAL);
|
||||
|
||||
assertEquals(LeadContextGauge.State.HIGH, reading.state(),
|
||||
"profiles." + LEAD_PROFILE + ".autoCompactWindow: 100000 resolves a HIGH threshold "
|
||||
+ "of 66666 tokens — 90000 live tokens must read HIGH against it. Mutating "
|
||||
+ "FleetdAssembly's window-lookup argument to `_ -> null` falls back to the "
|
||||
+ "fixed 200000 threshold, under which 90000 reads OK instead: " + reading);
|
||||
} finally {
|
||||
// Surefire runs the whole suite in one JVM fork (fleetd/pom.xml sets no forkCount /
|
||||
// reuseForks), so the scheduler/loops this assembly starts must be torn down here, on the
|
||||
// failure path too — hence try/finally rather than a bare statement at the end.
|
||||
runtime.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ class FleetdLeadContextSourceWiringTest {
|
||||
Map<String, String> liveLeadTerminals = Map.of(LEAD_TERMINAL, LEAD_NAME);
|
||||
|
||||
LeadHeartbeatLoop.LeadContextSource source = Fleetd.leadContextSource(new LeadContextGauge(),
|
||||
agentControlStub(), () -> liveLeadTerminals, name -> LEAD_NAME.equals(name) ? tmp.toString() : null);
|
||||
agentControlStub(), () -> liveLeadTerminals,
|
||||
name -> LEAD_NAME.equals(name) ? tmp.toString() : null, name -> null);
|
||||
|
||||
LeadContextGauge.Reading reading = source.readingFor().apply(LEAD_TERMINAL);
|
||||
|
||||
@@ -101,7 +102,7 @@ class FleetdLeadContextSourceWiringTest {
|
||||
@DisplayName("an unrecognised lead terminal resolves to UNKNOWN, not a thrown exception")
|
||||
void unrecognisedTerminalResolvesToUnknown() {
|
||||
LeadHeartbeatLoop.LeadContextSource source = Fleetd.leadContextSource(new LeadContextGauge(),
|
||||
agentControlStub(), Map::of, name -> null);
|
||||
agentControlStub(), Map::of, name -> null, name -> null);
|
||||
|
||||
assertEquals(LeadContextGauge.State.UNKNOWN, source.readingFor().apply("ghost-terminal").state());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package dev.ltms.fleet;
|
||||
|
||||
import dev.ltms.fleet.config.FleetConfig;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* {@link Fleetd#leadContextWindowLookup} is the factory wired into {@code
|
||||
* FleetMcp.LeadConfigDirSource} and {@code LeadHeartbeatLoop.LeadContextSource} so {@link
|
||||
* dev.ltms.fleet.lead.LeadContextGauge} scales its HIGH threshold against a lead's own profile's
|
||||
* effective auto-compact window instead of always the gauge's fixed fallback — the same {@code
|
||||
* fleet.leaders.<name>.profile} link {@link Fleetd#leadConfigDirLookup} already follows, one step
|
||||
* further to {@link FleetConfig.Profile#effectiveAutoCompactWindow()}.
|
||||
*/
|
||||
class FleetdLeadContextWindowLookupTest {
|
||||
|
||||
private static FleetConfig.Profile profileWithWindow(String name, Integer autoCompactWindow,
|
||||
Map<String, String> env) {
|
||||
return new FleetConfig.Profile(name, null, "claude-sonnet-5", null, null, null,
|
||||
"tab", "fleet", "w #{n}", null, null, null, null, null, null, env,
|
||||
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("a lead on a profile that sets autoCompactWindow resolves to that window")
|
||||
void leadOnAProfileWithAutoCompactWindowResolvesToIt() {
|
||||
Map<String, FleetConfig.Profile> profiles =
|
||||
Map.of("opus", profileWithWindow("opus", 250_000, Map.of()));
|
||||
Map<String, FleetConfig.Leader> leaders = Map.of("primary", leadOnProfile("opus"));
|
||||
Function<String, Long> lookup = Fleetd.leadContextWindowLookup(() -> profiles, leaders);
|
||||
|
||||
assertEquals(250_000L, lookup.apply("primary"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("yaml and env disagree: the lookup resolves the env value, not the yaml one")
|
||||
void yamlAndEnvDisagreeLookupResolvesTheEnvValue() {
|
||||
Map<String, FleetConfig.Profile> profiles = Map.of("opus",
|
||||
profileWithWindow("opus", 250_000, Map.of("CLAUDE_CODE_AUTO_COMPACT_WINDOW", "150000")));
|
||||
Map<String, FleetConfig.Leader> leaders = Map.of("primary", leadOnProfile("opus"));
|
||||
Function<String, Long> lookup = Fleetd.leadContextWindowLookup(() -> profiles, leaders);
|
||||
|
||||
assertEquals(150_000L, lookup.apply("primary"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a lead entry with no `profile:` resolves to null, not a thrown exception")
|
||||
void recogniseOnlyLeadWithNoProfileResolvesToNull() {
|
||||
Map<String, FleetConfig.Profile> profiles =
|
||||
Map.of("opus", profileWithWindow("opus", 250_000, Map.of()));
|
||||
FleetConfig.Leader recogniseOnly = new FleetConfig.Leader(null, "lead: primary", 1, "lead:", 10,
|
||||
"claude", "claude-sonnet-5");
|
||||
Map<String, FleetConfig.Leader> leaders = Map.of("primary", recogniseOnly);
|
||||
Function<String, Long> lookup = Fleetd.leadContextWindowLookup(() -> profiles, leaders);
|
||||
|
||||
assertNull(lookup.apply("primary"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a lead naming a profile that is not configured resolves to null, not a thrown exception")
|
||||
void leadOnAnUnconfiguredProfileResolvesToNull() {
|
||||
Map<String, FleetConfig.Leader> leaders = Map.of("primary", leadOnProfile("ghost-profile"));
|
||||
Function<String, Long> lookup = Fleetd.leadContextWindowLookup(Map::of, leaders);
|
||||
|
||||
assertNull(lookup.apply("primary"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a lead on a profile that resolves no window at all resolves to null")
|
||||
void leadOnAProfileWithNoWindowResolvesToNull() {
|
||||
Map<String, FleetConfig.Profile> profiles =
|
||||
Map.of("opus", profileWithWindow("opus", null, Map.of()));
|
||||
Map<String, FleetConfig.Leader> leaders = Map.of("primary", leadOnProfile("opus"));
|
||||
Function<String, Long> lookup = Fleetd.leadContextWindowLookup(() -> profiles, leaders);
|
||||
|
||||
assertNull(lookup.apply("primary"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an unrecognised lead name resolves to null, not a thrown exception")
|
||||
void unrecognisedLeadNameResolvesToNull() {
|
||||
Function<String, Long> lookup = Fleetd.leadContextWindowLookup(Map::of, Map.of());
|
||||
|
||||
assertNull(lookup.apply("ghost-lead"));
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package dev.ltms.fleet.config;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* {@link FleetConfig.Profile#effectiveAutoCompactWindow()} resolves the window a launched Claude
|
||||
* Code session actually runs on, not just the {@code autoCompactWindow:} launch flag — {@code env:
|
||||
* CLAUDE_CODE_AUTO_COMPACT_WINDOW} overrides that flag, so a profile setting both resolves from the
|
||||
* environment variable.
|
||||
*/
|
||||
class FleetConfigProfileEffectiveAutoCompactWindowTest {
|
||||
|
||||
private static FleetConfig.Profile profile(Integer autoCompactWindow, Map<String, String> env) {
|
||||
return new FleetConfig.Profile("sonnet", null, "claude-sonnet-5", null, null, null,
|
||||
"tab", "fleet", "w #{n}", null, null, null, null, null, null, env,
|
||||
null, null, true, null, null, null, null, null, autoCompactWindow, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("yaml and env disagree: the env var wins, not the yaml key")
|
||||
void yamlAndEnvDisagreeEnvWins() {
|
||||
FleetConfig.Profile p = profile(250_000, Map.of("CLAUDE_CODE_AUTO_COMPACT_WINDOW", "150000"));
|
||||
|
||||
assertEquals(150_000, p.effectiveAutoCompactWindow(),
|
||||
"the two inputs must give DIFFERENT thresholds (150,000 vs 250,000) and the env value must win");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("only autoCompactWindow set: that value resolves")
|
||||
void onlyAutoCompactWindowSetResolvesToIt() {
|
||||
FleetConfig.Profile p = profile(250_000, Map.of());
|
||||
|
||||
assertEquals(250_000, p.effectiveAutoCompactWindow());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("only the env var set: that value resolves")
|
||||
void onlyEnvVarSetResolvesToIt() {
|
||||
FleetConfig.Profile p = profile(null, Map.of("CLAUDE_CODE_AUTO_COMPACT_WINDOW", "150000"));
|
||||
|
||||
assertEquals(150_000, p.effectiveAutoCompactWindow());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("neither set: resolves to null")
|
||||
void neitherSetResolvesToNull() {
|
||||
FleetConfig.Profile p = profile(null, Map.of());
|
||||
|
||||
assertNull(p.effectiveAutoCompactWindow());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an unparseable env value falls back to autoCompactWindow rather than throwing")
|
||||
void unparseableEnvValueFallsBackToAutoCompactWindow() {
|
||||
FleetConfig.Profile p = profile(250_000, Map.of("CLAUDE_CODE_AUTO_COMPACT_WINDOW", "not-a-number"));
|
||||
|
||||
assertEquals(250_000, p.effectiveAutoCompactWindow());
|
||||
}
|
||||
}
|
||||
@@ -765,6 +765,92 @@ class FleetConfigTest {
|
||||
"a label that collides with a convention nobody reads is not a problem");
|
||||
}
|
||||
|
||||
// ── validatePanePlacementAgainstLeadTabs ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The hazard this guard closes: a pane-placed member lands inside the focused tab rather than
|
||||
* its own, so it can land inside a lead's labelled tab and be read back as that lead.
|
||||
*/
|
||||
@Test
|
||||
void aPanePlacedProfileWithALeadTabRefusesToStart(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("pane-hazard.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
port: 8080
|
||||
profiles:
|
||||
gx10:
|
||||
placement: pane
|
||||
fleet:
|
||||
leaders:
|
||||
opus:
|
||||
tab: "lead: opus"
|
||||
""");
|
||||
FleetConfig cfg = FleetConfig.load(f);
|
||||
|
||||
IllegalStateException e = assertThrows(IllegalStateException.class,
|
||||
cfg::validatePanePlacementAgainstLeadTabs);
|
||||
assertTrue(e.getMessage().contains("gx10"), "the message must name the offending profile");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aPanePlacedProfileWithNoLeadTabIsAllowed(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("pane-no-tab.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
port: 8080
|
||||
profiles:
|
||||
gx10:
|
||||
placement: pane
|
||||
fleet:
|
||||
leaders:
|
||||
opus:
|
||||
profile: gx10
|
||||
""");
|
||||
|
||||
assertDoesNotThrow(() -> FleetConfig.load(f).validatePanePlacementAgainstLeadTabs(),
|
||||
"a leader with no tab feeds nothing into the scanner, so pane placement is safe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aTabPlacedProfileWithALeadTabIsAllowed(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("tab-safe.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
port: 8080
|
||||
profiles:
|
||||
gx10:
|
||||
placement: tab
|
||||
fleet:
|
||||
leaders:
|
||||
opus:
|
||||
tab: "lead: opus"
|
||||
""");
|
||||
|
||||
assertDoesNotThrow(() -> FleetConfig.load(f).validatePanePlacementAgainstLeadTabs(),
|
||||
"a member in its own tab cannot land inside a lead's tab");
|
||||
}
|
||||
|
||||
/** Proves the reflective sweep behind {@code validateAll} really reaches this validator. */
|
||||
@Test
|
||||
void validateAllAlsoRefusesPanePlacementAgainstALeadTab(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("pane-hazard-sweep.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
port: 8080
|
||||
profiles:
|
||||
gx10:
|
||||
placement: pane
|
||||
fleet:
|
||||
leaders:
|
||||
opus:
|
||||
tab: "lead: opus"
|
||||
""");
|
||||
FleetConfig cfg = FleetConfig.load(f);
|
||||
|
||||
IllegalStateException e = assertThrows(IllegalStateException.class, cfg::validateAll);
|
||||
assertTrue(e.getMessage().contains("gx10"), "the message must name the offending profile");
|
||||
}
|
||||
|
||||
// ── CB-530/CB-579: the leaders registry ─────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
@@ -42,12 +42,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
* has right now. This is the proof that a future, real seventh validator on {@link
|
||||
* FleetConfig} would be swept automatically, without needing to add a real (unwanted)
|
||||
* seventh validator just to exercise the claim.</li>
|
||||
* <li>{@link #validateAllReachesEveryOneOfTodaysSixValidators()} proves {@link
|
||||
* <li>{@link #validateAllReachesEveryOneOfTodaysRealValidators()} proves {@link
|
||||
* FleetConfig#validateAll()} itself is wired to that same generic mechanism and genuinely
|
||||
* reaches each of today's six real validators — reusing the exact minimal failing
|
||||
* reaches seven of today's eight real validators — reusing the exact minimal failing
|
||||
* configurations {@code FleetConfigTest} already established for each one directly, so a
|
||||
* single call to {@code validateAll()} is shown to reproduce every one of those six
|
||||
* failures.</li>
|
||||
* single call to {@code validateAll()} is shown to reproduce every one of those seven
|
||||
* failures. The eighth, {@link FleetConfig#validateLeadRollover()}, has no case here yet —
|
||||
* a pre-existing gap tracked as fleetd #668.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Together with the direct-{@code Fleetd.main}-invocation tests in {@code
|
||||
@@ -62,8 +63,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
* the generic sweep — claim 1 proves {@link FleetConfig#invokeAllValidators} is generic, and claim
|
||||
* 2 proves {@code validateAll()} reaches today's six, and a hardcoded list satisfies both. So the
|
||||
* reflective sweep is a convenience, not the guarantee. The guarantee is {@link
|
||||
* #fleetConfigDeclaresExactlyTheseSixValidatorsToday()}: it fails the moment a seventh validator
|
||||
* is declared, which forces whoever adds it to look at this file.
|
||||
* #fleetConfigDeclaresExactlyTheseValidatorsToday()}: it fails the moment any validator is added
|
||||
* or removed, which forces whoever changes the set to look at this file.
|
||||
*/
|
||||
class FleetConfigValidateAllTest {
|
||||
|
||||
@@ -209,18 +210,19 @@ class FleetConfigValidateAllTest {
|
||||
+ "name) must all be skipped");
|
||||
}
|
||||
|
||||
// ── Claim 2: FleetConfig.validateAll() is wired to that mechanism and reaches all six today ──
|
||||
// ── Claim 2: FleetConfig.validateAll() is wired to that mechanism and reaches seven of eight today ──
|
||||
|
||||
/**
|
||||
* Reflectively enumerates {@link FleetConfig}'s own public, no-arg, void {@code validateXxx()}
|
||||
* methods (excluding {@code validateAll} itself) — the exact same filter {@link
|
||||
* FleetConfig#invokeAllValidators} applies. This is not the mechanism proof (that is claim 1,
|
||||
* above, on an unrelated class) — it is a visible denominator: today there are six, named
|
||||
* here, so a reader adding a seventh sees this assertion name the new count rather than a
|
||||
* silent pass at the old one.
|
||||
* above, on an unrelated class) — it is a visible denominator: today there are eight, named in
|
||||
* the {@code Set.of} below, so a reader adding or removing one sees this assertion name the new
|
||||
* count rather than a silent pass at the old one. The count lives only in that set, not in this
|
||||
* method's name, so the two cannot drift apart.
|
||||
*/
|
||||
@Test
|
||||
void fleetConfigDeclaresExactlyTheseSixValidatorsToday() {
|
||||
void fleetConfigDeclaresExactlyTheseValidatorsToday() {
|
||||
Set<String> names = new TreeSet<>();
|
||||
for (Method m : FleetConfig.class.getMethods()) {
|
||||
if (java.lang.reflect.Modifier.isPublic(m.getModifiers())
|
||||
@@ -233,7 +235,8 @@ class FleetConfigValidateAllTest {
|
||||
}
|
||||
assertEquals(new TreeSet<>(Set.of("validateAuthExposure", "validateLeadTabPrefixes",
|
||||
"validateSubscriptionProfiles", "validateCharters", "validateMembers",
|
||||
"validateModels", "validateLeadRollover")), names,
|
||||
"validateModels", "validateLeadRollover", "validatePanePlacementAgainstLeadTabs")),
|
||||
names,
|
||||
"FleetConfig's public validate*() methods changed. Do TWO things, in this "
|
||||
+ "order. First confirm validateAll() still delegates to "
|
||||
+ "invokeAllValidators(this) — a hardcoded list there passes every other "
|
||||
@@ -262,14 +265,17 @@ class FleetConfigValidateAllTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* The heart of claim 2: for each of today's six real validators, a minimal file that fails
|
||||
* The heart of claim 2: for seven of today's eight real validators, a minimal file that fails
|
||||
* ONLY that one — the exact fixtures {@code FleetConfigTest} uses to test each validator
|
||||
* directly — must also fail through {@link FleetConfig#validateAll()}. If a future edit to
|
||||
* {@code validateAll()} silently dropped one validator from the sweep (e.g. a typo'd name
|
||||
* filter), exactly one of these six would start passing when it must not.
|
||||
* {@code validateAll()} silently dropped one of these seven from the sweep (e.g. a typo'd name
|
||||
* filter), exactly one of them would start passing when it must not.
|
||||
*
|
||||
* <p>The eighth, {@link FleetConfig#validateLeadRollover()}, has no case here — a pre-existing
|
||||
* gap tracked as fleetd #668, not fixed by this change.
|
||||
*/
|
||||
@Test
|
||||
void validateAllReachesEveryOneOfTodaysSixValidators(@TempDir Path dir) throws Exception {
|
||||
void validateAllReachesEveryOneOfTodaysRealValidators(@TempDir Path dir) throws Exception {
|
||||
// validateAuthExposure: a non-loopback bind without token mode.
|
||||
assertValidateAllRefuses(dir, "auth-exposure.yaml", """
|
||||
bind:
|
||||
@@ -342,6 +348,20 @@ class FleetConfigValidateAllTest {
|
||||
allow:
|
||||
- model: claude-sonnet-5
|
||||
""", "rogue");
|
||||
|
||||
// validatePanePlacementAgainstLeadTabs: a pane-placed profile while a lead names a tab.
|
||||
assertValidateAllRefuses(dir, "pane-placement.yaml", """
|
||||
bind:
|
||||
host: 127.0.0.1
|
||||
port: 8765
|
||||
profiles:
|
||||
gx10:
|
||||
placement: pane
|
||||
fleet:
|
||||
leaders:
|
||||
opus:
|
||||
tab: "lead: opus"
|
||||
""", "gx10");
|
||||
}
|
||||
|
||||
private static void assertValidateAllRefuses(Path dir, String fileName, String yaml,
|
||||
|
||||
@@ -51,6 +51,7 @@ public final class FakeHerdr implements HerdrClient {
|
||||
private boolean noPanes = false;
|
||||
private volatile String agentStatus = "idle"; // steady-state agent.get status
|
||||
private volatile String agentType = "claude"; // detected agent kind on agent.get; null = undetected
|
||||
private volatile String agentSessionId = null; // agent_session.value on agent.get; null = omitted
|
||||
private volatile String readText = "worker transcript tail"; // canned agent.read output
|
||||
private int pinnedStarts = 0; // how many upcoming agent.start calls report a fixed pane
|
||||
private String pinnedStartTerminal;
|
||||
@@ -173,6 +174,16 @@ public final class FakeHerdr implements HerdrClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@code agent_session.value} that {@code agent.get} reports for {@code term_a} — the
|
||||
* default omits the field entirely (herdr not yet having resolved one), matching the real
|
||||
* daemon's own "not resolved yet" shape.
|
||||
*/
|
||||
public FakeHerdr agentSessionId(String sessionId) {
|
||||
this.agentSessionId = sessionId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make {@code agent.get} succeed normally for its first {@code okCalls} invocations, then fail
|
||||
* every call after that with {@code code} — fleetd #176 fix 1's "backend exited mid-wait"
|
||||
@@ -311,10 +322,12 @@ public final class FakeHerdr implements HerdrClient {
|
||||
}
|
||||
}
|
||||
String agentField = agentType == null ? "null" : "\"" + agentType + "\"";
|
||||
String sessionField = agentSessionId == null ? ""
|
||||
: ",\"agent_session\":{\"kind\":\"id\",\"value\":\"" + agentSessionId + "\"}";
|
||||
yield mapper.readTree(("""
|
||||
{"type":"agent_info","agent":{"terminal_id":"term_a","agent":%s,
|
||||
"agent_status":"%s","workspace_id":"w2","tab_id":"w2:t7","pane_id":"w2:p7"}}""")
|
||||
.formatted(agentField, agentStatus));
|
||||
"agent_status":"%s","workspace_id":"w2","tab_id":"w2:t7","pane_id":"w2:p7"%s}}""")
|
||||
.formatted(agentField, agentStatus, sessionField));
|
||||
}
|
||||
case "agent.read" -> mapper.readTree(mapper.writeValueAsString(
|
||||
java.util.Map.of("type", "agent_read", "read", java.util.Map.of("text", readText))));
|
||||
|
||||
@@ -232,8 +232,9 @@ class LeadTabScannerTest {
|
||||
|
||||
@Test
|
||||
void everyPaneInALeadTabResolvesAsThatLead() {
|
||||
// A human may split their own lead tab. Both panes are theirs, so both are that lead —
|
||||
// nothing fleetd placed can land here (see the worker-space test above).
|
||||
// A human may split their own lead tab. Both panes are theirs, so both are that lead.
|
||||
// A pane-placed member landing here instead is refused at startup by
|
||||
// FleetConfig.validatePanePlacementAgainstLeadTabs, not by this scanner.
|
||||
TopologyHerdr herdr = twoLeads().pane("w1:p1b", "w1:t1", "term_opus_split");
|
||||
|
||||
assertEquals("opus-5.0",
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package dev.ltms.fleet.lead;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@code LeadContextGauge}'s HIGH threshold scales with the caller's resolved effective
|
||||
* auto-compact window, so the margin it warns at makes sense against a window that can legally sit
|
||||
* as low as {@code 100_000}, not only against the fixed fallback. These properties pin that
|
||||
* scaling, its boundary, and the fallback used when no window is resolvable.
|
||||
*/
|
||||
class LeadContextGaugeHighThresholdTest {
|
||||
|
||||
private static final String SESSION_ID = "55555555-5555-5555-5555-555555555555";
|
||||
|
||||
private static String usageLine(long tokens) {
|
||||
return "{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"usage\":{"
|
||||
+ "\"input_tokens\":" + tokens + ",\"cache_read_input_tokens\":0,\"cache_creation_input_tokens\":0}}}";
|
||||
}
|
||||
|
||||
private static String writeTranscript(Path configDir, String sessionId, long tokens) throws IOException {
|
||||
Path projectDir = configDir.resolve("projects").resolve("some-project-slug");
|
||||
Files.createDirectories(projectDir);
|
||||
Files.writeString(projectDir.resolve(sessionId + ".jsonl"), usageLine(tokens) + "\n", StandardCharsets.UTF_8);
|
||||
return configDir.toString();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an effective window of 100000 reports HIGH strictly below 100000")
|
||||
void effectiveWindowOf100000ReportsHighStrictlyBelow100000(@TempDir Path tmp) throws IOException {
|
||||
// 90,000 is below the 100,000 window itself, but above a fixed 200,000 fallback would ever
|
||||
// reach — only a threshold that scales with the window can report HIGH here.
|
||||
String configDir = writeTranscript(tmp, SESSION_ID, 90_000);
|
||||
LeadContextGauge gauge = new LeadContextGauge();
|
||||
|
||||
LeadContextGauge.Reading reading = gauge.read(configDir, SESSION_ID, "claude", 100_000L);
|
||||
|
||||
assertEquals(LeadContextGauge.State.HIGH, reading.state(),
|
||||
"90,000 tokens against a 100,000 effective window must already be HIGH, with margin to spare "
|
||||
+ "before a compaction at the window itself");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("the boundary sits strictly between OK and HIGH on both sides")
|
||||
void boundarySitsStrictlyBetweenOkAndHighOnBothSides(@TempDir Path tmp) throws IOException {
|
||||
long window = 100_000L;
|
||||
long threshold = (long) (window * (2.0 / 3.0)); // 66,666
|
||||
|
||||
String belowConfigDir = writeTranscript(tmp.resolve("below"), SESSION_ID, threshold - 1);
|
||||
LeadContextGauge belowGauge = new LeadContextGauge();
|
||||
assertEquals(LeadContextGauge.State.OK,
|
||||
belowGauge.read(belowConfigDir, SESSION_ID, "claude", window).state(),
|
||||
"one token short of the threshold must stay OK");
|
||||
|
||||
String atConfigDir = writeTranscript(tmp.resolve("at"), SESSION_ID, threshold);
|
||||
LeadContextGauge atGauge = new LeadContextGauge();
|
||||
assertEquals(LeadContextGauge.State.HIGH,
|
||||
atGauge.read(atConfigDir, SESSION_ID, "claude", window).state(),
|
||||
"exactly at the threshold must already be HIGH");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("with no effective window resolvable, the threshold is still the fixed 200000 default")
|
||||
void noEffectiveWindowFallsBackToTheFixed200000Default(@TempDir Path tmp) throws IOException {
|
||||
String belowConfigDir = writeTranscript(tmp.resolve("below"), SESSION_ID, 199_999);
|
||||
LeadContextGauge belowGauge = new LeadContextGauge();
|
||||
assertEquals(LeadContextGauge.State.OK,
|
||||
belowGauge.read(belowConfigDir, SESSION_ID, "claude", null).state(),
|
||||
"one token short of the fixed default must stay OK when no window is resolvable");
|
||||
|
||||
String atConfigDir = writeTranscript(tmp.resolve("at"), SESSION_ID, 200_000);
|
||||
LeadContextGauge atGauge = new LeadContextGauge();
|
||||
assertEquals(LeadContextGauge.State.HIGH,
|
||||
atGauge.read(atConfigDir, SESSION_ID, "claude", null).state(),
|
||||
"the fixed default must still be 200,000 when no window is resolvable");
|
||||
|
||||
String legacyConfigDir = writeTranscript(tmp.resolve("legacy"), SESSION_ID, 200_000);
|
||||
LeadContextGauge legacyGauge = new LeadContextGauge();
|
||||
assertEquals(LeadContextGauge.State.HIGH,
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ class FleetMcpLeadContextGaugeWiringTest {
|
||||
LeadContextGauge contextGauge = new LeadContextGauge();
|
||||
AtomicReference<String> configuredDir = new AtomicReference<>(dirA.toString());
|
||||
FleetMcp.LeadConfigDirSource source = new FleetMcp.LeadConfigDirSource(name ->
|
||||
LEAD_NAME.equals(name) ? configuredDir.get() : null);
|
||||
LEAD_NAME.equals(name) ? configuredDir.get() : null, _ -> null);
|
||||
|
||||
String firstRead = textOf(listFleet(herdr, sessions, contextGauge, source));
|
||||
assertTrue(firstRead.contains("\"tokens\":11000"),
|
||||
|
||||
Reference in New Issue
Block a user