Act on a HIGH lead context: tell the idle lead to hand over #609
Open
opened 2026-09-20 11:54:04 +02:00 by ltms
·
3 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#609
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
LeadContextGauge(#602, #606) can now read how full a lead's own Claude Code context is,and
fleet_listreports it per lead row as{state: "ok"|"high"|"unknown", tokens?, compactions}.Measured on this host just now, on the live daemon (jar
597af9057197):So fleetd can see that a lead is full. Nothing acts on it. No lead is told, and no handover is
offered. The lead keeps working until Claude Code compacts it, which is the thing the operator
asked us to stop:
fleet_handoveralready exists (#480) and has run end-to-end once. The missing half is thenotice: something that reads the gauge and tells the lead, once, that it should hand over.
Where it goes
LeadHeartbeatLoop(CB-551),fleetd/src/main/java/dev/ltms/fleet/msg/LeadHeartbeatLoop.java.It already has every piece this needs: an opt-in config gate, a status gate, an idle debounce, a
nudge cap, a stand-down against
ReplyPushLoop, and a puredecide(...)function that is unittestable with a fake clock.
Hard limits
fleet_handoverkeeps needing
operatorConfirmed, andrequireOperatorConfirmstaystrue.does today.
spend turns on.
OKreading.UNKNOWNmeans "I could not look", not "it gotbetter". Re-arming on
UNKNOWNwould let a flapping gauge nudge the lead again and again.Note for whoever picks this up
leadHeartbeat:is absent from this host'sfleetd.yamltoday (measured 2026-09-20:grep -n 'leadHeartbeat' fleetd/fleetd.yamlreturns nothing; onlyleadRollover:at line 563).So the heartbeat loop is not constructed on the live fleet, and this feature will be inert here
until that block is added. That is a separate config change, not part of this unit.
Correction to the brief — one garbled sentence
My brief said, about the nudge text:
That sentence is broken. Here is what I meant, and it is what you should build:
Today a
HIGHreading always carries a non-nulltokens.LeadContextGaugeonly setsHIGHafter comparing a token count againstHIGH_THRESHOLD_TOKENS, so it cannot reachHIGHwith no number. I checked this in
LeadContextGauge.java.Still write the null branch. That invariant lives in another class, nothing asserts it, and
the nudge text is the wrong place to discover it is gone. If
tokens()is null, drop that clausefrom the sentence rather than printing
null. Add one test for it.This is a "free test case": a comment claiming an invariant is worth asserting, because the next
reader widens a gate on its strength.
Second thing, same brief
The brief tells you to keep the existing two public
LeadHeartbeatLoopconstructors working bydelegating them to a new one. To be exact about what "working" means: no existing test file may
need an edit to compile. If one does, that is a signal the new constructor's defaults are wrong,
not a signal to edit the test. Say so in your reply if it happens.
Review of PR #610 — one blocker, one known gap, one comment already fixed
I built the branch myself in a clean worktree:
mvn -o clean install, exit 0,1860 tests, 0 failures, 0 errors (149 surefire reports). Both required mutations went red on
the right tests. The implementation is good. Two reviewers found one real defect between them.
I already pushed
d7390ccto the branch, repairing a garbled comment the worker copied from myown brief. Code was fine; only the comment was unreadable.
BLOCKER — the latch means "decide() chose to tell", but it must mean "the text reached the pane"
LeadHeartbeatLoop.tick()commits the latch before it tries to send:and
injectNudgeswallows the failure:So one transient herdr failure marks the lead as told when nothing reached its pane. The lead
then gets zero notices for the rest of the HIGH stretch — the exact moment the whole feature
exists for.
On the config this fleet will actually run, this is the normal path, not an edge case. The
operator chose
quietNudgeCap: 0(2026-09-20), so thequietCount < quietNudgeCapgate cannever fire and there is no later nudge to carry the notice by accident. One failed send loses it
permanently.
Second half of the same flaw
contextNotice(boolean enabled, LeadContextGauge.Reading reading)takes no latch at all:So every pending-driven
INJECTre-appends the full 368-character notice while the context staysHIGH. That makes the notice's own closing sentence false:
These are not two bugs. They are one design flaw seen from both ends: the latch is set by the
decision, and read by nothing that knows whether the send worked.
The fix
Make the latch mean "the notice text was actually delivered to the pane", and make both the
decision gate and the text agree on it:
injectNudgereports whetheragents.sendreturned without throwing.tickpersistscontextNotifiedas true only when the decision said to notify and anotice was included and the send succeeded.
idleSinceNanosandquietCountkeep beingapplied unconditionally, exactly as today.
contextNoticegates on the latch as well, so a nudge only carries the notice when the leadhas not already been told in this HIGH stretch. Then "once per HIGH stretch" is true on all
three INJECT routes, not just the forced one, and the sentence above stops being a lie.
Keep the existing latch re-arm rule unchanged: only a positive
OKreading clears it, neverUNKNOWN.NOT a blocker — the
maincall-site gap is class-wide and pre-existingThe second reviewer reported that no test pins
Fleetd.java:577, so swappingleadContextSource(...)forLeadContextSource.none()would compile and leave the featurepermanently silent.
That is true, and the PR says so itself.
FleetdLeadContextSourceWiringTest's own javadoc:So #610 pins its factory to exactly the standard #602/#606 set, and is no worse than the
precedent. Holding this PR to a bar nothing else in the codebase meets would block the operator's
top-priority feature on a pre-existing debt.
A sweep is measuring the whole family right now, by mutating each
maincall site to its inertvariant and running the full suite. That will get one ticket covering every site, rather than one
PR paying for all of them. Do not add a
main-level test in this PR.Merged as
955b9ea(PR #610).The merged tree is byte-identical to the tree I built and mutation-tested (
52007dc5d6a953a9bdfdaf30793f644d519bc6ac), so these numbers apply to main exactly rather than to a similar tree:mvnexit 0.quietCount < quietNudgeCapto<=), which is what makes the other three kills evidence rather than an absence of evidence.Still to do before this does anything on this host
A merge is not a deployment. The running daemon holds the jar it started with, and
leadHeartbeat:is inConfigRef.DEFERRED_KEYS, so the config half is frozen too. I am holding the redeploy until the worker on the CI fix finishes, because a restart drops in-flight tickets.Configuration chosen for this host
The operator chose the narrow shape on 2026-09-20, after being shown both:
quietNudgeCap: 0is legal because the compact constructor rejects< 0, not<= 0. With it, the loop can nudge the lead in only two cases: real fleet work is waiting, or the lead's own context reads HIGH and it has not been told yet in this stretch. It can never send "you are idle, nothing is pending".That choice is also why the latch defect was a blocker rather than a nuisance. With no quiet nudges, a nudge carrying the notice is the only nudge, so one swallowed herdr failure would have marked the lead as told when nothing reached its pane, with no later nudge to carry the notice by accident.
Not fixed here
Fleetd.main's own one-line call toleadContextSourceis still unpinned. Pre-existing and class-wide — tracked in #612.