Skip to content

ci(release): wait for npm long enough for the MCP Registry to accept the version - #247

Closed
milstan wants to merge 3 commits into
mainfrom
milstan/fix-product-4151
Closed

milstan wants to merge 3 commits into
mainfrom
milstan/fix-product-4151

Conversation

@milstan

@milstan milstan commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Fixes leadbay/product#4151.

The MCP Registry re-checks npm before it registers a version, and answers
version '<x>' was not found (status: 404) while npm's read replica is still
catching up. The retry loop spanned 5 attempts at 30s, so it gave up at ~150s.

Measured from npm publish returning to the registry accepting the version on
the rerun:

release npm accepted registry accepted delay
0.39.1 03:32:18Z 03:36:24Z 246s
0.39.2 05:13:35Z 05:17:11Z 216s
0.39.4 06:28:57Z 06:33:44Z 287s
0.39.5 07:02:30Z 07:05:52Z 202s

Four of the last five releases needed gh run rerun --failed.

The loop now runs 20 attempts, ~10 minutes, twice the worst of those. The
30s sleep and the give-up behaviour are unchanged.

Proof. packages/mcp/test/audit/registry-publish-retry-window.test.ts
lifts the loop out of release.yml and runs it under bash against a stub
mcp-publisher that replays the verbatim 0.39.5 400 body until npm serves the
version. The sleep is compressed 1000x in the harness only, so the assertion is
about how many attempts the loop makes. On main today:

× registers the version when npm serves it after the worst observed delay
  Tests  1 failed | 2 passed (3)

After:

✓ registers the version when npm serves it after the worst observed delay
✓ gives up rather than looping forever once npm is clearly not coming
✓ publishes on the first attempt when npm already serves the version

pnpm -r test 962 passed, pnpm -r typecheck clean.

Not done here. The issue's last paragraph notes that a failed registry job
signals nothing, since npm, the GitHub Release and the hosted image are all
correct. No notification is added. No version bump, the release queue is
coordinated. mcp-v0.39.6 failed in Publish @leadbay/mcp to npm, which is a
different failure and is untouched.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narrowly-scoped CI fix: the MCP Registry publish retry loop goes from 5×30s (~150s) to 20×30s (~10min), matching the measured npm propagation delays (202–287s) cited in the description. Traced the new audit test (registry-publish-retry-window.test.ts) by hand: it extracts the loop verbatim from release.yml via regex and exercises it against a stub publisher with 1000x time compression; the math checks out (worst-case 287s delay needs attempt 11, well inside the new 20-attempt budget, and the "gives up" / "first-attempt success" cases are also correct). No generated files, tool descriptions, or WORKFLOWS.md rows are touched, so those CLAUDE.md gates don't apply. No bugs or logic issues found — nothing to block on.

…the version

The registry re-checks npm before it registers a version and answers 404
while npm's read replica is still catching up. The retry loop spanned 5
attempts at 30s, so it gave up at ~150s. Measured from `npm publish`
returning to the registry accepting on the rerun: 246s on 0.39.1, 216s on
0.39.2, 287s on 0.39.4, 202s on 0.39.5. Four of the last five releases
needed `gh run rerun --failed`.

20 attempts spans ~10 minutes, twice the worst of those.

The test lifts the loop out of release.yml and runs it against a stub
publisher that replays the 0.39.5 400 body until npm serves the version.
It fails on main today.

Fixes leadbay/product#4151

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@milstan
milstan force-pushed the milstan/fix-product-4151 branch from a1f9819 to 51dcbba Compare September 16, 2026 17:21
milstan added a commit that referenced this pull request Sep 16, 2026
* fix(evals): the verifier authenticates from the same credentials file as the tester

compose.oauth.yaml bound the credentials file into the tester service only, so a
local journey run with EVAL_CLAUDE_CREDENTIALS set and CLAUDE_CODE_OAUTH_TOKEN
not exported finished the product session and then exited at the verifier with
"Not logged in", writing no verdict.yaml.

Closes leadbay/product#4146

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mcp): excluding a sector takes it off the include list

`leadbay_adjust_audience` on prod US lens 40979, 2026-09-16: the user asked to
tighten a lens to construction instead of manufacturing. The call
`{sectors:["Construction"], exclude_sectors:["Manufacturing"]}` saved a filter
with include `["830","731"]` and exclude `["830"]` — Manufacturing included and
excluded at once, and the narrowing never happened (product#4148).

`mergeFilter` unioned the new ids into the include criterion and the excluded
ids into the exclude criterion, and neither pass took an id off the other list.
An id the caller names on one side now comes off the other, and a criterion
left with no sectors is dropped rather than written empty. Same rule both
directions, so re-including a sector the lens excludes also works.

Locations carry the same union in the same function and are untouched here.

Proof: `adjust-audience-exclude-narrows.test.ts` replays the lens 40979 call.
On main it fails with include `["830","731"]`, the exact prod payload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* evals: adjust-audience-dirty-taxonomy declares what a live FR tenant has

The case declared four sector rows — Pergola aluminium, a null name,
Pergola bioclimatique and Menuiserie — and a French taxonomy. None of
that is a tenant setting, so no live run could establish it and
prepared-state failed on the fixture wording.

Checked against prod-fr on 2026-09-16, all four /1.6/sectors/all
captures: zero rows named Pergola, zero null or empty labels, and the
tenant reports language en.

starting_state now names what those captures show: no Pergola row in
either language, menuiserie spread over several rows at different
registry depths, and véranda only on the row that also covers
greenhouses. Running the product's own matchSector over the live
registry leaves all three trade words unresolved, which is the
ambiguous_sectors outcome the case grades.

outcome-1, the null-name crash, is dropped. That row never appears on a
tenant, so live evidence is impossible. It stays locked deterministically
in packages/core/test/unit/composite/adjust-audience-dirty-taxonomy-regression.test.ts.

languages now lists en and fr. The verifier observed en; the journey
resolves identically in both.

Refs leadbay/product#4149

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci(release): wait for npm long enough for the MCP Registry to accept the version

The registry re-checks npm before it registers a version and answers 404
while npm's read replica is still catching up. The retry loop spanned 5
attempts at 30s, so it gave up at ~150s. Measured from `npm publish`
returning to the registry accepting on the rerun: 246s on 0.39.1, 216s on
0.39.2, 287s on 0.39.4, 202s on 0.39.5. Four of the last five releases
needed `gh run rerun --failed`.

20 attempts spans ~10 minutes, twice the worst of those.

The test lifts the loop out of release.yml and runs it against a stub
publisher that replays the 0.39.5 400 body until npm serves the version.
It fails on main today.

Fixes leadbay/product#4151

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mcp): a lead job that runs past a minute answers in time (product#4144)

A qualify_leads call could block longer than the MCP host allows a tool call.
The SDK cancels at 60s (DEFAULT_REQUEST_TIMEOUT_MSEC) and a host timeout is
indistinguishable server-side from a user cancel, so the user sees an error
while the job they approved keeps running, with no job_id to come back to.

Measured on FR prod, mcp_version 0.39.1, org LEADBAY, lens 5885, 2026-09-16:
the POST /mcp/qualify submit alone took about 20s, then the default 45s wait
ran on top of it. One uncached lead returned 62,120ms after the call started.
wait_seconds also accepted up to 180, and every still_running response
suggested the follow-up poll wait exactly 60.

Three bounds, one number. MAX_WAIT_SECONDS is 45, it caps what a caller may
ask for, and it is what next_poll suggests. clampWaitSeconds takes the time
the TOOL CALL began, so the wait gets what the quote and the submit leave of
the budget instead of a further full wait_seconds on top of them.

Proof: job-wait-host-ceiling.test.ts drives qualify_leads on a fake clock with
the measured 20s submit. On main the call returns at 65,000ms, at 200,000ms
when the caller asks for 180s, and hands back a 60s hint. After: 45,000ms,
45,000ms, and a 45s hint.

Not fixed here: the submit POST itself is unbounded. It is a write that may
already have charged, so it is never aborted mid-flight. A submit that alone
outlasts 45s still loses the call, and no measured submit came close.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mcp): find_new_leads gets the same whole-call budget

Review found the gap: find_new_leads has the identical submit-then-wait shape
through the same helper, so a slow POST /mcp/search left the wait running past
the host's 60s. clampWaitSeconds now takes that call's start time too, and the
test asserts it with the same 20s submit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(mcp): release 0.39.8

Stacks five reviewed fixes into one release.

- product#4144 a lead job that runs past a minute answers in time (#249)
- product#4148 excluding a sector takes it off the include list (#245)
- product#4151 the release waits for npm before the registry publish (#247)
- product#4146 the eval verifier authenticates like the tester (#244)
- product#4149 the dirty-taxonomy case declares what a live FR tenant has (#248)

No file is touched by more than one of the five, so the merges carried no
conflicts. On the stacked tree: build clean, 2882 tests green across 367 files,
typecheck clean, prompts:build leaves the generated files unchanged, and the
largest description is leadbay_import_leads at 16951 with 49 to spare.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mcp): a city swapped out of an audience comes off the include list too

The review bot on #250 spotted that the product#4148 fix covered `sector_ids`
only, while `location_ids` has the identical structure. "Lyon instead of Paris"
left Paris included AND excluded, exactly as manufacturing was.

Not a regression from this release, but this release's CHANGELOG claims that
narrowing an audience narrows it, and that claim is false while locations still
contradict themselves. So it is fixed here rather than filed.

`dropSectors` becomes `dropIds`, taking the criterion type. Two new tests fail
on the branch without this commit and pass with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@milstan

milstan commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

[Claude]: Landed in the stacked release #250, merged as 772e127 (0.39.8). Verified on main rather than assumed: this PR's code and its tests are present there. Closing the branch PR, not the work.

@milstan milstan closed this Sep 16, 2026
@milstan
milstan deleted the milstan/fix-product-4151 branch September 16, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant