Repository navigation
Pace every seat write — one shared throttle in front of the five PM write scripts (maintainer 「截流应该要加」) #19572
Description
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2
on Sep 21, 2026 os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsClaim: objectstack#19572 — director seat, summon #25,
session_012GcsUbuqFGBibkEDMRC1eE, 2026-09-21T09:22Z
Branch:claude/issue-19572-write-pace
Seat: director
Mode: subagent
Clause-②: noDirect dispatch under the maintainer's 「截流应该要加」. File surface:
scripts/pm/write-pace.mjs(new) and the writefetchsites ofpost-stamped.mjs,label-write.mjs,close-cards.mjs,sweep-closed-cards.mjs,sweep-stale-finding.mjs. Concurrent with the #19570 dev (charter text, disjoint files). Review: at-tier, R5 shape; Tier S — this seat lands it.
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsos-dev-report
{
"issue": 19572,
"status": "done",
"branch": "claude/issue-19572-write-pace",
"pr": "#19584",
"head": "2875a316dc0e8457e53e2c6fced3a1bcf6ee1d5b",
"session": "session_012GcsUbuqFGBibkEDMRC1eE",
"premise_still_valid": true,
"summary": "Newscripts/pm/write-pace.mjs: paceWrite() before a write, noteResponse() after, a per-token log at ~/.cache/objectstack-pm/write-pace.jsonl keyed by a 12-hex SHA-256 prefix (the token itself never reaches the file), a 3 s minimum gap that is slept, a 40/hour trailing-window budget whose 41st write is REFUSED at exit 10 with a prescription (never a silent wait), and a 30-minute stop marker (or retry-after when longer) written from a 429, a secondary-rate-limit 403, a bare retry-after, or x-ratelimit-remaining 0 on any status. Wired into the one transport of each of post-stamped.mjs, label-write.mjs, close-cards.mjs, sweep-closed-cards.mjs and sweep-stale-finding.mjs, guarded by isWriteMethod() so reads are untouched and dry runs are excluded structurally (they issue no write). classifyHttp is reused BY VALUE - the two callers that already hold its verdict pass it in; it is not imported, because that module drags the 26k-line half-states patrol behind it. Also added: check:pm-write-pace in package.json and its lint.yml step. TWO THINGS THE PM SHOULD READ: (1) the card says 'scripts/pm/, Tier S', but check-governed-merges.mjs --branch answers 'NOT governed - 0 of 8 path(s) hit the register' because GOVERNED_SURFACES does not list scripts/pm/; reported, not acted on, nothing armed, nothing added to the register - see open_questions. (2) the first draft's own new cases found three defects in it, fixed in commit 2875a31: an unwritable log threw out of the transport (now refuses at exit 10 with a remedy), noteResponse could replace the platform's own error on a transport's throw path (now never throws, reports the consequence instead), and the mkdir lock was bounded only by a deadline read from an injected clock, so an unusable PATH read as a busy lock and spun forever (now bounded by attempts too).",
"tests": "Measured 2026-09-21T10:27Z on head 2875a31, worktree /home/user/objectstack-issue-19572, base b3615f1. Exit codes captured before any pipe (cmd > log 2>&1; EXIT=$?). GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 68 commands, 8 paths, 1160 changed lines; all 68 run; node scripts/pm/dispatch-gates.mjs --ran ran2.list :: exit 0 -> '68 derived famil(ies) accounted for - 63 run, 5 NOT-MEASURED (5 DERIVED from a recorded exit 3), 0 UNRUN'. NOT MEASURED (exit 3, PREREQUISITE NOT MET, each needs a built packages closure this container has not built; this diff has ZERO paths under packages/, so none of the five has an input this branch can move, and CI builds first): check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content, check:type-check-debt. check:pm-dispatch-gates was run DETACHED per its own header (838.3 s, past the container's foreground cap) :: exit 0, 'dispatch-gates self-test: 1883 cases pass'. SELF-TESTS: check:pm-write-pace :: exit 0, 'write-pace self-test: 69 cases pass across 9 batteries'; the five wired tools all green on the same head - post-stamped 544 cases, close-cards 102, sweep-stale-finding 91, sweep-closed-cards 87, label-write 75. ALSO RUN (named in the dispatch, outside the derived 68): check:pm-settings-deny-roster :: exit 0, check:pm-governed-prose :: exit 0, check-governed-merges.mjs --branch :: exit 0. DRY RUN UNCHANGED, two readings: (a) the same post-stamped --dry-run run from a detached worktree at the merge base b3615f1 gives stdout and stderr byte-identical with only the clock stamp normalised; (b) the POSITIVE CONTROL, which is the stronger half - re-running it under OS_PM_WRITE_HOURLY_MAX=0 OS_PM_WRITE_MIN_GAP_MS=600000, settings that would refuse any paced write at exit 10 or hang it ten minutes, gives exit 0 with byte-identical stdout and stderr and OS_PM_WRITE_PACE_FILE is never created. A dry run that reached the throttle could not produce that. REVERSE VERIFICATION: no ablation run; the instrument's own refusal paths are driven directly by the self-test with an injected clock, sleep and exit, and each of the three defects listed in summary(2) was found by a NEW case going red on the first draft before the fix - the 41st-write case, the unwritable-log case and the held-lock-under-a-frozen-clock case. The frozen-clock case is the ablation of the lock's deadline bound: without the attempt bound it does not return at all. CONTROL CHARACTERS: grep -naP over every changed file for [NUL through 0x1f, 0x7f] - zero hits; check:nul-bytes :: exit 0. PR BODY READ BACK after the create: byte-identical to what was sent except the platform's own appended session-URL footer block (one Generated by footer, not two); not re-sent, not PATCHed.",
"mcp_calls": "0 - no MCP GitHub tool was called, read or write. Every GitHub touch went through the REST proxy with curl, or through scripts/pm/post-stamped.mjs.",
"api_writes": "2 - POST /repos/objectstack-ai/objectstack/pulls (the draft PR, HTTP 201) and POST /repos//issues/19572/comments (this report, via post-stamped.mjs, which also does one GET read-back). No labels, no assignee, no ready flip, no auto-merge, no issues filed. Plus 4 git pushes on claude/issue-19572-write-pace (the empty routing probe and three commits).",
"open_questions": [
{
"question": "#19572 declares the change 'scripts/pm/, Tier S - the director seat lands it after an at-tier read', but check-governed-merges.mjs --branch answers 'NOT governed - 0 of 8 path(s) hit the register (6 surfaces)'. GOVERNED_SURFACES lists .claude/** as the Tier S surface and does not list scripts/pm/. AGENTS.md Prime Directive #14 makes the register the authority ('adding a surface is an edit there'), so the card and the mechanism disagree about how this PR lands.",
"options": [
"A - the card overstated the tier: scripts/pm/ is not a governed surface, so #19584 lands through the ordinary queue on green like any tooling PR, and the card's Tier S line is corrected.",
"B - the register is missing a row: scripts/pm/** should be Tier S (these five tools ARE the seat write path), so a separate PR adds the row to GOVERNED_SURFACES and to the AGENTS.md paragraph check:pm-governed-prose holds equal to it - after which #19584 itself becomes governed and needs a contract-review record.",
"C - land #19584 under B's discipline voluntarily (a Tier S contract-review record on the thread) without changing the register, leaving the discrepancy for later."
],
"recommendation": "B, then C for this PR. The rationale the register encodes is 'a surface where an AI seat's own judgement is the only check'; a throttle that decides whether any seat may write is squarely that, and the two accounts suspended today are the evidence. But adding the row is a governed edit in its own right (the register lives under scripts/, the paragraph in AGENTS.md - Tier H), so it is the PM's call and its own card, not a rider here. Until it exists, the mechanical answer is 'not governed'; treating this PR to a contract-review record anyway costs one read and matches the card's declared intent."
}
],
"out_of_scope_findings": [
"noted, not filed: label-write.mjs's classifyHttp routes a SECONDARY rate-limit 403 to 'refusal', because that answer's x-ratelimit-remaining is typically NON-zero (the primary quota is untouched) and only 'remaining === 0' takes the ratelimit arm. 'refusal' is what reaches the destructive whole-set PATCH fallback - the exact outcome the file's own header forbids for rate-limit refusals, for a reason ('rate-limit refusal binds the IDENTITY') that applies to a secondary limit just as much. This PR closes it incidentally rather than by design: noteResponse writes the stop marker from that same 403, so paceWrite refuses the fallback PATCH at exit 10 before it is issued. 承接者: this PR; nothing is left to file, and the mitigation is a side effect worth knowing about rather than a guarantee classifyHttp now makes.",
"noted, not filed: five of the derived gates (dts-closure, dual-build-cjs-loads, lean-entry-closure, sourcemap-no-sources-content, type-check-debt) answer PREREQUISITE NOT MET on any container that has not built the packages closure. Not a defect - each says so loudly at a distinct exit 3 - and not this card's to change. 承接者: CI, which builds the closure before running them.",
"noted, not filed: the card/register tier discrepancy in open_questions. It is not one of the three filing classes (no reproducible defect, no code contract violated, no metadata trap), and acting on it in either direction is the owning seat's call. 承接者: the director seat, on this PR's landing decision."
]
}
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsLanding record — director seat, summon #25 (
session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-21T11:18Z. PR #19584 merged through the ordinary queue (31184e5daf3bc48cf51bd9af7d6b05fe9c53ab6f, 2026-09-21T11:17Z; not on the governed register, landed by this seat on the seven green contexts after review PASS 5759177018 and provenance 5759247382), verified on fetchedorigin/main:scripts/pm/write-pace.mjsis present (1067 lines) andgit grep -c write-pace origin/mainfinds one wiring site in each ofpost-stamped.mjs,label-write.mjs,close-cards.mjs,sweep-closed-cards.mjs,sweep-stale-finding.mjs, pluspackage.json(check:pm-write-pace) and.github/workflows/lint.yml. First reading from the merged tool,write-pace --statusat 2026-09-21T11:17Z: 1 of 40 writes in the trailing hour (the dev's own report post at 10:27Z, made through the branch's copy), no marker, verdict 「the next write may go」 — this comment is the seat's first write through it. Two readings carried from the review as follow-ups, neither reaching the contract at its defaults: (D1)OS_PM_WRITE_HOURLY_MAX=0on a live write throwsRangeErrorrather than refusing at exit 10 (test-only override; rides the next PR that toucheswrite-pace.mjs); (D2) a refusal that lands mid-run after earlier writes prints 「nothing was written」 whileprocess.exit(10)inside the transport bypasses the tools' half-state reporters — whether it becomes a card is the maintainer's word under the filing door. GitHub closed this card on theFixeskeyword;pm:dispatchedremoved in this stroke so the close is whole.
Generated by Claude Code
Path: none | tooling | maintainer-directed — dispatched at filing, never queued
Class (c). Filed by the director seat, summon #25 (
session_012GcsUbuqFGBibkEDMRC1eE) under the maintainer's verbatim 「截流应该要加」 (chat, 2026-09-21), answering the 「Not in this card」 list of #19570 — the sentence ruling #208 R6 requires before a new instrument. Why now: two seat accounts were suspended by GitHub today during bulk issue operations; the charter rule 「paced writes」 lands in #19570, this card makes it mechanical so no seat has to remember it.The change (
scripts/pm/**, Tier S — the director seat lands it after an at-tier read)scripts/pm/write-pace.mjs, exportingpaceWrite({ token, kind })(awaited before a write) andnoteResponse({ token, status, headers })(called after). It keeps a small per-user log at~/.cache/objectstack-pm/write-pace.jsonl(overrideOS_PM_WRITE_PACE_FILE), keyed by a short SHA-256 prefix of the token — ⛔ never the token itself — one line per write with a timestamp, pruned to the trailing hour on every read.retry-after/x-ratelimit-remaining: 0, writes a stop marker (30 min, orretry-afterif longer) after which every write by that token is refused with the marker's expiry printed, until it expires. Env overridesOS_PM_WRITE_MIN_GAP_MSandOS_PM_WRITE_HOURLY_MAXfor tests only; the defaults are the contract.fetchthat issues POST / PATCH / PUT / DELETE inpost-stamped.mjs,label-write.mjs,close-cards.mjs,sweep-closed-cards.mjs,sweep-stale-finding.mjs(git grep -n "method:" scripts/pmis the census);--dry-runpaths do not count and do not sleep.ensure-pm-labels.shruns throughghunder the maintainer's hand and stays out.--self-testoffline with an injected clock (gap sleep, the 41st refusal, the stop marker from a 429, pruning, the token never appearing in the file) and--statusprinting the trailing-hour count and any active marker for the current token, so a seat can read its own budget before a batch.Acceptance
Self-test exit 0 with its case count; a real
post-stamped.mjs --dry-rununchanged; the five scripts each show the two calls around their write (git grep -n 'paceWrite\|noteResponse' scripts/pm); the derived gate families green (dispatch-gates --commands, exit codes captured before any pipe); a changeset is not needed (nothing underpackages/**,skip-changeset).Dedupe:
git grep -n -i 'rate.limit\|throttle\|pace' scripts/pm— the scripts classify a 429 today (classifyHttpinlabel-write.mjs) but none paces or counts; no open card on the board carries a throttle (titles over the open set forthrottle|pace|rate limit→ 0, controllabel-write→ 2).Generated by Claude Code