@leadbay/mcp 0.31.1 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/dist/bin.js +1078 -210
- package/dist/http-server.js +1054 -219
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,189 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
+
## 0.32.0 — 2026-09-01
|
|
4
|
+
|
|
5
|
+
A poll-budget timeout stops being an error (product#4007). The import wizard's
|
|
6
|
+
phases are bimodal — ~7s or ~85s, essentially nothing in between — and
|
|
7
|
+
`DEFAULT_PER_PHASE_BUDGET_MS` (60s) sat squarely in the gap, so the same input
|
|
8
|
+
randomly succeeded or threw. One reported session: 21 successful imports and 9
|
|
9
|
+
timeouts on a single spreadsheet, all nine timeouts issued from one user
|
|
10
|
+
instruction. The agent re-issued the identical call nine times over eleven
|
|
11
|
+
minutes, because an error is a thing you retry. Account details are in
|
|
12
|
+
product#4007; this file is published to npm, so they stay there.
|
|
13
|
+
|
|
14
|
+
- **`IMPORT_BUDGET_EXHAUSTED` → `IMPORT_TIMEOUT`.** No alias. "Budget" meant
|
|
15
|
+
milliseconds and every reader — customer, support, engineer — parsed it as
|
|
16
|
+
money. There is no import billing cap. Keeping the old string alive in
|
|
17
|
+
telemetry would preserve the exact confusion the rename exists to end.
|
|
18
|
+
- **The blocking path returns `{status:"running", timed_out:true, importIds}`
|
|
19
|
+
instead of throwing.** `ImportPhaseTimeout` is now an internal signal raised by
|
|
20
|
+
the three poll sites (preprocess / process / records-terminal) and converted to
|
|
21
|
+
a result in `execute`. A real backend error — `IMPORT_PREPROCESS_FAILED`,
|
|
22
|
+
`IMPORT_PROCESSING_FAILED` — still throws; degradation is timeout-only.
|
|
23
|
+
- **A preprocess timeout parks the import, so it gets a detached finisher.**
|
|
24
|
+
Found by probing us-staging rather than by reading the code: `update_mappings`
|
|
25
|
+
is the MCP's *own* POST, and it only fires after preprocess. Walk away before
|
|
26
|
+
sending it and the wizard row sits inert forever — `pre_processing.finished`
|
|
27
|
+
true, `processing` absent, `total_records` 0, and `GET /records` answering
|
|
28
|
+
`400 in_progress` indefinitely. `summarizeImports` reads that shape as
|
|
29
|
+
**complete**, so without this the fix would have converted a loud error into a
|
|
30
|
+
silent "running… complete… no leads", which is worse than what it replaced.
|
|
31
|
+
`resumeParkedUpload` hands the uploaded chunk to a detached continuation that
|
|
32
|
+
commits the mappings, so `status:"running"` is a true statement. Process and
|
|
33
|
+
reconcile timeouts need nothing — the backend is already working. Verified
|
|
34
|
+
live on us-staging 2026-09-01: forced timeout → running at t+0, three real
|
|
35
|
+
leadIds out of `leadbay_import_status(importIds)` at t+10s.
|
|
36
|
+
- **`IMPORT_NOT_TERMINAL` is retired.** Its hint said *"Retry
|
|
37
|
+
leadbay_import_leads with the same input in 30s"* — our own contract asking
|
|
38
|
+
for the loop. That path degrades like the other two now.
|
|
39
|
+
- **`leadbay_import_status` returns leads on the `importIds[]` path.** It used
|
|
40
|
+
to carry `result` only when resolving a `handle_id` against the BulkTracker,
|
|
41
|
+
so an import that timed out mid-poll could be observed as `complete` and still
|
|
42
|
+
leave the agent with no leadIds — whose only route to them was re-running the
|
|
43
|
+
whole import. It now reads `GET /imports/{id}/records` once every named import
|
|
44
|
+
is complete. `MCP_ROW_ID` round-trips through the synthesized CSV, so this
|
|
45
|
+
needs no client-side state — which is what makes it work on the hosted MCP,
|
|
46
|
+
where there is no BulkTracker at all (`http-server.ts` never passes one).
|
|
47
|
+
A records read that fails downgrades to status-only; it never turns a readable
|
|
48
|
+
status into an error.
|
|
49
|
+
- **A still-settling record is in neither bucket.** `reconcileOneChunk` can call
|
|
50
|
+
an unresolved record `internal_error` because it only runs after the records
|
|
51
|
+
settled; a status poll has no such guarantee. The status-side reconciler
|
|
52
|
+
counts them as `result.still_settling` instead. Calling a pending row "failed"
|
|
53
|
+
is what sends an agent back into the loop.
|
|
54
|
+
- **`notification_id` survives a timeout.** It was collected from
|
|
55
|
+
`runOneChunk`'s return value, so a call that timed out after `update_mappings`
|
|
56
|
+
threw away the very notification that would have told the agent the import
|
|
57
|
+
finished. Threaded out at mint time, mirroring `onImportId`.
|
|
58
|
+
- **`import_and_qualify` passes the running shape through** rather than throwing
|
|
59
|
+
`IMPORT_ASYNC_UNEXPECTED`, which would otherwise fire on exactly the incident
|
|
60
|
+
being fixed. Reuses the literal its `wait_for_completion:false` branch already
|
|
61
|
+
returns; `handle_id` is now optional on both.
|
|
62
|
+
- **Multi-chunk honesty:** a timeout on chunk 1 of 3 leaves chunks 2-3 unsent.
|
|
63
|
+
Those rows are reported as `rows_pending_upload`, and the description says
|
|
64
|
+
they DO need a fresh call for that subset — the one case where re-importing is
|
|
65
|
+
right.
|
|
66
|
+
- Shared record-reading primitives (`normalizeDomain`, `readCell`,
|
|
67
|
+
`PUBLIC_MAILBOX_DOMAINS`, `reconcileRecords`) moved to
|
|
68
|
+
`composite/_import-records.ts`. `import-leads.ts` re-exports `normalizeDomain`
|
|
69
|
+
for backward compatibility.
|
|
70
|
+
|
|
71
|
+
**Review round 2** — nine findings from the Codex and Claude reviewers, all
|
|
72
|
+
real, all fixed:
|
|
73
|
+
|
|
74
|
+
- **A dry run and an import parked mid-commit are byte-identical on the wire.**
|
|
75
|
+
Probed on us-staging: both answer `total_records: 0`,
|
|
76
|
+
`pre_processing.finished: true`, `processing` absent, `mappings` populated by
|
|
77
|
+
the backend's own AI hints, and `400 in_progress` on BOTH `/leads` and
|
|
78
|
+
`/records`. So the row cannot decide completion — the endpoints can.
|
|
79
|
+
`leadbay_import_status` now asks, and demotes a row that merely *looks*
|
|
80
|
+
finished to `status:"running"`, `phase:"committing"`. Without that, the window
|
|
81
|
+
between preprocess finishing and the detached finisher committing reported
|
|
82
|
+
`complete` with no leads and the agent stopped polling — the exact failure
|
|
83
|
+
this release exists to prevent.
|
|
84
|
+
- **`dry_run` is carried by the caller**, because nothing else can carry it.
|
|
85
|
+
The timed-out running result sets it; `leadbay_import_status` takes it as a
|
|
86
|
+
parameter. Absent it, a validation pass reads as `committing` rather than
|
|
87
|
+
risking a "N leads imported" render of an import that committed nothing.
|
|
88
|
+
- **`GET /imports/{id}/leads` is the source of truth for lead ids**, unioned
|
|
89
|
+
over the records-derived set, which now only annotates them with
|
|
90
|
+
rowId/domain/name. It is what `import_and_qualify` already trusts and it
|
|
91
|
+
includes leads the import created rather than matched; records-only would
|
|
92
|
+
drop those silently.
|
|
93
|
+
- **A snapshot short of the declared `total_records` reports the shortfall as
|
|
94
|
+
`still_settling`.** `processing.finished` can flip before every row is
|
|
95
|
+
exposed; counting only visible rows let a partial snapshot read as final.
|
|
96
|
+
- **A record carrying `lead.id` while still `MATCHING`/`IMPORTING` stays
|
|
97
|
+
pending.** The module's own `isRecordTerminal` says settled means `IMPORTED`
|
|
98
|
+
or `NO_MATCH`; the leads bucket now honours it instead of trusting a
|
|
99
|
+
lead id that the wizard may yet re-match.
|
|
100
|
+
- **`import_and_qualify` propagates `timed_out`, `rows_pending_upload`,
|
|
101
|
+
`dry_run` and the malformed rows.** Dropping them meant a >100-row batch
|
|
102
|
+
silently lost every unuploaded chunk and the agent had no cue to poll.
|
|
103
|
+
- **Malformed rows survive the degraded result.** They are rejected
|
|
104
|
+
client-side and never reach the backend, so `leadbay_import_status` could
|
|
105
|
+
never reconstruct them — dropping them let the caller read the batch as fully
|
|
106
|
+
accounted for.
|
|
107
|
+
- **No customer identity in this file.** It ships in the package's npm `files`
|
|
108
|
+
list; incident details belong in product#4007, not on npm.
|
|
109
|
+
|
|
110
|
+
**Review round 3** — seven more findings on the round-2 code, all real:
|
|
111
|
+
|
|
112
|
+
- **A transient `/leads` failure was being swallowed as "endpoint missing".**
|
|
113
|
+
Only a 404 is benign; a 500 or auth error means the canonical set is unknown,
|
|
114
|
+
and a records-only `result` would silently omit whatever `/leads` would have
|
|
115
|
+
added. Now only 404 falls back — and fixing it immediately surfaced a missing
|
|
116
|
+
`/leads` mock in the MCP E2E test that the old catch had been hiding.
|
|
117
|
+
- **The canonical merge keyed a map by `leadId`,** which collapsed the several
|
|
118
|
+
rows records-mode deliberately allows on one lead (separate contacts on one
|
|
119
|
+
company) and lost each row's `rowId`. Every reconciled row is kept; only ids
|
|
120
|
+
`/leads` knows and no record exposed are appended.
|
|
121
|
+
- **A canonical id could resurrect a non-terminal record** through the union,
|
|
122
|
+
undoing the terminal gate added in round 2. Ids belonging to a record that is
|
|
123
|
+
still MATCHING / IMPORTING are held back.
|
|
124
|
+
- **The settling deficit counted raw fetched rows,** so a re-paged duplicate
|
|
125
|
+
masked a genuine shortfall. Measured on distinct rows now.
|
|
126
|
+
- **The detached finisher inherited the caller's budget.** A caller who passed
|
|
127
|
+
a short `total_budget_ms` is exactly the caller most likely to time out;
|
|
128
|
+
giving the finisher that same window let it fail the one job it exists to do
|
|
129
|
+
and leave the import parked. It now carries its own 10-minute budget, does
|
|
130
|
+
only what it must — poll preprocess, commit the mappings — and is skipped
|
|
131
|
+
entirely for a dry run, which is *supposed* to stop after preprocess.
|
|
132
|
+
- **Two tool descriptions still said `leadbay_import_status` returns
|
|
133
|
+
status/progress only**, contradicting the recovery path this release adds.
|
|
134
|
+
- **`readCell` never looked at a cell's `field` name.** A records-mode import
|
|
135
|
+
that maps the header `Web` to LEAD_WEBSITE returns
|
|
136
|
+
`{column_name: "Web", field: "LEAD_WEBSITE"}`, and coalescing to the first
|
|
137
|
+
present name let `column_name` shadow `field` — so an unmatched row lost its
|
|
138
|
+
domain and rendered as "needs attention" instead of "pending crawl", without
|
|
139
|
+
the domain needed to retry. Pre-existing, but the stateless recovery path is
|
|
140
|
+
what made it bite.
|
|
141
|
+
- **`importIds` is deduped.** The same handle twice doubled the declared row
|
|
142
|
+
count against a record set that dedupes, pinning `still_settling` above zero
|
|
143
|
+
on an import that had entirely finished.
|
|
144
|
+
- **Records mode returns `row_ids`.** `MCP_ROW_ID` is a UUID minted inside the
|
|
145
|
+
tool; the caller has never seen it, yet `leadbay_import_status` reports
|
|
146
|
+
recovered leads keyed by it. A row identified only by `CRM_ID` — no website
|
|
147
|
+
to correlate on — was untraceable back to the leadId it produced.
|
|
148
|
+
- **A rejected mapping commit no longer polls for ever.** The detached
|
|
149
|
+
finisher sends `update_mappings` on the caller's behalf; if the backend
|
|
150
|
+
refuses it (an invalid mapping answers `400 missing LEAD_NAME field`), the
|
|
151
|
+
row it leaves is byte-identical to one still committing — no error field
|
|
152
|
+
anywhere. Before the timeout became a success result this surfaced as a plain
|
|
153
|
+
error, so staying silent would be a regression introduced by that very
|
|
154
|
+
change. The finisher records the rejection in a memory-only, best-effort
|
|
155
|
+
registry and `leadbay_import_status` reports `failed` with the backend's own
|
|
156
|
+
message; a restart just falls back to the old "still committing" reading.
|
|
157
|
+
- **A completed dry run says so.** Polling with `dry_run:true` returned
|
|
158
|
+
`complete` with no `result` and no discriminator, which the rendering
|
|
159
|
+
contract turns into "✓ Import complete" — a lie about an import that
|
|
160
|
+
committed nothing. The response now echoes `dry_run`.
|
|
161
|
+
- **A foreign `MCP_ROW_ID` column is not trusted as an identity.** A web-UI
|
|
162
|
+
import's own file may carry that header holding blanks or one repeated value;
|
|
163
|
+
treating those as our synthetic ids collapsed unrelated records onto one
|
|
164
|
+
dedupe key. Only a `randomUUID()`-shaped value counts.
|
|
165
|
+
- **A canonical id is only published when the snapshot is complete.**
|
|
166
|
+
`pendingLeadIds` can only speak for rows that were actually fetched, so while
|
|
167
|
+
rows are missing an unvouched id might belong to one of them and still be
|
|
168
|
+
MATCHING — downstream qualification would then act on a lead the wizard may
|
|
169
|
+
yet re-match. `row_ids` propagates through `import_and_qualify` too.
|
|
170
|
+
- **Record dedupe falls back to the backend record id.** `importIds` need not
|
|
171
|
+
name an MCP-created import; a web-UI one carries no `MCP_ROW_ID`, so keying
|
|
172
|
+
only on that let a re-paged row count twice while another went missing — and
|
|
173
|
+
a raw count matching `total_records` then read as a complete snapshot.
|
|
174
|
+
|
|
175
|
+
**Not shipped, deliberately:** the issue's criterion 6 asks to flip
|
|
176
|
+
`wait_for_completion` to default `false`. `http-server.ts:336` never passes a
|
|
177
|
+
`bulkTracker`, so on hosted that path throws `BULK_TRACKER_UNAVAILABLE` today
|
|
178
|
+
(product#4005) — the flip would break 100% of hosted imports. `BulkRecord` also
|
|
179
|
+
carries no org field, so a shared store on the multi-tenant hosted process would
|
|
180
|
+
make handles cross-tenant resolvable; that needs its own design. Parked behind
|
|
181
|
+
#4005. The degrade-to-running fix delivers the same benefit without it.
|
|
182
|
+
|
|
183
|
+
**Backend, not this repo:** the bimodal ~85s phase itself, and
|
|
184
|
+
`POST /leads/resolve`'s 81-95s slow mode (17 of 40 calls in the same session,
|
|
185
|
+
~24 minutes of wall clock). Issue criteria 1 and 2.
|
|
186
|
+
|
|
3
187
|
## 0.30.0 — 2026-08-19
|
|
4
188
|
|
|
5
189
|
Encode the **single-country rule** across every location-accepting surface
|