docs(architecture): one creation limiter, three refusal shapes — never key on its text - #4793
Conversation
…r key on its text #4790 documented the creation limit as a 403 whose body names the secondary limit. That is one of three faces, and the guidance is incomplete in the direction that costs a missed detection. Measured 2026-09-19 while answering a review thread: `POST …/pulls/{n}/comments/{id}/replies` is refused as `422 {"resource":"PullRequestReview","code":"abuse","field":"base"}` — not a 403, and naming no rate limit at all. One reply was refused four times over 43 minutes that way and landed on the fifth attempt. So a watcher grepping for "secondary rate limit" sees nothing, and one checking for 403 sees nothing either, while `gh issue create` reports the same condition as exit 0 with both streams empty. The rule that survives all three: key the decision on the ABSENCE of what was asked for. No `id` or `number` came back, so it did not happen, whatever the wrapper said. Also records that the read limit is independent and can land immediately after a successful write — it did, six seconds later, delaying that reply's byte-compare by eight minutes. A failed read-back is therefore not evidence of a failed write, and the verification read needs its own budget. Verified: MeshWeaver.Documentation.Test 601 succeeded, 0 failed, 0 skipped. No internal links added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Missing response identifiers do not prove a write failed and could cause duplicate replies on retry.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Documents GitHub content-creation refusal variants and independent read-limit behavior.
Changes:
- Adds examples of 403, 422, and silent-success refusal shapes.
- Advises verification reads with separate rate-limit budgeting.
| File | Description |
|---|---|
ReadingCiSignals.md |
Expands guidance for creation-limit detection and verification. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| `403` sees nothing either. **Key the decision on the absence of what you asked for**: no `id` or | ||
| `number` came back, so it did not happen, whatever the wrapper said. |
There was a problem hiding this comment.
Valid, and it catches a contradiction inside the same section. Taking it.
I wrote that an absent id means "it did not happen", and two paragraphs later that a failed read-back is not evidence of a failed write. Both cannot be true: a response can be lost or suppressed after the server has committed, so the absent id licenses only "not confirmed". Acting on the stronger reading is how you get a duplicate reply — which on a review thread is worse than the original problem, because the stub case this section exists for is repaired by PATCH, and a second POST leaves the first standing beside it.
The revision says "not confirmed", and makes the retry conditional on an independent check rather than on the absent id: re-read the collection and look for your content before posting again. That is the same baseline rule the section already states for auditing somebody else's posts, now applied to your own retry, which is where it was missing.
Worth recording why the wrong version was tempting. Two of the three refusal shapes in the table genuinely do mean nothing was created — the 403 and the 422 are refusals at the API boundary. It was the third, the porcelain exiting 0 with empty streams, that I generalised from, and that one is ambiguous in exactly the way you name: silence from the wrapper says nothing about what the server did.
Fixed in the next push.
Copilot's finding is right and it caught a contradiction inside the same section. I wrote that an absent `id` means the write did not happen, and two paragraphs later that a failed read-back is not evidence of a failed write. Both cannot hold: a response can be lost or suppressed AFTER the server commits, so the absent id licenses only "not confirmed". The difference decides whether a retry is safe, and the stronger reading is how a duplicate gets made — worst in the case this section exists for, since a stub reply is repaired with PATCH and a second POST leaves the first standing beside it. The retry is now conditional on an independent check: re-read the collection and look for your own content. That is the baseline rule already stated below for auditing somebody else's posts, applied to your own retry, which is where it was missing. Also records why the wrong version was tempting: two of the three shapes, the 403 and the 422, are refusals at the API boundary and genuinely created nothing. It is the porcelain's silent exit 0 that is ambiguous, and I generalised from it. Verified: MeshWeaver.Documentation.Test 601 succeeded, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

One limiter, three refusal shapes
#4790 (merged) documents the content-creation limit as a
403whose body names the secondary limit. That is one of three faces, and the guidance is incomplete in the direction that costs a missed detection.Measured 2026-09-19 while an agent answered a review thread:
POST …/issuesover REST403, body names the secondary limitPOST …/pulls/{n}/comments/{id}/replies422 {"resource":"PullRequestReview","code":"abuse","field":"base"}— not a 403, names no rate limitgh issue create(GraphQL porcelain)One reply was refused four times over 43 minutes in the
422shape and landed on the fifth attempt. So a watcher grepping for "secondary rate limit" sees nothing there, and one checking for403sees nothing either.The rule that survives all three: key the decision on the absence of what you asked for. No
idornumbercame back, so it did not happen, whatever the wrapper said.The page also now records that the read limit is independent of the creation one and can land immediately after a successful write. It did, six seconds after that reply posted, delaying its byte-compare by eight minutes. So a failed read-back is not evidence of a failed write, and the verification read needs its own budget.
Verification
MeshWeaver.Documentation.Test: 601 succeeded, 0 failed, 0 skipped.DocumentationLinkIntegrityTestto resolve.Documentation only.
🤖 Generated with Claude Code