worker/589-f64303-2
993 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ae7845c375 |
fleetd #589 (Groups 1 & 2): pin 6 main() wiring sites with named factories
Extracts 6 inline wiring expressions from Fleetd.main() into named, directly-testable package-private static factories, following the FleetdLoopHealthSourceWiringTest (#584) shape, and adds one wiring test per factory: Group 1 (exhaustion/quarantine): - forwardingExhaustionSink(exhaustionSinkRef) — was inline ExhaustionSink.forwardingTo(exhaustionSinkRef::get) - publishExhaustionSink(...) — was two untested statements building the real sink and .set()-ing it into exhaustionSinkRef - liveExhaustedPatterns(config) — was inline new LiveExhaustedPatterns(() -> config.get().profiles()) - exhaustedPatternLookup(roster, liveExhaustedPatterns) — was an inline lambda resolving a herdr target to its profile's live pattern; silently losing this is the worst regression in the sweep, since a real usage-limit refusal would stop being classified as BACKEND_EXHAUSTED Group 2 (CB-596 credential policy): - claudeCodeLauncher(...) — was an inline `new ClaudeCodeLauncher(...)` whose memberCredentials supplier argument was untestable wiring - openCodeLauncher(...) — same, for OpenCodeLauncher Each new test pins its factory behaviorally (never via source-text assertions): built and confirmed RED by name against the named inert mutation, then confirmed GREEN again after restoring, and separately confirmed GREEN after a behavior-preserving reformat/local-variable extraction of the same call, to rule out a disguised source-text test. Suite: 1789 -> 1799 tests (+10, matching the 10 tests added), 0 failures, mvn -o clean install BUILD SUCCESS. Scope strictly limited to main()'s :214-:468 range per the ticket split with the concurrent worker handling Group 3 at line 500+. |
||
|
|
a639969a9a |
CLAUDE.md: a blocked lead consults architects, not the operator
The operator set this rule on 2026-09-19: when a decision blocks a lead, it consults one or more architect members, who are authorized to agree on one decision and unblock. The operator is not asked. Escalation stays open only for things outside the fleet's authority -- money, credentials, or a promise made to someone else. The paragraph also carries the reason the ticket record is mandatory rather than optional. The operator's old notification channel was the block itself: work stopped, so they found out. Taking the operator out of the loop removes that signal with it, so the decision goes on the ticket, which reaches them whether or not they are at a terminal when it is made. The rule has a second half aimed at architects, which lives in fleet.charters.architect and is applied per daemon -- filed as #591, because a charter can never reach a lead and charters do not travel between hosts. The missing notification event is #592. Block verified byte-identical with wiki 7-Use-Cases.md at 1d9bd1b. |
||
|
|
17c3a69c57 |
docs: CB-591 gateway page had two claims that went stale
The gateway's chat model is served under the stable alias `acoder`, and the model behind that alias changed on 2026-08-28 — it is Qwen3.8-27B now, not DeepSeek-V4-Flash. The old name is still served, so nothing broke, but it names a model this is not. Two claims on the page were wrong as a result, and both were written as current facts rather than dated measurements: - `/v1/models` returns exactly `["deepseek-v4-flash"]` — it returns 6 ids now. This sat under a heading saying it needs no re-testing. - the status banner said `local` and `gx` are both at `weight: 100` — `local` is at 0. Measured today against the live gateway: /v1/models returns acoder, qwen3.8-27b-nvfp4, deepseek-v4-flash and three embedding names; a completion sent as `deepseek-v4-flash` comes back reporting `"model": "acoder"`, which is the alias in plain sight. /v1/deployment reports generation 2026-08-28-qwen3.8-27b-nvfp4. §2 and §3 are left alone. They are the August plan, and rewriting them would destroy the record of the migration. fleetd.yaml moved to `acoder` in the same change. It is not tracked here. |
||
|
|
49a5875586 |
Merge #583: fleetd #582 — assert pending message-id cleanup at every publish cleanup site
All ten assertions proven live: six by the implementer, the last four by the lead. One contract build with four deleted removal lines produced exactly four named failures, one per site, with the total unchanged at 1825. |
||
|
|
634d33b50b | Merge worker/562-loop-health-wiring-test-99611c-5 | ||
|
|
1db79bcaa9 | Merge worker/581-completionresolver-cas-sites-0542b7-6 | ||
|
|
4507bc5a70 | Merge worker/571-attempted-outcome-5739f7-2 | ||
|
|
d7239ed23b |
fleetd #571: pin FleetMcp.formatReply's TIMED_OUT_UNCONFIRMED wording
CORRECTION 5 on the ticket: mutating the new arm's message text to the queued/working arm's text survived every existing test, because nothing asserted the specific wording. This adds one test that asserts the unconfirmed-delivery message and asserts it does NOT carry the queued/working arm's retry invitation — the distinction #571 exists for. No production code changes; formatReply's TIMED_OUT_UNCONFIRMED arm was already correct. |
||
|
|
dfeb9340b4 | fleetd #581: cover completion CAS removals | ||
|
|
1513d4f260 |
fleetd #562 follow-up: extract loopHealthSource factory, pin its wiring
PR #579's inline `new FleetMcp.LoopHealthSource(poller::health, ...)` in Fleetd.main had nothing a test could call directly. Measured: replacing poller::health with a constant () -> RUNNING compiled clean and left all 1771 tests green (see issue #562 comment "HOLD on PR #579"). Extracts the inline construction to a package-private Fleetd.loopHealthSource factory, the same style as the sibling capacitySource/healthCoverageSource factories, and adds FleetdLoopHealthSourceWiringTest with three separate assertions: the statusPoller half, the sessionReaper half, and the reaper == null branch (still STOPPED). |
||
|
|
4ca7d72303 | #582: assert pending message-id cleanup | ||
|
|
c0545d003d |
fleetd #571: make FleetApp.writeReply's inner Outcome switch exhaustive, no default
Ticket comments (17126, 17127) corrected the original acceptance criterion after this unit was already in flight: a hand-listed grep for the enum's constant names goes stale silently the moment a new constant lands, so the compiler must be the enumeration instead. sendOutcomeLabel (MessageService.java) and formatReply (FleetMcp.java) were already default-free switch expressions. The one gap was writeReply's inner "status" switch, which had `default -> "done"` — the exact value that would have lied about TIMED_OUT_UNCONFIRMED. Remove the default and list every Outcome constant explicitly; REPLIED, COMPLETED_UNREPLIED, QUESTION and STALE_TURN get an arm too even though the outer switch always dispatches them first, so the inner switch stays exhaustive on its own. The outer switch (a statement, not an expression) keeps its own default — Java does not require exhaustiveness there regardless, and "everything not terminal is a 202" is an intentional catch-all. Verified with the proof the ticket asked for: added a scratch 11th Outcome constant after deleting all default arms and confirmed all three switch-expression sites (and no test file) fail to compile without an arm for it, one at a time, then removed the scratch constant. |
||
|
|
275ac0d251 | fleetd #562: surface loop health | ||
|
|
c1e06c9e12 |
fleetd #571: add TIMED_OUT_UNCONFIRMED so an ATTEMPTED delivery is not reported as never-arriving
MessageService.send's TimeoutException branch collapsed Injector.Cancellation.ATTEMPTED (fleetd #551 — the send call was made but its outcome is unknown) into Outcome.TIMED_OUT_QUEUED, which promises the caller the message will never arrive. On this route agent.prompt may already have pasted and submitted the text, so a caller's natural recovery (resend) risks a double delivery. Add Outcome.TIMED_OUT_UNCONFIRMED and route ATTEMPTED to it. Update the three readers found by searching for the enum's constant names (not `Outcome.`, which misses FleetMcp's unqualified `case REPLIED ->` switches and would false-positive on ConfigRef's unrelated Outcome record): - MessageService.sendOutcomeLabel: add it to the "timeout" metric label group. - FleetMcp.formatReply: its own case, warning against a blind retry (distinct from the generic "retry or poll status" message the other timeouts get). - FleetApp.writeReply: its own "unconfirmed" status and detail text, so it no longer falls through the switch's default -> "done" arm, which would have reported "the delegation completed" for the one case where delivery is unconfirmed. |
||
|
|
204da67d66 |
Merge #576: fleetd #575 — one finally covers answer()'s STALE_TURN exit
Third instance of the #572 shape on this file: one invariant kept at N sites, asserted at fewer than N. Here answer()'s inner try opened AFTER the Task lookup/registration and the STALE_TURN early return, so that return was covered only by a hand-rolled copy of the finally's cleanup pair. The fix widens the try upward and deletes the copy, so every exit runs the one finally exactly once. rendezvous.open(workerSession) correctly stays outside it — nothing to clean if it never opened. This fixed no live leak, and the code comment says so: neither rendezvous.answerAsk nor clearAsyncQuestion(turnId, false) can throw, so nothing ever left through the old gap uncovered. It is a structure fix, the ticket's own fallback case. Verified here, not taken from the worker's report: - baseline on the branch: 1766 tests, 0 failures, from Maven and from an independent sum over target/surefire-reports/*.txt. - my own mutation, located fresh: move the inner `try {` back down below the STALE_TURN return — the exact pre-fix structure, minus the hand-rolled pair. Result: 1766 run, exactly 1 failure, and it is the new test — MessageServiceTest.answerLosingTheRaceToAnAlreadyAnsweredAskStillReturnsStaleTurnAndCleansUpOnce:545 "the forward waiter this answer() call opened must be closed after a STALE_TURN return". One failure, not a crowd: the new test is the only thing holding this path. - the worker's own mutation removed the whole finally and took 8 other tests with it. That proves the finally runs; it does not prove the STALE_TURN path reaches it. Mine does. The new test hook answerAskLapseRaceHookForTest follows the file's existing askTimeoutRaceHookForTest convention. Closes #575. |
||
|
|
b091c51eee |
fleetd #575: widen answer()'s try so one finally covers its STALE_TURN exit
The waiter cleanup pair (asyncTasksByWaiter.remove + rendezvous.close) was duplicated: two sites sit in a finally, the third was hand-rolled inline before answer()'s early STALE_TURN return, structurally outside any finally. Both rendezvous.answerAsk and clearAsyncQuestion(turnId, false) are total (cannot throw), so the gap never leaked in practice. But the duplicate was untested: mutating it away left all 1765 tests green, while the two finally-protected sites are each killed by 8-36 tests. Same shape as #572. Fix: widen the try to wrap the Task registration and the STALE_TURN check, so the single finally covers every exit and the hand-rolled copy is gone. Added a race hook + regression test that deterministically reproduces the 'ask lapsed between the lookup and the unblock' case and proves the fix still returns STALE_TURN and cleans up exactly once. |
||
|
|
84d631b030 |
Merge #572: answer()'s session-lock release is pinned on all four exits
fleetd #572. MessageService releases its per-session lock in a finally at two sites. Removing the
send() one failed 22 tests and errored 1. Removing the answer() one left the whole suite green.
Skip that unlock and the thread holds the session lock forever, so every later send or answer to
that session blocks permanently — no exception, no log line.
Tests only. Against current main the diff is one file, MessageServiceTest.java, +178 lines.
Four new tests, one per exit of answer(): normal REPLIED, TIMED_OUT_WORKING, the ExecutionException
rethrow, and the InterruptedException rethrow. Each proves REACQUISITION rather than the return
value — a bounded follow-up send on the SAME session must not come back BUSY, and send reports BUSY
only when tryLock itself timed out, so a non-BUSY probe is specifically evidence the lock was free.
Lead verification, re-running rather than accepting the worker's numbers, on the branch merged with
main at
|
||
|
|
3f8c38fc54 |
Merge #567: pin LeadMailbox.inspect's probe-channel close
fleetd #567. LeadMailbox.inspect opens a probe channel and closes it in a finally. The production
code was already correct; nothing asserted it, so a future refactor could drop the close and leak an
AMQP channel per inspect() call with the suite green.
Test only. LeadMailbox.java is untouched — sha256 a2cd99be77345b7e... before and after.
The test asserts the CONSEQUENCE rather than the return value: it caps the connection at three
channels (LeadMailbox uses two, consume and publish), runs a successful inspect, then requires a
replacement channel. If the probe is left open, the broker has no channel number left and
createChannel() returns null. A test that only checked inspect()'s MailboxState would pass under the
mutation, which is the whole reason this hole existed.
Lead verification, re-running rather than accepting the worker's numbers, on the branch merged with
main at
|
||
|
|
a4dbc8f8b7 |
fleetd #572: pin answer()'s session-lock release across all four exits
MessageService.answer() releases its per-session lock in an outer finally (MessageService.java:1218) that mutation testing showed was covered but unasserted: removing that line left all 1750 existing tests green, because every existing test on this path checks answer()'s return value, never that the lock it took is actually reacquirable afterward. If it leaked, a session would be wedged forever with no exception and no log line. Adds four tests, one per exit of answer() (normal REPLIED reply, TIMED_OUT_WORKING, ExecutionException rethrow, InterruptedException rethrow), each proving the lock is reacquirable via a bounded (300ms) follow-up send on the same session rather than merely checking answer()'s own outcome. No production change. |
||
|
|
ed2fd6646a |
Merge #561: the completion/session listener fan-out survives either half throwing, and both sites are pinned
fleetd #561. Fleetd composed two TurnListener halves as `completion.X(); sessions.X();`, so a throw from the first half skipped the second. The anonymous class is now a package-private factory, Fleetd.turnListener(completion, sessions), built on two helpers that always attempt both halves and rethrow whatever escaped — a second failure attached with addSuppressed rather than dropped, so it still reaches StatusPoller's catch (Throwable). The wiring at Fleetd.java:498 calls that factory, so the seam under test is the real caller. Lead verification, on a merged tree, re-running the checks rather than accepting the worker's: exit 0, 1761 tests from Maven and from an independent sum over 131 surefire reports. The interesting part is what the first round MISSED. Two helpers maintain one invariant — "the second half always runs" — and the first round's five tests asserted it at only one site. Measured: bothMustRun reverted to the pre-fix bug -> 1 named failure (pinned) bothMustRunKeepingSecondResult the SAME bug -> 1755/1755 GREEN (unpinned) failure.addSuppressed(t) deleted -> 1755/1755 GREEN (unpinned) Both survivors are now killed by new tests, re-verified by the lead after the fix: sessionHalfStillRunsWhenTheCompletionHalfThrowsSynchronouslyForPostAction and bothFailuresEscapeWhenBothHalvesThrowDistinctExceptions, each failing alone under its own mutation. The rule this cost us, worth carrying: COUNT ASSERTIONS PER SITE, NOT PER INVARIANT. The total being non-zero is what hides a zero at one site, and extracting a shared helper makes it worse rather than better — it does not reduce the number of sites, only how many are visible. Credit to the fleet01 lead, who predicted this shape before an instance was found. onDelivered stays deliberately unguarded. Its comment now gives the real reason — CompletionResolver.captureBaseline already catches RuntimeException around its scrape and fails open, so that half does not realistically throw — instead of the previous reason, which was true but about registration rather than about this pair. A correct conclusion resting on a wrong premise reads exactly like a verified one. |
||
|
|
5441a2b321 | fleetd #567: assert inspect closes probe channel | ||
|
|
384867dfa3 |
Merge #551: ATTEMPTED is its own cancellation answer, and the javadoc stops claiming a timed-out send never arrived
fleetd #551. The injector polls a queued entry off the queue and marks it ATTEMPTED BEFORE the irreversible AgentControl#send call, not after. So a Throwable escaping that call can never leave the entry QUEUED at the head (the #546 re-send hazard) and can never be recorded as a confident NOT_DELIVERED for text that may already be in the pane. Cancellation.ATTEMPTED is added as a third answer. NOT_DELIVERED stays reserved for confirmed absence: the readiness grace expiring, drop(), or a herdr *_not_found error, which the rest of this codebase already reads as definitely-absent rather than inconclusive. Also fixes three javadoc/comment sites that claimed a timed-out send definitely did not arrive: TIMED_OUT_QUEUED, hasQueuedDelivery, the queuedDeliveries field, and the comment in send()'s timeout branch. Two claims were wrong, not merely stale: "on every route it will not arrive later" is false on the ATTEMPTED route, and "may already hold a partial paste" understates it — agent.prompt pastes AND SUBMITS in one call, so the target may hold a complete, running turn. Verified by the lead on a merged tree: mvn -o clean install from fleetd/, exit 0, 1754 tests from Maven and from an independent sum over 130 surefire reports. Mutation: folding ATTEMPTED back into NOT_DELIVERED in cancellationOf gives 3 red, each naming the property (anErrorFromSendRemovesTheMessageAndMarksItAttempted:740, aHerdrExceptionFromSendStillSurfacesButNowReportsAttempted:781, aHerdrExceptionAfterThePasteIsNeverRecordedAsConfidentlyNotDelivered:806). The final round is comment-only, proven mechanically rather than by reading: stripping every comment from MessageService.java before and after and collapsing whitespace gives byte-identical code. |
||
|
|
f40c19ecf0 |
fleetd #551 shape sweep: fix stale ATTEMPTED-route javadoc/comments in MessageService
hasQueuedDelivery's javadoc, the queuedDeliveries field javadoc, and a code comment in send()'s timeout path all still made two claims that ATTEMPTED (fleetd #551) falsifies: a blanket "the message will not arrive later" across every route, and "the terminal may already hold a partial paste" — but agent.prompt pastes AND submits in one call, so the target may hold a complete, already-submitted turn. Each now names the three Injector.Cancellation routes (CANCELLED, NOT_DELIVERED, ATTEMPTED) and says plainly that only the first two establish the message will not arrive later. Comment/javadoc only. No behaviour change: Injector.java is untouched (sha256 0c689b6cf36275c0da45497a74b2bd4f5a3d80c4dbda77d46670c66004e53b69) and no test was added. mvn -o clean install: BUILD SUCCESS, Tests run: 1754, Failures: 0, Errors: 0 (unchanged from before this commit). |
||
|
|
034e17bb32 |
fleetd #561 follow-up: pin the session half of bothMustRunKeepingSecondResult
Two helpers maintain one invariant (the second callback half always runs, even when the first throws): bothMustRun and bothMustRunKeepingSecondResult. Only bothMustRun's "session half still runs" direction was asserted (sessionHalfStillRunsWhenTheCompletionHalfThrowsSynchronously, via onTurnComplete). bothMustRunKeepingSecondResult — the helper onTurnCompleteWithPostAction uses — could be reverted to the pre-#561 broken shape and the suite stayed green. Adds two tests to FleetdTurnListenerCompositionTest: - sessionHalfStillRunsWhenTheCompletionHalfThrowsSynchronouslyForPostAction: mirrors the existing onTurnComplete case for onTurnCompleteWithPostAction/ bothMustRunKeepingSecondResult. - bothFailuresEscapeWhenBothHalvesThrowDistinctExceptions: proves a second, distinct failure from the session half is preserved via addSuppressed rather than silently dropped when both halves of bothMustRun throw. Also rewords the onDelivered comment in Fleetd.turnListener: it previously said this pair is safe because registration survives a throw via #556's Injector wiring, which is true but is not why THIS pair is unguarded. CompletionResolver.captureBaseline already catches RuntimeException around its scrape read and fails open, so completion.onDelivered does not realistically throw. Comment text only, no logic change. |
||
|
|
68b428c484 |
fleetd #551 rework (comment 17058): fix TIMED_OUT_QUEUED javadoc for ATTEMPTED
Javadoc-only change. #551 added Injector.Cancellation.ATTEMPTED, which MessageService.send's timeout path folds into Outcome.TIMED_OUT_QUEUED alongside CANCELLED and NOT_DELIVERED (the collapse itself is unchanged behaviour and is being tracked as a separate follow-up ticket). TIMED_OUT_QUEUED's javadoc — landed by #513 to state the routes that reach it — named only two routes and said "on every route it will not arrive later", with a "may already hold a partial paste" caveat. Both claims are now stale: ATTEMPTED is a third route, and because agent.prompt pastes AND submits in one call, that route may mean the target holds a complete, already-submitted turn and is working on it right now. Names all three routes, says which one is uncertain, and drops the now-false blanket claim. No behaviour change. |
||
|
|
e20ccab1eb |
fleetd #561: harden the completion/session TurnListener fan-out
Fleetd's turnListener composition had four callbacks (onTurnComplete, onTurnCompleteWithPostAction, and both onTurnFailed overloads) built from two bare, unguarded statements each. onDelivered's registration was already fixed structurally by #556; these four had the identical fragility and were still untested: nothing enforced that the completion resolver's half ran before the session half beyond call order in the source, so a future reorder (or a throwing session listener sequenced first) could silently skip the completion resolver's effect and strand a caller for its full timeout. Extracted the composition to a package-private static factory, Fleetd.turnListener(completion, sessions), and hardened it with bothMustRun/bothMustRunKeepingSecondResult: both callback halves are always attempted regardless of whether the other throws, and whatever escapes is rethrown afterward (never swallowed) so it still reaches StatusPoller's catch (Throwable) and logs at ERROR. FleetdTurnListenerCompositionTest builds this real composition from a real CompletionResolver and a throwing fake sessions half, and asserts the completion resolver's effect (the waiter resolving) survives the session half throwing, for all four callbacks, plus a mirror case showing the session half still runs when the completion half throws first. onTurnCompleteWithPostAction keeps completion-before-session as a functional requirement (resolveBeforePostAction must run before the context-reset housekeeping can erase the pane), not just fault tolerance, so it is not reorder-symmetric like the other three — documented in Fleetd.turnListener's javadoc. |
||
|
|
d83821bbce |
fleetd #551: record the delivery attempt before the irreversible send
The Injector wrote the delivery outcome AFTER calling AgentControl.send(), so a HerdrException thrown from the response half of that call (herdr already replied, or may have) was recorded as a confident NOT_DELIVERED for text that may already be sitting in the worker's pane. Poll the queue entry and mark it Pending.State.ATTEMPTED before send() is called, not after. On success it is upgraded to DELIVERED; on an ordinary failure it stays ATTEMPTED (honest uncertainty), except a herdr *_not_found error, which the rest of this codebase already treats as a confirmed absence and which now still writes NOT_DELIVERED. cancellationOf gets a matching third answer (Cancellation.ATTEMPTED) instead of folding the new state into NOT_DELIVERED, so a caller that cancels an already-attempted delivery is told the truth too. |
||
|
|
ba2f4d16f8 |
Merge #555: the redeploy main flow is lifted into tested predicates, and the guard now catches functions below the SOURCED line
fleetd #555. Eight main-flow decisions in scripts/redeploy-fleetd.sh move into predicate and dispatch functions the suite can source and test. The guard test test_no_untested_main_flow_conditionals stops new bare conditionals reappearing. The rework closes a hole the lead found (comment 17012): a conditional wrapped in a function defined BELOW the SOURCED guard was invisible to the guard, and such a function can never be sourced, so it can never be tested. The guard now fails on any function definition after the guard line, on its own. Verified by the lead on a tree with main merged in, redeploy-fleetd.sh at its pristine sha 4ffacc5185807d39720a3484d85b922413806eb5347318265bd8897dfd61e8d9: bash 5.3.9 -> exit 0, 0 lines matching ^FAIL: /bin/bash 3.2.57 -> exit 0, 0 lines matching ^FAIL: Three mutations, each restored to the pristine sha afterwards: a bare conditional appended to the main flow -> exit 1, reported by line the same conditional wrapped in a function below the SOURCED guard (the found hole) -> exit 1, "function defined after the SOURCED guard (line 1038) - it cannot be sourced, so it cannot be tested" the new FUNC emission deleted from the guard -> that same case returns to exit 0, so the new assertion is what catches it. Anchor count 1 -> 0, test-redeploy-fleetd.sh restored to sha 0d713a3092a0c0ea8c05663ffa0cb1595e21b9d73872e662eb99b5035fd38151. |
||
|
|
db4c98ac60 |
Merge #556: the Injector owns turn registration, and the #553 backstop is pinned too
fleetd #556. Registration moves off the TurnListener fan-out onto its own narrow
TurnRegistrar seam, wired directly to CompletionResolver::register in Fleetd.java,
so it survives any listener throwing regardless of call order.
Verified by the lead on a tree with main (
|
||
|
|
8f80d267a0 |
fleetd #555 rework: catch function definitions after the SOURCED guard
Comment 17012 on #555 found a hole in test_no_untested_main_flow_conditionals: the guard's function-body detection treats anything inside a function as "fine, out of scope for this scan" — but a function DEFINED after the SOURCED guard line can never be reached by sourcing this script (sourcing stops before the main flow runs), so its body is untestable by construction while still reading to the guard as safely inside a function. mainflow_bare_conditionals now also emits a FUNC record for every function opened after the guard line (reusing the same open-brace detection already used for depth tracking), and test_no_untested_main_flow_conditionals treats any such record as a violation on its own, independent of what the function's body contains or whether the allowlist would otherwise excuse a bare conditional inside it. Proof (redeploy-fleetd.sh restored to 4ffacc5185807d39720a3484d85b922413806eb5347318265bd8897dfd61e8d9 after each): - CONTROL — a bare conditional appended to the main flow is still caught: EXIT=1, "found 1 untested main-flow if/elif/case line(s) ... line 1341: if [ "$MY_CONTROL_BARE" = 1 ]; then :; fi" - CANDIDATE — the same conditional wrapped in a function defined after the boundary, previously invisible (EXIT=0), is now caught: EXIT=1, "line 1341: function defined after the SOURCED guard (line 1038) — it cannot be sourced, so it cannot be tested: newfunc_below_the_boundary() {" Full suite re-run green on both bash 5.3.9 and /bin/bash 3.2.57 (macOS system bash): exit 0, 0 FAIL lines, reached the final PASS line, on both. No change to redeploy-fleetd.sh; the 8 lifted decisions, their mutation proofs, and the allowlist all stand as before. |
||
|
|
738d34a609 |
fleetd #556 rework: pin registration on the #553 finally backstop path
Comment 17009: there are TWO registrar.register(target, sent.token()) call sites in Injector's delivery method — the ordinary path inside `if (sent != null)`, and the fleetd #553 finally backstop, reached only when an earlier block throws before the ordinary path ever runs. The lead's mutation on Injector.java:703 (the backstop call) survived the full suite: the existing #553 regression test for this exact scenario (aRuntimeExceptionFromOnTurnCompleteStillCompletesTheNextDelivery) asserts only that the delivered future completes, never that the turn is registered with CompletionResolver — so a redesign that dropped registration from the backstop would reopen this ticket's own defect on precisely the path #553 exists for, with every existing test green. Adds aRuntimeExceptionFromOnTurnCompleteStillLeavesTheNextDeliveryRegisteredOnTheRecoveryPath: drives the same construction as the existing #553 test (onTurnComplete throws for a previous turn, forcing the next turn's delivery down the finally backstop) and additionally asserts the new turn is registered with CompletionResolver and carries the correct waiter — the same assertion the ordinary-path test makes, now made on the recovery path. Proven by mutation: removing Injector.java:703 alone (exact-line anchor 1 -> 0) turns the new test red with its own assertion message; restored and confirmed byte-identical (sha256 8fcb698afccc254b0c99d3a4bf9e960c0e7c85e542024e870c1e815dce6d62a9, matching the pre-mutation tree); re-run green as a control. Full suite after restore: 1750 tests, 0 failures, 0 errors, 0 skipped (Maven's own summary and an independent sum over surefire-reports/*.txt agree), BUILD SUCCESS. |
||
|
|
a46e4058ac |
fleetd #556: make turn registration structural, independent of any TurnListener
The Injector owns the invariant "every delivered turn has a registered waiter," but before this the only thing that satisfied it was CompletionResolver.captureBaseline, called from inside a TurnListener callback wired in Fleetd.java. Any TurnListener that throws (from onDelivered or elsewhere) could break the invariant with no way for the Injector to detect it. #553 only made the one reachable listener behave via a try/finally backstop; it did not remove this structural dependency. Add a narrow TurnRegistrar functional interface, decoupled from TurnListener, whose only job is registering a delivered turn's waiter. CompletionResolver now implements it via a new register() method (extracted from captureBaseline's registration half; captureBaseline keeps its own full body unchanged, so existing direct callers/tests are untouched). Injector gets an explicit registrar field/constructor family (auto-derived from the TurnListener via instanceof where that still works, explicit where Fleetd's anonymous fan-out listener can't implement two interfaces at once) and calls registrar.register(...) directly and unconditionally in both the ordinary delivery path and the #553 finally backstop, before turnListener.onDelivered(...) — so registration no longer depends on that notification callback succeeding. Fleetd.java wires completion::register explicitly as the registrar, bypassing the turnListener fan-out for registration purposes. CB-116 ordering (onTurnComplete reads the PREVIOUS turn's inFlight entry before the new turn's registrar.register() runs) and the two-arg inFlight.remove(target, turn) vs one-arg distinction on the completion path are both preserved unchanged. #561's order-dependent test (asserting Fleetd.java's completion.onDelivered -> sessions.onDelivered call order) does not exist anywhere in this repo at this branch point — nothing to delete. Adds 5 tests: a TurnListener that throws from every callback still leaves the delivered turn registered and resolvable; the new turn's registration still runs after the previous turn's completion is read (CB-116 guard, pinned as a call-order assertion); and three tests naming the two-arg-remove invariant directly (a superseded turn's terminal handling must not evict its successor's registration) across resolve()'s plain-completion branch, its echoed-noReportMessage sub-path, and fail(). |
||
|
|
f4f5f3106e |
Merge #513: TIMED_OUT_QUEUED has four routes, and the javadoc now says which
The old javadoc said a TIMED_OUT_QUEUED message was "still sitting in the injector's
per-target queue". It is not — send() has already given up on it and it will never
arrive. That wrong claim told an operator to wait for a message that was never coming.
The first fix replaced it with a narrower wrong claim: that Injector.cancel() cancelled
the entry and "the target never saw a word of it". That describes one of four routes.
TIMED_OUT_QUEUED is returned whenever injector.cancel() returns anything but DELIVERED:
CANCELLED the Pending was still queued and this call removed it
NOT_DELIVERED Injector.java:400 - the herdr agent.prompt call threw
NOT_DELIVERED Injector.java:419 - readiness grace expired, never attempted
NOT_DELIVERED Injector.java:691 - drop(), the target is gone
On the last three, cancel() cancels nothing: it reads a state another path already set
(cancellationOf, Injector.java:288-290). And on the herdr-threw route, agent.prompt
pastes and submits in one call, so a throw does not prove the pane stayed clean - an
operator told "never saw a word" will not go and look at the one place the evidence is.
The javadoc now states the two facts that hold on every route - the message will not
arrive later, and it is not in any queue - and attaches "the target saw nothing" only to
the CANCELLED case. Five blocks: the enum constant, queuedDeliveries, hasQueuedDelivery,
hasOrphanedDelegation, and the inline comment in the TimeoutException branch that seeded
the wording.
Comment-only; no logic changed.
Verified on a tree merged with main (fast-forward to
|
||
|
|
8d79d229ff |
fleetd #555: lift 8 main-flow decisions into tested predicate/dispatch functions
redeploy-fleetd.sh's main flow had 8 bare if/case decisions (CHECK_ONLY
short-circuit, drain-gate entry+confirm, supervisor report/stop/start
dispatch, health-poll decision, HAD_OLD_PID computation) that lived outside
any function, so the 67-test suite could not reach them and any one could be
silently inverted with the whole suite green.
Follows the existing swap_if_built/refuse_drain_gate pattern: each bare
guard becomes a small predicate or dispatch function (should_stop_for_check,
drain_gate_required/drain_confirmed/run_drain_gate, report_supervisor_state,
dispatch_stop, dispatch_start, health_is_up/report_health,
compute_had_old_pid), called unconditionally by the main flow so the
decision itself is unit-testable in isolation.
Adds a structural guard, test_no_untested_main_flow_conditionals, that scans
the main flow (everything after the SOURCED guard) for bare if/elif/case
lines outside any function body, tracking function boundaries via this
file's one consistent name() { / } convention. It fails on any new bare
conditional not covered by MAIN_FLOW_ALLOWED_CONDITIONALS, an explicit
exact-text allowlist of the report-only/display conditionals and the two
#504-family supervisor elif branches that stay out of scope for this
ticket. This is the "shape, not the eight sites" guard the ticket asked
for: a ninth bare decision fails immediately, naming its line.
Out of scope, not touched: #504 items 2/3/4 and #528 item 2 (same
untested-main-flow family) — the seam here generalizes to make them
testable too, but lifting them was left for their own tickets.
|
||
|
|
cb64bc8157 |
fleetd #513: rework — TIMED_OUT_QUEUED has four routes, not one
Comment 16984 on the ticket showed my first pass (
|
||
|
|
ed28b51f12 |
fleetd #513: fix TIMED_OUT_QUEUED javadoc — cancelled, not queued
Two javadoc blocks (queuedDeliveries field, hasQueuedDelivery) said a timed-out message is still sitting in the injector's per-target queue. CB-640 made send() cancel it via Injector.cancel() instead, so the message is gone and will never arrive. Rewrote both to describe cancellation. Also fixed a third instance of the same stale claim in hasOrphanedDelegation's javadoc, and added a line to the TIMED_OUT_QUEUED enum constant's own comment clarifying the name is kept but no longer means the message stays queued. Per the ticket's follow-up comment: no rename (TIMED_OUT_QUEUED reaches FleetApp.java REST mapping and FleetMcp.java — out of scope here) and no behavior change; comments only. |
||
|
|
4f9aba40e7 |
Merge #558: the ticket is the pull channel, and the brief is write-once
Docs only, 16 additions / 3 deletions — read by the lead in full, per the under-50-lines self-review rule in CLAUDE.md. Verified on the merged tree (main |
||
|
|
dab697fae0 |
Merge #559: progress watchdog for StatusPoller and SessionReaper loops (fleetd #544)
Verified by the lead on a merged tree (main |
||
|
|
bfac14108f |
fleetd #544: pin the sticky-STOPPED-across-restart invariant
Review of PR #559 (issue comment #16944) found a surviving mutant: removing watchdog.reset() from StatusPoller.start() (and the identical line in SessionReaper.start()) passed the entire suite. stoppedByCaller is sticky and reset() — called only from start() — is the only thing that clears it. Both loops document start() as idempotent and loop()'s own error log says "it can be restarted", so stop() followed by start() is an anticipated path. Without reset() wired into start(), health() would report STOPPED forever after a restart even though the loop is genuinely running again. Add aRestartedLoopReportsRunningAgainNotStoppedForever to both StatusPollerWatchdogTest and SessionReaperWatchdogTest, pinning "an intentional stop must not outlive the restart that follows it". Verified via the standard mutation cycle: exact-line anchor (not regex, to avoid the \Q-style false match the reviewer flagged) counted pristine 1 -> mutated 0, test goes red with its own message, restored, shasum -a 256 byte-identical, green again. mvn clean install: exit 0, BUILD SUCCESS, Tests run: 1734, Failures: 0, Errors: 0, Skipped: 0 (cross-checked against 130 surefire report files). No production code changed — the reset() call under test was already correct; it simply had nothing pinning it. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
7a3b2bb7ee | Merge pull request 'fleetd #552: warn instead of aborting when the post-restart mktemp fails' (#560) from worker/552-post-restart-mktemp-abort-bc2672-4 into main | ||
|
|
f188947750 |
fleetd #552: warn instead of aborting when the post-restart mktemp fails
By the time the fresh-log mktemp ran, the daemon had already been stopped, the jar swapped, and the new daemon started — an unguarded mktemp failure there aborted the whole script anyway, so a caller read the resulting non-zero exit as "the redeploy failed" and would restart an already-correctly-restarted daemon. Extract the mktemp into capture_fresh_log_region, guarded the same way unload_launchd_if_loaded/stop_systemd_if_loaded guard their own, but warn instead of die: there is nothing left to protect by refusing after a successful restart. The trap is now installed before the assignment it cleans up, using an FRESH_LOG="" sentinel readers can check. classify_amqp_connection_errors and report_shutdown_drain both gain a new state (REDEPLOY_AMQP_CHECK_SKIPPED / REDEPLOY_DRAIN_STATE=skipped) for an uncapturable log region, distinct from "captured a region with nothing in it" — and the result section gains a matching branch, so a skipped capture can never read as a clean bill of health. |
||
|
|
f606fccf7f | Merge pull request 'fleetd #553: register the rendezvous waiter in onStatus's finally backstop' (#557) from worker/553-onstatus-completion-leak-0da881-2 into main | ||
|
|
735b6af976 |
fleetd #544: progress watchdog for StatusPoller and SessionReaper loops
Each loop's virtual-thread runner (StatusPoller, SessionReaper) can die or get permanently parked in a herdr call with no read timeout, and nothing observed it: /healthz stayed green and Thread.isAlive() kept reporting true the whole time. Add LoopWatchdog (dev.ltms.fleet.inject — see its javadoc for why not dev.ltms.fleet.health, which would close a package cycle through session): each loop now records a monotonic last-completed-round timestamp (injectable LongSupplier clock, same pattern as Injector/SessionManager) and exposes it as a three-state health() fact — RUNNING, STALLED (dead or parked, indistinguishable from outside), STOPPED (stop() was called on purpose, never an alarm). This is the fleetd #512 shape: one flag cannot carry both "halted on purpose" and "halted unexpectedly", so stop() marks its own state explicitly instead of leaving state() to infer it from staleness. Staleness thresholds are derived from each loop's own poll interval with a documented multiplier: StatusPoller 40x (250ms -> 10s), SessionReaper 12x (5000ms -> 60s). Scope: observability only, per the ticket's own comment. No restart/recovery mechanism, no /healthz or REST/MCP wiring beyond the public health() API, no change to the per-item catch(Throwable) behavior (#543) or a process-wide uncaught-exception handler (ruled out on #538), and no deadline added to the herdr read itself (a separate, real ticket). 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
8b4320ed24 |
fleetd #553: split sentHandled's two meanings so onDelivered's own throw still completes the future
Lead review of PR #557 (ticket comment 16916) found one path left open: sentHandled is set to true BEFORE onDelivered() runs (correctly, per the earlier fix), so when onDelivered() itself throws on the normal path, the finally's 'if (sent != null && !sentHandled)' guard skipped the whole recovery -- completion included -- and left sent.delivered() pending forever for a message that really was delivered. sentHandled must guard only the onDelivered RE-CALL (the permanent-suppression hazard), never the future completion, since CompletableFuture.complete/ completeExceptionally are idempotent and a no-op on the already-handled path. Split the one flag's two jobs: the outer 'if (sent != null)' now always runs the recovery block, and '!sentHandled' moved onto just the onDelivered call inside it. Added anOnDeliveredThrowOnTheNormalPathStillCompletesTheDeliveryFuture, proven with the lead's own mutation (reverting !sentHandled onto the outer if): the new test goes red while anOnDeliveredThrowAfterItsOwnRegistrationDoesNotRunASecondTime stays green, showing the two concerns are genuinely separate. |
||
|
|
351ee1ea6d |
CLAUDE.md: the ticket is the pull channel, and the brief is write-once
A send to a working member is accepted and returns a ticket, then is never delivered. That happened three times in one session here, and the member was released still executing a brief that had been retracted twice. The receipt is true — it is a fact about the mailbox, when what was needed was a fact about the pane. The fleet01 lead named the mechanism: a push delivery needs the recipient free at send time, while a pull channel needs only that they look before acting. So the ticket is not more reliable than the mailbox, it is a different direction, and its success depends on the member's procedure rather than on the timing of the send. Both halves have to be written down, because each is useless alone: - Member (turn contract, new item 4): re-read the ticket before acting on anything told earlier, and again before committing. A ticket comment that contradicts the brief is newer and wins. - Lead (step 5): all corrections go to the ticket, and the brief is write-once. The member cannot check which source is newer — it just always prefers the ticket — so revising a brief in place makes it obey the rule and do the wrong thing. A first brief for a unit not yet running is not a correction. wiki/7-Use-Cases.md is updated to keep the canonical block byte-identical; the sync check passes. The wiki submodule pointer is deliberately left unstaged. |
||
|
|
d4a51c6274 |
fleetd #553: register the rendezvous waiter in onStatus's finally backstop, not just the delivery future
The previous try/finally around onStatus's post-monitor region completed sent.delivered() but never registered sent.token().waiter() when an earlier listener threw. That waiter is registered only by turnListener.onDelivered(), inside the very if (sent != null) block the finally backstops, so a caller was told its send landed and then waited out its full timeout for an answer that could never resolve (worse than a plain hang). The finally now does that block's whole job on the unhandled path: it calls onDelivered() (when sendError == null) before completing the future, guarded by its own try/catch(Throwable) so a failure there cannot mask the original throwable. A sentHandled flag, set true at the START of the normal block (before any side effect), tells the finally whether that already ran, so a throw partway through onDelivered cannot trigger a second, late captureBaseline that would permanently suppress the turn's completion. Also removed a leftover duplicated forget.accept(target) call (with a stray 'MUTATION-TEST-3' comment) in the notReady block — residue from the previous worker's own mutation testing that was not fully reverted. |
||
|
|
26f380a00b |
Merge #554: portable hash256, a third state for an unhashable jar, and the shell suite in CI (#550)
Closes fleetd #550, all three items. Verified by me on the branch at |
||
|
|
b8182c96c2 |
fleetd #550: pin hash256's algorithm against a literal SHA-256 test vector
test_jar_id_defaults_to_live_and_reports_explicit_path's reference hash is computed by calling hash256 itself (needed so it doesn't call the Linux-crashing bare shasum directly). That made subject and reference the same instrument: they agree no matter which algorithm hash256 actually runs, so a mutation swapping both of hash256's arms for the wrong algorithm was invisible to the suite. Adds test_hash256_computes_a_real_sha256, pinned against the published SHA-256 test vector for the 3-byte input "abc" (ba7816bf8f01...), written as a literal constant rather than computed by any hasher at test time. Verified the constant myself both ways (sha256sum and shasum -a 256) before writing it in. |
||
|
|
3da44eed63 |
fleetd #550: replace shasum with a portable hash256 helper, add a Linux CI job for the shell suite
jar_id() in redeploy-fleetd.sh called shasum directly, which does not exist on GNU coreutils Linux (Debian/Ubuntu/etc.) — there it silently reported an existing jar as "absent" with exit 0, because the missing command made `cut` succeed on empty input and pipefail's failure was then swallowed by the `|| echo "absent"` fallback. The shell test suite hit the same tool at test-redeploy-fleetd.sh:298-299 and died at exit 127 with zero FAIL lines printed — the same shape as a clean pass on the one channel anyone would check. Adds one hash256() helper (prefer sha256sum, fall back to shasum -a 256, same idiom already used in probe-member-credentials.sh) and points jar_id and the test suite's own reference hash at it. jar_id now has three distinct answers instead of two: absent, a hash, or "unhashable" when neither hasher is on PATH — "absent" is never used for a file that exists. Adds a CI job (shell-tests) that runs scripts/test-redeploy-fleetd.sh on ubuntu-latest, gated on the step's own exit code rather than a FAIL-line count, since a suite that dies before running is exactly what a green run also looks like by that count. New tests: test_jar_id_reports_unhashable_when_no_hasher_on_path (stubbed PATH with neither hasher) and test_no_unguarded_macos_only_hasher_calls (a shape check across every script under scripts/, not named lines — #545 already showed this idiom spreading from two sites to six). |
||
|
|
93a9ed3f83 |
Merge #549: widen Injector's delivery catch to Throwable (#546)
Closes the re-delivery window that merging #543 opened. I caused that; this closes it the same day. Verified by me on the branch at |