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.
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.
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