Skip to content

Commit 6387793

Browse files
fleet-write: the executor applies write-pace rule ⑤ to a stroke of 5+ content-creating actions, and the seat budgets for it (#24810)
Fixes #24755 Clause-②: no ## What this changes The relay executor (`scripts/pm/fleet-write/execute.mjs`) now applies write-pace rule ⑤ itself. When one stroke carries `DEFAULT_BATCH_THRESHOLD` (5) or more content-creating actions, it announces ONE batch on the runner (`announceBatch`, kind `fleet-write stroke`) after the sender gate and before the first write. The writes from the first through the last content-creating one then leave `DEFAULT_BATCH_GAP_MS` (30 s) apart instead of rule ①'s 3 s. Below the threshold nothing changes. Seat side (`scripts/pm/fleet-write/dispatch.mjs`). The packer, the run-poller and the executor all read the same definitions: - `CONTENT_CREATING_OPS` is derived from the op table by `createsContent`, never listed. It is an op whose first request is a REST `POST` carrying prose (`title` / `body`), or one that creates or moves a card (`ANNOTATED_OPS`). Today that is `comment`, `issue_create`, `pr_create` and `transfer`. Edits, label / assignee / reviewer writes, the ready / auto-merge flips, the base sync and the workflow dispatch do not count. - `strokePacing(payload)` returns: - the stroke's content-creating count and its write count; - the batch: the writes through the last content-creating one; - the pacing: (batch writes − 1) × `DEFAULT_BATCH_GAP_MS`; - the budget. - The budget is `RELAY_JOB_TIMEOUT_MS` less `DEFAULT_CEILING_MS`: - `RELAY_JOB_TIMEOUT_MS` mirrors the relay job's `timeout-minutes: 10`. The self-test pins it to `.github/workflows/fleet-write.yml`. - `DEFAULT_CEILING_MS` (5 min) is the allowance an ordinary stroke already has from dispatch to completion. - That leaves 5 min of pacing, so one stroke holds at most 11 batched writes. Every number is derived from those constants; none is written in by hand. - `packRequest` refuses a stroke whose pacing is over the budget, for every sender and before anything leaves. The refusal names the per-op doors (`issue-create.mjs`, `post-stamped.mjs`, `issue-transfer.mjs`, `close-cards.mjs`, `label-write.mjs`) behind one `write-pace.mjs --announce-batch N`. The executor refuses such a stroke again, with zero requests (exit 2), in case a sender skipped the packer. - `sendFleetWrite` adds the stroke's pacing to its completion ceiling and prints one line saying so. A batched stroke is waited for. It is never reported UNCONFIRMED for pausing as it was told to. - If the runner's pace log cannot record the announcement, the stroke is refused before the first write (exit 10). That is the same fail-closed rule `paceWrite` applies. `scripts/pm/write-pace.mjs` is only read: `announceBatch`, `DEFAULT_BATCH_THRESHOLD`, `DEFAULT_BATCH_GAP_MS`, `fmtDuration`, `unrecordableText` and `paceFilePath`. The workflow is untouched. ## Reach Relay run https://github.com/objectstack-ai/objectstack/actions/runs/39369675259 was created at 2026-10-20T04:27:32Z and completed at 04:28:28Z. It created 15 issues on objectstack-ai/objectui: - the first, objectstack-ai/objectui#13122, at 04:27:43Z; - the second, objectstack-ai/objectui#13123, at 04:27:46Z, 3 s later; - the last, objectstack-ai/objectui#13137, at 04:28:25Z. That is 15 cards in 42 s from one identity. The job spent 11 s before its first write. All times are read from the run and from the issues' `created_at`. ## Runtime against the two ceilings | content-creating actions in one stroke | pacing on the runner | seat waits up to | job (10 min timeout) | outcome | |---|---|---|---|---| | 1 to 4 | rule ① only (3 s gaps) | 5 min (unchanged) | about 1 min | unchanged | | 5 | 2 min | 7 min | about 2.5 min | paced | | 11 | 5 min | 10 min | about 5.5 min | paced: the largest stroke that fits | | 12 | 5.5 min | n/a | n/a | refused at the packer, and again at the executor | | 15 (the reach stroke) | 7 min | n/a | n/a | refused: send it through `issue-create.mjs` behind `--announce-batch 15` | | 20 (`MAX_ACTIONS`) | 9.5 min | n/a | over the timeout once overhead is added | refused | A landing stroke (`pr_ready` + `automerge_enable`) carries no content-creating action. Every per-op door sends one card per stroke, with at most one. Neither changes. ## Tests (HEAD 4a9257f) Each new self-test battery has a pinned case floor, and each file's battery-count floor is raised (execute 17, dispatch 21). `execute.mjs --self-test`, battery "the batch gap on the runner" (10 cases, on an injected clock and a stubbed platform): - The pin: 5 `issue_create` actions in one stroke land with gaps `[30000,30000,30000,30000]`. Their sum equals `strokePacing`'s `pacingMs`. - Write-pace records one announcement before the first write. - One action below the threshold keeps 3 s gaps. - Non-creating writes after the batch go back to 3 s. - A landing stroke is unchanged. - 12 creates get exit 2 with ZERO requests, and the refusal names the doors. - An unrecordable announcement gets exit 10, after the sender-gate read and nothing else. `dispatch.mjs --self-test`, battery "the batch gap at the seat" (13 cases): - The derived op set, plus the predicate on rows the test defines itself. - `strokePacing` at, below and above the threshold, counting a pull's label leg. - The budget holds at least a threshold-sized batch. - Past `maxBatchWrites` the packer refuses and the CLI exits 2. At `maxBatchWrites` the stroke packs. - `RELAY_JOB_TIMEOUT_MS` equals the workflow's `timeout-minutes`. - A batched run that completes after the ordinary ceiling is `success`, and the ceiling grew by exactly the pacing. The same run with an unbatched stroke is `timeout`. Results: | self-test | cases | batteries | before | |---|---|---|---| | `execute` | 138 | 17 | 128 / 16 | | `dispatch` | 235 | 21 | 222 / 20 | | `validate` | 158 | 11 | unchanged | | `with-fleet` | 32 | n/a | all pass | Reverse mutations, each through `scripts/ablation-replace.mjs`. Each mutation landed (anchor hits 1 then 0, blob changed) and was restored (blob equal to HEAD, `git diff HEAD` empty). Every one turned the self-test red: | mutation | what was removed | red cases | what it showed | |---|---|---|---| | M1 | the executor's `announceBatch(...)` call | execute 5 of 138 | the pin's gaps became `[3000,3000,3000,3000]` (re-run on 4a9257f) | | M2 | the seat's ceiling extension | dispatch 1 of 235 | `timeout` at 300000 | | M3 | the packer's budget refusal | dispatch 2 of 235 | the packer case and the CLI case | | M4 | the executor's budget refusal | execute 2 of 138 | exit 0 with 13 requests | | M5 | the `ANNOTATED_OPS` arm of `createsContent` | dispatch 2 of 235 | `transfer` dropped out of the set | ## Gates (HEAD 4a9257f) `node scripts/pm/dispatch-gates.mjs --commands` derived 40 commands for this diff, the same as the dispatch order's list. All 40 exited 0, `pnpm check:pm-dispatch-gates` included (fast tier, 1913 cases). Running `--ran` with every exit code recorded reported `40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN`. Every other family is CI's. ## Acceptance notes - The budget refusal lives in the packer and the executor, not in `validate.mjs`'s `strokeErrors`. It is a rule about the run's pacing, not the payload schema, and `validate.mjs` cannot import the seat's ceiling without an import cycle. As a result, an over-budget stroke on the runner is refused by the execute step (zero writes, exit 2, summary written), not by the validate step. - The batch runs from the stroke's first write through its last content-creating one. A non-creating write placed before the first create is therefore paced at the batch gap too, which errs on the safe side. - A run that stops early leaves its unspent batch in the runner's pace log. The runner is ephemeral. A local `--payload-file` run on a persistent home would keep the batch until its own expiry at most, the same as any early-stopping `--announce-batch` caller. - A batch split into strokes of 4 still gets rule ① gaps inside each stroke. Across strokes, `sendFleetWrite` waits for each run before returning. For a batch, the contract is still the seat-side `--announce-batch` in front of the per-op doors; a direct `dispatch.mjs` stroke is for landing. Noted, not filed. - The branch is one commit behind `origin/main`. That commit, a43405d, is a one-line doc edit to `.claude/skills/pm-dispatch/references/filing-gate.md` and does not overlap this diff, so it was not merged in. The queue rebuilds on current main. ## 维护者速读(草稿) **改了什么**:中继(fleet-write)执行器在同一笔里含 5 个及以上"创建内容"的动作(新建 issue、评论、PR、转移卡片)时,先在 runner 上宣告批次,随后这些写入每次间隔 30 秒(write-pace 规则 ⑤)。发送端用同一公式算出这笔会暂停多久,相应延长等待;超出中继 job 10 分钟预算(单笔最多 11 个批次写)的笔在发送前被拒,并指向逐 op 工具。 **为什么改**:两个席位账号曾因短时间大量创建内容被平台限制,规则 ⑤ 正是为此加的截流;但中继路径漏了这道闸 —— 10-20 当天一笔 15 张卡 42 秒内落完。 **风险与代价(含回滚)**:一笔 5 张卡现在约需 2 分钟、11 张约 5 分钟,这是设计,不是变慢;单笔超过 11 个批次写会被拒,需改走逐 op 工具加 `--announce-batch`。落地笔(ready + automerge)与逐 op 工具的单卡笔不受影响。回滚:revert 本 PR 即恢复 3 秒间隔。 **席位意见**: **你要做的**:无需操作;`scripts/pm/**` 非受管面,席位 ACCEPT 后翻 ready 入队。 --- _Generated by [Claude Code](https://claude.ai/code/session_01HEdP5tPhqQfzwRmKDxwn85)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3983965 commit 6387793

2 files changed

Lines changed: 402 additions & 15 deletions

File tree

0 commit comments

Comments
 (0)