fleetd #609: nudge an idle lead to hand over when its own context reads HIGH #610

Merged
ltms merged 3 commits from worker/fleetd-609-context-high-nudge-01f62b-1 into main 2026-09-20 12:37:43 +02:00
Member

Closes fleetd #609.

What changed

LeadHeartbeatLoop can now append a text-only notice to its heartbeat nudge when
the idle lead's own LeadContextGauge reading is HIGH, gated by the new
leadHeartbeat.contextHighNudge config key (default off — an upgraded daemon
never silently acquires the behavior).

  • LeadContextGauge.Reading.unknown() widened from package-private to public,
    so LeadHeartbeatLoop.LeadContextSource.none() (a different package) can
    return the same inert "I could not look" reading without a parallel constant.
    No other change to LeadContextGauge.
  • FleetConfig.LeadHeartbeat gains a 4th component, contextHighNudge. A 3-arg
    convenience constructor keeps every pre-existing call site compiling unchanged.
  • LeadHeartbeatLoop.decide takes the lead's own LeadContextGauge.State and a
    contextNotified latch. The latch fires the notice once per HIGH stretch, is
    cleared only by a later OK reading (never by UNKNOWN, which means "could
    not look," not "got better"), and never spends the quietNudgeCap budget.
    This never rolls a pane by itself — only the operator can approve a handover,
    via the lead's own fleet_handover.
  • Fleetd wires a new leadContextLookup / leadContextSource factory pair
    (mirroring the existing leadConfigDirLookup idiom) into main()'s
    LeadHeartbeatLoop construction.
  • fleetd.example.yaml documents the new key (required by
    FleetConfigTest.everyNestedConfigKeyIsDocumentedInTheExample — outside the
    strict 3-file scope, but needed for the existing test to pass).

Ticket comment (id 17381) addressed

  • The nudge text drops the token clause instead of printing null when
    reading.tokens() is null (defensive; today a HIGH reading always carries
    non-null tokens, verified in LeadContextGauge.java) — covered by
    contextNoticeOmitsTheTokenClauseRatherThanPrintingNull.
  • "No existing test file may need an edit to compile": grep -rln "new LeadHeartbeatLoop(" src/test/java shows only LeadHeartbeatLoopTest.java
    constructs LeadHeartbeatLoop directly. Its edits were driven entirely by the
    decide() signature expansion this ticket's design explicitly asked for
    (adding the context/contextNotified parameters), not by any inadequacy in
    the new constructor's defaults — the two pre-existing public constructors are
    byte-for-byte unchanged and no caller of them needed any edit.

Tests

mvn -o clean install from fleetd/:

Tests run: 1860, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

New tests: 13 added to LeadHeartbeatLoopTest.java (properties A-H for
decide()'s context/latch behavior, plus 5 contextNotice tests), plus two new
files, FleetdLeadContextLookupTest.java (4 tests) and
FleetdLeadContextSourceWiringTest.java (2 tests), covering the new Fleetd
factories end to end with a real transcript file.

Mutation testing (per the ticket's requirement)

Mutation 1 — made decide()'s latch always clear on any non-HIGH reading
(including UNKNOWN, not just OK) — real red:

dFlappingBetweenHighAndUnknownNeverReInjectsWhileLatched FAILED
  "UNKNOWN means 'I could not look', not 'it got better' — it must not clear the latch"
  expected: <true> but was: <false>
Tests run: 27, Failures: 1

Mutation 2 — made the contextHigh && !latch gate ignore the latch (always
INJECT on HIGH) — real red:

cASecondTickWithTheLatchAlreadySetDoesNotTellTheLeadAgain FAILED
  "the lead was already told once about this HIGH stretch — telling it every tick would be nagging, not a notice"
  expected: <QUIET_DONE> but was: <INJECT>
dFlappingBetweenHighAndUnknownNeverReInjectsWhileLatched FAILED (side effect)
Tests run: 27, Failures: 2

Both mutations reverted; mvn -o test -Dtest=LeadHeartbeatLoopTest green again
before committing.

Closes fleetd #609. ## What changed `LeadHeartbeatLoop` can now append a text-only notice to its heartbeat nudge when the idle lead's own `LeadContextGauge` reading is `HIGH`, gated by the new `leadHeartbeat.contextHighNudge` config key (default off — an upgraded daemon never silently acquires the behavior). - `LeadContextGauge.Reading.unknown()` widened from package-private to public, so `LeadHeartbeatLoop.LeadContextSource.none()` (a different package) can return the same inert "I could not look" reading without a parallel constant. No other change to `LeadContextGauge`. - `FleetConfig.LeadHeartbeat` gains a 4th component, `contextHighNudge`. A 3-arg convenience constructor keeps every pre-existing call site compiling unchanged. - `LeadHeartbeatLoop.decide` takes the lead's own `LeadContextGauge.State` and a `contextNotified` latch. The latch fires the notice once per HIGH stretch, is cleared only by a later `OK` reading (never by `UNKNOWN`, which means "could not look," not "got better"), and never spends the `quietNudgeCap` budget. This never rolls a pane by itself — only the operator can approve a handover, via the lead's own `fleet_handover`. - `Fleetd` wires a new `leadContextLookup` / `leadContextSource` factory pair (mirroring the existing `leadConfigDirLookup` idiom) into `main()`'s `LeadHeartbeatLoop` construction. - `fleetd.example.yaml` documents the new key (required by `FleetConfigTest.everyNestedConfigKeyIsDocumentedInTheExample` — outside the strict 3-file scope, but needed for the existing test to pass). ## Ticket comment (id 17381) addressed - The nudge text drops the token clause instead of printing `null` when `reading.tokens()` is null (defensive; today a `HIGH` reading always carries non-null tokens, verified in `LeadContextGauge.java`) — covered by `contextNoticeOmitsTheTokenClauseRatherThanPrintingNull`. - "No existing test file may need an edit to compile": `grep -rln "new LeadHeartbeatLoop(" src/test/java` shows only `LeadHeartbeatLoopTest.java` constructs `LeadHeartbeatLoop` directly. Its edits were driven entirely by the `decide()` signature expansion this ticket's design explicitly asked for (adding the `context`/`contextNotified` parameters), not by any inadequacy in the new constructor's defaults — the two pre-existing public constructors are byte-for-byte unchanged and no caller of them needed any edit. ## Tests `mvn -o clean install` from `fleetd/`: ``` Tests run: 1860, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` New tests: 13 added to `LeadHeartbeatLoopTest.java` (properties A-H for `decide()`'s context/latch behavior, plus 5 `contextNotice` tests), plus two new files, `FleetdLeadContextLookupTest.java` (4 tests) and `FleetdLeadContextSourceWiringTest.java` (2 tests), covering the new `Fleetd` factories end to end with a real transcript file. ### Mutation testing (per the ticket's requirement) Mutation 1 — made `decide()`'s latch always clear on any non-HIGH reading (including `UNKNOWN`, not just `OK`) — real red: ``` dFlappingBetweenHighAndUnknownNeverReInjectsWhileLatched FAILED "UNKNOWN means 'I could not look', not 'it got better' — it must not clear the latch" expected: <true> but was: <false> Tests run: 27, Failures: 1 ``` Mutation 2 — made the `contextHigh && !latch` gate ignore the latch (always `INJECT` on HIGH) — real red: ``` cASecondTickWithTheLatchAlreadySetDoesNotTellTheLeadAgain FAILED "the lead was already told once about this HIGH stretch — telling it every tick would be nagging, not a notice" expected: <QUIET_DONE> but was: <INJECT> dFlappingBetweenHighAndUnknownNeverReInjectsWhileLatched FAILED (side effect) Tests run: 27, Failures: 2 ``` Both mutations reverted; `mvn -o test -Dtest=LeadHeartbeatLoopTest` green again before committing.
agent added 1 commit 2026-09-20 12:15:14 +02:00
fleetd #609: nudge an idle lead to hand over when its own context reads HIGH
CI / shell-tests (pull_request) Failing after 6s
CI / contract (pull_request) Successful in 48s
CI / build (pull_request) Failing after 2m5s
60496831c2
LeadHeartbeatLoop can now append a text-only notice to its nudge when the lead's
own LeadContextGauge reading is HIGH and leadHeartbeat.contextHighNudge is on.
Fires once per HIGH stretch (a latch, cleared only by a later OK reading; UNKNOWN
neither sets nor clears it), never spends the quietNudgeCap budget, and never
rolls a pane itself — only the operator can approve a handover.

- LeadContextGauge.Reading.unknown() widened to public for LeadContextSource.none()
- FleetConfig.LeadHeartbeat gains contextHighNudge (null/false = off, unchanged default)
- LeadHeartbeatLoop.decide gains context/contextNotified; Fleetd wires a new
  leadContextLookup/leadContextSource factory pair (LeadHeartbeatLoop.LeadContextSource)
- fleetd.example.yaml documents the new key
ltms added 1 commit 2026-09-20 12:21:00 +02:00
fleetd #609 review: repair a garbled comment carried over from the brief
CI / shell-tests (pull_request) Failing after 8s
CI / contract (pull_request) Successful in 1m32s
CI / build (pull_request) Failing after 1m41s
d7390ccd37
The brief's sentence about a null token count at HIGH was broken, and the
worker copied it into the source verbatim. The code was already right; only
the comment was unreadable.

Says what is actually true: a HIGH reading always carries a non-null token
count today, because LeadContextGauge only reaches HIGH by comparing a number
against HIGH_THRESHOLD_TOKENS. That invariant lives in another class and
nothing asserts it, so the branch stays.
agent added 1 commit 2026-09-20 12:34:23 +02:00
fleetd #609 review: the context latch must mean the notice reached the pane
CI / shell-tests (pull_request) Failing after 7s
CI / contract (pull_request) Successful in 49s
CI / build (pull_request) Failing after 2m6s
89cb8ff79b
Fixes the PR #610 review blocker: LeadHeartbeatLoop committed contextNotified
before injectNudge attempted the send, so a transient herdr failure marked the
lead as told when nothing reached its pane, and contextNotice() carried no
latch at all, so a pending-driven INJECT re-appended the notice on every tick
while the context stayed HIGH.

- injectNudge now reports whether agents.send succeeded and persists
  contextNotified only when a notice was actually included in the text and
  the send did not throw. The latch is split out of applyDecision (kept for
  idleSinceNanos/quietCount, applied unconditionally as before) so it is
  written on the success path only, once per branch in tick().
- contextNotice gained an overloaded 3-arg form gated on the latch as it
  stood before the tick's decision; the existing 2-arg form delegates to it
  with alreadyNotified=false, so all pre-existing callers/tests are unchanged.
- tick() is now package-private (mirrors ReplyPushLoop#tick(String)) so tests
  can drive the real send path with a fake AgentControl instead of only the
  pure decide() function.
- Added tests I-L covering: a failed send does not consume the notice and
  retries; a successful send does; the text is gated when the latch is
  already set; and the notice appears exactly once across three differently
  driven INJECTs.

Both required mutations verified red and reverted:
1. Setting the latch from the Decision regardless of send outcome -> test I
   (iAFailedSendDoesNotConsumeTheNotice) fails.
2. Dropping the latch argument at the contextNotice call site -> tests K
   (kAPendingDrivenInjectWithTheLatchAlreadySetSendsNoNotice) and L
   (lTheNoticeAppearsExactlyOnceAcrossThreeDifferentlyDrivenInjects) fail.
ltms merged commit 955b9ea013 into main 2026-09-20 12:37:43 +02:00
ltms deleted branch worker/fleetd-609-context-high-nudge-01f62b-1 2026-09-20 12:37:43 +02:00
Sign in to join this conversation.