Compare commits

...

9 Commits

Author SHA1 Message Date
Dai Ha 656588f597 fleetd #661: add the pane-placement case to the validateAll reachability enumeration
CI / shell-tests (pull_request) Failing after 7s
CI / contract (pull_request) Successful in 53s
CI / build (pull_request) Failing after 1m40s
validateAllReachesEveryOneOfTodaysSixValidators only covered six of
the eight real validators; the new validator was reachability-tested
only from FleetConfigTest, in a different file from the one whose job
is to enumerate every validateAll-reachability case.

Add the pane-placement case to the enumeration, rename the method to
drop the hardcoded count (validateAllReachesEveryOneOfTodaysRealValidators),
and correct the surrounding claims to say seven of eight, naming
validateLeadRollover as the one case still missing (fleetd #668, not
fixed here).
2026-10-03 19:50:16 +02:00
Dai Ha e33377b2ca fleetd #661: fix LeadCount javadoc, dangling @link, and the validator-count word
CI / shell-tests (pull_request) Failing after 7s
CI / contract (pull_request) Successful in 1m3s
CI / build (pull_request) Failing after 1m50s
LeadLauncher.LeadCount's javadoc carried the same false member-space-
exclusion claim as the three comments fixed earlier in this ticket;
its neighbouring body comment in countLeads was already correct and
is unchanged.

FleetConfigValidateAllTest's canary test is renamed to drop the
number from its name (the count now lives only in the Set.of literal
and the javadoc, so the two cannot drift), which also fixes the
dangling {@link} to the old name and the stale 'seventh' wording.
2026-10-03 19:42:50 +02:00
Dai Ha f288cee2bb fleetd #661: refuse pane placement when a lead tab is configured
CI / shell-tests (pull_request) Failing after 6s
CI / contract (pull_request) Successful in 45s
CI / build (pull_request) Failing after 1m55s
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 by LeadTabScanner, which does not exclude the member space in
production. Add FleetConfig.validatePanePlacementAgainstLeadTabs(),
wired automatically into validateAll() by the existing reflective
sweep, to refuse that combination at startup.

Also correct three stale comments that claimed a member-space
exclusion already blocked this path, in LeadTabScanner, FleetConfig's
validateLeadTabPrefixes javadoc, and LeadTabScannerTest.
2026-10-03 19:34:34 +02:00
Dai Ha 9417de1123 Merge PR #662: fleetd #659 — remove the dead back-compat forms around LeadConfigDirSource/leadContextLookup
CI / shell-tests (push) Failing after 6s
CI / contract (push) Successful in 1m33s
CI / build (push) Failing after 1m58s
2026-10-03 19:11:18 +02:00
Dai Ha 03d92be751 fleetd #659: remove the dead back-compat forms around LeadConfigDirSource/leadContextLookup
CI / shell-tests (pull_request) Failing after 8s
CI / contract (pull_request) Successful in 1m41s
CI / build (pull_request) Failing after 2m31s
FleetMcp.LeadConfigDirSource's 1-arg constructor, Fleetd.leadContextLookup's
4-arg overload, and Fleetd.leadContextSource's 4-arg overload each existed only
to keep old call sites compiling, and each silently resolved no auto-compact
window — reverting any caller that picked one up to LeadContextGauge's fixed
200,000 HIGH threshold, the exact defect #637 fixed. Removed all three and
updated the 8 call sites across 4 test files to pass the window lookup
explicitly.

Added FleetdLeadContextSourceWindowAssemblyTest: no existing test called the
real assembled LeadHeartbeatLoop far enough to prove FleetdAssembly's
window-lookup argument into Fleetd.leadContextSource actually reaches the
gauge. Mutating that argument to `_ -> null` compiled clean and left the whole
suite green; the new test fails against that mutation. Needed a small FakeHerdr
addition (agentSessionId(..)) since its default agent.get response carries no
session id.
2026-10-03 18:55:24 +02:00
Dai Ha 136bec8e28 Merge PR #660: fleetd #637 — scale the lead context HIGH threshold with the effective auto-compact window
CI / shell-tests (push) Failing after 6s
CI / contract (push) Successful in 1m4s
CI / build (push) Failing after 2m35s
2026-10-03 16:27:55 +02:00
Dai Ha ae94d511d7 fleetd #637: pin the fleet_list window wiring and fold the threshold into the gauge's cache key
CI / shell-tests (pull_request) Failing after 9s
CI / contract (pull_request) Successful in 1m1s
CI / build (pull_request) Failing after 2m22s
Fleetd.leadConfigDirSource built its window argument with nothing calling the
factory itself to prove it, so a mutation to a no-op lookup left the whole
suite green. Add a wiring test that calls the factory directly, modeled on
FleetdLeadConfigDirSourceWiringTest's shape for the configDir half of the
same factory.

LeadContextGauge's result cache keyed only on (configDir, sessionId), but the
cached Reading's state now depends on the caller's resolved effective window.
Fold the derived threshold into the cache key so two reads of the same
session with different windows inside the TTL each report against their own
window.
2026-10-03 16:18:57 +02:00
Dai Ha 436b026696 fleetd #637: scale LeadContextGauge's HIGH threshold with the effective auto-compact window
HIGH_THRESHOLD_TOKENS was a hardcoded 200_000, while the event it warns about
(auto-compaction) is configured per profile via autoCompactWindow and can legally go
as low as 100_000 — making HIGH unreachable before a compaction on such a profile.

LeadContextGauge.read now takes an optional effective window and fires HIGH at 2/3 of
it, falling back to the fixed 200_000 when no window is resolvable (unresolved callers,
including the pre-existing 3-arg read(), keep today's behaviour exactly).

FleetConfig.Profile.effectiveAutoCompactWindow() resolves that window the way a
launched Claude Code session actually reads it: env.CLAUDE_CODE_AUTO_COMPACT_WINDOW
wins over the autoCompactWindow launch flag when both are set.

Wired into both real consumers: fleet_list's context row (FleetMcp.LeadConfigDirSource,
widened with a back-compat constructor so no unrelated call site changes) and the lead
heartbeat's context-high nudge (Fleetd.leadContextLookup/leadContextSource, widened the
same way).
2026-10-03 16:11:41 +02:00
Dai Ha 905fa3a454 Merge PR #658: fleetd #656 — regression tests for both redact() leaks
CI / shell-tests (push) Failing after 12s
CI / contract (push) Successful in 1m28s
CI / build (push) Failing after 1m54s
2026-10-03 16:10:38 +02:00
20 changed files with 883 additions and 65 deletions
@@ -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"));
}
}
@@ -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"),