#175's model read-back fires a FALSE POSITIVE on any member spawned without a worktree — and the quarantine it announces never happens #234
Closed
opened 2026-09-03 04:26:09 +02:00 by ltms
·
2 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#234
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?
Found on the live daemon within one minute of deploying the #176 merge (
main@0f08b93, pid 45674, jarecb9645758b3), by spawning one probe member per backend. Two separate defects, both in code merged earlier today for #175.Neither was caught by the #175 verification. That is the most useful part of this report — see "Why my own verification missed it" at the end.
Defect 1 — the read-back compares against a foreign session, and accuses a healthy profile
I spawned a plain
fleet_spawn{profile: terra}with no worktree. The daemon logged:terra was running fine. The check read a different, three-day-old session belonging to a different profile.
OpenCodeSessionDiscoverykeys both its lookups on the worker's cwd:The member's cwd was the lead's cwd,
/Users/dai.ha/LTMS/claude-bridge. That directory holds many old sessions:Note the date: 2026-08-31, three days before this spawn. A brand-new idle member has not written its own row yet, so "most recently updated session in this directory" is somebody else's old session.
fleet_listduly reportedagentSessionId: ses_fa945e505ffepqOZNfQ24cfOjsfor the new terra member — a stale id from agxrun.So #209's identity resolution is wrong too, not only #175's model check. #175 merely made the wrong answer visible by acting on it.
The false premise, stated in the code
OpenCodeSessionDiscovery's own javadoc says the determinism is structural:That premise is false.
fleet_spawnprovisions a worktree only when the caller passesworktree. Without it the member inherits the lead's cwd — the default path, and the one I took. The word "every" is doing the damage: it reads as an invariant, so nobody re-checked it.Why this is worse than a wrong log line
It quarantines on it. A quarantine is keyed on the credential, and both
solandterracarrycredentialId: openai-shared. So one false positive on terra is designed to take outsolas well — both OpenAI profiles, forquarantineCooldownSeconds: 1800. That is half the fan-out capacity, removed for 30 minutes, because of a session row from three days ago.Suggested direction
Key on the session id, not the directory. The launcher already resolves an
agentSessionId; the model check should read the row for that id and nothing else. If the id is not resolved yet, that is UNKNOWN — return, do not compare, exactly as #175 already does for absent evidence. Do not try to make the directory heuristic smarter (atime_updated >= spawn timefilter would narrow it, not fix it, and would still pick a sibling member sharing the cwd).While fixing it, correct the javadoc rather than leaving a false invariant in the file.
Defect 2 — the quarantine it announces never happens
The ERROR says "quarantining this profile's credential". It did not.
fleet_listone minute later, withquarantineCooldownSeconds: 1800:No
credentialId, noquarantinedForSeconds, andfreeis not forced to 0 — the three things CB-583'scapacityViewadds for a quarantined profile. And the sink's own log line never appears: grepping the whole log since the restart forquarantinreturns exactly one line, the ERROR above.Fleetd'slog.warn("credential '{}' quarantined for {}s ...")never ran.The cause is in the sink (
Fleetd.java:348):It resolves
target→ session → profile → credential throughsessions.roster(). The #175 check calls it fromSessionAwareHandle.agentSessionId(), which runs during spawn — timestamps show the mismatch ERROR at09:22:28.891, 0.7s afterpeer pane=w8:p1Y reached injectable stateat09:22:28.205, before the member is registered in the roster. No roster entry ⇒.ifPresentno-ops ⇒ silence.So the check detects, logs, and does nothing. The one saving grace today: defect 2 is the only reason defect 1 did not cost me
solandterrafor half an hour. Two bugs cancelling out is not a working feature.This is the same shape as #113 and as the "a test on the seam does not prove the caller" family: the response to a detection was never proved on the real path. #175's tests all exercised the checker, and the checker is fine.
Suggested direction
.ifPresentis a silent failure on a control path. When a quarantine is requested for a target the sink cannot resolve, it must not fall through quietly — either resolve the profile from something available at spawn time (the launcher knows its own profile; it does not need the roster), or log loudly that the quarantine was requested and could not be applied. A control that cannot act must say so.Whichever fix is chosen, the test has to start from the real call — a spawn that triggers the check — and assert the credential actually ends up quarantined. A test that calls the sink directly will pass today.
Why my own verification missed it
Recorded deliberately, because the method failed, not just the code.
I did live-prove #175 before closing it. That probe used a member on a fresh worktree — a unique directory with exactly one session row, its own. That is the one configuration in which the directory heuristic is guaranteed correct. I proved the feature on the path where its assumption holds, then reported it verified.
The probe that found this took thirty seconds and differed in one way: no worktree, which is the default for
fleet_spawn. So the verification covered the special case and skipped the default.Two rules from this:
Related
#175 (introduced both, merged today) · #209 (the directory-keyed lookup both share; its identity resolution is wrong for the same reason) · #113 (checkers narrower than they look) · #232 (closed: the same read-back has no evidence at all for
autoCompactWindow)Review of PR #236 — defect 1 accepted, defect 2's fix is dead on the production path
Defect 1 (the model check keyed on
directoryinstead of the resolved session id) is fixed correctly.actualModelForSessionId(id)is a primary-key lookup,SessionAwareHandlecaches the resolved id in anAtomicReferenceso a sibling session cannot make it drift, and the javadoc that claimed "every fleetd worker runs in its own unique git worktree" is corrected. Accepted.Defect 2 is not fixed. The 3-arg
onExhausted(target, reason, profile)overload never reaches the real sink.Fleetd.java:177builds the sink theOpenCodeLauncheris actually constructed with (:193):That is a lambda. A lambda implements only the interface's one abstract method, so it inherits the new 3-arg default, which discards the profile and calls the 2-arg. The real chain is:
So the original bug still happens. The new ERROR log is an improvement — the failure is at least loud now instead of silent — but the quarantine the check announces still does not occur.
Proof
I wrote this into the PR's own tree and ran it. It fails:
expected: <gx> but was: <null>.Why 1127 green tests missed it
Every test injects a sink straight into
OpenCodeLauncherand walks around the forwarding hop. This is the same shape as the other entries in this repo's "a test on the seam does not prove the caller" list: the test exercises the interface, and production goes through one more object than the test does.The PR author flagged exactly the right area themselves — "
Fleetd.java's anonymous-class sink logic is exercised only indirectly ... not by a standalone Fleetd-level unit test" — and the gap they flagged is where the defect is.The general rule
A default method on an interface is invisible to a lambda. Adding an overload to an interface makes the unit of work every existing implementation of that interface, not just the caller being fixed. Any
ExhaustionSinkbuilt as a lambda or a method reference silently drops the hint.Sent back on the same branch: fix the forwarder to be an anonymous class forwarding both overloads, audit every other
ExhaustionSinkvalue inmain/for the same shape, and add a test that drives the hint through the composed production wiring so that turning the forwarder back into a lambda goes red.Fixed and merged to
mainas838a701.ExhaustionSink's 3-argument method (carrying the profile) is now the single abstract method, so a caller must supply the profile. The false positive is gone: a member spawned without a worktree no longer trips the model read-back.The design point is worth keeping, because it decided how the fix was shaped. The obvious fix was to add a
defaultoverload carrying the extra argument. That would have shipped dead, with a fully green suite: a lambda implements only the single abstract method, so adefaultoverload is invisible to every existing lambda. Every call site would have kept using the old path and no test could have noticed.Inverting it instead — making the richest method abstract and the poor one the default — forces every implementation to be revisited, because a lambda with the wrong arity stops compiling. The unit of work becomes every implementation, not just the interface.
That property then paid for itself twice at merge time. I merged this before the four parallel #201 units on purpose. It turned a collision in a shared test file into a loud compile error: git auto-merged
CompletionResolverTestwith no conflict markers, and the build failed with exactly one error, because a 2-argument lambda was now illegal. Git compares text, and a lambda's arity is a type fact — nothing textual is in conflict, so only a compiler can find it.Verified: 1215 tests on
main, 0 failures, 0 compile errors.