feat(mcp)!: the job ticket is Leadbay's own id; delete the file we kept (product#4005) - #197
feat(mcp)!: the job ticket is Leadbay's own id; delete the file we kept (product#4005)#197milstan wants to merge 14 commits into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Synced with main; @leadbay/mcp is |
…b dead end (PR #197 review) Five defects from the #197 review. The first two are the ones that mattered: the rewrite lost capability it never had to lose. Per-lead progress and channel-aware 'done' were computed from the bulk store, so they went out with it. They never needed the store — they needed lead_ids, titles, email and phone, all of which enrich_titles already returns and the agent already carries. Restored on the lead_ids path, along with the title-scoping that stops a lead's pre-existing CFO email inflating a CEO run, and the rule that a contact is done only once the channel THIS run requested has landed. A phone-only run no longer counts an email-enriched contact as finished. The paragraph in enrich-titles telling the agent to redo that by hand with get_contacts is gone; the tool does it again. bulk_enrich_status now takes the same anyOf as qualify_status, so lead_ids alone is a valid call. That removes a dead end: the job lookup is a scan, not a fetch-by-id — the backend routes no GET /notifications/{id} — so an archived job was unfindable, and we ship the tool that archives it. New shared readNotificationById also searches the archived set and pages, and the two error hints no longer claim a 30-day org-scoped retention that does not exist. Also removed four instructions to use handles that no longer exist — import_leads' handle_id in its param description and outputSchema, enrich-titles' bulk_id and durability ('persisted bulks.json'), and dead handle_id spreads in import_and_qualify. enrich-titles declared re_used while the code returned reused, and listed a mode the code no longer returns while omitting one it does. Restored the ctx.progress ticks and the titles / email / phone echo, so the reply states its own scope. Ten new tests across three files, covering the phone-only case, title scoping, partial_failures, and lookup of an archived or paged-past job — none of which had coverage. 2187 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
[Claude]: all five defects fixed. 1 + 2 — per-lead progress and channel-aware 3 — the archived dead end is closed. 4 — the four dead instructions are gone, plus two you did not list: 5 — Tests: 10 new across 3 new files — the phone-only case, title scoping, What a user can do after this that they could not before: start an enrichment, qualification or import on What they can no longer do: pass Verified: |
There was a problem hiding this comment.
Solid rewrite overall — the launch-guard claim/settle split correctly closes the product#4039 race, and readNotificationById's archived+paginated scan fixes the dead-end from the prior review round. Two issues before merge, both stemming from the bulk_id→notification_id rename not being swept everywhere it needed to be:
enrich-titles.tsaccidentally added the newnotification_id/lead_ids/reusedoutput fields toinputSchemainstead ofoutputSchema— the tool's own declared contract no longer documents the job id it mints. Worse, at least one real consumer (packages/components/src/runtime.ts'senrichment()helper) still reads the deletedbulk_idfield and will now silently report every enrichment launch as instantly complete with no job ever polled.getting-started.tshas the same stale reference in its onboarding-tour prompt text.bulk-enrich-status.ts'soutputSchema.requiredstill claimsnotification_idis always returned, but the new lead_ids-only path can omit it.
Neither is exercised by this PR's own tests since the boundary is mocked, so a repo-wide grep for bulk_id/handle_id/qualify_id before merge would be worthwhile given this is explicitly a no-transition-period breaking change.
| reused: { | ||
| type: "boolean", | ||
| description: "True when an identical launch inside the 5-minute window was reused instead of spending quota again.", | ||
| }, |
There was a problem hiding this comment.
notification_id, lead_ids and reused are declared under inputSchema.properties here, not outputSchema. Their descriptions ("Carry it to leadbay_bulk_enrich_status", "True when an identical launch...was reused") are plainly describing return values — and execute() does in fact return all three (lines 88/94/145). But outputSchema (line 260+) previously documented the equivalent fields as bulk_id/re_used/durability and this PR deletes them without adding replacements back — so the schema for the very field this whole PR introduces as the new job handle (notification_id) is now undocumented in the output, while incorrectly implied to be a launch parameter. EnrichTitlesParams (top of file) confirms execute() never reads notification_id/lead_ids/reused from input, so as inputs they're just inert — this looks like the three properties were pasted into the wrong schema block.
Separately: packages/components/src/runtime.ts's enrichment() helper (not touched by this PR) still reads r.bulk_id from the leadbay_enrich_titles result and calls leadbay_bulk_enrich_status({bulk_id}). With bulk_id gone, bulkId is now always null, so every real launch takes the "no job" branch and reports {all_done:true, no_job:true} immediately — silently faking completion for any artifact/widget built on that SDK helper. packages/components/test/domain.test.ts didn't catch it because it mocks the tool call directly with {bulk_id:"b1"} rather than exercising the real (renamed) core output. Worth a repo-wide grep for bulk_id/handle_id/qualify_id before merge — packages/core/src/composite/getting-started.ts:435 also still tells the onboarding-tour agent to poll "with the returned bulk_id".
| }, | ||
| }, | ||
| required: ["bulk_id", "status", "leads", "overall_progress", "all_done"], | ||
| required: ["notification_id", "status", "leads", "overall_progress", "all_done"], |
There was a problem hiding this comment.
outputSchema.required lists notification_id as always present, but the new lead_ids-only path (no notification_id passed in) returns an object that omits the notification_id key entirely (...(params.notification_id ? { notification_id: ... } : {}) further down). The declared schema no longer matches what a lead_ids-only call actually returns.
…s ids (PR #197 review) Repo-wide sweep for bulk_id / handle_id / qualify_id / BulkTracker after the store was deleted. What was still reading the old contract: - enrich_titles declared notification_id, lead_ids and reused under inputSchema; they are outputs. Moved. - The artifact SDK's lb.enrichment() (packages/components) read r.bulk_id and polled bulk_enrich_status({bulk_id}), so every widget enrichment reported done instantly with no job polled. It now carries notification_id + lead_ids and forwards titles/email/phone so the per-lead rollup is scoped. Its test mocked the deleted field and had to change; artifact-runtime.generated.ts is regenerated from it. - The live eval harness imported LocalBulkStore from @leadbay/core (no longer exported) and plumbed LEADBAY_BULK_STORE_PATH; both removed. - core's import-and-qualify live smoke imported jobs/bulk-store.js and asserted qualify_id / BULK_NOT_FOUND; rewritten to lead_ids + lens_id and QUALIFY_JOB_NOT_FOUND. mcp's live smoke polled bulk_enrich_status({bulk_id}); now LEADBAY_TEST_NOTIFICATION_ID. - Copy that told the assistant to poll with a handle that no longer exists: getting-started tour text, bulk_qualify_leads and import_and_qualify schema descriptions, the cancellation paragraph in server instructions + README, the qualify_top_n prompt's "BulkTracker-not-configured" rule, and the dead LEADBAY_BULK_STORE_ALLOW_MEMORY env in the dxt smoke. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Follow-up review on commit 6296313 (previous review at 252dfd9 requested changes).
Both prior findings are fixed: notification_id/lead_ids/reused moved from inputSchema to outputSchema in enrich-titles.ts, with the stale bulk_id reference in runtime.ts's enrichment() helper and getting-started.ts's onboarding prompt both updated to the new handle shape; and bulk-enrich-status.ts's outputSchema.required no longer claims notification_id is always present.
The new code in this diff (per-kind ENRICH_JOB_WRONG_KIND/QUALIFY_JOB_WRONG_KIND guards using inferKind, the ENRICH_JOB_NO_COUNTERS path, and the qualify-status not-found fallback when lead_ids+lens_id are supplied) is well covered by the new bulk-enrich-status-kind.test.ts and qualify-status-kind.test.ts unit tests, and I traced the logic against those tests without finding defects. CHANGELOG/README/prompt-template updates are consistent with the code changes.
No new significant issues found. (Note: I was unable to invoke the review-dismissal endpoint directly in this environment — posting this as a COMMENT review instead, since no outstanding issues remain from the prior CHANGES_REQUESTED review.)
|
main shipped |
|
main shipped |
|
main shipped |
… (product#4005, product#4039) Replaces branch milstan/4005-v2, which reverted two merged PRs. Rebuilt on 9580fad by re-applying the change rather than replaying the branch. The store existed, per its own header, 'while the Leadbay backend doesn't yet issue a real bulk_id'. It does now: GET /1.6/mcp/jobs/{id} (mcp_jobs) and GET /1.6/notifications, which the backend ADR calls the single user-facing unit for asynchronous operations. Both durable and org-scoped. Launches return the backend's id; status tools poll by it. Deleted jobs/bulk-store.ts, ToolContext.bulkTracker, both entrypoint wirings, eight BULK_* codes and three env vars — no volume, nothing at rest on the pod or on the user's disk. The double-launch guard now CARRIES its state instead of leaving the caller to infer it. beginLaunch returns owned | in_flight | settled. Reading an in-flight claim as settled is what produced status:'running' with a null notification_id or an empty importIds — and since importIds is the only handle on hosted, that answer loses the import for good. import_and_qualify also abandons its claim when the fan-out throws, and no longer logs a skip it does not perform (product#4039). Deletions against main are bulk-store.ts and its own tests only. Every test file from #182, #191, #192, #195 is present and passing; runWithRequestSignal and fetchTerminalNotifications are intact. 2177 tests passing, typecheck clean. BREAKING CHANGE: bulk_id, handle_id and qualify_id are gone as inputs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…b dead end (PR #197 review) Five defects from the #197 review. The first two are the ones that mattered: the rewrite lost capability it never had to lose. Per-lead progress and channel-aware 'done' were computed from the bulk store, so they went out with it. They never needed the store — they needed lead_ids, titles, email and phone, all of which enrich_titles already returns and the agent already carries. Restored on the lead_ids path, along with the title-scoping that stops a lead's pre-existing CFO email inflating a CEO run, and the rule that a contact is done only once the channel THIS run requested has landed. A phone-only run no longer counts an email-enriched contact as finished. The paragraph in enrich-titles telling the agent to redo that by hand with get_contacts is gone; the tool does it again. bulk_enrich_status now takes the same anyOf as qualify_status, so lead_ids alone is a valid call. That removes a dead end: the job lookup is a scan, not a fetch-by-id — the backend routes no GET /notifications/{id} — so an archived job was unfindable, and we ship the tool that archives it. New shared readNotificationById also searches the archived set and pages, and the two error hints no longer claim a 30-day org-scoped retention that does not exist. Also removed four instructions to use handles that no longer exist — import_leads' handle_id in its param description and outputSchema, enrich-titles' bulk_id and durability ('persisted bulks.json'), and dead handle_id spreads in import_and_qualify. enrich-titles declared re_used while the code returned reused, and listed a mode the code no longer returns while omitting one it does. Restored the ctx.progress ticks and the titles / email / phone echo, so the reply states its own scope. Ten new tests across three files, covering the phone-only case, title scoping, partial_failures, and lookup of an archived or paged-past job — none of which had coverage. 2187 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s ids (PR #197 review) Repo-wide sweep for bulk_id / handle_id / qualify_id / BulkTracker after the store was deleted. What was still reading the old contract: - enrich_titles declared notification_id, lead_ids and reused under inputSchema; they are outputs. Moved. - The artifact SDK's lb.enrichment() (packages/components) read r.bulk_id and polled bulk_enrich_status({bulk_id}), so every widget enrichment reported done instantly with no job polled. It now carries notification_id + lead_ids and forwards titles/email/phone so the per-lead rollup is scoped. Its test mocked the deleted field and had to change; artifact-runtime.generated.ts is regenerated from it. - The live eval harness imported LocalBulkStore from @leadbay/core (no longer exported) and plumbed LEADBAY_BULK_STORE_PATH; both removed. - core's import-and-qualify live smoke imported jobs/bulk-store.js and asserted qualify_id / BULK_NOT_FOUND; rewritten to lead_ids + lens_id and QUALIFY_JOB_NOT_FOUND. mcp's live smoke polled bulk_enrich_status({bulk_id}); now LEADBAY_TEST_NOTIFICATION_ID. - Copy that told the assistant to poll with a handle that no longer exists: getting-started tour text, bulk_qualify_leads and import_and_qualify schema descriptions, the cancellation paragraph in server instructions + README, the qualify_top_n prompt's "BulkTracker-not-configured" rule, and the dead LEADBAY_BULK_STORE_ALLOW_MEMORY env in the dxt smoke. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nt without counters answers honestly (product#4005 live e2e) Found on staging, one fresh server process per call: - bulk_enrich_status fed a QUALIFICATION's notification_id reported it as a finished enrichment (2/2, all_done). qualify_status fed an ENRICHMENT's id reported a running qualification. Neither checked what the notification was. Both now use inferKind (links[].type / file_import_id / bulk_progress) and refuse with ENRICH_JOB_WRONG_KIND / QUALIFY_JOB_WRONG_KIND naming the right tool. This is the BULK_WRONG_KIND guard the store had, restored on the backend's id. - The finished enrichment row on staging carried in_progress + title + the bulk_enrichment link but NO bulk_progress, so a notification_id-only poll said "not a bulk job". It now answers ENRICH_JOB_NO_COUNTERS with the backend's running/finished flag and asks for the lead_ids the launch returned, which always answer (verified: the lead_ids path resolves the same job after it was archived). - qualify_status threw QUALIFY_JOB_NOT_FOUND even when the caller had passed lead_ids + lens_id, the fallback its own hint recommends. It falls through to the per-lead path now, as bulk_enrich_status already did. - bulk_enrich_status's outputSchema required notification_id, which the lead_ids-only path omits. Dropped from required. - Both templates and the qualify_status hint no longer claim a 30-day, organization-scoped retention the backend does not have. - CHANGELOG: the 0.34.0 entry was missing. Tests: bulk-enrich-status-kind.test.ts, qualify-status-kind.test.ts (new files), using the exact rows staging returned. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The branch was renumbered from 0.34.0 to 0.35.0 after main released 0.34.0 (ChatGPT connector URL). Every release on main carries a customer-facing entry in the root CHANGELOG; this PR never had one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One line: the notifications-inbox snippet now says notification_id inside account_status, whose template main changed in #201. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6296313 to
265d3ec
Compare
Caught by test/audit/server-json-version.test.ts after the renumbering. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… true
`long-running-tools` swapped `qualify_id` for `notification_id` on both
tools at once, but async `import_and_qualify` returns `import_ids` and no
`notification_id` at all. Each tool now names its own handle and its own
status tool.
`enrich_titles` told the agent to poll with `{notification_id, lead_ids}`.
`lead_ids` is what switches `bulk_enrich_status` to the per-lead path, and
on that path `titles`/`email`/`phone` are what make "done" mean the roles
and channel THIS run asked for. Without them every contact counts, so a
lead whose CFO was email-enriched months ago satisfies a fresh phone-only
CEO run and `all_done` is true on the first poll — enrichment reported as
finished before anything landed. All three instruction strings carry the
scope now.
`import_status` was told it "does not re-reconcile records or return
refreshed leads". It does: the importIds path still reconciles the
wizard's records and returns `result.{leads,not_imported,still_settling}`,
which is what its own outputSchema declares. The `still_settling`, the
`committing` phase and the qualify-the-imported-leads step were deleted
with that sentence; all three are back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… for ever
`commitMappings` (POST /imports/{id}/update_mappings) is the MCP's own
call — nothing on the backend sends it. So a chunk we walk away from
before that POST sits inert for ever, and `import_status` classifies the
row as COMPLETE (pre_processing.finished && !processing): the user is
told an import finished that committed nothing, and the rows they
uploaded are somewhere they can never reach.
The blocking path already hands such an upload to `resumeParkedUpload`.
The `wait_for_completion:false` path did not — its catch only logged, and
that path is the one the hosted server now runs for the first time.
Same guard, wired into the background catch. It covers every un-committed
chunk, not just the one that threw: a 100-row chunk size means a larger
import leaves later chunks parked identically. The chunk that threw is
resumed only on a PREPROCESS timeout — the commit is sent immediately
after preprocess, so a later-phase error means it already went out and
re-sending it would re-trigger processing. A dry run is never resumed;
committing its mappings would turn a validation pass into a real import.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a release `auto-tag.yml` fires on a push to main whose diff touches `packages/mcp/package.json`, and from there tags, publishes to npm and the MCP Registry, cuts a GitHub release and builds the image Argo deploys to mcp.leadbay.app. `release.yml` and `build-image.yml` have no push-to-main trigger of their own, so that one file is the whole lever. Several PRs are landing before the next release and the batch gets tested as a whole first. Leaving the file untouched means merging this fires CI and nothing else. Both CHANGELOGs keep their `## 0.35.0` sections. Nothing asserts that they agree with package.json, and `release.yml` builds its notes by matching `## <version>` in packages/mcp/CHANGELOG.md — so the entry is already where the release PR will look for it. That PR sets both version files and re-titles the sections if the batch ships as something other than 0.35.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Follow-up review on the diff since commit 6296313 (previous review at that commit was COMMENTED, no outstanding issues). The two new fix commits (e81a403 handle-rename correctness, 7170d8a parked-import resume) both check out except for one gap in the parked-upload resume logic — see inline comment. Everything else in the delta (the notification_id/titles/email/phone poll-scope additions in enrich-titles.ts and the long-running-tools/import-status template restorations) matches its commit description and looks correct.
| // the error, because the loop never reached it. | ||
| if (!opts.dryRun) { | ||
| const parkedFrom = | ||
| err instanceof ImportPhaseTimeout && err.phase === "preprocess" |
There was a problem hiding this comment.
This resume guard only fires for ImportPhaseTimeout("preprocess"), but pollPreprocess (called with no try/catch of its own inside completeUploadedChunk, before commitMappings) can also throw IMPORT_PREPROCESS_FAILED when the backend reports pre_processing.error, or a plain transport error mid-poll — neither is an ImportPhaseTimeout. In those cases the current chunk is genuinely uncommitted (the loop never reached commitMappings), but parkedFrom becomes inFlight + 1, which skips it. That chunk is then never handed to resumeParkedUpload and sits parked forever — the exact failure this commit sets out to close, just via a different preprocess-time error than a timeout. The new test (import-leads-parked-resume.test.ts) only exercises the timeout case, so this gap isn't caught.
…op promising one Same defect as the last commit, four surfaces further on. The rename gave `import_and_qualify` a singular `notification_id` in prose. It has never had one: its qualify phase is a per-lead /web_fetch fan-out, so the backend mints no qualification job — the NOTE on `notification_ids` in import-and-qualify.ts says so, and its outputSchema declares `lead_ids` and `lens_id` and no `notification_id` at all. Following the prose was not a no-op. `notification_ids[]` holds the FILE-IMPORT notifications, and handing one of those to `qualify_status` is rejected as QUALIFY_JOB_WRONG_KIND — so the documented way to resume an import_and_qualify was an error. `qualify_status`, `import_and_qualify` and the import-result render block now split the two launchers: bulk_qualify_leads resumes by `notification_id`, import_and_qualify by `lead_ids` + `lens_id`, and the import half by `import_ids`. `qualify_status`'s enrichment redirect also dropped the scope params — same false-completion as the enrich_titles fix, so it carries titles/email/phone now too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… too Its "not for the qualification half" line still sent the agent to qualify_status with a `notification_id` that import_and_qualify does not have, and its when-to-use called that tool's ids `importIds` when it returns `import_ids`. Both now name what each launcher actually hands back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
This diff only touches qualify-status error text, notification_id documentation (templates + generated file), and adds a new test — all correct and consistent with the schemas they describe. However, it doesn't touch import-leads.ts, so the outstanding issue from the previous review (parked-upload resume guard only covers ImportPhaseTimeout("preprocess"), missing IMPORT_PREPROCESS_FAILED/transport errors mid-preprocess-poll) is still unaddressed. Keeping changes-requested until that's fixed or explicitly deferred.
…he error Review on the previous commit, and it is right. `pollPreprocess` also throws IMPORT_PREPROCESS_FAILED when the backend reports a `pre_processing.error`, and a transport blip mid-poll throws neither that nor ImportPhaseTimeout. Both leave the chunk uncommitted, and both fell into the `inFlight + 1` branch that skips it — parked for ever, which is the failure the commit set out to close. The error type cannot answer this: the same transport blip one phase later happens AFTER the commit, and re-sending it would re-trigger processing. So `completeUploadedChunk` now signals the commit as it goes out, and the loop reads that flag instead of guessing. Two existing tests needed a change, against the standing "don't modify existing test files" rule, so calling it out: their `wait_for_completion:false` case scripts the whole background flow but never waits for it, so the detached half outlived the test and its calls landed in the NEXT test's mock. It leaked before this commit too; the broader recovery just made it leak one more request. Both now wait for the background to finish. No assertion changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Follow-up review on the diff since commit 2c33e6f (previous review at that commit requested changes over the parked-upload resume gap).
The outstanding issue is fixed correctly: completeUploadedChunk now takes an onCommitSent callback that fires the instant commitMappings resolves, and runImportInBackground tracks a committed flag per chunk instead of inferring commit state from the error type (ImportPhaseTimeout("preprocess")). This closes the gap the prior review flagged — a backend pre_processing.error or a transport blip mid-preprocess-poll now correctly parks the current chunk too, not just a budget timeout. The three new/updated tests in import-leads-parked-resume.test.ts exercise exactly the three relevant branches (timeout resume, non-timeout preprocess-failure resume, and no-resume-after-commit-sent) and I traced each mock sequence against the code — all check out.
Note: the two await new Promise(...3_000) additions to existing test files (import-and-qualify.test.ts, import-leads.test.ts) technically modify existing test files, against repo convention — but this is transparently disclosed in the commit message, fixes a real cross-test leak (a detached background task's calls landing in the next test's mock), and changes no assertions. Not blocking.
No new significant issues found. (Unable to invoke the review-dismissal endpoint in this environment — posting as COMMENT since no outstanding issues remain from the prior CHANGES_REQUESTED review.)
Closes leadbay/product#4005. Closes leadbay/product#4039.
Replaces #196, which was closed. Same goal, clean history — that branch was replayed onto a base rather than rebased and silently reverted two merged fixes while deleting their tests. This one is rebased on current
mainand verified against that specific failure.The problem, in plain terms
Three tools — qualify a batch of leads, import leads, import-and-qualify — fail on the Leadbay server that Claude and ChatGPT connect to. Nine customers hit it, 53 times.
leadbay_import_and_qualifyhas never once worked there.The cause: the MCP keeps its own file listing the jobs it has started, so it can hand the assistant a ticket to check on them later. That file is only ever created by the version people install on their own computer. The server version runs a different entry point and never creates it, so every job that needs a ticket fails instantly.
Why we delete the file instead of fixing it
The file exists because of a note in its own header: "while the Leadbay backend doesn't yet issue a real bulk_id." It does now, in two places:
GET /1.6/mcp/jobs/{id}GET /1.6/notificationsEvery job the MCP starts already receives one of these and already hands it to the assistant. Our file was a second, worse copy: an id we invented, no protection between customers, and it dies whenever the server restarts.
So the ticket becomes the Leadbay id. Nothing is stored. That also removes the whole question of where to store it — no disk to attach, no volume to configure, and none of the customer's imported rows sitting on our server.
Proven live, twice
Before writing any code, against staging: starting a job returns an id, that id is listable one second later with progress counts, and it resolves from three separate processes with fresh logins.
After, through the MCP server itself: start a job, then check on it from two brand-new server processes. Resolved both times. That is a server restart, live, through the real tools.
What changes for the assistant
enrich_titles,bulk_qualify_leadsnotification_id+ the lead idsimport_leads,import_and_qualifyimportIdsbulk_enrich_status,qualify_statusnotification_idimport_statusimportIds, which it already acceptedBreaking:
bulk_id,handle_idandqualify_idare gone as inputs, with no transition period. They were valid for minutes on a local install and never worked on the server, so there is nothing to transition.What is kept
A small in-memory guard, alive for five minutes, so that if the assistant fires the same request twice we do not charge the customer twice. It does not need to survive a restart: losing it costs at most one duplicate inside a five-minute window, which is the right trade for not owning a datastore.
This also closes product#4039. The earlier version of that guard had a real flaw, raised in review on #196: while a job was still being started, a second identical request was told "it's running" and handed a ticket that did not exist yet. On the server that is an unrecoverable answer —
importIdsis the only ticket, so an empty one means the rows are uploaded somewhere the assistant can never find. Fixed by making "still starting" an explicit state the callers have to read, rather than something they infer.Guarding the failure that caused #4005 in the first place
The original bug was the server being built without the file while every test passed.
packages/mcp/test/http-no-store-e2e.test.tsnow drives the real server on a real socket and checks that a second, separate request can resolve the id. There is no longer an option to leave out, so the two versions cannot drift apart again.Checked against how #196 went wrong
pnpm -r build,pnpm -r typecheck,pnpm -r test— 2,177 tests passing.Also
Eight tool descriptions, four prompts and three shared snippets rewritten.
qualify_statushad been telling the assistant its ticket was "persisted to~/.leadbay/bulks.jsonwith a 30-day TTL and survives MCP restart" — every clause of that was false.Merging this cuts no release
packages/mcp/package.jsonstays at0.34.0.auto-tag.ymlfires on a push tomain whose diff touches that one file, and from there tags, publishes to npm and
the MCP Registry, cuts a GitHub release and builds the image Argo deploys to
mcp.leadbay.app.
release.ymlandbuild-image.ymlhave no push-to-main triggerof their own, so leaving that file alone means merging this runs CI and nothing
else. Several PRs land before the next release and the batch gets tested as a
whole first.
Both CHANGELOGs keep their
## 0.35.0sections —release.ymlbuilds its notesby matching
## <version>inpackages/mcp/CHANGELOG.md, so the entry isalready where the release PR will look for it. That separate PR sets
package.json+server.jsonand re-titles the sections if the batch ships assomething other than 0.35.0. This is still a breaking change and should read as
one when it does ship.
Review fixes
Four findings, all of them this PR's own regressions.
long-running-toolsswappedqualify_idfornotification_idon bothtools at once, but async
import_and_qualifyreturnsimport_idsand nonotification_idat all. Each tool now names its own handle and its ownstatus tool.
enrich_titlestold the assistant to poll with{notification_id, lead_ids}.lead_idsis what switchesbulk_enrich_statusto the per-leadpath, and on that path
titles/email/phoneare what make "done" mean theroles and channel THIS run asked for. Without them every contact counts, so a
lead whose CFO was email-enriched months ago satisfies a fresh phone-only CEO
run and
all_doneis true on the first poll — enrichment reported as finishedbefore anything landed. All three instruction strings carry the scope now.
import_statuswas told it "does not re-reconcile records or returnrefreshed leads". It does, and its own
outputSchemasays so. Thestill_settling, thecommittingphase and the qualify-the-imported-leadsstep were deleted with that sentence; all three are back.
commitMappingsis the MCP's own POST, so a chunk abandoned before it sitsinert while
import_statusclassifies the row as complete — the user istold an import finished that committed nothing. The blocking path already
hands such an upload to
resumeParkedUpload; thewait_for_completion:falsepath only logged, and that path is the one the hosted server now runs for the
first time. Beyond the literal finding, the resume covers every un-committed
chunk rather than only the one that threw: chunks are 100 rows, so a larger
import leaves the later ones parked identically. A dry run is never resumed —
committing its mappings would turn a validation pass into a real import.
Sweeping the rest of the rewritten prose for the same class turned up more of it.
import_and_qualifyhas no qualificationnotification_id— its qualifyphase is a per-lead
/web_fetchfan-out, so the backend mints no job; itsnotification_ids[]are the FILE-IMPORT ones, and handing one of those toqualify_statusis rejected as the wrong kind. Four surfaces promised thatfield anyway. They now split the two launchers:
bulk_qualify_leadsresumes bynotification_id,import_and_qualifybylead_ids+lens_id, the importhalf by
import_ids.qualify_status's enrichment redirect had dropped thepoll scope too, so it carries
titles/email/phonenow.Review then caught the parked-import fix itself: keying the recovery off
ImportPhaseTimeout("preprocess")missed a backendpre_processing.errorand atransport blip mid-poll, both of which leave the chunk equally uncommitted. The
error type cannot answer that question — the same blip one phase later lands
after the commit, where re-sending it would re-trigger processing. So
completeUploadedChunksignals the commit as it goes out and the loop readsthat flag instead of guessing.
Three new test files, and two existing ones changed — flagged against the
standing rule:
import-leads.test.tsandimport-and-qualify.test.tseachscript a whole
wait_for_completion:falsebackground flow but never wait forit, so the detached half outlived the test and its calls landed in the next
test's mock. Both now wait. No assertion changed.
pnpm -r build,-r typecheck,-r testgreen — 2,253 tests.Not in this change
product#4032 and product#4033 land next, on top of this.
🤖 Generated with Claude Code