fix(fleet-write): read every relayed body back — a stored body that is not the bytes sent exits 4, never 0 - #20806
Merged
objectstack-fleet[bot] merged 4 commits intoSep 30, 2026
Conversation
…dy that is not the bytes sent A run concluding success proved the executor's requests were answered, never that the board holds the bytes the seat sent. sendFleetWrite now reads back every action that carried a body and judges the stored bytes with post-stamped's own classifyReadBack: a measured mismatch is failure with notStored (exit 4, op, object and first differing byte named), a body no locator can read or find is unverified (exit 6), never success. Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
…1,699-byte round trip and exit 4 through the CLI Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
…en the read-back is gone Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
…er the raw glyph Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
objectstack-fleet
Bot
deleted the
claude/issue-20571-fleet-write-byte-exact
branch
September 30, 2026 09:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #20571
Clause-②: no
What this changes
scripts/pm/fleet-write/dispatch.mjsonly.sendFleetWriteis the one send path every relay door shares (dispatch.mjs --actions-file,with-fleet.sh --via dispatch, post-stamped, issue-create, label-write, issue-transfer, close-cards). After a run concludessuccess, it now reads back every action that carried abodyand compares the stored bytes with the bytes sent. Until now asuccessrun showed only that the executor's requests got 2xx answers. It never showed that the board holds what the seat sent, and the card's 41,699-byteissue_patchexited 0 twice with U+FFFD stored in it.classifyReadBack/sentBodyLanded, imported. There is no second copy. The declared normalisations (the platform footer block, a stripped trailing newline) are exactly the ones post-stamped already forgives. The import is dynamic because post-stamped imports this file statically, and a static import back would create a cycle. One cell is added, and it applies topr_createonly. The platform appends a rule plus one session-URL footer after every byte sent (the fourth shape inplatform-readings.md, where the sent body is a strict prefix of the stored one). The cell forgives that append and nothing inside the bytes sent.BODY_OPSis derived fromops.mjs: comment, comment_edit, issue_patch, issue_create and pr_create.READ_BACK_LOCATORSmust name exactly that set, and this is pinned, so a new body op gets read back as soon as it lands.landed: success, unchanged.not-stored: the object the action wrote was read, and a byte sent is not the byte stored at that offset. Result:state: failurewithnotStored: true, exit 4 (EXIT_NOT_STORED). The output names the op, the object and the first differing byte, and counts extra U+FFFD.unverified: the object could not be read, or a new comment, issue or pull could not be found. Result:state: unverified, exit 6. A comment can only be found by its content, so a comment that matches nothing is alwaysunverified, nevernot-stored.mainat top level. The read-back imports post-stamped, which imports this file, so a pending top-level await is a deadlock. Measured: exit 13, "unsettled top-level await" (ablation B below).main(argv, deps)takes a test-onlydepsso the self-test can drive the card's stroke through the real CLI exit code.--jsongainsnot_storedand per-actionread_backrows.How each caller reads the new outcomes (one exit vocabulary, no caller edited)
failure+notStoredkeeps the state every caller already reads as "not kept whole: go READ, never fall back".relayExitFormapsfailureto itsEXIT_NOT_STORED(4).exitForResultnow returns 4 for it. That covers issue-create (itsEXIT_READ_BACK_MISMATCHis 4), label-write, issue-transfer and close-cards.unverifiedis a state no caller names, so each one falls to its non-success branch:exitForResultgives 6, and post-stamped'srelayExitForgives its default 6. It is never a fall-back to direct (onlyno-runis) and never 0.The cause: measured and not reproduced in the relay path
The triage asked for measurement of where the bytes are split. Every leg this repository executes, and the live relay end to end from this container, came out byte-exact:
toJSON, modelled as 2-space JSON), and the executor's own PATCH request. It goes through pack, wire, the platform's parse,FLEET_WRITE_PAYLOADintovalidate.mjs --from-envin its own process, andexecuteFleetWrite's captured request. The result is byte-for-byte identical. The same wire decoded one 16 KiB chunk at a time (the defect the card suspects) reads back NOT STORED at exactly the body byte the first wire boundary split.7a09eee1and compared with the local blobs:platform-readings.md47,980 B,os-dev.md37,191 B, pm-dispatchSKILL.md29,167 B. All three are byte-identical.commenton finding(fleet-write): anissue_patchbody of about 41 KB comes back with two multi-byte characters replaced by U+FFFD, about 16 KB apart, identically on a re-send — anddispatch.mjsreports success without comparing the read-back #20571 made with this branch'sdispatch.mjs, using the same straddling construction (41,699 bytes, every 16 KiB boundary of body, wire, env text and executor request inside a character). Requestfw-20260930T070826Z-391d62, run36682018227, comment5906059721. The read-back reported IDENTICAL: 41,699 bytes sent, 41,699 stored. An independent curl read agreed: byte-identical, 0 U+FFFD, authored byobjectstack-fleet[bot].So nothing in the path this repository executes, and nothing in the live relay from this container, splits a character. Not measured: the filing seat's own container egress; whatever produced that seat's actions file (
dispatch.mjsreads the file whole); and anissue_patchon a pull request specifically (the probe was a comment). The read-back makes any split downstream of the actions file loud (exit 4, with the offset). A split before the file cannot be seen from here, because the file is the only "sent" this tool holds. That question is in the report, not in this diff.Verification
Every reading below was taken at this PR's head
4a8dafbb.Gates: the 36 families
dispatch-gates --repo objectstack-ai/objectstack --commandsderives from this diff (it matches the 36 the dispatch listed), pluscheck:pm-issue-transferandcheck:pm-with-fleet. Those two are the self-tests of the other files that namefleet-write/dispatch(os-dev rule 5). Every one exited 0 at4a8dafbb.dispatch-gates --ranover the exit-annotated record reports: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero.The self-test verdict line:
fleet-write/dispatch self-test: 146 cases pass across 15 batteries. The four new batteries are pinned by name with floors: targets 6, verdict 13, round trip 8, end to end 17. The wiring battery went from 4 to 5 (the entry pin), and the battery floor from 11 to 15.Ablations
Each ablation ran against the committed implementation through
scripts/ablation-replace.mjswith an EXIT/INT/TERM restore trap. Each restore was proven by the tool (blob == HEAD,git diff HEADempty).readBackStrokereplaced with{ state: 'none' }). The self-test goes RED, 14 of 146. The card's stroke with a corrupted read-back comes back through the CLI as[0, false, null], which is exit 0 and exactly the card's defect. With the read-back in place it is exit 4.Acceptance notes
These were noted but not filed. None of them is a reproducible defect with a public door.
dispatch.mjsborrows its judge from post-stamped through a dynamic import, which is safe only while no entry in post-stamped's static import graph awaits at top level while sending a body. Today that is onlydispatch.mjs(fixed and pinned here); label-write sends no body. Moving the classifier into a leaf module that both files import would remove the cycle. No PR touching post-stamped's read-back core is currently open to carry it.not-storedorunverifiedoutcome reaching post-stamped or issue-create prints their existing "relay run FAILED" prefix before this file's detail. The exit codes are correct; the prefix is theirs.Generated by Claude Code