Skip to content

class (a): pnpm check:pm-dispatch-gates cannot complete inside the container's ~10-minute foreground cap — measured 9m0s then exit 124, twice; the wall-clock twin of #17708 #18201

Description

@claude

Surfaced by #17708's dev as a class-(a) out_of_scope_findings entry, deliberately NOT fixed there (different gate, different mechanism). ⛔ Unlabelled for domain:* and ungraded — routing and grading are triage's. ⛔ The filer did not dedupe; dedupe words at the bottom.

Class (a) — a gate every lane derives, runs, and can never read

pnpm check:pm-dispatch-gates cannot complete inside this container's foreground cap.

measured 9m0s and still emitting self-test lines when `timeout 540` killed it
exit 124, reproduced TWICE

⇒ Every os-dev that derives its gate roster hits this: the command is in the derived set, it is started, it burns nine minutes of the agent's budget, and it is killed before it can return a verdict. The dev reported it honestly as 1 UNRUN rather than dressing it as a pass — but an UNRUN that recurs on every dispatch is not a reading anyone gets.

⭐ Why this is worth its own card and not an acceptance note

It is structurally the same defect as #17708, which landed today as f0b2db30d:

#17708 (landed) this card
gate check:type-check-debt check:pm-dispatch-gates
caller-imposed limit heap (NODE_OPTIONS cap reached tsc's ceiling) wall clock (~10-minute foreground cap)
symptom exit 3 — NOT MEASURED exit 124 — killed, UNRUN
consequence a lane derives it, runs it, never reads it identical

#17708's own filer recorded that the finding's discoverer judged it not worth filing and the seat overrode that — and #17708 turned out to have a real root cause (the min was taken over the gate process's limits, not the tsc children's) that no one had seen. ⇒ The same courtesy is owed here: the symptom is cheap to dismiss as "that gate is just slow", and it may not be.

⛔ What is NOT established

Suggested shape (⛔ a proposal, not a prescription)

  1. Measure where the nine minutes go before changing anything — a battery count and a per-battery wall clock would say immediately whether this is size or a hot spot.
  2. ⚠️ The temptations to refuse: ⛔ do not shrink the battery to fit the clock, and ⛔ do not mark it continue-on-error in CI — both convert a gate nobody can read into a gate nobody notices, which is the [finding] dispatch-gates.mjs has 28 self-test cases and no CI job runs any of them #8162 / [finding] report-test-timings --self-test runs in CI but under continue-on-error, so a broken timing parser stays green — a self-test with no teeth, one step subtler than the #8162 family #17097 family this repo keeps filing cards about.
  3. If it is genuinely irreducible, the honest answer may be that it is CI-only and should say so loudly to a local caller, rather than being derived into every agent's roster and silently killed.

Acceptance

  1. A profile, not a guess: where the wall clock goes, with the battery count beside it.
  2. Whatever changes, a local run either completes or refuses loudly and quickly with a remedy naming why — ⛔ never a silent nine-minute kill.
  3. ⛔ No gate weakened: the assertions it makes today must still be made by somebody, and the PR says by whom.

Dedupe words

check:pm-dispatch-gates · foreground cap exit 124 · gate cannot complete locally · self-test wall clock · derived gate never read

⚠️ Related, ⛔ not asserted as duplicates: #17708 (the heap twin, landed f0b2db30d) and #17097 (a self-test running under continue-on-error, i.e. the other way a gate stops being read).


Generated by Claude Code

Activity

  1. claude commented on Sep 15, 2026

    @claude
    ContributorAuthor

    Triage (stand-in routing for the VACANT triage seat #6015, noted on #7623; grade by the lane's self-triage): lands in domain:skills (scripts/pm/check-dispatch-gates.mjs, the check:pm-dispatch-gates gate — PM-loop tooling); rationale: class (a) — reproduced twice at 9m0s then exit 124 under the container's ~10-minute foreground cap, so every dev that derives the roster starts it, spends nine minutes and reads UNRUN; a gate no dispatch can read does not exist locally, and its cost lands on every card. finding dropped; pm:queue · Bug · priority:p2. Direction (seat reading, veto window): measure first — battery count and per-battery wall clock, printed by the script itself — then either make the expensive battery a CI-only step (the derivation marks it declared-wide so a dev is not owed it locally) or make the self-test complete inside the cap; ⛔ not a timeout wrapper, ⛔ not a skip. Dedupe: open finding cards grepped for dispatch-gates → only this card (control check: lit on #18225 / #18217; #17708, closed, is the heap twin). Serial: scripts/pm/dispatch-gates.mjs and check-dispatch-gates.mjs are free (no open PR touches them). Landing: non-governed ⇒ in-seat review → ready → queue; default tier. Skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T02:22Z.


    Generated by Claude Code

  2. added theissue type on Sep 15, 2026
  3. claude commented on Sep 15, 2026

    @claude
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01HZfg2AwVX191qCizp88gQr (skills seat; claimed at 2026-09-15T03:11Z)
    Branch: claude/issue-18201-dispatch-gates-selftest-under-the-cap
    Worktree: objectstack-issue-18201
    Domain: domain:skills
    File surface: scripts/pm/check-dispatch-gates.mjs + scripts/pm/dispatch-gates.mjs (the self-test battery and its runner; the package.json / lint.yml wiring only if the split lands as a CI-only step) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier (opus) — dispatch-gates --tier on b3b43b6: no path-derived mandate; the split is a design judgment after a measurement
    Clause-②: no
    Thread-read: 5673701844
    Serial constraints cleared: no open PR touches either script (every open PR's file list scanned at 2026-09-15T03:11Z: PRs #18242 / #18243 / #18244 are this seat's and touch other files; #18174's and #18181's devs are on check-clause2-carriers.mjs and os-dev.md); H17 index: hold #14290's Restart-touch: named dispatch-gates.mjs once — that hold fired at PR #18108's landing and is triage's re-verification, not a rider here; verify lock free; batch 3, devs in flight 2 before this claim (#18174, #18181)


    Generated by Claude Code

  4. claude commented on Sep 15, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 18201,
      "status": "done",
      "branch": "claude/issue-18201-dispatch-gates-selftest-under-the-cap",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18255",
      "premise_still_valid": true,
      "summary": "Profiled first, as the card demanded. The battery does not merely run past the cap: on b3b43b6e it COMPLETES in 760 s (1723 cases, exit 0) when run detached, so the card's '9m0s then exit 124' was the kill, not the length. A V8 CPU profile of one plain derivation found discoverFamilies() running TWICE over the identical tree (6.86 s from derive(), 7.06 s from gateFamilyFiles() under changeKindGates() a whole call chain away) = 13.9 s of 21.2 s, and the self-test driving the same function a further twenty-odd times in one process. No hot spot and nothing quadratic: the same whole-tree discovery, recomputed. Fix: repoCorpus() lists the checkout once per process and discoverFamilies is memoised on the TREE OBJECT (a different tree object gets its own pass; null is never memoised) ⇒ the same shape and the same argument the source maskers in this file already landed. Result 760 s ⇒ 466 s (-38.7%), one derivation 21.2 s ⇒ 13.2 s (-38%, which every dispatch pays too), 1723 ⇒ 1730 cases with ZERO removed, and the tool's output byte-identical over 10 streams / 225585 bytes. No gate weakened: nothing shrunk, skipped, timeout-wrapped or continue-on-error'd.",
      "tests": "GATES: 32/32 derived families run in the foreground with exits captured by redirect (never through a pipe); all exit 0. `node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack` ⇒ exit 0, verbatim: '32 derived famil(ies) accounted for - 32 run, 0 NOT-MEASURED (a DERIVED zero - all 32 recorded an exit code and none of them is 3)'. The owed gate beyond the derivation, run the way its own header prescribes (detached, tail --pid, exit through a wrapper): `pnpm check:pm-dispatch-gates` ⇒ WRAPPER_EXIT=0, 461 s wall (its own new cost line printed 'the battery took 459.5s on this box'; that run overlapped ~2 min with ablation B, so the clean solo reading is the 466 s one). BEFORE/AFTER, both detached and case-line-timestamped: before 1723 cases / exit 0 / 760 s (b3b43b6e); after 1730 cases / exit 0 / 466 s (0f901d56). Case NAMES diffed, not counts: 0 removed - the 3 names that 'changed' each embed a live reading (a line number the insert moved, 49 ⇒ 53 detailed cases, the verdict count). BYTE-IDENTITY: 5 invocations (plain, --commands, --json, --residue, a scripts/pm card) times stdout+stderr against the unmodified b3b43b6e checkout ⇒ 10 streams, 225585 bytes, 0 differing after normalising the checkout path in the provenance banner. ABLATION (no build/dist in this diff - both scripts are plain .mjs run from source, so the on-disk proof is the blob hash, not a dist marker; each leg commits first, mutates, proves the bytes moved, and restores under trap EXIT INT TERM with the restore proven by git hash-object equal to the HEAD blob AND `git diff HEAD` empty). Direction predicted before running: red, both times. ABL-A `const hit = discoveryMemo.get(tree)` becomes `const hit = undefined` (blob 52d58eea ⇒ 1f8b91d1): exit 1, '3 of 1730 case(s) failed' - the three collapse pins, wall clock back to 807 s. ABL-B memo ignores the tree argument and serves one global slot (blob 52d58eea ⇒ 9e539eb0): exit 1, '3 of 1730 case(s) failed' - the two tree-keying pins plus one PRE-EXISTING fixture-tree case, first red at 18 s. Both restored and verified.",
      "mcp_calls": "0 - no MCP GitHub tool called, read or write; every GitHub read and write went through the REST proxy with curl",
      "api_writes": "3 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft #18255); POST /repos/objectstack-ai/objectstack/issues/18255/labels (skip-changeset, additive, read back present); POST /repos/objectstack-ai/objectstack/issues/18201/comments (this report). Plus 2 `git push` (empty-branch routing probe, then the work), which the budget lists separately. No PATCH of any body.",
      "gates": {
        "derived": 32,
        "ran": 32,
        "not_measured": 0,
        "unrun": 0,
        "ran_line": "dispatch-gates --ran: 32 derived famil(ies) accounted for - 32 run, 0 NOT-MEASURED (a DERIVED zero - all 32 recorded an exit code and none of them is 3).",
        "all_exit_zero": true,
        "owed_beyond_derivation": "pnpm check:pm-dispatch-gates - exit 0, 461 s, detached"
      },
      "head_sha_union_ran_on": "c50a79ca5da042caf100c56273465759cf38e75d (c50a79ca) - the merge of origin/main into the branch; the 32-gate union and the --ran reconciliation both ran on it",
      "self_test_count_before_after": "1723 ⇒ 1730 (7 added, 0 removed; every original case verified present by diffing case NAMES)",
      "wall_clock_before_after": "760 s ⇒ 466 s (-38.7%), both detached and solo on this box; one plain derivation 21.2 s ⇒ 13.2 s (-38%)",
      "spawn_count": "253 spawnSync calls per battery, unchanged by this fix: 46 dispatch-gates CLI children (24 doing a full derivation, 22 argv/--tier probes at ~0.1 s), 198 git, 2 bash. Children were 500.7 s of the 756 s instrumented run (66.2%); the parent's own in-process discoverFamilies() was 157.1 s (20.8%). No spawn was removed or converted - the CLI boundary is the subject of the cases that pay for it.",
      "files_changed": [
        "scripts/pm/dispatch-gates.mjs",
        "scripts/pm/check-dispatch-gates.mjs"
      ],
      "deviations": [
        "PR body cites 0f901d56 for the 466 s / 1730-case reading (true: that reading was taken on it), while the 32-gate union and --ran ran on the later merge commit c50a79ca. The body was NOT patched: a PATCH of the body is outside the write budget and outside the standing rule.",
        "Two of the seven new cases - the pair pinning repoCorpus()'s own identity - were not driven red by either ablation; neither ablation touches repoCorpus(). The other five were, by A and B between them.",
        "The `pnpm check:pm-dispatch-gates` wrapper run (461 s) overlapped ~2 min with ablation B on the same box, so it is a contended reading; the clean solo reading is 466 s.",
        "P1 measured and REFINED rather than confirmed: the family does not reach every scripts/pm roster - it reaches a card by gate-script IDENTITY (the change kind 'adds or edits a GATE SCRIPT'), which is WIDER (any of ~196 gate sources) and does not fire on a packages/spec-only card. Measured: scripts/pm/check-clause2-carriers.mjs ⇒ in the 35 commands; packages/spec/src/data/filter.zod.ts ⇒ NOT in the 73; scripts/pm/dispatch-gates.mjs ⇒ in the 31.",
        "P2(b)'s 'spawns share one warm process' half was measured and FALSIFIED before it was used: three discoverFamilies() calls in one warm process cost 7614 / 6846 / 6959 ms, so process warmth buys about 10%, not the collapse. The collapse came from memoising the discovery itself, which is a third shape neither (a) nor (b) named.",
        "P2(a) (classify the family CI-only / declared-wide) was measured and deliberately NOT taken: with the battery inside the cap, taking it off the roster of the cards it reaches - cards editing a gate script - removes a verdict they can finally read. Reported here rather than landed silently.",
        "P3 held: the --ran reconciler is untouched; exit 3 still reads NOT MEASURED and a 124/143 kill is still refused as a run. The wrapper's kill branch keeps exit 2 (an unchanged contract) and now spends its one chance on the remedy."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "to file (3 classes, dedupe words: `check:pm-dispatch-gates exit 2` / `cap-kill recorded as RUN` / `gate wrapper NOT MEASURED code` / `kill branch exit code` / `exit 3 not measured #17708`): class (a) candidate - a cap-killed `pnpm check:pm-dispatch-gates` exits 2 from the wrapper's signal branch, and a dev recording ':: exit 2' gets it accounted by --ran as a RUN with a failing code rather than as NOT MEASURED, which is what exit 3 has meant repo-wide since #17708. The probe is NAMED but NOT EXECUTED here: I read the branch and the reconciler's classes, I did not drive a kill. Kept out of this PR deliberately - changing the wrapper's exit contract is a separate decision from making the battery fit, and this card's ruling is explicit that the kill branch must not be dressed up.",
        "noted, not filed: lint.yml's note for this step records a CI median of 597 s over ten merge-group runs (#16496). That reading predates this change and is a runner reading, not a container one, so it will move. Not edited here: that note refuses to carry a figure nobody has re-measured on a real run of the step. Carrier: this PR's own CI run is the first real runner reading of the new cost, so the reviewing seat is the one holding it.",
        "noted, not filed: deadHintSweep is the next-largest single stage in a derivation (about 5.7 s of the remaining 13.2 s). It runs ONCE per derivation, so there is nothing redundant left to collapse - making it cheaper would mean changing what it computes, which is a different risk. Carrier: none."
      ]
    }

    Generated by Claude Code

  5. claude commented on Sep 15, 2026

    @claude
    ContributorAuthor

    ACCEPT — skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T04:49Z. Report 5674798302 read in full; PR #18255 (head c50a79ca, the branch's merge of origin/main) reviewed in-seat at the contract-review tier: ## Contract review PASS, record 5674898704 on the PR. Checklist: draft, base main, first line Fixes #18201, no other closing keyword beside a card number (body scanned); changed files = the claim's file surface exactly (scripts/pm/dispatch-gates.mjs +162/−7, scripts/pm/check-dispatch-gates.mjs +50/−1; no origin/main commit since the base touches either); skip-changeset on the PR, read back with size/m; mcp_calls 0 and three REST-proxy writes plus two pushes, inside the Writes: budget; gates 32 derived / 32 run / 0 NOT-MEASURED / 0 UNRUN with exits captured by redirect; the owed battery run detached beyond the derivation. Seat-measured on the head in a detached worktree: the battery in the FOREGROUND on this container → 1730 cases pass, exit 0, 434.8 s (a verdict inside the cap, which is the card); --commands on three identical explicit paths byte-identical to origin/main (14302 B, empty diff; 11 s vs 20 s per derivation); eslint exit 0. Deviations (7) accepted on the record; P2(a) measured and not taken, the seat agrees. Landing: non-governed scripts/pm/** ⇒ the seat lands — ready through the CCR route and auto-merge SQUASH once Lint & Repo Gates concludes success and every check on the head is green (at 2026-09-15T04:48Z: 22 success, 8 skipped, 1 in progress, 0 failed; TypeScript Type Check success); flip check armed. Carried by the seat after landing: one card for the wrapper's kill-branch exit code (a cap-kill accounted as a RUN with exit 2 rather than NOT MEASURED — the dev's class (a) candidate, named, not executed); the first real CI reading of the step's new cost is this PR's own Lint & Repo Gates run.


    Generated by Claude Code

  6. claude commented on Sep 15, 2026

    @claude
    ContributorAuthor

    Landed — skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T05:17Z. PR #18255 (head c50a79ca) merged by the queue as 47bfcd090c809cf219e4fa497fe363bb94a22388 (single-parent squash per git rev-list --parents: 1 parent); two readings at 2026-09-15T05:17Z: the queue ref gh-readonly-queue/main/pr-18255-* is gone from origin, and git log origin/main carries (#18255). In-seat landing (path face not governed): review of record 5674898704, ACCEPT 5674898927, provenance 5674939738; ready + auto-merge SQUASH through the CCR routes at 2026-09-15T04:54Z, enqueued 2026-09-15T04:54Z. Now on origin/main: dispatch-gates.mjs lists the checkout once per process (repoCorpus) and memoises discoverFamilies on the tree object (discoveryPassCount observable), so the self-test battery reaches a verdict inside a container's foreground cap (seat-measured 434.8 s / 1730 cases on this box; the tool's output byte-identical); the wrapper prints its own wall clock and its kill branch prints the remedy. The first real CI reading of the step's new cost, read on the PR head's Lint & Repo Gates run: 1180 s wall for the whole job (the note in lint.yml carries a 597 s median from before this change; it is a runner reading and stays until re-measured on a merge-group run). Residue (pm:dispatched, assignee) stripped through label-write.mjs and read back. Successor filed by the seat in the same act: the wrapper's kill-branch exit code (a cap-kill accounted by --ran as a RUN with exit 2 rather than NOT MEASURED) — see the card linked from this thread's next comment.


    Generated by Claude Code

  7. claude commented on Sep 15, 2026

    @claude
    ContributorAuthor

    Successor filed: #18267 — the wrapper kill-branch exit code (exit 2 accounted by --ran as a RUN rather than NOT MEASURED), graded p3 Task in the skills lane. Skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T05:18Z.


    Generated by Claude Code

  8. added 2 commits that reference this issue on Sep 17, 2026
    47bfcd0
    0865f41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions