Skip to content

Pace every seat write — one shared throttle in front of the five PM write scripts (maintainer 「截流应该要加」) #19572

Description

@os-project-manager

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)

  1. One helper, scripts/pm/write-pace.mjs, exporting paceWrite({ token, kind }) (awaited before a write) and noteResponse({ token, status, headers }) (called after). It keeps a small per-user log at ~/.cache/objectstack-pm/write-pace.jsonl (override OS_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.
  2. Rules, defaults as the charter states them: at least 3 s since the previous write by the same token (sleep the remainder, print one line saying so); at most 40 writes in the trailing 60 min per token — the 41st is refused with a distinct exit code and a prescription (the count, when the window reopens, what to fold into fewer writes), ⛔ never a silent wait; a 429, or a 403 whose body names the secondary rate limit, or retry-after / x-ratelimit-remaining: 0, writes a stop marker (30 min, or retry-after if longer) after which every write by that token is refused with the marker's expiry printed, until it expires. Env overrides OS_PM_WRITE_MIN_GAP_MS and OS_PM_WRITE_HOURLY_MAX for tests only; the defaults are the contract.
  3. Wired into every write path — the fetch that issues POST / PATCH / PUT / DELETE in post-stamped.mjs, label-write.mjs, close-cards.mjs, sweep-closed-cards.mjs, sweep-stale-finding.mjs (git grep -n "method:" scripts/pm is the census); --dry-run paths do not count and do not sleep. ensure-pm-labels.sh runs through gh under the maintainer's hand and stays out.
  4. --self-test offline with an injected clock (gap sleep, the 41st refusal, the stop marker from a 429, pruning, the token never appearing in the file) and --status printing 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-run unchanged; 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 under packages/**, skip-changeset).

Dedupe: git grep -n -i 'rate.limit\|throttle\|pace' scripts/pm — the scripts classify a 429 today (classifyHttp in label-write.mjs) but none paces or counts; no open card on the board carries a throttle (titles over the open set for throttle|pace|rate limit → 0, control label-write → 2).


Generated by Claude Code

Activity

  1. os-project-manager commented on Sep 21, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: objectstack#19572 — director seat, summon #25, session_012GcsUbuqFGBibkEDMRC1eE, 2026-09-21T09:22Z
    Branch: claude/issue-19572-write-pace
    Seat: director
    Mode: subagent
    Clause-②: no

    Direct dispatch under the maintainer's 「截流应该要加」. File surface: scripts/pm/write-pace.mjs (new) and the write fetch sites of post-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

  2. os-project-manager commented on Sep 21, 2026

    @os-project-manager
    CollaboratorAuthor

    os-dev-report
    {
    "issue": 19572,
    "status": "done",
    "branch": "claude/issue-19572-write-pace",
    "pr": "#19584",
    "head": "2875a316dc0e8457e53e2c6fced3a1bcf6ee1d5b",
    "session": "session_012GcsUbuqFGBibkEDMRC1eE",
    "premise_still_valid": true,
    "summary": "New scripts/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

  3. os-project-manager commented on Sep 21, 2026

    @os-project-manager
    CollaboratorAuthor

    Landing 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 fetched origin/main: scripts/pm/write-pace.mjs is present (1067 lines) and git grep -c write-pace origin/main finds one wiring site in each of post-stamped.mjs, label-write.mjs, close-cards.mjs, sweep-closed-cards.mjs, sweep-stale-finding.mjs, plus package.json (check:pm-write-pace) and .github/workflows/lint.yml. First reading from the merged tool, write-pace --status at 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=0 on a live write throws RangeError rather than refusing at exit 10 (test-only override; rides the next PR that touches write-pace.mjs); (D2) a refusal that lands mid-run after earlier writes prints 「nothing was written」 while process.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 the Fixes keyword; pm:dispatched removed in this stroke so the close is whole.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions