Merging #543 made re-delivery reachable: an Error after Injector's send now loops and types the same brief again #546
Closed
opened 2026-09-12 09:14:27 +02:00 by ltms
·
1 comment
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#546
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?
I caused this by merging #543 (fleetd #538) an hour ago. Filed immediately.
The fleet01 lead proposed this mechanism days ago. I measured it and told them it was
unreachable, and I was right at the time. #543 removed the second defect that was hiding it.
Their reasoning was correct; only my "unreachable" was, and it stopped being true at
cc302fe.Measured on
mainat7611b69.The chain, every step measured
1.
Injector's delivery catch is narrow.Two catch clauses in the whole file, both
RuntimeException.HerdrException extends RuntimeException, so every ordinary herdr failure is caught — only anError, or any othernon-
RuntimeExceptionthrowable, gets past.2. On that path the message is never removed from the queue. Neither
t.queue.poll()at:384nor the one at
:394runs, andp.stateis never written. ThePendingstays at the head oft.queuewithstate == QUEUED. The throwable unwinds out ofsynchronized (t), releasing themonitor.
3.
StatusPolleris the only production caller, and it now swallows that throwable.4. So the loop continues, peeks the same
Pending, and sends it again.Injector.java:378isPending p = t.queue.peek();— a peek, not a poll. The entry is still there and stillQUEUED, sothe next round re-enters the same
tryand callssendon the same text.Why this was not a defect yesterday, and is one today
Before #543,
StatusPoller.loopcaught onlyHerdrExceptionandRuntimeException. AnErroratstep 3 escaped, and the
status-pollervirtual thread died. The message was never re-delivered,because the only thread that could re-deliver it was gone. That was my measurement, and it was
correct against that revision.
#543 fixed exactly that — the loop now survives — which is right and I am not proposing to revert
it. But surviving means the loop comes back round, and step 4 is what "coming back round" means when
step 2 left the queue untouched.
This is
a-defect-on-paper-is-not-a-reachable-defectread backwards: a fix can createreachability. The paper defect and the blocker were in two different files, owned by two different
tickets, and neither ticket's reviewer could see the other half.
What it costs when it fires
The
sendat:383is what types the brief into the member's pane. If the throwable arrivesafter herdr has typed the text, the member has the brief, the queue still thinks it does not,
and the next round types it in again. A worker gets the same brief twice, in the same pane, as two
separate injections.
If the throwable is persistent, this repeats every poll interval — a brief retyped into a pane
several times a second, with one
log.errorper round from #543's new line.I have not observed this. The mechanism is measured; the arrival of an
Errorat:383is thesame reachability argument as #538's, which leans on #413 (a
mvn cleanin the tree deletes therunning daemon's jar, so a not-yet-loaded class fails with
NoClassDefFoundErroron first use) andon the fleet01 lead's real
NoClassDefFoundErroron their host on 2026-09-10.The fix
Widen
Injector.java:391tocatch (Throwable e), the same widening #543 applied one layer up. Theexisting handler already does the right thing — drop the poisoned message, mark it
NOT_DELIVERED,surface the error — and the comment above it already says so. It is scoped one class too narrow, in
exactly the way #538 found
StatusPollerscoped one class too narrow.#538 said "do not touch
Injector.java", and that was right for that ticket. It is wrong now, andthis ticket exists to say so.
Check
Injector.java:495for the same shape while you are there. I have not read it and make noclaim about it.
Acceptance
Errorfrom thesendseam atInjector.java:383and asserts the messageis removed from the queue and marked
NOT_DELIVERED— not leftQUEUED. Failing before the fix.Error, a secondonStatusround does not send the same textagain. This is the one that pins the behaviour this ticket is about, so it is not optional.
RuntimeExceptionbehaviour must be unchanged: a test that aHerdrExceptionstillproduces
NOT_DELIVEREDand still surfaces to the caller, so widening the catch does not quietlychange the ordinary path.
message, restore, confirm byte-identical with the full
shasum -a 256, and run a green control.@TempDir.Credit and correction
The fleet01 lead raised this as §1(i) of a coordination message, derived from line numbers and
nesting depths I had sent them — they cannot read
Injectoron their host. Their reconstruction ofthe block was accurate. I closed it as unreachable, correctly, and then merged the change that
opened it. Their §1(i) is hereby reopened as this ticket.
Related: #538 / PR #543 (the fix that created the reachability), #544 (the missing supervisor),
#413 (the route that makes
NoClassDefFoundErrorreachable here), #412.Fixed by PR #549, merged into
mainat93a9ed3. Filed and closed the same day it was created,which is right, because I created it by merging #543.
Verified by me on the branch, not from the worker's report. Three mutations of my own — reverting
the catch, deleting the
t.queue.poll()in the catch arm, and writingDELIVEREDinstead ofNOT_DELIVEREDat:400— each went red with the new tests' own messages, each restored tosha256
97c560b6e33fc49a1772abec92e5bbab613f8991deba2220d30221a2f546ba14, green control after.Merged tree: exit 0,
Tests run: 1719, Failures: 0, Errors: 0, Skipped: 0(1716 + 3).Two things this ticket did not close, both now filed
#551 — the delivery record can be wrong, and that predates this ticket. The fleet01 lead argued
that widening the catch would create a new defect: an
Errorafter the text was typed would recordNOT_DELIVEREDfor a delivery that happened. They asked to be argued out of it before the workermerged, so I measured it:
agent.prompt"pastes the text ... and submits it in the same call"(
AgentControl.java:110-117), andUnixSocketHerdrClient.call()doeswriteFullythendecodeResult(readLine(ch)). So every failure in the response half — a dropped connection, amalformed line, an error result — is a
HerdrException, which is aRuntimeException, whichthe narrow catch at
:391already caught before today.So the contradiction is already live on the ordinary path, with no
Erroranywhere. This wideningextends a pre-existing weakness to one more throwable class; it does not introduce it. Their
underlying diagnosis is right and is #551, with their wording on it: no catch arm can distinguish
died-before-typing from died-after-typing, because the distinguishing fact was never written down.
The fix is ordering — record first, then send — which needs a third
Pending.Stateand changes whatNOT_DELIVEREDentitles a caller to say, so it is a contract change, not a catch width.#553 — the shape the worker reported out of scope is worse than either of us scoped it. They
flagged the same narrow
catch (RuntimeException)at the resubmit nudge (:500) and correctlydeclined to fix it, assessing it as "skips one debug log and one Enter nudge, the next round
retries". That is right about the nudge and stops one line short.
onStatushas exactly twotryblocks in its whole body and no
try/finally, and the block that completes the caller's future islast. Anything that throws in any earlier block — including an ordinary
RuntimeExceptionfroma listener callback, which
Fleetd.java:494-528routes straight intocompletionandsessions—skips
sent.delivered().complete(null)entirely. The message is already off the queue, alreadymarked
DELIVERED, already typed into the pane, and the future is never completed in eitherdirection.
That one needs no
Errorat all, so it is more reachable than this ticket was.The rule worth keeping
This ticket said it as "a fix can create reachability". #553 is the same sentence one block down,
and the general form is sharper: removing a crash does not remove the half-finished state the
crash used to discard — it makes that state permanent and quiet. #543 stopped the thread dying.
#549 stopped the queue entry sticking. Both are right. Together they turn #553 from "the daemon
dies" into "one caller waits forever and everything looks healthy".
Credit to the fleet01 lead throughout: this ticket was their §1(i), derived from line numbers and
nesting depths I had sent them, because they cannot read
Injectoron their host.