Files
qwen-code/patches
3c95e7cffb fix(cli): make the ink useBoxMetrics loop guard independent of machine speed (#11835)
* fix(cli): make the ink useBoxMetrics loop guard independent of machine speed

The measurement budget added in #11565 refilled on elapsed wall-clock time
(32 measurements per 16ms), but React's nested-update cap is a fixed count of
50. The guard therefore only won that race while ~32 commits fit inside one
16ms window (~0.5ms each, including a Yoga layout pass). Under CPU contention
or on a slower platform the window refilled mid-cascade, the count reset to 0,
the budget never tripped, and React threw "Maximum update depth exceeded"
(#185) out of the commit phase - which is what the regression suite caught on
loaded CI runners, and why lastFrame() came back empty: onRender never ran.

Spend budget only on measurements that actually change the metrics, since
those are the only ones that schedule another render of the component, and
refill it on events in React's own update chain rather than on elapsed
milliseconds: a render the hook did not cause, a measurement that settled, and
a terminal resize. The cap is 16 self-driven renders - the same burst the old
budget allowed in practice, since updateMetrics runs twice per render - which
sits well under React's 50 and no longer depends on how fast the machine is.
No timer handle is introduced, so fake-timer tests still drain and React's act
queue is unaffected.

Adds a deterministic reproduction that pins machine speed by advancing
performance.now() 16ms per read: it fails with the reported #185 stack before
this change and passes after. Also adds regression coverage that the guard
still stops an oscillation in far fewer commits than React tolerates, and that
a box driven by external re-renders keeps reporting fresh metrics well past a
single budget.

Fixes #11817

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmu0yjkfs3z

* fix(cli): settle the ink box-metrics budget per commit, not per render

The guard settled its budget in the render body. React invokes a render body
twice under StrictMode, which `DEBUG=1 npm run dev` renders the CLI inside:
the first invocation spends budget and the second refills it, so the count
never drains, the guard never trips, and React's #185 fires - the same failure
#11817 reports, on the one code path that renders inside StrictMode.

Settle the budget from the root layout listener instead. It runs once per
commit and never during render, so a doubled render body cannot double count
it, and the measurement that schedules a render marks the commit that follows
as the hook's own: that commit spends budget, any other commit refills it.
The cap of 16 and the resize refill are unchanged.

Adds the StrictMode case to the regression suite - it fails with #185 against
both the previous patch and unpatched ink - and rewords the wall-clock case to
say what its performance.now() spy actually establishes.

* style(cli): keep the guard test React import on one line

Adding StrictMode to the existing one-line import is what Prettier produces;
the expanded form failed the lint job's Prettier check.

* test(cli): pin the resize refill, the budget bounds and the shared oscillator

The resize refill is the only path that reaches an instance whose budget
already tripped, and dropping it left every case green. The render bounds
also admitted any constant up to React own cap and pinned no floor at all.

Folding the counting fixture back into the oscillator removes the second
copy of the oscillation rule, and the metrics dispatch is now skipped when
the caller already knows the value did not change.

* fix(cli): arm the ink box-metrics guard on a hasMeasured transition

Round-2 review findings on the previous revision.

The budget is now armed when `hasMeasured` flips as well as when a measurement
changes: `setHasMeasured` schedules a commit of its own with no changed
measurement behind it, so a consumer gating the measured element on
`hasMeasured` took the refill branch on every commit and ran to React's cap.
Only the transition arms it - that call is unconditional, and arming on the
call would charge the external commits that keep a streaming box measuring.

Test changes, all pinned by mutation:

- The render counter is commit-scoped (reported from an effect, not the render
  body) and the budget bound now runs in both StrictMode modes. With a
  render-body counter the StrictMode arm reports 34 against `<= 24`.
- The StrictMode case pins the clock, so it is red against the pre-PR guard
  instead of green; it now fails with `Maximum update depth exceeded` and the
  commit-phase stack.
- Dropped the try/catch around that mount. It does not contain a #185 that
  escapes the commit phase - every case after it fails either way - so it only
  cost the stack trace.
- Corrected the resize case's title and comment: any commit in the ink root
  refills a tripped instance, not only a resize. A resize is special in that
  ink recomputes it with no React commit at all.
- Folded the clock pinning into one helper, restored in a `finally`.

Verified at this head: guard suite 10/10; the 8-file `useBoxMetrics` consumer
suite 173 passed; loaded A/B (6 CPU burners on 4 cores, 8 consecutive rounds,
`--retry=0`) 8/8 green here against 8/8 red on the pre-PR guard. The patch
round-trips: pristine `[email protected]` + `npx patch-package` reproduces
`build/hooks/use-box-metrics.js` at blob `a6d1a4d`.

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): pin the multi-pass budget headroom and correct the guard comments

The lower bound on the budget was only pinned as "more than 2 commits", and no
cap tighter than the oscillator needs can violate that: the oscillator settles
after cap + 2 commits, so a cap of 2 leaves the suite green while a consumer
legitimately needing a second measurement pass freezes mid-convergence. Add a
consumer that halves its own width on every measurement and assert the settled
width reaches the frame. It settles at 3px on the current budget and freezes at
16px with the budget lowered to 2.

The guard comments claimed a fixed per-commit trip depth, and that onLayout runs
for any commit in the ink root. The charge happens per layout-listener
invocation, and the listener is only registered while the measured ref resolves
to a root, so a consumer that detaches its measured element is charged for
roughly every other commit and trips at up to twice the count. Say that instead,
and stop promising headroom the constant does not deliver for that shape.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmu1r4dew54

* test(cli): pin the commit-path refill and the gated cascade's depth

Three review findings on the loop-guard suite, plus the PR description's stale
numbers.

The commit-path refill of an already-tripped instance had no witness: every
other recovery in the file goes through stdout.emit('resize'), and the
external-rerender case never trips (its count oscillates between 0 and 1), so
the comment on the resize case asserted a guarantee nothing checked. The new
case trips the oscillator, then bumps a sibling that owns its own state and
shifts the oscillator's left, and asserts the oscillator runs a second cascade
off that commit. Both refill paths are now pinned separately: deleting the
onResize reset reddens only the resize case, while a partial refill or one
gated on the instance not being tripped reddens only the new case (measured
deltas 1 and 0, against 16 at this head).

HasMeasuredGatedBox reports its commits through the same onRender counter its
siblings use, and the case now asserts the cascade's depth (<= 34) alongside
the frame. The frame alone is parity-blind - this fixture detaches its measured
element every other commit, so whichever of its two states it freezes in, the
last non-blank frame is the fallback render - and the suite stayed green with
the budget at 23 or 26, which takes this cascade to 46 or 52 commits against
React's own cap of 50.

ConvergingBox drops the `from`/`floor` props no caller ever sets and inlines
the 32 and 3 that its comment's arithmetic is about, so the multi-pass headroom
property cannot be quietly disabled by a mount that parameterises it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmu1zp0la5i

* docs(cli): narrow the guard comments to the bound it delivers

The vendored patch's two comments promised that "a cascade trips after the
same number of charged invocations on any machine", which review measured to
be false for two self-sustaining shapes: a cascade re-entering through a
parent's state, and one where two instances in a single component refill each
other's budget. Neither reaches the count - zero clamps - and only React's
50-update cap stops them, because the budget is per hook instance while the
cap is per root.

Both shapes are now named in the comment, with #11858 as the tracked rewrite.
The detached-ref caveat and the wall-clock comparison are unchanged, and the
hook is byte-identical apart from these comment lines: 12/12 tests pass.

Refs #11817

* test(cli): pin the loop guard's exhaustion precondition and cascade floor

Two recovery cases preconditioned on `afterTrip > 1` where the refill lines
they pin depend on the budget having been exhausted, and the gated cascade
case asserted a depth ceiling with no floor, so an early-settling hook passed
both. Assert exhaustion (`>= 16`, the vendored budget) at both preconditions
and floor the gated case's depth at the same constant.

Mutation-verified on the real suite: a one-line `hasMeasured` latch drops the
gated cascade from 32 commits to 2 and now reddens it (`2 >= 16`); a hook that
dedupes seen layouts drops both recovery cases to `afterTrip=3` and now reddens
them, where the resize case previously stayed green with the `onResize` reset
deleted. Deleting either refill line alone still reddens only its own case.

* fix(cli): scope the box metrics guard to the ink root

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-09-16 11:43:37 +00:00
..