Commit graph

3,583 commits

Author SHA1 Message Date
cliffhall
a7c4c5260e fix: bound the 401/403 body read so an unending body is released (#2297)
Copilot review round 1. With the tracker below the auth-challenge
interceptor, its background clone kept draining a 401 body after the
interceptor cancelled its own branch — so a body that never ended left the
connection live. Read a 401/403 body up front instead, capped at 64 KiB and
2s, and cancel the clone's reader when either bound is hit so the
interceptor's cancel releases the source. The body now lands on the entry
itself before the challenge is thrown.

Also cover the web path end to end: a streamable-http 401 on
/api/mcp/send reaches the event stream with its status and body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SG6q6YEDJyJB5H2Hr8nBDS
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-14 01:01:15 -04:00
cliffhall
af4b574234 fix: record an intercepted 401's status, headers and body (#2297)
The network tracker wrapped the auth-challenge interceptor, so a 401/403
never reached it as a response: the interceptor cancelled the body and
threw, and the tracker logged only the thrown error. NetworkEntry renders
its response section only when a status is present, so the body — and the
WWW-Authenticate resource_metadata and scope — were never shown.

Move the tracker below the interceptor so it records the real response and
clones its body before the interceptor cancels it, and stop awaiting that
cancel: with the tracker's clone as the other tee branch, the cancel settles
only once the clone reads to the end, which would hang on an unending body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SG6q6YEDJyJB5H2Hr8nBDS
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-14 00:25:19 -04:00
Cliff Hall
3e728a3b8c
Merge pull request #2363 from modelcontextprotocol/v2/fix/2028-stdio-delayed-responses
fix: reset relay request-response waits on progress notifications (#2028)
2026-09-13 21:50:36 -04:00
cliffhall
516531677e test: drop an unnecessary double cast in the relay progress test helper (#2028)
The backendHoldingSend fetch mock is already declared with vi.fn<typeof fetch>(),
so the trailing `as unknown as typeof fetch` was redundant and tripped the
repo's no-double-cast rule. Return the typed mock directly; the test project
typechecks without it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 21:38:21 -04:00
Cliff Hall
fb36797ec2
Merge pull request #2362 from modelcontextprotocol/v2/fix/2320-connection-timeout-default
feat: bound connect attempts with a 30 s default connectionTimeout
2026-09-13 21:34:45 -04:00
cliffhall
fb28b0fe64 test: address Copilot review — tighten progress-token guard and de-flake relay tests (#2028)
- progressTokenOf now delegates to the SDK's ProgressTokenSchema instead of a
  bare typeof check, so fractional and past-MAX_SAFE_INTEGER numbers are
  rejected (matching isProgressToken in inspectorClient.ts); tests cover both.
- Browser relay timeout tests use fake timers with advanceTimersByTimeAsync and
  flush the SSE consumer between pushes, replacing real-sleep windows that a
  loaded runner could race (#1596).
- The log-only timeout test holds and awaits its rejection rather than
  swallowing it in an empty catch.
- progressToken.test.ts gains a file header; the remote-session malformed-token
  fixture uses a single-cast helper instead of `as unknown as`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 21:30:29 -04:00
cliffhall
167381174f test/docs: address Copilot review round 1 (#2320)
- Assert headersToServerSettings carries DEFAULT_CONNECTION_TIMEOUT_MS.
- Type the fake transports in inspectorClient.test.ts as the SDK's
  Transport via two helpers instead of an as-unknown-as double cast; the
  two pre-existing copies of the same fakes in that describe block use the
  helpers too.
- Bring serverList.ts's converter comments and specification/v2_servers_file.md
  in line with the new contract: 30 s default, explicit 0 persisted, and
  requestTimeout's 0 unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4h8Xa6Ed9zwtv5ojX35MT
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 21:16:23 -04:00
cliffhall
cd31d76371 fix: reset the relay's request-response waits on progress notifications (#2028)
The web client drives its MCP Client through a two-hop relay
(RemoteClientTransport -> Node backend -> upstream transport). The relay
added two request-response waits that the direct transports don't have —
the backend's RemoteSession.waitForRequestResponse and the browser's
RemoteClientTransport.waitForSseResponse — and both were hardcoded to 60s
and blind to notifications/progress. The SDK Client's own request timeout
resets on progress (resetTimeoutOnProgress, which the Inspector enables),
but the relay's progress-blind waits fire first and shadow it, so a
long-running tool that reports progress is cancelled at 60s on the web
client alone while it completes over the CLI/TUI and other MCP clients.

Reset both relay waits on a notifications/progress whose progressToken
matches the pending request id, mirroring the SDK's resetTimeoutOnProgress.
A shared browser-safe progressTokenOf helper extracts the token. Log
messages (notifications/message, i.e. ctx.info) deliberately do not reset
the deadline, matching the SDK and every other client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 21:13:30 -04:00
cliffhall
e641edeeab feat: bound connect attempts with a 30 s default connectionTimeout (#2320)
connectionTimeout defaulted to 0, which InspectorClient.connect() read as
"no bound at all" — so out of the box nothing the Inspector owns bounded a
connect attempt, and the only thing ending a hung handshake was the SDK's
60 s per-request timeout on initialize, reporting "Request timed out".

- Add DEFAULT_CONNECTION_TIMEOUT_MS (30000) to core/mcp/types.ts. An absent
  value resolves to it on the client, on disk read, in the web form draft,
  and in the CLI's header-only settings shell. An explicit 0 keeps its
  documented meaning (no timeout; --connect-timeout 0) and is now persisted
  so the opt-out round-trips; the default is the omit-sentinel, as for
  taskTtl.
- Key the post-timeout transport teardown to the timer actually firing, not
  to connectionTimeout > 0 — otherwise a non-zero default would tear down
  every failed connect, including a recoverable 401 whose transport the
  auth recovery holds open.
- The timeout message names the bound and where to raise it: "Connection
  timed out after N ms. To accommodate a slower server, you may increase
  the timeout value in Server Settings."
- The CLI's UNREACHABLE classifier now matches that message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4h8Xa6Ed9zwtv5ojX35MT
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 20:54:23 -04:00
Cliff Hall
153ee7dd15
Merge pull request #2361 from modelcontextprotocol/v2/chore/2359-sdk-watch-example
chore: refresh the stale ext-apps example in the sdk-watch comments
2026-09-13 20:13:25 -04:00
Cliff Hall
357c8d97b4
Merge pull request #2358 from modelcontextprotocol/v2/chore/1745-ext-apps-v2-peer
chore(deps): bump ext-apps to 2.0.0 and drop the SDK-v1 shims
2026-09-13 20:01:15 -04:00
cliffhall
c74976ab3b docs: stop claiming the ext-apps lockfile already resolves above its floor (#2359)
AGENTS.md's SDK-watch section made the same current-state claim the
sdk-watch.mjs comments did; with the manifest and lockfile both at 2.0.0 it
was false. Describe the mechanism (a caret range CAN resolve higher) instead.
Raised by Copilot on #2358 as a suppressed finding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 20:00:56 -04:00
cliffhall
4a1e1ad7a3 refactor: re-export AppRendererClient from types.ts instead of defining it twice (#1745)
Dropping the v1 translation proxy made the protocol's alias identical to the
one core/mcp/types.ts already exported. Keep one definition and re-export it,
so the two cannot drift.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 19:49:46 -04:00
cliffhall
b0391561a1 docs: address Copilot review round 1 (#1745)
- requestAppElicitation.ts, appElicitation.ts, docs/test-servers.md:
  ext-apps 2.0.0 does not "predate" #733 (the PR is from July; the
  release from September) — it simply does not include it. Reworded as
  omission, and the version the appElicitation.ts JSDoc names is now
  2.0.0 rather than 1.7.5.
- core/mcp/types.ts: the public `AppRendererClient` JSDoc described the
  app-renderer proxy; it is the real SDK `Client`, handed out only while
  connected, since #1745 removed the proxy.
- appCapabilities.test.ts: the two comments verified against 1.7.5 now
  cite 2.0.0, which the test runs against.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 19:42:48 -04:00
cliffhall
534a1384b1 chore: refresh the stale ext-apps example in the sdk-watch comments
#2358 bumped `@modelcontextprotocol/ext-apps` from `^1.7.4` to `^2.0.0`,
leaving two comments in `scripts/sdk-watch.mjs` still describing the
current install as `^1.7.4` and illustrating the "compare the installed
version, not the declared range" mechanism with a 1.8.0 target.

Both now describe the mechanism in terms of "a caret range" and "a patch
or minor ahead within the same major" rather than any concrete version,
so the example cannot go stale on the next bump. Comments only; the
1.7.4/1.7.5 fixtures in `sdk-watch.test.mjs` are test data for the
comparison logic and are untouched.

Closes #2359

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 19:37:52 -04:00
cliffhall
a42d7fd99e refactor: drop the ext-apps SDK-v1 shims now that 2.0.0 peers on SDK v2
Every site tagged `ext-apps#702` carried a workaround for ext-apps
peering on the v1 SDK while the Inspector runs v2. With 2.0.0 the
package's `AppBridge` extends the v2 `Protocol`, takes the v2 `Client`
and `Implementation` directly, and registers its list-changed forwarding
on that client with `setNotificationHandler("notifications/…", handler)`,
so:

- core/mcp/apps.ts: `getToolUiResourceUri(tool)` takes the v2 `Tool`
  without a cast; `UiMetaShape` is replaced by ext-apps' own
  `McpUiToolMeta` / `McpUiResourceMeta`, which resolve under NodeNext
  since ext-apps#705. `AppInfo.csp` / `.permissions` / `.visibility`
  take the named types too.
- core/mcp/inspectorClient.ts: `getAppRendererClient()` returns the SDK
  client itself; the Proxy that translated the 1.x schema-first
  `setNotificationHandler(Schema, handler)` into the method-string form,
  its `notificationMethodFromSchema` helper and its cached field are
  gone. `AppRendererClient` is now `Client` rather than `unknown`.
- createAppBridgeFactory.ts: `new AppBridge(client, HOST_INFO, …)` with
  no `as unknown as`; `resolveSourceUri` passes the tool straight through.
- AppRenderer.tsx: `sendToolResult(result)` — its params ARE the v2
  `CallToolResult` now.
- requestAppElicitation.ts: the double cast on the `elicitation/create`
  send goes too — v2 `Protocol.request` takes the standard `Request`
  shape, which `ElicitRequest` is. The helper itself stays: ext-apps#733
  is not in 2.0.0.
- core/mcp/extensions.ts: `MCP_APP_MIME_TYPE` re-exports ext-apps'
  `RESOURCE_MIME_TYPE` from `/app-bridge`; `UI_EXTENSION_KEY` stays
  hardcoded because `EXTENSION_ID` is still `/server`-only in 2.0.0.

Tests: the Proxy-translation test is removed with the Proxy; the
`getAppRendererClient` tests assert the real `Client` instance and that
disconnect withdraws it; the `useInspectorClient` sentinel is a real
(unconnected) `Client`. Comments that cited ext-apps 1.7.5 now cite 2.0.0
where the fact still holds (#733 still unreleased).

Closes #1745
Refs #2309

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 19:21:06 -04:00
cliffhall
84fccd024d chore(deps): bump @modelcontextprotocol/ext-apps to ^2.0.0
ext-apps 2.0.0 peers on the SDK v2 split packages (client/core ^2.0.0,
server ^2.0.0 optional) and zod ^4.2.0, all of which the root manifest
already pins, so the v1 @modelcontextprotocol/sdk@1.30.0 it used to drag
in transitively leaves the tree: `npm ls @modelcontextprotocol/sdk` is
now empty. Root-only declaration per Dependency placement; no client
manifest names ext-apps.

Refs #1745, #2309

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 19:21:06 -04:00
Cliff Hall
bb12a33ed7
Merge pull request #2355 from modelcontextprotocol/v2/docs/2354-quality-gate-doc
docs: bring quality-gate.md up to date after #2338 and document the gate lease
2026-09-13 18:38:29 -04:00
Cliff Hall
96b0531196
Merge pull request #2357 from modelcontextprotocol/v2/chore/2356-stale-gate-comments
docs: correct two stale gate comments after #2354 (gate-lease MAX_WAIT_MS, main.yml table pointer)
2026-09-13 18:36:07 -04:00
cliffhall
f9a3248de2 docs: make the MAX_WAIT_MS doc pointer order-independent
Copilot round 1: the comment claimed docs/quality-gate.md already describes
the total queueing budget, which is only true once PR #2355 merges. State it
as a keep-in-step pointer instead, so it holds under either merge order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 18:23:56 -04:00
cliffhall
9181cb318b docs: address Copilot review round 3 (#2354)
Narrow the lease's guarantees to what acquireLease and finishLease do; say
where no release ever comes; and record the concurrent-sibling arm of the
maxWorkers measurement beside the solo one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 18:20:28 -04:00
cliffhall
5705a9e97c docs: correct two stale gate comments after #2354
`MAX_WAIT_MS` in scripts/gate-lease.mjs said the 45-minute wait only ever
expires against a live holder still running after 45 minutes. The budget is
measured from the waiter's first `acquireLease` attempt and is not reset when
the lease changes hands, so a queue of healthy gates deeper than about ten
also exhausts it. The comment now says so, matching docs/quality-gate.md.

The "Run cross-client smokes" step in .github/workflows/main.yml pointed at
a CI-vs-local table in the root README that the README no longer carries; it
now points at docs/quality-gate.md, which declares itself canonical.

Comment-only; no behaviour change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 18:12:25 -04:00
cliffhall
a57f2654a9 docs: address Copilot review round 2 (#2354)
The 45-minute wait is a total queueing budget in the skill too; the lease
promises the holder's release, not every waiter's acquisition; and what it
removes is gate-on-gate contention, for as many starts as fit the budget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 18:09:18 -04:00
cliffhall
09f625c197 docs: address Copilot review round 1 (#2354)
The process-group signal guarantee is POSIX-only; the 45-minute wait is a
total queueing budget, not a hung-holder detector; the fixed-port range is
6295–6299 including smoke:web:tabs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 17:58:06 -04:00
cliffhall
296eb9504f docs: bring quality-gate.md up to date after #2338 and document the gate lease (#2354)
Every stage description now matches the scripts on v2/main: the quiet-gate
baseline figures, the CI job budgets (#2333), the smoke-helper budgets that
are named constants or stated defaults, the maxWorkers decision (#2336), the
Storybook forced pre-bundle (#2340), the gate-lease test suite (#2339/#2346),
and a local:storybook row. Adds the multi-agent section at the end: what the
lease does, why a queue rather than a load wait or a worker cap, what it means
for capacity, and what it does not do.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 17:47:32 -04:00
Cliff Hall
14661b1a94
Merge pull request #2353 from modelcontextprotocol/v2/fix/2350-skillsscreen-geometry-second-state
fix: sample the SkillsScreen geometry only once the Frontmatter highlighter has landed
2026-09-13 12:59:22 -04:00
cliffhall
274e3adca4 fix: sample the SkillsScreen geometry only once the Frontmatter highlighter has landed
The "Long Skill Document" story compared the section heights before and
after a collapse-then-reopen of the Skill Resource panel, and #2291 wrapped
the second sample in a `waitFor` on the reading that the first was fine and
the second was taken mid-layout. It was not: `[…, 155, 285]` is the layout
with the Frontmatter section showing `CodeHighlight`'s plain `Code`
placeholder (a 57px `<pre>`), and `[…, 178, 262]` is the same layout once
the lazily imported Prism runtime and YAML grammar have landed and the
highlighter has replaced it (an 80px `<pre><code>`). Both are settled; the
section's content changed between the two samples, so no retry converges.

Whether the swap has happened when the story reaches its first sample
depends on how long those dynamic imports take. On a quiet machine an
earlier story in the file has already loaded them; under the gate's load
they land between the samples. Running the story on its own, so the cache
is cold, reproduces the failure deterministically: 3/3 red with the exact
four numbers from #2350, 3/3 green with this change, 3/3 red reverted. The
whole storybook project under a concurrent unit-suite load (load average
38-51) is 3/3 green with it.

The wait's budget is stated at the site: three dynamic imports through the
Vite server every story iframe shares measure 100-150ms cold on an idle
server and exceeded Testing Library's 1000ms default under load, so it
takes the same 10s the Ace-worker wait in `JsonObjectInput.stories.tsx`
carries, under the project's 15s `testTimeout`.

Closes #2350

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 12:41:20 -04:00
Cliff Hall
7ec4eec018
Merge pull request #2352 from modelcontextprotocol/v2/docs/2347-asyncutiltimeout-pin-comments
docs: stop justifying the asyncUtilTimeout pin by pointing at #2335
2026-09-13 12:37:32 -04:00
Cliff Hall
4673e04e13
Merge pull request #2351 from modelcontextprotocol/v2/chore/2346-gate-lease-test-tmp-cleanup
chore: remove gate-lease.test.mjs temp directories after the suite
2026-09-13 12:23:44 -04:00
cliffhall
f8536cad5d docs: address Copilot review round 1
The 5000 arms of #2335's re-run each had exactly one red test — the
asyncUtilTimeout guard refusing the raised value — so "0 failures in
every arm" was a misstatement; both comments now say so. And the
AGENTS.md rule still gave the expiring-wait mechanism as the reason the
pin was measured worse, which the corrected record contradicts; it now
cites both measurements and keeps the mechanism as the hazard the rule
guards against.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 12:19:26 -04:00
cliffhall
4e19598d6b docs: stop justifying the asyncUtilTimeout pin by pointing at #2335
Both web test-setup comments explained the 1000ms pin by saying the
tests that let a Testing Library wait expire "are the real defect and
are #2335; revisit when they are gone." #2335 found there are no such
tests, and its interleaved three-arm re-run showed a raise to 5000 is a
no-op on the passing path. The comments now record that finding and
keep the pin on the measured ground: raising buys nothing, so the value
stays until a measurement says otherwise.

Comment-only; the pin and the runtime assertion are unchanged.

Closes #2347

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 11:55:42 -04:00
cliffhall
1ebf11b660 chore: remove gate-lease.test.mjs temp directories after the suite
Closes #2346

`freshDir()` now records every `gate-lease-test-*` directory it creates and
a top-level `after` hook removes them all once the file's tests are done.
Every test that starts a child awaits its exit before asserting, so nothing
holds a lock inside them by then. Measured on one `npm run test:scripts`:
14 new directories left in $TMPDIR before, 0 after; 647/647 tests pass in
both arms. No assertion changed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 11:55:24 -04:00
Cliff Hall
1030f3d8f9
Merge pull request #2349 from modelcontextprotocol/v2/chore/2333-smoke-ci-budgets
chore: state the smoke-helper and CI wall-clock budgets (#2333)
2026-09-13 02:17:23 -04:00
cliffhall
e0cd94b03e fix: address Copilot review round 1 (#2333)
A runner that threw ETIMEDOUT landed in the catch and read as
unavailable, bypassing the split the previous commit added. The thrown
error is now normalized onto the `{ error }` path so the timeout check
covers both shapes; a test pins the thrown case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 01:59:44 -04:00
cliffhall
3f7ba20ae6 chore: state the smoke-helper and CI wall-clock budgets (#2333)
The five rows #2323 left as "assess", each either stated with its
reasoning and measurement, or shared with a constant that already has one.

- F4/F5: `driveAppFlow` and `connectViaDeepLink` default to
  `BROWSER_TIMEOUTS.ui` / `.roundTrip` instead of restating 30_000 /
  45_000 by hand; values unchanged, now one place to raise. A test in each
  helper pins the defaults.
- F1: `announced-child.mjs` states `DEFAULTS` ({ timeoutMs: 30_000,
  pollMs: 250 }) with the measurement — boot-to-announce of the composable
  test server is 135ms median at load average 15, and a passing start pays
  one poll interval, not the budget.
- F7: `pty.mjs` names `SCRIPT_PROBE_TIMEOUT_MS` (5s; the probe spawns in
  1.3ms median, 3.6ms max under load) and stops folding ETIMEDOUT into
  "no script(1)": a starved probe now fails `smoke:tui` loudly instead of
  turning it into an exit-0 skip.
- G1: every job in `.github/workflows/main.yml` declares
  `timeout-minutes` as a hung-job guard — roughly twice the slowest run
  observed over the last 40 push runs (and the last three releases for the
  publish jobs), rounded up to the next five minutes, with the range stated
  per job.

AGENTS.md's timeout section now points at where these live instead of at
this issue.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 01:50:33 -04:00
Cliff Hall
4a522ebc8c
Merge pull request #2345 from modelcontextprotocol/v2/chore/2336-vitest-max-workers
docs: record why maxWorkers stays unset, with the measurement (#2336)
2026-09-13 00:54:52 -04:00
cliffhall
99d6b193d9 docs: state both worker defaults with their lower clamp (#2336)
Copilot review round 2: both expressions omitted Vitest's `max(…, 1)`, so
as written they described zero workers on a one-core runner. Now
`max(availableParallelism() - 1, 1)` for the forks pool and
`max(min(12, availableParallelism() - 1), 1)` for the browser pool, and the
browser pool's single-worker condition names all three of its terms
(headless, file parallelism, provider support).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 00:40:12 -04:00
cliffhall
1c243377be docs: state the Storybook browser-pool exception (#2336)
Copilot review round 1: "every project inherits availableParallelism() - 1"
overstated the scope. The five forks-pool projects do; web's storybook
project runs the browser pool, whose default is min(12, availableParallelism()
- 1) — the same 7 on this machine, but a different expression, capped on
larger machines and 1 unless headless — and none of the arms measured it. A
maxWorkers set on that project would still apply (getThreadsCount reads it
first), so it is not exempt from the decision, only unmeasured; both files
now say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 00:25:07 -04:00
cliffhall
f128ec62d0 docs: record why maxWorkers stays unset, with the measurement (#2336)
No Vitest project sets `maxWorkers`, so every one inherits Vitest's
`availableParallelism() - 1` (7 on the team's M3). #2336 asked whether that
number should be stated or capped. Measured at the leased baseline (#2339),
interleaved default / --maxWorkers=4 / default, three rounds each: a cap of 4
costs +14% wall on the web unit suite and +39% on coverage:web, saves 18-22%
CPU, and prevents zero failures (0 of 18 runs either way). Against a sibling
session's concurrent unit run it is a small politeness trade with no
throughput gain; the only failures seen were one test's 5s inner waitFor at
14 workers, which is a site to fix rather than a reason to cap.

So the default is kept, as a chosen value: the reasoning and the numbers sit
beside TIMEOUTS in vitest.shared.mts, and AGENTS.md carries the rule so a
reviewer can cite it against a future per-project cap. CI is unaffected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-13 00:13:39 -04:00
Cliff Hall
f16a51d46c
Merge pull request #2344 from modelcontextprotocol/v2/feat/2339-gate-lease
feat: queue concurrent local:gate runs on a machine-wide lease (#2339)
2026-09-12 23:13:22 -04:00
cliffhall
17e85a8745 fix: address Copilot review round 4 (#2339)
Handle SIGQUIT (Ctrl-\) on the same tree-shutdown path as SIGINT, SIGTERM
and SIGHUP. The gate runs in its own process group, so a signal the wrapper
does not handle ends the wrapper alone and orphans the gate behind a lease
that then goes stale under it. The signal test is parametrized over SIGTERM
(143) and SIGQUIT (131).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 22:57:30 -04:00
cliffhall
d3e5432532 fix: address Copilot review round 3 (#2339)
- The spawn itself — spawnSpec and the synchronous argument validation in
  child_process.spawn — now sits inside the try whose finally releases the
  lease, so a command that fails before it starts cannot strand the lock
  until stale takeover. Tested with a non-array args value.
- A genuine release failure no longer falls through to the "released
  after" line; the lock may still be blocking the queue, and the message
  that says so is the last word.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 22:42:57 -04:00
cliffhall
2c8dbd3cd0 fix: address Copilot review round 2 (#2339)
- The holder-record removal is best-effort and can no longer throw past
  release(); ERELEASED on release is the library having already dropped a
  taken-over lock, so it is ignored rather than described as a lock that
  "goes stale on its own".
- The signal test's stand-in now spawns a descendant of its own, so the
  whole-process-group guarantee is what the test asserts: a regression to
  child.kill() or a non-detached spawn leaves the descendant alive and fails.
- The three docs no longer claim a dead holder is always recovered in 30s:
  a stale lock directory that cannot be removed runs the wait to its
  45-minute cap and names the path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 22:30:05 -04:00
cliffhall
60bbe7675f fix: address Copilot review round 1 (#2339)
- A non-ELOCKED acquire error with the lock directory still present (a
  stale lock the library could not remove: ENOTEMPTY, EACCES, EROFS) now
  waits like a held lock instead of degrading to an unleased run; only a
  lock that cannot be created at all degrades.
- Spawn with a shell on Windows (npm is npm.cmd there) and quote the
  arguments for cmd.exe via the existing winShellArgs; exported as
  spawnSpec so the branch is unit-tested from POSIX.
- Release the lease on every way out, including a spawn failure, via a
  finally around the child wait.
- Guard every lock removal (release and the library's exit handler) on the
  directory's identity, mirroring core/auth/node/file-lock.ts, so a holder
  whose lock was taken over never removes the winner's.
- The signal test stops the wrapper in a finally, so a failed setup
  assertion cannot leak its idle grandchild.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 22:15:52 -04:00
cliffhall
f99402918d feat: queue concurrent local:gate runs on a machine-wide lease (#2339)
`npm run local:gate` now runs its stages under `scripts/gate-lease.mjs`, a
`proper-lockfile` lease under `os.tmpdir()` (or `$XDG_RUNTIME_DIR`), so a gate
started in a second worktree waits for the first instead of overlapping it.

Measured on v2/main @ aa56551b: a quiet gate is 257s green; two started
together had one fail at 279s (`smoke:web:chromium` found port 6298 held by
the other gate's smoke) and the survivor take 338s. Under the lease, three
gates started within seconds ran back to back (259s, then 262s, then the
third), each green with zero timeouts.

The lease grants rather than polls a threshold, so two waiters cannot
deadlock on each other; a holder that dies stops refreshing and is taken over
after 30s; a signal to the wrapper stops the whole process group, releases,
and exits 128+signal; the wait names the holder's pid and worktree and gives
up after 45 minutes. `INSPECTOR_SKIP_GATE_LEASE=1` bypasses it. A lock that
cannot be created runs the gate unleased with a warning.

`scriptChainRuns` looks through the wrapper (and only it) so the existing
vouches about what `local:gate` runs keep reading the real chain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 22:00:33 -04:00
Cliff Hall
aa56551be2
Merge pull request #2343 from modelcontextprotocol/v2/chore/2341-gate-single-test-pass
chore: run each client's test suite once in local:gate, not twice
2026-09-12 21:08:55 -04:00
cliffhall
12d99a8e13 docs: name the bare pass as the one measured and removed (#2341)
Round 7's one suppressed finding: "that second pass" read as the
instrumented coverage pass; the ~80s is the bare pass that ran first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 20:51:00 -04:00
cliffhall
61fcada059 fix: address Copilot review round 6
- `reachableScripts` now follows every npm spelling of `run` — the
  canonical `run-script` and its `run`/`rum`/`urn` aliases — with a unit
  case, so `npm run-script validate:web` appended to `local:gate` fails
  the whole-gate check instead of slipping past it.
- docs/quality-gate.md `verify:skills` row, AGENTS.md skills rule 1 and the
  #2085 narration now describe the actual chain (`validate:guards`, reached
  by both `validate` and `local:validate`) and the qualified promise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 20:40:44 -04:00
cliffhall
9cff77e6d9 fix: address Copilot review round 4
- Reword the CI guarantee in AGENTS.md, docs/quality-gate.md and the
  pre-push-gate skill: a green gate means every check CI applies has
  passed here — the strongest predictor of green CI, not a proof — and
  name the residual (a test that passes only when slowed) as #1596's
  class to fix rather than headroom to keep.
- docs/quality-gate.md: cli's validate DID build, through `test`'s
  `pretest` hook; say that the build goes with the dropped `test` leg and
  `coverage:cli` builds once, later — in both places.
- Update the two source comments that still described `local:gate` as
  running `validate` (verify-skills.mjs, verify-build-gate.mjs).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 20:25:00 -04:00
cliffhall
ae9ba01bb6 fix: address Copilot review round 2
Trace reachability from `local:gate` itself, not only its first stage, when
asserting that no `validate` / `validate:<client>` is reached — a later
stage could reintroduce the bare suite just as easily as the first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 20:09:03 -04:00