fix(pm): declare ISSUE_BODY_LIMIT from a measurement, in the unit the platform refuses in - #18796
Merged
Merged
Conversation
… platform refuses in The constant read 65536 with no provenance and was false in both directions at once: bodies four times larger store fine (objectstack#6015 read 150,507 chars / 257,945 bytes byte-complete, and 72,754 chars / 125,561 bytes after compaction), while the refusal that does exist sits above it and is SILENT -- the platform keeps the old body and answers 200. So the boundary was bisected on a throwaway issue opened for it (objectstack#18793, closed completed with the table as its body), 17 measurement writes on 2026-09-17 through post-stamped's exit register, each read back byte-exact: 262,144 bytes STORED, 262,145 REFUSED. One value, bracketed on both sides, 256 KiB exactly. The unit was measured too, not assumed: 262,145 bytes carrying only 222,145 characters was REFUSED where a character-counted cap would have taken it with 40,000 to spare, and the same multi-byte shape at 262,144 bytes / 222,144 characters STORED. The platform counts UTF-8 bytes. renderMarkdown's trim counted `.length` -- UTF-16 code units -- against a budget whose cap is in bytes, over rows that are largely CJK prose. It now counts bodyBytes, as familyLedgerReservation and the size pins do; H6 next door has counted bytes since it was written. MARKDOWN_BODY_BUDGET does not move: a corrected reading is not a licence to print more, and the byte guard only ever cuts earlier than the character one did. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 17, 2026
os-justin
marked this pull request as ready for review
September 17, 2026 21:52
This was referenced Sep 17, 2026
This was referenced Sep 17, 2026
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 #18664
Clause-②: no
The defect
scripts/pm/check-half-states.mjsdeclaredexport const ISSUE_BODY_LIMIT = 65536;with a docblock asserting "GitHub's hard cap on an issue body ... A body that exceeds the cap is REJECTED by the API", and no provenance at all: no unit, no reading, no date. It was false in both directions at once. Bodies four times larger store fine, so the number is too strict where it is used to bound a report; and a real refusal boundary sits above it that nothing was watching, and crossing it is SILENT — the platform keeps the OLD body, answers 200 and reports nothing (which is precisely whypost-stamped.mjsgrewEXIT_NOT_STORED). One constant, over-strict where it cuts and blind where the refusal lives.Premise re-derived on
mainbefore any editupdated_at2026-09-17T20:04:42Z). The card's own numbers are therefore quoted here as the card's, not re-taken. The falsification survives the compaction anyway and is re-taken here: 72,754 chars / 125,561 bytes is past 65,536 in EITHER unit, so no reading of the old constant survives.6de7a2d6e6), at:17148/:17149, with the renderer's cut at:19420and the pin at:28367.SEAT_BODY_SOFT_LIMITis at:2037(the card's:1802is stale); it and H6 were READ and are untouched, as the card directs.MARKDOWN_BODY_BUDGETat:19420, notISSUE_BODY_LIMIT— the cap is what the budget was chosen to sit under, and it appears in the self-test pins. The defect is the same; the mechanism has one step in it.The measurement
A throwaway issue was opened for it — objectstack#18793, never a seat post and never another card — and its body rewritten 17 times with filler of chosen byte sizes through
scripts/pm/post-stamped.mjs --body=18793, each write read back byte-exact. Budget set for this: 18 writes (a dozen was the guide; the exact bracket cost 15, the unit question 2, the record 1). Exit 0 with classidentical= every byte sent is stored; exit 4 (EXIT_NOT_STORED) = the platform kept the old body.Rows 1 and 2 re-take the card's two endpoints as this run's OWN bracket rather than inheriting them. Rows 3 to 15 bisect it.
Interval reached: a single value. 262,144 bytes stored; 262,145 bytes refused. The card forbade writing 262,144 as "known" unless exactly that size landed and one byte more was refused — row 14 and row 15 are that pair. 262,144 is 256 KiB, which is why the guess was tempting; it is reported here because the bisection landed on it, not because it is round.
The unit answer
Rows 16 and 17 fill the body with a 20,000-character run of U+4E2D (three UTF-8 bytes each) plus ASCII padding, so byte length and character length differ by 40,000.
The platform counts UTF-8 BYTES.
Incidental, recorded because the tool's own header calls the cell unmeasured: every landed write above read back
identical— no trailing-newline strip (these bodies carry none) and no synthesised footer on an ISSUE body. The cell for a body that already ends in the footer block stays unmeasured; none of these did.The probe issue is closed
completed, with the bisection table as its final landed body.The constant, before and after
Before:
export const ISSUE_BODY_LIMIT = 65536;, docblock "GitHub's hard cap on an issue body ... A body that exceeds the cap is REJECTED by the API".After:
export const ISSUE_BODY_LIMIT = 262144;, and the docblock now carries the provenance the old one lacked — the UNIT (UTF-8 bytes), BOTH sides of the bracket (262,144 bytes STORED/262,145 bytes REFUSED), the DATE (2026-09-17) and the PROBE ISSUE (objectstack#18793) — plus the correction that the refusal is silent rather than an API rejection, and the reason the round number is not the reason.The guard and the budget, in one unit
The renderer's cut at
:19420countedline.lengthagainstMARKDOWN_BODY_BUDGET, and seeded its accumulator withbody.length + indexText.length. That unit is UTF-16 code units — JS.length— while the platform refuses in bytes, over rows that are largely CJK prose, where the two differ by 3x.bodyBytes()(a new tiny export,Buffer.byteLength(..., 'utf8')). The+ 1stays: the joining newline is one byte in UTF-8.familyLedgerReservationis reserved out of the same budget, so it returns bytes too — which is what its own header has said since it was written ("in bytes"); it was measuring.length. Its two digit-slack terms are ASCII digits, already byte-correct.h6SeatBodyOversized) has countedBuffer.byteLengthsince it was written. The renderer now agrees with it and with the platform.MARKDOWN_BODY_BUDGETdoes NOT move — it stays 60,000, now BYTES. A cap that was mis-measured is a correction to a reading, not a licence to print more; how much the anchor issue prints is a fold decision nobody has taken, and this PR takes none. Because bytes are never fewer than characters, the new guard cuts at or before where the old one did: strictly narrowing, never widening, which is what the card asks for.FAMILY_LEDGER_WORST_CASE_BYTESdocblock's stale sentence about "the 5,536-byte headroom ... (65,536)" is corrected in the same place, with a note that its two measured figures were taken with.lengthunder a name that said BYTES.The pins
The pin at
:28367is re-pointed to the measured cap and re-measured in bytes, and twelve other size pins across the suite move to bytes with it (they asserted.lengthagainst a budget the guard no longer counts that way). A new floored battery is added in the shape PR #18756 landed —SELF_TEST_BATTERIESgains'ISSUE_BODY_LIMIT measured cap': 37andSELF_TEST_BATTERY_FLOORrises 5 to 6, with the two sibling floor pins that name the roster size updated. 40 cases register against it:bodyBytes('U+4E2D')is 3 where.lengthis 1; ASCII agrees; an absent body is 0 and not a throw.Read the counterfactual honestly, and the battery comment says so: it overruns the renderer's BUDGET, not the platform's measured cap, which no character count can reach at today's budget. The budget is the thing that stops meaning anything when it is kept in the wrong unit — and the thing that would become dangerous the day anybody raises it toward the cap. That is an argument for landing the unit fix before, not instead of, any budget decision.
Ablation
From the COMMITTED fix (
5fc8e0ce57), two legs, each mutating on disk, proving the mutation by grep counts on both the removed and the injected text AND bygit hash-objectagainst the HEAD blob, running the suite, and restoring withgit checkout HEAD -- pathverified by hash equality and an emptygit diff HEAD, under atrap ... EXIT INT TERMwith absolute paths.HEAD blob
153d10a0158ba27906a5400df8a9c486d77ae061.Leg A — put
65536back. Mutated blob7d34ca5db0377d33d7e5062792dd54a51edbe763; removed-text count 1 to 0, injected-text count 0 to 1. Self-test exit 1, 5 cases red, every one of them in the new battery, nothing pre-existing red:Leg B — put the character-counting accumulator back (
let used = body.length + indexText.length + ledgerReservation;). Mutated blobd1ec008854fb628f8822232cbfa4344ace4d2342. Self-test exit 1, 9 cases red: two in the new battery (the CJK trim and the over-cap body's budget bound) and seven of the file's PRE-EXISTING budget pins —markdown: ...and under the renderer's own budget,markdown: ...and the body is still under budget,H17 budget, the④ budgetbox,#13947 order,#13947 reservedand#13947's flood pin. That is the intended direction and worth reading twice: those seven pins are red under the old guard because this PR re-measured them in bytes. Under.lengththey could not fail — and the reason they fail now is that this file's own fixtures already carry enough multi-byte text (em dashes in the row messages) that a character-bounded body overruns the byte budget it was supposed to keep. The old guard was not bounding the thing that matters, on this suite's own inputs.Both legs restored: hash matches the HEAD blob and
git diff HEADis empty. Working tree clean afterwards.Self-test
4,881 cases before, 4,921 after.
Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom the worktree, no hand-fed path list — 38 commands derived against the change set it took from the merge base itself (1 path:scripts/pm/check-half-states.mjs). Every one run, exit code captured by redirect-then-$?, never through a pipe. Reconciled with--ran: 38 derived, 38 run, 0 unrun.pnpm check:pm-dispatch-gatesis the 38th. It runs past this container's foreground ceiling, so it was started DETACHED with its output redirected to a file, never under a foreground timeout. Its verdict is reported in the delivery comment on #18664, read from that log at report time; if it had not reached a verdict by then it is recorded there as NOT MEASURED by name, not as a green.Repo-wide
pnpm lint(eslint . --no-inline-config): exit 0, whole repo, no narrowing, run at5fc8e0ce57. A control-character scan over the edited file (grep -naPover the C0 set plus DEL) matched nothing, exit 1.Acceptance notes
Observations from this work, filed nowhere and changed nowhere, per the scope rule:
.github/workflows/cross-repo-issue-closer.yml:166,docs-drift-check.yml:557,merge-queue-triage.yml:178,scheduled-full-run-card.yml:125andtest-nightly-tiers.yml:436, each bounding a log tail or a report against "GitHub's 65536-character comment limit". This PR measured the ISSUE BODY cap only; the COMMENT cap is not measured, so nothing here shows those five are wrong — what it shows is that they carry a number of the same provenance as the one just falsified, in a unit ("character") that the body cap has now been measured NOT to use. Worth a card and its own bisection; out of this card's one-file surface. Dedupe words:65536 comment limit workflow log tail truncation.FAMILY_LEDGER_WORST_CASE_BYTES's two measured figures (2,285 B / 3,754 B) were taken with.lengthunder a name that says BYTES. Their pins are now measured in bytes and still fit the 6,000 ceiling, so nothing is wrong; the numbers in the prose are simply readings in the other unit. Noted in the docblock rather than re-measured, because re-measuring them is a diff about that constant and not this one.MARKDOWN_BODY_BUDGETtoward the measured cap is now possible and is deliberately NOT done here. It changes what the standing patrol's anchor issue prints, which is a fold decision for a seat or the maintainer, not a consequence of correcting a measurement. Successor: none queued — it wants a card of its own if anybody wants the longer report.🤖 Generated with Claude Code
Generated by Claude Code