fleetd #234: key opencode model check on resolved session id; make spawn-time quarantine actually happen #236
Closed
agent
wants to merge 0 commits from
worker/cb234-session-id-keyed-04e1fc-1 into main
pull from: worker/cb234-session-id-keyed-04e1fc-1
merge into: fleet:main
fleet:main
fleet:worker/fleetd-612-unita-87807e-1
fleet:worker/612-b3-mcpwirings-da2b58-3
fleet:worker/612-b2-cb185-176d3a-2
fleet:worker/612-b1-completion-457459-1
fleet:worker/612-agaps-73a926-2
fleet:worker/608-sleeps-3a64ff-3
fleet:worker/621-b4520b-1
fleet:worker/618-b83894-2
fleet:worker/fleetd-615-e05481-5
fleet:worker/lead-autocompact-5f1ab2-3
fleet:worker/fleetd-613-f85deb-3
fleet:worker/fleetd-608-flaky-nudge-test-d0c2d1-3
fleet:worker/lead-context-gauge-ad404f-1
fleet:worker/gauge-wiring-9158c1-4
fleet:worker/redeploy-slowstart-ead0e5-5
fleet:worker/charter-bytes-13668c-6
fleet:worker/rollover-outcome-291483-2
fleet:worker/589-f64303-2
fleet:worker/593-1a8025-5
fleet:worker/589-fcd2aa-1
fleet:worker/568-9fdaa2-3
fleet:worker/571-attempted-outcome-5739f7-2
fleet:worker/581-completionresolver-cas-sites-0542b7-6
fleet:worker/562-loop-health-wiring-test-99611c-5
fleet:worker/562-surface-loop-health-7df5cc-4
fleet:worker/575-waiter-cleanup-sites-62ad80-1
fleet:worker/572-answer-lock-release-46a9ae-5
fleet:worker/567-probe-channel-leak-a38fc5-6
fleet:worker/551-record-before-send-7cbf56-1
fleet:worker/561-listener-fanout-survives-a-throw-61d538-2
fleet:worker/555-redeploy-main-flow-seam-65c2f5-2
fleet:worker/556-injector-owns-registration-e027a5-1
fleet:worker/552-post-restart-mktemp-abort-bc2672-4
fleet:worker/553-onstatus-completion-leak-0da881-2
fleet:worker/550-shasum-linux-196132-1
fleet:worker/538-loop-dies-on-error-4a5eeb-6
fleet:worker/426-health-coverage-ef1fd4-4
fleet:worker/504-failed-reported-clean-3cfd66-3
fleet:worker/537-capturedlog-close-e4c437-2
fleet:worker/459-broken-link-targets-cadc17-5
fleet:worker/535-appender-leak-fe74c1-1
fleet:worker/512-part2-shutdown-detection-434701-9
fleet:worker/529-logger-level-sweep-2a5533-8
fleet:worker/528-drain-gate-call-site-5de83d-7
fleet:charter/forge-mcp-vs-token
fleet:worker/521-swap-guard-unpinned-28e931-5
fleet:worker/519-probe-test-harness-d25ab8-4
fleet:worker/525-logger-level-leak-1b4eb0-6
fleet:worker/518-fleetmcp-resolver-wiring-8ef96c-1
fleet:worker/512-drain-complete-line-7edd71-3
fleet:worker/517-abort-branch-and-jar-id-41b641-2
fleet:worker/500-9e52c9-3
fleet:worker/509-4912f4-2
fleet:worker/511-9a4b23-1
fleet:worker/493-479f45-2
fleet:worker/505-03f8b2-1
fleet:worker/492-followup-detect-unclear
fleet:worker/501-a31fa0-7
fleet:worker/498-451d1c-5
fleet:worker/494-1015ce-2
fleet:worker/492-209647-1
fleet:worker/489-001902-2
fleet:worker/480-relative-handover-path-906323-1
fleet:worker/480-b-handover-skill-45bf1f-5
fleet:worker/474-followup-source-pin-f54a55-17
fleet:worker/474-charter-check-on-reload-f54a55-17
fleet:worker/466-quarantine-repeatcount-report
fleet:worker/393-opencode-skill-seeding-71854b-13
fleet:worker/469-canonical-tool-names-2a472a-16
fleet:worker/466-quarantine-escalation-5ae9c1-15
fleet:worker/446-hot-exhausted-pattern-0af580-6
fleet:worker/464-charter-tool-name-guard-a85635-12
fleet:worker/463-listfleet-default-fails-open-f1c76c-11
fleet:worker/458-invariant-5-by-purpose-862f9a-10
fleet:worker/439-coordinator-row-gate-bc032a-8
fleet:worker/449-herdr-protocol-576015-4
fleet:worker/450-abstract-spawn-599e1c-5
fleet:worker/437-ack-refuses-177d91-1
fleet:worker/444-placement-window-feb56a-2
fleet:worker/440-helddurable-derived-d462d7-13
fleet:worker/425-rework-placement-resolve-c58ba1-9
fleet:worker/421-lead-peek-held-msgs-cdbad2-10
fleet:worker/435-fixed-policy-cap-fe11de-12
fleet:worker/422-gate-state-observability-9e79d6-11
fleet:worker/431-memberregistry-live-readers-cdbad2-10
fleet:worker/424-architect-slot-hot-038b41-7
fleet:worker/422-model-gate-spawn-c29f48-6
fleet:worker/425-default-profile-live-f55534-8
fleet:worker/415-coverage-wording-2cbf9c-5
fleet:worker/416-3ad1da-1
fleet:worker/418-588283-3
fleet:worker/deterministic-stamp-race-409-3cb7b6-10
fleet:worker/armed-reads-live-config-404-ed931f-9
fleet:worker/reply-peer-refusal-391-5a34bd-7
fleet:worker/models-allowlist-aa9e9b-3
fleet:worker/ttl-stamp-race-399-f1122f-8
fleet:worker/scrub-receipt-400-316b3e-5
fleet:worker/exhaustion-detection-395-105105-6
fleet:worker/scrub-abort-394-316b3e-5
fleet:fix/scrub-uid-abort
fleet:worker/task-scrub-517574-2
fleet:worker/t386-clock-bd5b78-4
fleet:worker/t384-scrub-813790-5
fleet:worker/t381-cc-748314-2
fleet:worker/t373-336973-2
fleet:worker/t365-3920c5-3
fleet:worker/t358-6e989b-1
fleet:worker/t355-8b321c-1
fleet:worker/fleetd-369-hermetic-git-tests-e8b19a-3
fleet:worker/fleetd-368-stale-lead-binding-f5682e-2
fleet:worker/fleetd-360-deploy-units-0d3793-1
fleet:worker/359-dead-lead-tabs-f1253b-4
fleet:worker/362-worktree-skills-c03e51-3
fleet:worker/361-coord-visibility-655144-1
fleet:362-plugin-visibility-and-drift
fleet:worker/errscan-bed2ca-2
fleet:worker/amqp-log-identity-bed2ca-2
fleet:worker/withdefaults-guard-561704
fleet:worker/sleepguard-82076d-1
fleet:worker/fd334-9ee1b6-5
fleet:worker/fd348-f1ab27-4
fleet:worker/fd335-a71c35-1
fleet:worker/fd342-174a17-2
fleet:worker/fd345-490d0f-3
fleet:worker/fleetd-337-5ec7d4-21
fleet:worker/fleetd-341-af5a6b-24
fleet:worker/fleetd-339-5ca0a2-23
fleet:worker/fleetd-338-83a4a1-22
fleet:worker/fleetd-333-281f46-18
fleet:worker/fleetd-329-11bdbb-16
fleet:worker/fleetd-330-2770fb-17
fleet:worker/fix-326-50506e-15
fleet:worker/fix-324-3e9bbf-14
fleet:worker/fix-323-b8287d-13
fleet:worker/fix-316b-bd0860-11
fleet:worker/fix-318-76ca36-9
fleet:worker/fix-317-486aec-8
fleet:worker/fix-315-ce47c5-6
fleet:worker/fix-307-275890-6
fleet:worker/fix-308-b4f664-7
fleet:worker/fix-309-ec3939-8
fleet:worker/fix-310-7a3974-9
fleet:worker/fix-302-52ad0e-9
fleet:worker/fix-298-ce1acb-8
fleet:worker/fix-297-66bd11-7
fleet:worker/fix-296-104622-6
fleet:worker/fix-293-bare-closetab-eb22b5-3
fleet:worker/fix-280-gone-ask-lapse-bca98e-2
fleet:worker/fix-290-reapidle-guard-coverage-9b0dd1-1
fleet:worker/fix-285-trust-seed-8f3565-10
fleet:worker/fix-284-backend-error-seat-85912c-11
fleet:worker/fix-282-chained-ask-e6d0bb-8
fleet:worker/fix-283-teardown-leaks-f40dfa-9
fleet:worker/fix-281-pin-handler-actions-4921ac-7
fleet:worker/audit-rendezvous-lifecycle-d072ae-2
fleet:worker/audit-health-placement-1a2476-6
fleet:worker/audit-teardown-exits-e207a5-3
fleet:worker/audit-launcher-asymmetry-27e370-4
fleet:worker/audit-rest-authz-6ca53c-5
fleet:worker/investigate-275-abandon-asking-fdef52-8
fleet:worker/fix-274-worktree-leak-b0095d-7
fleet:worker/fix-273-exhausted-pattern-9665b5-6
fleet:worker/fleetd-267-model-check-bd8068-1
fleet:worker/fleetd-131-archunit-18b834-7
fleet:worker/fleetd-266-sshagent-rename-a014ff-6
fleet:worker/fleetd-184-uid-claim-8e1f31-4
fleet:worker/fleetd-184-warn-b381ee-10
fleet:worker/fleetd-184-docs-be1d12-9
fleet:worker/fleetd-257-9bf010-7
fleet:worker/fleetd-103-23a113-6
fleet:worker/fleetd-247-342356-5
fleet:worker/fleetd-116-04dea8-4
fleet:worker/fleetd-252-a830e0-3
fleet:worker/fleetd-111-7e8673-9
fleet:worker/fleetd-155c-f8ef4b-8
fleet:worker/fleetd-176-b928ca-3
fleet:worker/fleetd-249-7a7878-2
fleet:worker/cb248-composition-root-b-9acdf7-15
fleet:worker/cb148-envrc-default-fa6c82-12
fleet:worker/cb201-unit5-wiring-6c12e6-8
fleet:worker/cb241-fallback-echo-1175e9-11
fleet:worker/cb149-trust-dialog-2392a5-9
fleet:worker/cb134-148-overlay-visible-c9b986-10
fleet:worker/cb201-unit3-nudge-abdf5c-6
fleet:worker/cb201-unit2-policy-c1102c-5
fleet:worker/cb201-unit4-outcome-a13bfa-7
fleet:worker/cb201-unit1-classifier-91b9b1-4
fleet:worker/cb201-227-refine-831980-3
fleet:worker/cb175-model-readback-0f085f-1
fleet:worker/cb222-charter-tmpdir-17f013-1
fleet:worker/cb226-architect-slot-race-cd3aa8-3
fleet:worker/cb224-worktree-root-group-024523-2
fleet:worker/cb-123-role-demotion-c600f7-2
fleet:worker/cb-219-opencode-roots-1f677e-1
fleet:worker/cb214-claude-session-id-b9eab4-4
fleet:worker/cb213-zdotdir-wrong-process-dd6de4-3
fleet:worker/cb211-exhaustion-classification-9546e0-2
fleet:worker/cb137-ambiguous-task-4df3d8-4
fleet:worker/cb209-agentsessionid-4dfdb6-2
fleet:worker/cb185-hostenvnames-2692b5-3
fleet:worker/cb206-opencode-sqlite-128718-2
fleet:worker/cb185-worktree-group-fc0c99-1
fleet:worker/cb-137-ask-ticket-e7760c-2
fleet:worker/cb-172-broker-uri-d36ae4-4
fleet:worker/cb-175-model-readback-76ead6-3
fleet:worker/cb-161-pane-ancestry-293510-1
fleet:worker/cb-164-rebase-885863-8
fleet:worker/cb-164-empty-scrape-false-success-1a80af-3
fleet:fix/cb-197-ticket-ttl-from-completion
fleet:worker/cb-189-remote-url-coverage-4692f3-1
fleet:worker/cb-185-blockers-027756-4
fleet:worker/cb-192-gap-log-11b631-2
fleet:worker/cb-633-fix-5f4396-3
fleet:worker/cb185-router-d6436d-3
fleet:worker/cb185-router-routing-gaps-9e9d33-3
fleet:worker/cb185-paneids-992586-2
fleet:worker/cb-633-allow-list-union-ed374b-1
fleet:worker/cb-157-credential-in-remote-url-496e44-2
fleet:worker/cb-641-health-herdr-evidence-8f1f54-6
fleet:worker/cb-640-health-msg-evidence-99c9cd-1
fleet:worker/cb-642-fleets-status-skill-bbbc40-5
fleet:cb-634-ide-mcp
fleet:worker/lead-comms-wiring-c014b9-7
fleet:worker/lead-mailbox-c19577-6
fleet:worker/autocompact-window-82bc2f-5
fleet:worker/cb-634-probe-18056f-4
fleet:worker/cb635-broker-urienv
fleet:worker/cb-632-config-retry-8e0efa-7
fleet:lead/cb-622e-claude-md
fleet:lead/cb-622-followup
fleet:worker/cb-622a-165dff-1
fleet:lead/cb-622d-opencode-mount
fleet:worker/cb-622b-717c67-2
fleet:worker/cb-622c-ab7759-3
fleet:worker/cb-617b2-20ca4b-3
fleet:worker/cb-617a-5c2f4a-1
fleet:worker/cb596-4e49ef-3
fleet:worker/cb586-10500c-1
fleet:worker/cb-606-b9343a-25
fleet:worker/cb604-1445f8-24
fleet:worker/cb582-477374-21
fleet:worker/cb584-8c2281-22
fleet:worker/cb600-e6b9a9-20
fleet:worker/cb602-ce257f-19
fleet:worker/cb601-b42837-18
fleet:worker/cb598-6c7ba7-17
fleet:worker/cb599-740fe4-16
fleet:worker/cb597-282224-15
fleet:worker/cb590fix-185e9a-10
fleet:worker/cb528-recovery-race
fleet:worker/cb594-96bead-8
fleet:worker/cb590-916766-2
fleet:worker/cb527-997d99-3
fleet:worker/cb592-env-leak-3cbf9c-1
fleet:worker/cb588-async-ticket-nudge-3218f7-5
fleet:worker/cb578b-9dcb13-6
fleet:worker/cb581-d24826-5
fleet:worker/m2-u5-ef8c42-15
fleet:worker/cb578a-516499-2
fleet:worker/cb576-01a04b-17
fleet:worker/cb579-lead-tab-acba06-20
fleet:worker/cb580-terminal-health-ed6058-21
fleet:worker/cb577-f36fdc-18
fleet:worker/cb573b-3db06f-16
fleet:worker/cb568c-f36fdc-18
fleet:worker/cb568-drop-cause-c3ac1c
fleet:worker/cb575-cancelled-notification-c3ac1c
fleet:worker/m4-sol-a2cbec-3
fleet:worker/cb574-async-ask-c3ac1c
fleet:worker/cb573-health-model-8ca857-14
fleet:worker/cb572-unknown-target-7f2e35-13
fleet:worker/u4-700706-9
fleet:worker/u3-b9fcb6-6
fleet:worker/u2-ef5b68-4
fleet:worker/u1-469dce-1-clean
fleet:worker/u1-469dce-1
fleet:worker/cb-564-health-events-70cf7e-2
fleet:worker/cb-565-recycle-drops-role-98e58f-3
fleet:worker/cb-563-missing-reply-df2866-1
fleet:worker/cb-562-readiness-gate-silent-6c23c9-3
fleet:worker/cb-560-architect-presence-da8155-1
fleet:worker/cb-561-architect-silent-off-a71cab-2
fleet:worker/cb-548-bind-architect-slot-fe1b8c-1
fleet:worker/parity-overlay-settings-5fb711-1
fleet:secrets-central-store
fleet:cb-559-hot-key-correction
fleet:cb-557-fleet-role-pools
fleet:worker/cb-553-maxload-explicit-spawn-305ee3-6
fleet:worker/cb-551-idle-lead-heartbeat-f1633c-1
fleet:worker/cb-544-drain-preserves-worktree-925fad-3
fleet:worker/cb-552-docs-sync-1cb9cf-4
fleet:worker/cb-548-rendezvous-guard-rebased
fleet:worker/cb-548-rendezvous-guard-116b53-10
fleet:worker/cb-548-authz-v2-586df6-8
fleet:worker/cb-548-authz-264363-5
fleet:salvage/cb-528b-codex-home
fleet:salvage/cb-528a-codex-launcher
fleet:CB-518-primary-flow
fleet:feature/peer-launcher-spi
fleet:cb-103-injector
No Reviewers
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#236
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 "worker/cb234-session-id-keyed-04e1fc-1"
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?
fleetd #234 — session-id-keyed model check + spawn-time quarantine that actually happens
Two coupled defects, one owner (per the ticket) to avoid a merge conflict.
Defect 1 — model read-back keyed on directory, not the resolved session id
OpenCodeSessionDiscovery.actualModelForDirectoryqueriedWHERE directory = ?, the same heuristicsessionIdForDirectoryuses. A defaultfleet_spawn(noworktree:) shares the lead's cwd with every other worker and every past session ever run there, so the model read-back could silently compare against a different session's row (this was measured live: a freshterramember's check compared against a 3-day-oldgxsession in the same directory).Fix: renamed to
actualModelForSessionId(sessionId), keyed on the primary keyidinstead ofdirectory.OpenCodeLauncher'sSessionAwareHandlenow caches the resolved id once it is first non-null (AtomicReference,compareAndSet(null, id)), so a later sibling row appearing in the same shared directory can never flip which session's evidence gets read. If the id has not resolved yet, that is UNKNOWN — never compared (fleetd #175's existing rule).sessionIdForDirectory(fleetd #209) is left exactly as-is and still directory-based, per the ticket's explicit instruction — a comment now explains why the heuristic is unavoidable at that layer (nothing else disambiguates a session at this layer, and diffing before/after races under concurrent spawns).Defect 2 — the quarantine the ERROR log announces never actually happened
The ERROR log said "quarantining this profile's credential" but
Fleetd.java'sExhaustionSinklambda resolvedtarget -> session (via sessions.roster()) -> profile -> credential.OpenCodeLauncher's model-mismatch check fires fromSessionAwareHandle.agentSessionId(), called duringSessionManager.acquire()before the new session is registered in the roster — the lookup found nothing and.ifPresentsilently no-opped.Fix: added a default 3-arg
ExhaustionSink.onExhausted(target, reason, profile)overload (defaults to the existing 2-arg method, soCompletionResolver's two call sites — which always call with a target already live in the roster — are unchanged).OpenCodeLauncheralready holds its ownFleetConfig.Profileand now passes its profile name directly, bypassing the need for roster resolution at this call site entirely.Fleetd.java's sink (now an anonymous class, not a lambda, so it can actually override the 3-arg method) tries the roster first, falls back to the hint, and — if neither resolves — logs loudly at ERROR naming target/reason/profile-hint instead of silently no-opping.Mutation-tested, not just asserted
Both fixes were proven load-bearing by reverting each independently and confirming its new test fails with a real message, then restoring and confirming it passes again:
Defect 1 — reverted
OpenCodeLauncher.java+OpenCodeSessionDiscovery.javato HEAD, ranOpenCodeLauncherTest#modelCheckReadsTheResolvedSessionsOwnRowNotWhateverIsNewestInTheSharedDirectory:Restored, re-ran: green.
Defect 2 — mutated the one call site in
OpenCodeLauncher.javaback to the 2-argonExhausted(target, reason)call (the pre-fix shape), ranOpenCodeLauncherTest#aSpawnTimeModelMismatchActuallyQuarantinesTheCredentialThroughTheRealAcquirePath:Restored, re-ran: green. (A second new test,
aRosterOnlySinkSilentlyDropsTheSpawnTimeQuarantine, pins the pre-fix roster-only-sink behavior directly, using a plain 2-arg lambda that cannot see the hint.)Both new tests go through the real production call path (
SessionManager.acquire()->handle.agentSessionId()-> the sink), not a direct call into the sink or the discovery class.Build
mvn clean install(unpiped, fromfleetd/):Files changed
src/main/java/dev/ltms/fleet/member/OpenCodeSessionDiscovery.javasrc/main/java/dev/ltms/fleet/member/OpenCodeLauncher.javasrc/main/java/dev/ltms/fleet/inject/ExhaustionSink.javasrc/main/java/dev/ltms/fleet/Fleetd.java(confined to theExhaustionSinklambda ~line 348 — #115's startup-warning work in the rest of that file is untouched)src/test/java/dev/ltms/fleet/member/OpenCodeSessionDiscoveryTest.javasrc/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.javaCaveats for review
sessionIdForDirectory(#209) intentionally left directory-based, per the ticket — only a comment was added explaining why.~/.local/share/opencode/opencode.db— all discovery tests build a temporary SQLite fixture via@TempDir.The round-1 fix was dead on the real production path. Fleetd.java:177 builds a forwarding sink (needed because the adapters are constructed before `sessions` exists, breaking a genuine cycle) as a LAMBDA: ExhaustionSink forwardingExhaustionSink = (target, reason) -> exhaustionSinkRef.get().onExhausted(target, reason); A lambda can only implement the interface's one abstract method (the 2-arg overload), so it silently inherited the 3-arg overload's default body, which drops the profile hint and calls back into the 2-arg method. OpenCodeLauncher is constructed with this forwarder, so the hint it supplies (its own already-known profile name) was thrown away before it ever reached the real sink built later in Fleetd.main -- reproducing the exact silent no-op round 1 was sent to fix. The 1127 tests from round 1 all injected a sink directly into OpenCodeLauncher and never went through this forwarding hop, so none of them could see it. Fix: forwardingExhaustionSink is now an anonymous class overriding both overloads, each delegating to whatever exhaustionSinkRef currently holds. Audited every other ExhaustionSink value in main/: the only other one is ExhaustionSink.none() (a lambda), which is safe regardless of arity since both its 2-arg body and the inherited 3-arg default are true no-ops. New tests: - ExhaustionSinkForwardingHazardTest: isolates the hazard at the interface level (a lambda forwarder drops the hint; an anonymous-class forwarder does not), independent of Fleetd.java's specific wiring. - OpenCodeLauncherTest#theSpawnTimeQuarantineSurvivesTheFleetdStyleForwardingHop: replicates Fleetd.java's actual construction order (forwarder built and handed to the launcher first, real sink built and pointed at via the AtomicReference afterward) and drives the quarantine through it via the real SessionManager.acquire() path. Both proven by mutation: temporarily rewriting each fixed forwarder back into the pre-fix lambda makes its test fail with a real assertion message (both matched exactly: "expected: <gx> but was: <null>" for the interface proof, "expected: <true> but was: <false>" for the composed-wiring test); restoring makes it pass again. No reverts were committed. mvn clean install: Tests run: 1130, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.Round 2 — Defect 2's forwarding hop was also broken, now fixed
You were right:
Fleetd.java:177'sforwardingExhaustionSinkwas a lambda, so it could only implement the 2-argonExhausted(target, reason)and silently inherited the 3-arg overload's default body — droppingOpenCodeLauncher's profile hint before it ever reached the real sink. Confirmed by reading the code at that line and by reproducing your proof test.Fix
Fleetd.java:177—forwardingExhaustionSinkis now an anonymous class overriding both overloads, each delegating toexhaustionSinkRef.get():Item 3 — every other
ExhaustionSinkvalue in main/Searched
src/main/javafor every construction ofExhaustionSink. BesidesFleetd.java's two sinks (the forwarder just fixed, and the real one built later — already an anonymous class overriding both overloads, unchanged), the only other value anywhere in main/ isExhaustionSink.none():This IS a lambda, but it is safe regardless of arity: its 2-arg body is empty, and the inherited 3-arg default just calls that same empty 2-arg body. There is no hint to drop because it does nothing either way.
CompletionResolverandOpenCodeLauncher's no-sink constructors both use it as an inert default — no fix needed there. No other lambda or method-referenceExhaustionSinkexists in main/.Before/after — the exact proof test you gave me
Added
ExhaustionSinkForwardingHazardTest#profileHintSurvivesTheForwardingHopUsedInProduction(your snippet, insrc/test/java/dev/ltms/fleet/inject/). Mutated its forwarder back to a lambda (the pre-fix shape) and ran it:Matches your result exactly. Restored the anonymous-class forwarder, re-ran: green.
A second test in the same file,
aLambdaForwarderDropsTheProfileHintBeforeItReachesTheRealSink, documents the mechanism directly (asserts the real sink's 3-arg method DOES still run through its own default, but with the hint already null — not "never called").Composed-wiring test — drives the hint through production-shaped wiring, not just the interface
Added
OpenCodeLauncherTest#theSpawnTimeQuarantineSurvivesTheFleetdStyleForwardingHop. It replicatesFleetd.java's actual construction order: anAtomicReference<ExhaustionSink>seeded withExhaustionSink.none(),OpenCodeLauncherconstructed against a forwarder pointed at that reference (mirroringadaptersbeing built beforesessionsexists), and only afterward builds the real hint-aware sink and points the reference at it (mirroringexhaustionSinkRef.set(exhaustionSink)at the end ofFleetd.main). It then drives the mismatch through the realSessionManager.acquire()path and asserts the credential actually gets quarantined.Mutation proof: rewrote the test's forwarder back into a lambda, ran it:
Restored the anonymous-class forwarder, re-ran: green. Confirmed via
git status --shortafter restore that only the intended 3 files carry changes (no stray edits left from the mutation).Build
mvn clean install, unpiped, run from insidefleetd/(notmvn -pl fleetdfrom the repo root):Files changed this round
src/main/java/dev/ltms/fleet/Fleetd.java— still confined to theExhaustionSinkwiring (the forwarder at ~line 177 this time, not the ~348 lambda from round 1)src/test/java/dev/ltms/fleet/inject/ExhaustionSinkForwardingHazardTest.java(new)src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java(new test added)Pushed to
worker/cb234-session-id-keyed-04e1fc-1, same PR (#236), no new PR opened.Round 2's tests never reached Fleetd.java at all: both new tests declared their OWN local copy of the forwarding shape instead of calling production's. Mutating Fleetd.java's real forwarder back into the broken lambda left those copies untouched, so the whole suite stayed green while production had regressed to exactly the bug being fixed -- proven live by the reviewer. Fix: extracted the forwarding shape into one named factory, ExhaustionSink.forwardingTo(Supplier<ExhaustionSink> target), with the "why a lambda here is wrong" explanation moved onto it (the one place the shape is now written). Fleetd.java's forwarder collapses to one line: ExhaustionSink forwardingExhaustionSink = ExhaustionSink.forwardingTo(exhaustionSinkRef::get); Both new tests now call this same factory instead of rebuilding an anonymous class inline, so they exercise the identical object production builds: - ExhaustionSinkForwardingHazardTest: calls ExhaustionSink.forwardingTo directly and asserts the hint reaches the real sink through it. - OpenCodeLauncherTest#theSpawnTimeQuarantineSurvivesTheFleetdStyleForwardingHop: same factory call, inside the full Fleetd-shaped construction order (forwarder built first, real sink pointed at via the AtomicReference afterward), driven through the real SessionManager.acquire() path. Mutation proof, this time on production code only: deleted the factory's 3-arg override (falls back to the interface default, dropping the hint) -- both new tests go red with no test file touched: ExhaustionSinkForwardingHazardTest...: expected: <gx> but was: <null> OpenCodeLauncherTest...ForwardingHop: expected: <true> but was: <false> Tests run: 68, Failures: 2 Restored, re-ran: green (Tests run: 68, Failures: 0). Confirmed Fleetd.java carries no lambda ExhaustionSink anywhere (grep). ExhaustionSink.none() stays a lambda on purpose -- both its overloads are true no-ops regardless of arity, so there is no hint to drop. mvn clean install: Tests run: 1129, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.Round 3 — extracted the forwarding shape into one shared factory, both tests now call it
You measured it correctly: round 2's two new tests each built their OWN local copy of the forwarding shape instead of calling anything
Fleetd.javabuilds. MutatingFleetd.java's real forwarder back into a lambda left those copies untouched, so the suite stayed green while production had regressed. Confirmed by reproducing your grep (everyFleetdreference in both test files was in a javadoc comment, none in executable code) and by re-running the same mutation you described.Fix —
ExhaustionSink.forwardingToAdded to
ExhaustionSink.java:Fleetd.java's forwarder is now one line:Moved the "why a lambda here is wrong" explanation onto the factory's javadoc, per your instruction, with a short pointer comment left at the
Fleetd.javacall site.Both new tests rewritten to call the factory, not rebuild it
ExhaustionSinkForwardingHazardTest#forwardingToDeliversTheProfileHintToWhateverSinkTheSupplierCurrentlyReturns— callsExhaustionSink.forwardingTo(ref::get)directly.OpenCodeLauncherTest#theSpawnTimeQuarantineSurvivesTheFleetdStyleForwardingHop— same call inside the full construction-order replica, driven through the realSessionManager.acquire()path.No hand-built forwarder remains in either test file. (I also deleted the round-2 documentation-only test that demonstrated the hazard with a deliberately-broken local lambda — it never called production code either way, and is now redundant with the factory-level test.)
Acceptance
1 & 2 — mutation on the factory body itself, no test file touched:
Red:
Restored, green:
3 —
grep -n "ExhaustionSink" src/main/java/dev/ltms/fleet/Fleetd.java: no lambda anywhere — the forwarder is now aforwardingTo(...)call, and the real sink (built later, oncesessionsexists) is unchanged from round 2, still an anonymous class overriding both overloads.ExhaustionSink.none()stays a lambda on purpose: both its overloads are true no-ops regardless of arity, so there is no hint to drop.4 — full build, unpiped, from inside
fleetd/:(1129 vs round 2's 1130 — net -1 from deleting the now-redundant documentation-only test above.)
The check you asked me to satisfy
Verified directly: the mutation above only touched
ExhaustionSink.java(the factory body), touched no test file, and both new tests went red with the exact assertions above.Pushed to
worker/cb234-session-id-keyed-04e1fc-1(commitc935b18), same PR #236, no new PR.Round 4 (final) — inverted ExhaustionSink so the bug class cannot be written
Made the 3-arg
onExhausted(target, reason, profile)the interface's single abstract method; the 2-arg form is now adefaultthat delegates withprofile = null. A lambda declared againstExhaustionSinktoday has no 2-arg overload left to bind to — the compiler forces three parameters.1 — abstract/default inverted
2 —
CompletionResolver's call sites: untouchedgit diff --statonCompletionResolver.javabefore staging: zero output — the file has no diff at all. Its two call sites still readexhaustionSink.onExhausted(target, reason), which now resolves to the default, behaving identically to before.CompletionResolverTest: 41 tests, 0 failures (part of the 109-test run below), proving this. Its fiveExhaustionSinklambdas needed a mechanical third parameter added to keep compiling against the new abstract method ((target, reason) -> ...→(target, reason, profile) -> ...) — no assertion in that file changed. Diff for one representative site:(Flagging:
CompletionResolverTest.javais technically outside my file-ownership list for this ticket, but the interface inversion you asked for makes this edit unavoidable everywhereExhaustionSinkis implemented — it's the one file outside my assigned scope this round touched, and only mechanically.)3 — mutation re-run against the new shape
forwardingTois now a single-line 3-arg lambda, so round 3's "delete the 3-arg override" doesn't apply verbatim — the equivalent mutation is making the factory call the 2-arg default instead of passing the hint through:Red, no test file touched:
Restored, green:
(109 = the 68 from before plus CompletionResolverTest's 41, run together to also confirm item 2 in the same pass.)
4 — compiler proof (not committed)
Wrote a scratch file OUTSIDE the worktree (
/tmp/cb234-scratch/...), copied the realExhaustionSink.javanext to it, compiled withjavacdirectly:Deleted the scratch directory afterward (
rm -rf /tmp/cb234-scratch) — nothing staged or committed from it.5 — full build, unpiped, from inside
fleetd/Other knock-on cleanups
ExhaustionSink.none()— now a 3-arg lambda, still a genuine no-op, safe by construction rather than by care.Fleetd.java's real sink, and the twoOpenCodeLauncherTestsinks that were anonymous classes overriding both overloads — all now plain lambdas, since the 2-arg override each carried was pure boilerplate once the interface supplies it as a default.Pushed to
worker/cb234-session-id-keyed-04e1fc-1(commit31b028e), same PR #236, no new PR.Merged to main in
838a701. Round 4 is the one that ships.I verified the inversion with
javacagainst the real interface file, not against a copy:The bug class is now unrepresentable rather than merely tested, which is the point. It proved itself straight away: merging Unit 1 (#240) on top produced exactly one compile error — a two-argument lambda that Unit 1 had added to
CompletionResolverTest. Under the old shape that lambda would have compiled, run, and silently dropped the profile.Full integrated build after the merge: 1163 tests, 0 failures, 0 compile errors.
For the record, the three rounds this took, because the pattern is worth remembering:
default, soFleetd.java:177's two-argument lambda kept compiling and the fix never ran in production.forwardingTofactory fixed the call site but still allowed the bad shape elsewhere.The lesson: when you add an overload, the unit of work is every implementation, and a
defaultmethod is invisible to a lambda.Pull request closed