|
|
|
@@ -76,6 +76,7 @@ public final class LeadHeartbeatLoop {
|
|
|
|
|
private final Metrics metrics; // CB-512 pattern: nullable — no registry in unit tests
|
|
|
|
|
private final LeadContextSource contextSource; // fleetd #609
|
|
|
|
|
private final boolean contextHighNudge; // fleetd #609: opt-in, like the loop itself
|
|
|
|
|
private final boolean requireOperatorConfirm; // fleetd #621: mirrors leadRollover.requireOperatorConfirm
|
|
|
|
|
|
|
|
|
|
/** When the current idle stretch began (nanos), or {@link #NOT_IDLE}. Single scheduler thread only. */
|
|
|
|
|
private long idleSinceNanos = NOT_IDLE;
|
|
|
|
@@ -106,12 +107,32 @@ public final class LeadHeartbeatLoop {
|
|
|
|
|
* fleetd #609: as above, plus the lead's own context source and whether a HIGH reading should
|
|
|
|
|
* append a hand-over notice to the loop's nudge. Pass {@link LeadContextSource#none()} and
|
|
|
|
|
* {@code false} to keep the pre-#609 behaviour exactly (both existing public constructors do).
|
|
|
|
|
*
|
|
|
|
|
* <p>fleetd #621: delegates to the full constructor with {@code requireOperatorConfirm=true} —
|
|
|
|
|
* the pre-#621 wording ("ask the operator ... only the operator can approve the roll") assumed
|
|
|
|
|
* the config default, so every caller of this overload keeps that text byte-identical.
|
|
|
|
|
*/
|
|
|
|
|
public LeadHeartbeatLoop(PrimaryRegistry primaryRegistry, AgentControl agents, ReplyInbox inbox,
|
|
|
|
|
Supplier<List<MemberSession>> roster, ReplyPushLoop pushLoop,
|
|
|
|
|
ScheduledExecutorService scheduler, LongSupplier clock,
|
|
|
|
|
long idleAfterNanos, long backoffMs, int quietNudgeCap, Metrics metrics,
|
|
|
|
|
LeadContextSource contextSource, boolean contextHighNudge) {
|
|
|
|
|
this(primaryRegistry, agents, inbox, roster, pushLoop, scheduler, clock,
|
|
|
|
|
idleAfterNanos, backoffMs, quietNudgeCap, metrics, contextSource, contextHighNudge, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* fleetd #621: as above, plus the daemon's effective {@code leadRollover.requireOperatorConfirm}
|
|
|
|
|
* value — threaded into {@link #contextNotice(boolean, LeadContextGauge.Reading, boolean, boolean)}
|
|
|
|
|
* so the notice's wording tracks the config the daemon actually enforces (see {@code
|
|
|
|
|
* LeadRollover.confirm}) instead of always asserting the operator gate is on.
|
|
|
|
|
*/
|
|
|
|
|
public LeadHeartbeatLoop(PrimaryRegistry primaryRegistry, AgentControl agents, ReplyInbox inbox,
|
|
|
|
|
Supplier<List<MemberSession>> roster, ReplyPushLoop pushLoop,
|
|
|
|
|
ScheduledExecutorService scheduler, LongSupplier clock,
|
|
|
|
|
long idleAfterNanos, long backoffMs, int quietNudgeCap, Metrics metrics,
|
|
|
|
|
LeadContextSource contextSource, boolean contextHighNudge,
|
|
|
|
|
boolean requireOperatorConfirm) {
|
|
|
|
|
this.primaryRegistry = primaryRegistry;
|
|
|
|
|
this.agents = agents;
|
|
|
|
|
this.inbox = inbox;
|
|
|
|
@@ -125,6 +146,7 @@ public final class LeadHeartbeatLoop {
|
|
|
|
|
this.metrics = metrics;
|
|
|
|
|
this.contextSource = contextSource;
|
|
|
|
|
this.contextHighNudge = contextHighNudge;
|
|
|
|
|
this.requireOperatorConfirm = requireOperatorConfirm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -344,7 +366,7 @@ public final class LeadHeartbeatLoop {
|
|
|
|
|
// d.contextNotified() is the value to persist once delivery is confirmed, not the value the text
|
|
|
|
|
// itself should be built from. Otherwise a HIGH stretch that is still latched would never see the
|
|
|
|
|
// notice at all, defeating the very check this fixes.
|
|
|
|
|
String notice = contextNotice(contextHighNudge, reading, contextNotified);
|
|
|
|
|
String notice = contextNotice(contextHighNudge, reading, contextNotified, requireOperatorConfirm);
|
|
|
|
|
var lead = primaryRegistry.primaryTerminal();
|
|
|
|
|
boolean sent = lead.isPresent() && trySend(lead.get(), fleet.nudgeText() + notice, notice);
|
|
|
|
|
// The latch becomes true only when all three hold: decide() chose to notify, a notice was
|
|
|
|
@@ -381,8 +403,9 @@ public final class LeadHeartbeatLoop {
|
|
|
|
|
/**
|
|
|
|
|
* fleetd #609: the text appended to a nudge when the lead's own context is full — {@code ""}
|
|
|
|
|
* whenever the notice does not apply, so callers can unconditionally append this without an extra
|
|
|
|
|
* branch. Wording stays plain (CEFR B1) and honest that only the operator approves a roll — this
|
|
|
|
|
* loop only ever prints text, it never calls {@code fleet_handover} itself.
|
|
|
|
|
* branch. Wording stays plain (CEFR B1) and honest about who actually gates the roll — see the
|
|
|
|
|
* {@code requireOperatorConfirm} overload (fleetd #621) for which check that is. This loop only
|
|
|
|
|
* ever prints text, it never calls {@code fleet_handover} itself.
|
|
|
|
|
*
|
|
|
|
|
* @param enabled the {@code leadHeartbeat.contextHighNudge} config flag
|
|
|
|
|
* @param reading the lead's current {@link LeadContextGauge} reading
|
|
|
|
@@ -401,9 +424,33 @@ public final class LeadHeartbeatLoop {
|
|
|
|
|
* closing sentence ("You will not be told again until your context reads ok.") false. {@link
|
|
|
|
|
* #injectNudge} is the only caller that passes a non-default {@code alreadyNotified}.
|
|
|
|
|
*
|
|
|
|
|
* <p>fleetd #621: delegates with {@code requireOperatorConfirm=true} — the pre-#621 default and the
|
|
|
|
|
* value every existing caller of this overload (including every test written before #621) already
|
|
|
|
|
* assumed, so the text this overload returns stays byte-identical.
|
|
|
|
|
*
|
|
|
|
|
* @param alreadyNotified whether the lead has already been told about the current HIGH stretch
|
|
|
|
|
*/
|
|
|
|
|
static String contextNotice(boolean enabled, LeadContextGauge.Reading reading, boolean alreadyNotified) {
|
|
|
|
|
return contextNotice(enabled, reading, alreadyNotified, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* fleetd #621: as {@link #contextNotice(boolean, LeadContextGauge.Reading, boolean)}, but the closing
|
|
|
|
|
* instructions also track the daemon's effective {@code leadRollover.requireOperatorConfirm} value,
|
|
|
|
|
* instead of always asserting that only the operator can approve the roll.
|
|
|
|
|
*
|
|
|
|
|
* <p>{@code LeadRollover.confirm(...)} already honours this flag: when it is {@code false}, the daemon
|
|
|
|
|
* itself gates the roll on the three handover-file checks alone (exists, modified after the {@code
|
|
|
|
|
* open()} request, and no older than {@code maxDocAgeSeconds}) and never consults {@code
|
|
|
|
|
* operatorConfirmed}. Before this parameter existed, this notice told the lead to ask the operator
|
|
|
|
|
* regardless — so a lead that followed its own instructions asked anyway, and setting the config knob
|
|
|
|
|
* to {@code false} stopped the daemon refusing the roll without stopping the operator being
|
|
|
|
|
* interrupted. This parameter is how the text is kept honest about which gate is actually live.
|
|
|
|
|
*
|
|
|
|
|
* @param requireOperatorConfirm the effective {@code leadRollover.requireOperatorConfirm} value
|
|
|
|
|
*/
|
|
|
|
|
static String contextNotice(boolean enabled, LeadContextGauge.Reading reading, boolean alreadyNotified,
|
|
|
|
|
boolean requireOperatorConfirm) {
|
|
|
|
|
if (!enabled || alreadyNotified || reading.state() != LeadContextGauge.State.HIGH) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
@@ -420,10 +467,18 @@ public final class LeadHeartbeatLoop {
|
|
|
|
|
sb.append(" (").append(reading.compactions()).append(' ').append(compactionWord)
|
|
|
|
|
.append(" so far).");
|
|
|
|
|
}
|
|
|
|
|
sb.append(" A fresh session would work better. To hand over: call fleet_handover(action=\"open\"), "
|
|
|
|
|
+ "write the file it names, ask the operator, then call fleet_handover(action=\"confirm\", "
|
|
|
|
|
+ "token, operatorConfirmed). Only the operator can approve the roll. You will not be told "
|
|
|
|
|
+ "again until your context reads ok.");
|
|
|
|
|
if (requireOperatorConfirm) {
|
|
|
|
|
sb.append(" A fresh session would work better. To hand over: call fleet_handover(action=\"open\"), "
|
|
|
|
|
+ "write the file it names, ask the operator, then call fleet_handover(action=\"confirm\", "
|
|
|
|
|
+ "token, operatorConfirmed). Only the operator can approve the roll. You will not be told "
|
|
|
|
|
+ "again until your context reads ok.");
|
|
|
|
|
} else {
|
|
|
|
|
sb.append(" A fresh session would work better. To hand over: call fleet_handover(action=\"open\"), "
|
|
|
|
|
+ "write the file it names, then call fleet_handover(action=\"confirm\", token). Decide for "
|
|
|
|
|
+ "yourself when to confirm: the roll goes through if the handover file exists, was "
|
|
|
|
|
+ "changed after you opened it, and is not older than maxDocAgeSeconds. You will not be "
|
|
|
|
|
+ "told again until your context reads ok.");
|
|
|
|
|
}
|
|
|
|
|
return sb.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|