@leadbay/mcp 0.37.0 → 0.38.1

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/dist/bin.js CHANGED
@@ -434,7 +434,7 @@ var init_client = __esm({
434
434
  // Cancelled while queued — nothing was ever put on the wire, which is what
435
435
  // makes this safe to report as "not sent" even for a write.
436
436
  cancelledBeforeSendError() {
437
- return this.makeError("REQUEST_CANCELLED", "The request was cancelled before it was sent.", "Re-call the tool if you still want the result \u2014 nothing reached the API, so nothing was charged.");
437
+ return this.makeError("REQUEST_CANCELLED", "The request was cancelled before it was sent.", "Re-call the tool if you still want the result \u2014 nothing reached the API, so nothing ran.");
438
438
  }
439
439
  releaseSemaphore() {
440
440
  this.activeRequests--;
@@ -774,6 +774,11 @@ var init_client = __esm({
774
774
  if (status === 401) {
775
775
  return this.makeError("AUTH_EXPIRED", "Leadbay rejected this request (401)", retried ? "Tokens don't expire on a timer, so this isn't stale. Already auto-retried once and it 401'd again \u2014 usually a Leadbay-side hiccup, but can also mean the user logged out. Try again shortly, else report it." : "Tokens don't expire on a timer, so this isn't stale. This call wasn't auto-retried, so it's the first attempt \u2014 a Leadbay-side hiccup, or the user logged out. Try again once, else report it.", endpoint, null, status);
776
776
  }
777
+ const refusalCode = typeof parsed?.error === "string" ? parsed.error : parsed?.error?.code;
778
+ if (status === 429 && (refusalCode === "rate_limited" || refusalCode === "active_job_cap") && /^\/mcp\/(search|qualify)(\?|$)/.test(endpoint)) {
779
+ const wait = retryAfter ? `${retryAfter}s` : "a few minutes";
780
+ return this.makeError("QUOTA_EXCEEDED", refusalCode === "rate_limited" ? `Too many lead jobs started this hour \u2014 retry in ${wait}` : "Too many lead jobs running at once", (refusalCode === "rate_limited" ? `Leadbay starts at most 10 new lead jobs per org in a rolling hour. ` : `Leadbay runs at most 3 lead jobs per org at a time. `) + `This is a pacing limit, not credits: a top-up does not lift it, so do not offer one and do not talk about credits. Jobs already started keep running \u2014 read them with leadbay_lead_job_status meanwhile. ` + (refusalCode === "rate_limited" ? `Wait ${wait}, then retry the same call once. If it is refused again, the hour has not rolled over yet: wait longer rather than retrying in a loop.` : `Retry this call when one of the running jobs finishes.`), endpoint, retryAfter, status);
781
+ }
777
782
  if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
778
783
  const hintBase = retryAfter ? `Wait ${retryAfter}s before retrying` : "Wait, then retry";
779
784
  return this.makeError(
@@ -1599,7 +1604,7 @@ account resurfaced:
1599
1604
  `;
1600
1605
  leadbay_account_status = `## WHEN TO USE
1601
1606
 
1602
- Trigger phrases: "what's my account status", "how much quota do I have", "what lens am I on", "I topped up / I bought credits / I added credits".
1607
+ Trigger phrases: "what's my account status", "how much quota do I have", "what lens am I on", "I topped up / I bought credits / I added credits", "what version of Leadbay am I running".
1603
1608
 
1604
1609
  Do NOT use for: "show me leads" \u2192 \`leadbay_pull_leads\`.
1605
1610
 
@@ -1608,6 +1613,7 @@ Prefer when: meta question about account, quota, active lens, or top-up recovery
1608
1613
  Examples that SHOULD invoke this tool:
1609
1614
  - "What's my account status?"
1610
1615
  - "How much quota do I have left this week?"
1616
+ - "Which version of the Leadbay connector is this?"
1611
1617
 
1612
1618
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
1613
1619
  - "Show me today's leads."
@@ -1633,6 +1639,8 @@ Show the user's account state \u2014 admin rights, language, last-active lens, q
1633
1639
 
1634
1640
  **After a user tops up, do NOT keep refusing \u2014 RETRY.** If the user signals they topped up / bought credits / added credits, the previous QUOTA_EXCEEDED is invalidated the moment the Stripe webhook lands. RE-CALL \`leadbay_account_status\` to pick up the new state AND retry the originally failed call. The retry itself does not require a successful account_status check first \u2014 a topped-up user has cleared the throttle whether or not your cached snapshot reflects it yet. If the retry hits the wall again, only then re-offer top-up / wait. **A stale quota snapshot is never a reason to gate-keep a topped-up user.**
1635
1641
 
1642
+ **\`mcp_version\`** is the version of the Leadbay MCP server answering the call. When the user asks which Leadbay version they are running, answer with it.
1643
+
1636
1644
  **\`notifications\` block.** The response now includes a top-level \`notifications\` array listing background work the user (or agent) initiated that has since completed (\`bulk_enrich\`, \`bulk_qualify\`, \`import\`). These are signals to revise prior agent outputs the just-finished work might have made stale \u2014 they're NOT a pending-task list for the user. After revising (or confirming nothing is affected), call \`leadbay_acknowledge_notification(notification_id)\`. Full handling protocol below.
1637
1645
 
1638
1646
  ## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
@@ -1661,7 +1669,7 @@ Some Leadbay tool responses include a \`_meta.notifications\` array listing **ba
1661
1669
 
1662
1670
  - **Previous turn / before an MCP restart, and the user has NOT asked about it** \u2014 don't poll for it in the background. Simply continue the conversation; the next time you call any tool, the completed-work entry appears in \`_meta.notifications\` (also on \`leadbay_account_status.notifications\`). This is the ambient push path \u2014 leave it to do its job. **But if the user explicitly asks for status or to "wait for it to finish"** (e.g. a multi-turn flow where a job was launched in a prior turn and this turn says "wait for enrichment to finish, then \u2026"), DO poll its status tool now until done, exactly as for a this-turn job below \u2014 the ambient push only surfaces *completed* work, so it can't answer a live "is it done / wait for it" request while the job is still running.
1663
1671
  - **This turn (you just launched it)** \u2014 the DEFAULT is: do NOT end your turn on the "launched" ack; stay active and poll the job's status tool in a loop until it reports done, then report the finished result yourself, rather than spinning forever or deferring the result to a later turn. (Two exceptions, detailed below: the user explicitly asked NOT to wait / to run it in the background; or it's a large qualification/import that's async by design \u2014 in those cases hand back the handle instead of looping.) Each status tool has its OWN terminal signal \u2014 poll until:
1664
- - \`leadbay_bulk_enrich_status\` \u2192 \`all_done:true\` \u2014 OR \`overall_progress.done\` holds steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (some contacts are unresolvable, so \`all_done\` can stay false forever). Don't call a plateau from the first few back-to-back reads \u2014 early on \`done\` sits flat while the backend spins up. Once the plateau is real, report what resolved and name what didn't.
1672
+ - \`leadbay_bulk_enrich_status\` \u2192 \`all_done:true\` \u2014 OR \`overall_progress.done\` holds steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (a reservation the provider never answers stays \`done:false\`, so \`all_done\` can stay false forever). Don't call a plateau from the first few back-to-back reads \u2014 early on \`done\` sits flat while the backend spins up. Once the plateau is real, report what resolved and name what didn't.
1665
1673
  - \`leadbay_qualify_status\` \u2192 \`still_running\` is empty: every launched lead has finished or failed. (\`in_progress\` also reads \`false\` on the fast path, but it can be \`null\` on the legacy/fallback read \u2014 so treat an empty \`still_running\` as terminal on its own; only require \`in_progress:false\` when that field is actually present.) LIKE imports, large qualification runs are async by design: \`leadbay_bulk_qualify_leads\` defaults to \`wait_for_completion:false\` for \`count > 5\` or chained workflows because blocking can time out, and \`leadbay_qualify_status\` may take minutes/hours. So don't force a long polling loop on a big run \u2014 return the handle/progress and let completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked to wait, or it's a small run that finishes quickly. A small \`wait_for_completion:true\` run you can poll to \`still_running\` empty inline.
1666
1674
  - \`leadbay_import_status\` \u2192 \`status:"complete"\` (or \`"failed"\`). BUT imports are the exception to the stay-active loop: a large \`leadbay_import_leads({wait_for_completion:false})\` is meant to return a handle and resolve over minutes, and the tool does ONE refresh pass per call. Don't block the conversation looping on it \u2014 surface the returned progress/handle and let the completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked you to wait for the import, or it's a small import that finishes quickly.
1667
1675
 
@@ -1742,7 +1750,7 @@ days"), computed against now \u2014 mirroring the widget's "r\xE9initialis\xE9 d
1742
1750
  raw value is an ISO-8601 timestamp.
1743
1751
 
1744
1752
  **Top-up (optional, subordinate).** When \`quota.topup\` is present, you MAY add one
1745
- small line below the windows: \`Top-up: $<remaining_cents/100> of $<total_credit_cents/100> left\`.
1753
+ small line below the windows: \`Top-up: $<(remaining_cents / 100).toFixed(2)> of $<(total_credit_cents / 100).toFixed(2)> left\`.
1746
1754
  Keep it secondary \u2014 the three window gauges are the headline. Omit when null.
1747
1755
 
1748
1756
  **Legend** (once, below): \`\` \`\u25B0\` used \xB7 \`\u25B1\` remaining \`\`.
@@ -1974,7 +1982,7 @@ WHEN NOT TO USE: the user wants a plain data answer (route to leadbay_pull_leads
1974
1982
  `;
1975
1983
  leadbay_bulk_enrich_status = `Check status + per-lead contacts for a bulk enrichment you previously launched via leadbay_enrich_titles. Pass the \`notification_id\` for the job counters in one call, and/or the \`lead_ids\` + \`titles\` + \`email\` / \`phone\` the launch returned for per-lead progress. \`lead_ids\` alone is a valid call and is the reliable one: the job lookup is a scan of your recent notifications, so an archived job may not be found, and an enrichment notification does not always carry counters (then the tool answers \`ENRICH_JOB_NO_COUNTERS\` with the backend's running/finished flag and asks for \`lead_ids\`) \u2014 but the leads always answer. When \`include_contacts=true\` (opt-in), includes each contact's email/phone_number/job_title/enrichment.done.
1976
1984
 
1977
- WHEN TO USE: poll this REPEATEDLY after leadbay_enrich_titles returns a \`notification_id\`, staying active until the job is done \u2014 don't stop after one check, and don't hand the turn back to the user while progress is still climbing. "Done" = \`all_done:true\`, OR \`overall_progress.done\` has held steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (some contacts are unresolvable and never flip, so \`all_done\` can stay false forever \u2014 don't spin indefinitely). Do NOT declare a plateau from the first few back-to-back reads: right after launch, \`overall_progress.done\` can sit flat while the backend is still spinning the job up, so space your polls out and give it real elapsed time before treating a flat count as terminal. Also do NOT declare a plateau while the result carries \`partial_failures\` \u2014 a flat \`done\` there means a transient per-lead fetch error (e.g. a 429), NOT an unresolvable contact; keep polling (respecting any \`retry_after\`) or surface it as a temporary status failure, rather than reporting those leads as permanently unresolved. Default \`include_contacts=false\` for the cheap interim polls; set \`include_contacts=true\` on the read you report from to pull each lead's enriched contacts for the completion report.
1985
+ WHEN TO USE: poll this REPEATEDLY after leadbay_enrich_titles returns a \`notification_id\`, staying active until the job is done \u2014 don't stop after one check, and don't hand the turn back to the user while progress is still climbing. "Done" = \`all_done:true\`, OR \`overall_progress.done\` has held steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (a reservation the provider never answers stays \`done:false\`, so \`all_done\` can stay false forever \u2014 don't spin indefinitely; a contact with nothing found does flip, to \`done:true\` with \`credits_used:0\`). Do NOT declare a plateau from the first few back-to-back reads: right after launch, \`overall_progress.done\` can sit flat while the backend is still spinning the job up, so space your polls out and give it real elapsed time before treating a flat count as terminal. Also do NOT declare a plateau while the result carries \`partial_failures\` \u2014 a flat \`done\` there means a transient per-lead fetch error (e.g. a 429), NOT an unresolvable contact; keep polling (respecting any \`retry_after\`) or surface it as a temporary status failure, rather than reporting those leads as permanently unresolved. Default \`include_contacts=false\` for the cheap interim polls; set \`include_contacts=true\` on the read you report from to pull each lead's enriched contacts for the completion report.
1978
1986
 
1979
1987
  WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that already includes enriched contacts for a single lead.
1980
1988
 
@@ -2008,17 +2016,17 @@ a launcher only for a subset that never started, never for the whole batch:
2008
2016
  that are the ones that never started. Only then call
2009
2017
  \`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
2010
2018
  that is merely slow looks identical to one that never launched over a few
2011
- polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
2019
+ polls, and re-launching it uses the user's quota twice \u2014 when unsure, tell the user
2012
2020
  rather than guess.
2013
2021
 
2014
2022
 
2015
- ## QUOTA \u2014 show where the user stands after the spend
2023
+ ## QUOTA \u2014 show where the user stands after the run
2016
2024
 
2017
2025
  Enrichment consumes QUOTA (the per-window allowance), not a separate credit wall. Once the job is done (all_done, or a plateau \u2014 see WHEN TO USE), show the user their refreshed quota: call \`leadbay_account_status\` and render the per-window quota it returns (the canonical surface). The result's \`credits_remaining\` field is **advisory internal context only \u2014 do NOT display it**: it comes from \`billing.ai_credits\` (a consumed counter, not remaining), so printing \`_(N credits remaining)_\` can show a fresh/quota-backed account a false "0 remaining." Never render a credits balance; the \`leadbay_account_status\` quota gauge is the only place the user's standing is shown. Do NOT report a "credits used" figure for this run either: the per-contact cost can't be scoped to this specific enrichment (a lead's contact list mixes in earlier runs), so any "X used" number would be misleading. Do the account_status refresh ONCE at completion \u2014 not on every in-progress poll.
2018
2026
 
2019
2027
  ## COMPLETION REPORT \u2014 what to tell the user when the job is done
2020
2028
 
2021
- The result always carries \`overall_progress:{done,total,done_ratio}\` and, with \`include_contacts:true\`, \`leads[]\` each with contacts' \`email\` / \`phone_number\` / \`job_title\` / \`enrichment.done\`. \`bulk_progress:{total_count,success_count,failure_count,quota_hit_count}\` is present only when you passed a \`notification_id\` AND the job was found; derive counts from \`overall_progress\` rather than assuming \`bulk_progress\` is there. With \`lead_ids\`, each entry carries \`enrichment_progress:{done,total}\` \u2014 \`done\` counts only contacts whose REQUESTED channel has landed, scoped to the \`titles\` this run enriched, so a lead's pre-existing CFO email cannot inflate a CEO run. A contact counts as done only when the REQUESTED channel actually landed \u2014 for a phone run, \`enrichment.done:true\` with no \`phone_number\` is NOT done (the contact may have been email-enriched earlier); read \`email\` / \`phone_number\` against the requested channels, don't rely on the \`enrichment.done\` flag alone (\`overall_progress\` already accounts for this). \`include_contacts\` returns each lead's FULL contact list (it fans out through \`leadbay_get_contacts\`), so it can include contacts of other roles that were enriched in earlier runs \u2014 filter your report to the \`titles\` this bulk enriched (match each contact's \`job_title\`), don't attribute a pre-existing email of an unrelated role to this run. Report it yourself in the SAME turn, without a reprompt and without deferring to a scheduled re-check: name which of the just-enriched contacts now have emails / phones, the done/total counts, and \u2014 if \`bulk_progress\` is present \u2014 any \`quota_hit_count\` (if non-zero, say some contacts were skipped because the quota window was exhausted, and point to \`leadbay_account_status\` for the wait-or-top-up choice). If you stopped on a plateau (not \`all_done\`), say so plainly \u2014 report the resolved contacts and name the ones that didn't resolve, keyed to the requested channel and the returned fields (no \`email\` \u2192 "no email found"; no \`phone_number\` \u2192 "no phone number found") \u2014 rather than implying the job fully finished. Then show refreshed quota via \`leadbay_account_status\` (see QUOTA above); do NOT print a credits-remaining line.
2029
+ The result always carries \`overall_progress:{done,total,done_ratio}\` and, with \`include_contacts:true\`, \`leads[]\` each with contacts' \`email\` / \`phone_number\` / \`job_title\` / \`enrichment.done\`. \`bulk_progress:{total_count,success_count,failure_count,quota_hit_count}\` is present only when you passed a \`notification_id\` AND the job was found; derive counts from \`overall_progress\` rather than assuming \`bulk_progress\` is there. With \`lead_ids\`, each entry carries \`enrichment_progress:{done,total}\` \u2014 \`total\` counts the reservations this run made (scoped to the \`titles\` it enriched, and to reservations whose \`enrichment.email_requested\` / \`phone_requested\` match the \`email\` / \`phone\` you pass, so a lead's pre-existing CFO email cannot inflate a CEO run) and \`done\` counts those that have settled (\`enrichment.done:true\`, found or not). The \`enrichment\` record sits on the \`source:"paid"\` entry and never carries \`email\` / \`phone_number\`: the revealed values land on the same person's \`source:"org"\` entry (match on name and \`job_title\`). Read what landed from the org entries \u2014 an org entry with \`email\` = email found, with \`phone_number\` = phone found \u2014 and a settled paid entry with \`credits_used:0\` = nothing found for that person. \`include_contacts\` returns each lead's FULL contact list (it fans out through \`leadbay_get_contacts\`), so it can include contacts of other roles that were enriched in earlier runs \u2014 filter your report to the \`titles\` this bulk enriched (match each contact's \`job_title\`), don't attribute a pre-existing email of an unrelated role to this run. Report it yourself in the SAME turn, without a reprompt and without deferring to a scheduled re-check: name which of the just-enriched contacts now have emails / phones, the done/total counts, and \u2014 if \`bulk_progress\` is present \u2014 any \`quota_hit_count\` (if non-zero, say some contacts were skipped because the quota window was exhausted, and point to \`leadbay_account_status\` for the wait-or-top-up choice). If you stopped on a plateau (not \`all_done\`), say so plainly \u2014 report the resolved contacts and name the ones that didn't resolve, keyed to the requested channel and the returned fields (no \`email\` \u2192 "no email found"; no \`phone_number\` \u2192 "no phone number found") \u2014 rather than implying the job fully finished. Then show refreshed quota via \`leadbay_account_status\` (see QUOTA above); do NOT print a credits-remaining line.
2022
2030
  `;
2023
2031
  leadbay_bulk_qualify_leads = `Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch). Pass \`wait_for_completion:false\` to return quickly with \`{status:'running', notification_id}\`; poll leadbay_qualify_status with that id. With \`wait_for_completion\` omitted/true, the legacy behavior polls until the answers are populated or a budget is exhausted. Already-qualified leads (those with a non-null \`ai_agent_lead_score\`) are silently no-ops on the backend, so this composite paginates past them to find fresh candidates. On 429 mid-fanout, stops launching but keeps polling already-launched leads.
2024
2032
 
@@ -2035,7 +2043,7 @@ WHEN NOT TO USE: to qualify a single specific lead \u2014 that's leadbay_qualify
2035
2043
  Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
2036
2044
  \`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
2037
2045
  running result, that work is queued on Leadbay and runs to completion, and the
2038
- quota it costs is already committed. A discovery, preview or \`dry_run\` result
2046
+ quota it uses is already committed. A discovery, preview or \`dry_run\` result
2039
2047
  launched nothing and is not covered here.
2040
2048
 
2041
2049
  The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
@@ -2043,7 +2051,7 @@ waiting, never the job. \`cancelled: true\` means we stopped watching, not that
2043
2051
  work stopped. What to do next depends on what you are holding:
2044
2052
 
2045
2053
  - **A handle.** Poll the status tool with it, and do not launch the work that
2046
- handle covers a second time \u2014 that spends the quota again on the same rows.
2054
+ handle covers a second time \u2014 that uses the quota again on the same rows.
2047
2055
  \`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
2048
2056
  under that name. A qualification started by \`leadbay_import_and_qualify\` has no
2049
2057
  notification of its own: resume it with
@@ -2057,7 +2065,7 @@ work stopped. What to do next depends on what you are holding:
2057
2065
  finished. Calling the same tool again with the same arguments will usually hand
2058
2066
  back the job already launched rather than starting a second one, but that guard
2059
2067
  is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
2060
- are about to re-run before you spend the user's quota on it.
2068
+ are about to re-run before you use the user's quota on it.
2061
2069
 
2062
2070
 
2063
2071
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
@@ -2547,18 +2555,18 @@ WHEN NOT TO USE: for bulk enrichment by job title across many leads \u2014 use l
2547
2555
  ## A launched job cannot be stopped, and this tool has no retry guard
2548
2556
 
2549
2557
  Leadbay has no cancel. Once this call returns having actually launched, the work
2550
- is queued on Leadbay and runs to completion, and the quota it costs is already
2551
- committed. A \`dry_run\` result reached no backend and spent nothing. The user
2558
+ is queued on Leadbay and runs to completion, and the quota it uses is already
2559
+ committed. A \`dry_run\` result reached no backend and used nothing. The user
2552
2560
  cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
2553
2561
  never the job.
2554
2562
 
2555
2563
  Unlike the composite launchers, this tool has **no double-launch guard**: calling
2556
- it again always issues a new paid launch, even seconds later with identical
2564
+ it again always issues a new launch that uses quota again, even seconds later with identical
2557
2565
  arguments. So when a call returns nothing at all, do not simply retry. Read the
2558
2566
  record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
2559
2567
  lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
2560
2568
  see whether the launch already landed, and tell the user what you are about to
2561
- spend before spending it again.
2569
+ run before running it again.
2562
2570
 
2563
2571
 
2564
2572
  ## QUOTA, NOT CREDITS
@@ -2569,7 +2577,7 @@ Enrichment is gated by QUOTA (the per-window allowance in \`leadbay_account_stat
2569
2577
 
2570
2578
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
2571
2579
  `;
2572
- leadbay_enrich_titles = `Order contact enrichments by job title across many leads. Contacts are NOT returned by default with a lead (Leadbay keeps enrichment out-of-band to control cost); the agent requests them on demand via this tool when it's ready to actually reach out. Two modes: (A) NO \`titles\` param \u2014 returns the available titles + Leadbay's \`title_suggestions\` + \`auto_included_titles\` + a count of enrichable contacts, so the agent can ask the user which titles to enrich. (B) \`titles\` given \u2014 calls preview, then launches if there's anything enrichable. On 429 returns \`{status:'quota_exceeded'}\` cleanly. Selection lifecycle is wrapped in a try/finally so the user's selection is left clean even on error.
2580
+ leadbay_enrich_titles = `Order contact enrichments by job title across many leads. Contacts are NOT returned by default with a lead (Leadbay keeps enrichment out-of-band); the agent requests them on demand via this tool when it's ready to actually reach out. Two modes: (A) NO \`titles\` param \u2014 returns the available titles + Leadbay's \`title_suggestions\` + \`auto_included_titles\` + a count of enrichable contacts, so the agent can ask the user which titles to enrich. (B) \`titles\` given \u2014 calls preview, then launches if there's anything enrichable. On 429 returns \`{status:'quota_exceeded'}\` cleanly. Selection lifecycle is wrapped in a try/finally so the user's selection is left clean even on error.
2573
2581
 
2574
2582
  WHEN TO USE: as the agent's go-to enrichment entry point, immediately before proposing outreach.
2575
2583
 
@@ -2580,7 +2588,7 @@ WHEN NOT TO USE: to enrich a single named contact \u2014 that's leadbay_enrich_c
2580
2588
  Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
2581
2589
  \`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
2582
2590
  running result, that work is queued on Leadbay and runs to completion, and the
2583
- quota it costs is already committed. A discovery, preview or \`dry_run\` result
2591
+ quota it uses is already committed. A discovery, preview or \`dry_run\` result
2584
2592
  launched nothing and is not covered here.
2585
2593
 
2586
2594
  The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
@@ -2588,7 +2596,7 @@ waiting, never the job. \`cancelled: true\` means we stopped watching, not that
2588
2596
  work stopped. What to do next depends on what you are holding:
2589
2597
 
2590
2598
  - **A handle.** Poll the status tool with it, and do not launch the work that
2591
- handle covers a second time \u2014 that spends the quota again on the same rows.
2599
+ handle covers a second time \u2014 that uses the quota again on the same rows.
2592
2600
  \`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
2593
2601
  under that name. A qualification started by \`leadbay_import_and_qualify\` has no
2594
2602
  notification of its own: resume it with
@@ -2602,7 +2610,7 @@ work stopped. What to do next depends on what you are holding:
2602
2610
  finished. Calling the same tool again with the same arguments will usually hand
2603
2611
  back the job already launched rather than starting a second one, but that guard
2604
2612
  is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
2605
- are about to re-run before you spend the user's quota on it.
2613
+ are about to re-run before you use the user's quota on it.
2606
2614
 
2607
2615
 
2608
2616
  ## ENRICHMENT CONSUMES QUOTA \u2014 the model to reason with
@@ -2611,17 +2619,17 @@ Each email reveal and each phone reveal **consumes quota** (the per-window daily
2611
2619
 
2612
2620
  ## CONSENT \u2014 email is the default channel; phone is opt-in; never launch silently
2613
2621
 
2614
- The \`email\` channel defaults **ON**; \`phone\` defaults **OFF**. A bare "enrich these titles" is **NOT** consent to spend quota.
2622
+ The \`email\` channel defaults **ON**; \`phone\` defaults **OFF**. A bare "enrich these titles" is **NOT** consent to use quota.
2615
2623
 
2616
2624
  **When the user asks to enrich without naming channels, ASK which channels via \`ask_user_input_v0\`** \u2014 email is included by default, so the real question is whether to add phone: \`"Enrich email only, or email + phone? (phone reveals use more quota)"\` \u2192 \`["Email only", "Email + phone"]\`. Then launch with the chosen channels (\`email:true\` always; \`phone:true\` if they picked email + phone). Only skip this question if the user already named the channel(s) explicitly ("just emails", "get their phone numbers too", etc.).
2617
2625
 
2618
- **To preview with ZERO spend risk on ANY host, pass \`dry_run:true\`** (or \`confirm:false\`). Either returns \`enrichable_contacts\` + the \`would_launch\` channels and launches nothing \u2014 guaranteed, regardless of host. Use this as your first call. Then surface the volume, get the user's explicit go-ahead (and channel choice, above), and re-call with \`confirm:true\` (and explicit \`email:true\`/\`phone:true\` for the channels they chose) to launch.
2626
+ **To preview with nothing launched on ANY host, pass \`dry_run:true\`** (or \`confirm:false\`). Either returns \`enrichable_contacts\` + the \`would_launch\` channels and launches nothing \u2014 guaranteed, regardless of host. Use this as your first call. Then surface the volume, get the user's explicit go-ahead (and channel choice, above), and re-call with \`confirm:true\` (and explicit \`email:true\`/\`phone:true\` for the channels they chose) to launch.
2619
2627
 
2620
- Do NOT rely on a bare call (no \`confirm\`, no \`dry_run\`, no channels) as a "safe preview": on an elicitation-capable host it asks the user and withholds on decline (\`mode:"needs_confirmation"\`), but on a host WITHOUT elicitation (some direct/embedded callers) a bare call launches the default email spend directly. If you're unsure whether the host can elicit, use \`dry_run:true\`/\`confirm:false\` for the preview. Passing \`email:true\`/\`phone:true\` (or \`confirm:true\`) always counts as consent and launches.
2628
+ Do NOT rely on a bare call (no \`confirm\`, no \`dry_run\`, no channels) as a "safe preview": on an elicitation-capable host it asks the user and withholds on decline (\`mode:"needs_confirmation"\`), but on a host WITHOUT elicitation (some direct/embedded callers) a bare call launches the default email reveal directly. If you're unsure whether the host can elicit, use \`dry_run:true\`/\`confirm:false\` for the preview. Passing \`email:true\`/\`phone:true\` (or \`confirm:true\`) always counts as consent and launches.
2621
2629
 
2622
2630
  ## SHOW WHAT WILL RUN, AND WHERE QUOTA STANDS
2623
2631
 
2624
- **BEFORE (confirm before launching).** The discover / preview_only / dry_run modes return \`enrichable_contacts\` (the volume that would be enriched). Tell the user plainly: **"This will enrich {enrichable_contacts} contacts (email + phone reveals consume quota)."** then confirm the channels + go-ahead via \`ask_user_input_v0\` before launching. Do NOT quote an exact cost or a "credits" figure \u2014 the per-reveal rate is backend-side and enrichment is gated by quota, not a credit balance. The \`credits_remaining\` field is advisory context only; never present it as a spend gate and never refuse based on it.
2632
+ **BEFORE (confirm before launching).** The discover / preview_only / dry_run modes return \`enrichable_contacts\` (the volume that would be enriched). Tell the user plainly: **"This will enrich {enrichable_contacts} contacts (email + phone reveals consume quota)."** then confirm the channels + go-ahead via \`ask_user_input_v0\` before launching. Do NOT quote an exact cost or a "credits" figure \u2014 the per-reveal rate is backend-side and enrichment is gated by quota, not a credit balance. The \`credits_remaining\` field is advisory context only; never present it as a gate and never refuse based on it.
2625
2633
 
2626
2634
  **AFTER (show refreshed quota).** Once the job is done (see the STAY ACTIVE section below), call \`leadbay_account_status\` and show the refreshed per-window quota \u2014 the canonical surface \u2014 so the user sees the usage they just consumed. Do NOT invent a "credits used" figure for the run (per-run cost can't be scoped reliably \u2014 a lead's contacts mix earlier enrichments).
2627
2635
 
@@ -2629,7 +2637,7 @@ Do NOT rely on a bare call (no \`confirm\`, no \`dry_run\`, no channels) as a "s
2629
2637
 
2630
2638
  When a launch returns \`mode:"launched"\` with a \`notification_id\`, the enrichment runs ASYNC on the backend \u2014 the tool returns immediately, before any email/phone is attached. **Unless the user explicitly said to start it in the background / not to wait** (e.g. "kick it off, I'll check later", "don't wait for it"), stay active and report in-turn \u2014 do NOT end your turn on the ack, and do NOT say "I'll let you know when it's done." (If the user DID ask you not to wait, honor that: hand back the \`notification_id\` and a one-line "running \u2014 you can ask any time". Only promise that completion will auto-surface via \`_meta.notifications\` when the launch returned a non-null \`notification_id\`; if \`notification_id\` is null (the nullable-backend path), say instead that you'll re-check when asked / they should ask again later \u2014 nothing surfaces automatically without a notification id. Don't force a poll loop against explicit intent.) In the default (stay-active) case: call \`leadbay_bulk_enrich_status({notification_id})\` in a loop, re-polling until the job is done (small batches typically finish in under ~2 min). Pass \`include_contacts:true\` on the read you intend to report from, so you get each lead's enriched contacts back. Note that \`include_contacts\` returns each lead's FULL contact list (it fans out through \`leadbay_get_contacts\`), which can include contacts of OTHER roles that were already enriched in earlier runs \u2014 so **filter your report to the \`titles\` you just enriched** (match each contact's \`job_title\` to the requested titles). Don't present a pre-existing CFO/Sales email as part of this CEO/Owner/Manager run. Then \u2014 on your own, without waiting for the user to reprompt \u2014 report the enrichment: which of the just-enriched contacts now have emails / phones, and the counts from \`overall_progress\` (\`done\`/\`total\`). \`leadbay_bulk_enrich_status\` also returns \`bulk_progress.success_count\` / \`failure_count\` / \`quota_hit_count\` on the notification fast path \u2014 use those when present, but the per-lead path returns \`overall_progress\` only, so don't assume \`bulk_progress\` exists (see the status tool's COMPLETION REPORT). Then show refreshed quota via \`leadbay_account_status\` (see AFTER above).
2631
2639
 
2632
- **"Done" = \`all_done:true\` OR the resolvable work has plateaued.** Keep polling while \`overall_progress.done\` is still climbing. But \`total\` counts every matching contact, and some (unresolvable titles, contacts with no findable email) never flip to done \u2014 so a job can sit below 100% with \`all_done:false\` forever. A plateau is only real once the job has had time to run: do NOT declare it from the first few back-to-back reads (early on \`done\` can sit at its initial value while the backend is still spinning the job up). Give it at least ~90s\u20132 min of actual elapsed polling \u2014 space your polls out (~15\u201330s apart) rather than firing them back-to-back \u2014 and only treat the set as complete when \`overall_progress.done\` has held steady across several spaced polls over that window. Then stop polling and report what resolved, naming the ones that didn't. Key the "didn't resolve" wording off the channels the user actually requested and the returned contact fields (contacts carry \`email\` and \`phone_number\`) \u2014 a contact enriched for phone that came back with no \`phone_number\` is "no phone number found", one with no \`email\` is "no email found", email+phone that got neither is "no contact details found"; if \`quota_hit_count\` is non-zero say those were skipped because the quota window was exhausted. Do NOT hard-label every non-success as "no email found" when phone was requested. Do NOT spin indefinitely waiting for \`all_done\` on contacts the engine won't resolve, and do NOT \`ScheduleWakeup\` / defer the finished list to a later turn \u2014 deliver the resolved results in THIS reply.
2640
+ **"Done" = \`all_done:true\` OR the resolvable work has plateaued.** Keep polling while \`overall_progress.done\` is still climbing. But a reservation the provider never answers stays \`done:false\` \u2014 so a job can sit below 100% with \`all_done:false\` forever (a contact with no findable email does flip, to \`done:true\` with \`credits_used:0\`). A plateau is only real once the job has had time to run: do NOT declare it from the first few back-to-back reads (early on \`done\` can sit at its initial value while the backend is still spinning the job up). Give it at least ~90s\u20132 min of actual elapsed polling \u2014 space your polls out (~15\u201330s apart) rather than firing them back-to-back \u2014 and only treat the set as complete when \`overall_progress.done\` has held steady across several spaced polls over that window. Then stop polling and report what resolved, naming the ones that didn't. Key the "didn't resolve" wording off the channels the user actually requested and the returned contact fields (contacts carry \`email\` and \`phone_number\`) \u2014 a contact enriched for phone that came back with no \`phone_number\` is "no phone number found", one with no \`email\` is "no email found", email+phone that got neither is "no contact details found"; if \`quota_hit_count\` is non-zero say those were skipped because the quota window was exhausted. Do NOT hard-label every non-success as "no email found" when phone was requested. Do NOT spin indefinitely waiting for \`all_done\` on contacts the engine won't resolve, and do NOT \`ScheduleWakeup\` / defer the finished list to a later turn \u2014 deliver the resolved results in THIS reply.
2633
2641
 
2634
2642
  The \`_meta.notifications\` push is the FALLBACK for a job launched in an EARLIER turn or across an MCP restart \u2014 it is NOT a reason to stop early on a job you just launched this turn.
2635
2643
 
@@ -2771,8 +2779,8 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
2771
2779
 
2772
2780
  3-col table of delivered leads in returned order: col 1 = 10-segment fit
2773
2781
  bar + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3
2774
- = contact + purchased channels. ALWAYS close with the honest funnel line
2775
- (matched/examined/delivered/stop reason/spend) \u2014 especially on 0
2782
+ = contact + found channels. ALWAYS close with the honest funnel line
2783
+ (matched/examined/delivered/stop reason) \u2014 especially on 0
2776
2784
  delivered. Full algorithm below.
2777
2785
 
2778
2786
  ---
@@ -2780,19 +2788,19 @@ delivered. Full algorithm below.
2780
2788
  Submit a net-new lead search: the backend matches an ICP seed against the full
2781
2789
  company universe, applies hard filters, skips what the org already knows
2782
2790
  (\`novelty: org\`), optionally qualifies against the org's own intelligence
2783
- (questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally buys
2791
+ (questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally reveals
2784
2792
  contact channels. Polls up to \`wait_seconds\` (default 45); a longer job returns
2785
2793
  \`still_running\` + \`next_poll\` \u2014 hand off to \`leadbay_lead_job_status\`. Jobs run
2786
2794
  \u226430 min, results kept 30 days.
2787
2795
 
2788
- **Free vs paid \u2014 never spend silently.** Default (\`qualify: false\`,
2796
+ **Free vs usage quota \u2014 never use quota silently.** Default (\`qualify: false\`,
2789
2797
  \`channels: []\`) is FREE: company profile + fit score + cached research +
2790
- contact identity. Paid: \`qualify: true\` (~94 cost_cents per candidate
2791
- EXAMINED, capped by \`exploration_cap\`/\`max_cost\`) and \`channels\` (email 25c /
2792
- phone 250c, success-only). Enforced in code: a paid call is WITHHELD unless it
2798
+ contact identity. \`qualify: true\` (per candidate EXAMINED, capped by
2799
+ \`exploration_cap\`/\`max_cost\`) and \`channels\` (only when a value is found) draw
2800
+ on the org's usage quota; nothing is invoiced. Enforced in code: such a call is WITHHELD unless it
2793
2801
  carries \`confirm: true\` \u2014 nothing is submitted and you get
2794
2802
  \`mode: "needs_confirmation"\` with a real quote to show the user. Re-call with
2795
- \`confirm: true\` on their go-ahead ("spend / get their emails" counts).
2803
+ \`confirm: true\` on their go-ahead ("go ahead / get their emails" counts).
2796
2804
  \`confirm: false\` vetoes. Free needs no consent. **Preview free first** \u2014
2797
2805
  reshaping an off-profile seed is free, exploring it with \`qualify: true\` is
2798
2806
  not.
@@ -2800,7 +2808,7 @@ not.
2800
2808
  **Ad-hoc exclusions ("no chains") are enforced by NO tier** \u2014 \`filters\` has no
2801
2809
  exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP,
2802
2810
  which need not mention chains; the seed's inverse only shifts ranking.
2803
- Violators can survive, be paid for and be delivered \u2014 post-filter them yourself
2811
+ Violators can survive, use quota and be delivered \u2014 post-filter them yourself
2804
2812
  and say the tier didn't enforce it. Durable enforcement \u2192
2805
2813
  \`leadbay_refine_prompt\`.
2806
2814
 
@@ -2847,9 +2855,9 @@ measured:
2847
2855
 
2848
2856
  **Parameter notes**
2849
2857
  - \`request_id\` (REQUIRED) is the retry contract: SAME value retrying the same
2850
- ask (same live job, no double spend); NEW for a changed ask. Derive from ask
2858
+ ask (same live job, no double launch); NEW for a changed ask. Derive from ask
2851
2859
  + archetype + date: \`gyms-dallas-2026-07-28\`.
2852
- - Never lower \`min_ai_score\` together with \`channels\` \u2014 that buys emails for
2860
+ - Never lower \`min_ai_score\` together with \`channels\` \u2014 that reveals emails for
2853
2861
  leads the AI just scored as junk.
2854
2862
  - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014
2855
2863
  wait, don't hammer).
@@ -2889,7 +2897,7 @@ when nothing was delivered.
2889
2897
  **Column 3 \u2014 Contact**
2890
2898
 
2891
2899
  - \`[Name](linkedin) \xB7 role\` (linked name mandatory when a LinkedIn URL
2892
- exists; plain name otherwise). Below it, the PURCHASED channels only:
2900
+ exists; plain name otherwise). Below it, the FOUND channels only:
2893
2901
  \`\u2709 value\` / \`\u260E value\` inline as plain text (they auto-linkify).
2894
2902
  - Channel statuses: \`delivered\` \u2192 show value; \`already_owned\` \u2192 value +
2895
2903
  *(already yours)*; \`masked\` \u2192 "on file \u2014 reveal via channels";
@@ -2900,16 +2908,17 @@ when nothing was delivered.
2900
2908
 
2901
2909
  **The funnel line (mandatory, after the table):**
2902
2910
 
2903
- One short line narrating the delivery honestly, from \`funnel\` + \`cost\` +
2911
+ One short line narrating the delivery honestly, from \`funnel\` +
2904
2912
  \`explain.scope_notes\`:
2905
2913
 
2906
2914
  > Matched N \xB7 examined E \xB7 qualified Q \xB7 disqualified D \u2192 **delivered X of
2907
- > the Y asked** \xB7 stopped: <stop_reason in plain words> \xB7 spent C.CC.
2915
+ > the Y asked** \xB7 stopped: <stop_reason in plain words>.
2908
2916
 
2909
- **Money: divide, then symbol.** Every amount (\`cost.spent\`,
2910
- \`estimated_cost.max\`, quotes) is \`cost_cents\` \u2014 divide by 100, two decimals,
2911
- so \`165\` renders \`1.65\`, NEVER \`165.00\`. Symbol from the account region: US
2912
- \`$\`, France \`\u20AC\`, unknown \u2192 bare. Never hard-code \`$\`: it misstates a charge.
2917
+ **No money, anywhere.** \`cost.*\`, \`estimated_cost.max\` and quotes are internal
2918
+ usage units. Never render them, never convert them to a currency, never call
2919
+ them a charge: the user's plan or top-up covers this work, and a price reads as
2920
+ a bill. If the user asks what a job used, show \`leadbay_account_status\`'s quota
2921
+ windows.
2913
2922
 
2914
2923
  "of the Y asked" needs \`summary.items_requested\`, which submits carry but a
2915
2924
  later \`leadbay_lead_job_status\` snapshot does not. Without it write **delivered
@@ -2917,7 +2926,7 @@ X** and stop \u2014 never back-fill Y from \`matched\`/\`examined\` (they count
2917
2926
  candidates), never guess it.
2918
2927
 
2919
2928
  Plain-word stop reasons: \`target_reached\` \u2192 omit (success), \`pool_exhausted\` \u2192
2920
- "ran out of matching candidates", \`max_cost\` \u2192 "hit the cost cap", \`quota\` \u2192
2929
+ "ran out of matching candidates", \`max_cost\` \u2192 "hit the job's usage cap", \`quota\` \u2192
2921
2930
  "hit an org quota", \`time_budget\` \u2192 "hit the 30-min time budget".
2922
2931
 
2923
2932
  **When \`delivered\` is 0**: NEVER say just "no results". Render no table; give
@@ -2942,7 +2951,7 @@ table \`Ref \u2192 Outcome\` translating \`status_reason\` to plain words:
2942
2951
  \`low_confidence_identity\` \u2192 "couldn't safely match \u2014 check \`resolution.alternatives\`",
2943
2952
  \`no_matching_contact\` \u2192 "no contact with the requested title",
2944
2953
  \`disqualified\` \u2192 "evaluated: does not fit" (evidence is in the item when owned),
2945
- \`enrichment_failed\` \u2192 "channel could not be sourced (not billed)".
2954
+ \`enrichment_failed\` \u2192 "channel could not be sourced (no quota used)".
2946
2955
 
2947
2956
  **\`items_truncated\`**: rows are a PREFIX, not the batch. Say so, and offer
2948
2957
  \`leadbay_lead_job_status(job_id, since: next_since)\` for the rest.
@@ -2991,10 +3000,10 @@ Pick the 2-3 options that match what actually happened \u2014 never all seven:
2991
3000
  | Observation | Suggest | Calls |
2992
3001
  |---|---|---|
2993
3002
  | Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds: 60) |
2994
- | Free run delivered on-profile leads | "Qualify these N against your criteria (paid \u2014 \`dry_run\` first)" | leadbay_qualify_leads(prior_deliveries: {job_id}) |
3003
+ | Free run delivered on-profile leads | "Qualify these N against your criteria (uses quota \u2014 \`dry_run\` first)" | leadbay_qualify_leads(prior_deliveries: {job_id}) |
2995
3004
  | Delivered leads look right | "Draft outreach for the top ones" | leadbay_prepare_outreach |
2996
3005
  | Delivered 0 or off-profile | "Reshape the example and retry" (name the fix from funnel + scope_notes) | leadbay_find_new_leads (NEW request_id) |
2997
- | Stopped at cost cap (\`stop_reason: max_cost\`) | "Raise the cap to X and get the remaining N" \u2014 X in the account's currency per the funnel-line rule, never a hard-coded \`$\` | leadbay_find_new_leads, NEW request_id (same-id only dedupes onto a LIVE job) + higher max_cost + \`count\` = the SHORTFALL (\`items_requested\` \u2212 delivered), not the original + \`exclude_lead_ids\` = the examined-but-REJECTED ids (novelty covers delivered; these are what it misses \u2014 without them the rerun re-buys the same losers) |
3006
+ | Stopped at the job's usage cap (\`stop_reason: max_cost\`) | "Raise the job's cap and get the remaining N" \u2014 no amount, no currency | leadbay_find_new_leads, NEW request_id (same-id only dedupes onto a LIVE job) + higher max_cost + \`count\` = the SHORTFALL (\`items_requested\` \u2212 delivered), not the original + \`exclude_lead_ids\` = the examined-but-REJECTED ids (novelty covers delivered; these are what it misses \u2014 without them the rerun re-buys the same losers) |
2998
3007
  | Stopped on org quota (\`stop_reason: quota\`) | "Check which window is exhausted and when it resets" \u2014 never a re-run: it cannot clear an org quota and burns a submit slot to stop in the same place | leadbay_account_status |
2999
3008
  | Stopped on org quota and the user does not want to wait | "Top up to finish this run" | leadbay_create_topup_link |
3000
3009
  | User wants these tracked in Leadbay | "Add the keepers to a campaign" | leadbay_create_campaign / leadbay_add_leads_to_campaign |
@@ -3484,7 +3493,7 @@ Per step: \`gate_label\` / \`gate_description\` are the widget's forward option,
3484
3493
  | 1 | Check my account | \`leadbay_account_status\` (no args) |
3485
3494
  | 2 | Pull today's leads | \`leadbay_pull_leads\` (no args) |
3486
3495
  | 3 | Draft the first email | \`leadbay_prepare_outreach\` \u2014 \`leadId\` ONLY, never \`enrich\` |
3487
- | 4 | Find who to email | \`leadbay_enrich_titles\` \u2014 free preview, then a consented paid reveal |
3496
+ | 4 | Find who to email | \`leadbay_enrich_titles\` \u2014 free preview, then a reveal on consent |
3488
3497
 
3489
3498
  Steps 1, 2 and 3 carry \`branches[]\`, and steps 3 and 4 carry \`spend\` (+ \`quota_note\` on 4). Every step also carries \`explain\` (say this BEFORE firing) and \`next_steps\` (\`{question, options[]}\` \u2014 already the widget's shape, map it verbatim).
3490
3499
 
@@ -3496,9 +3505,9 @@ The manifest also carries **\`keep_going\`**: the closing cheat-sheet of *what y
3496
3505
 
3497
3506
  **Step 1 shows the real account, and is silent about two things.** The click is labelled *check my account status*, so deliver it: user + org, then the **full quota windows** the way the web app renders them \u2014 Daily / Weekly / Monthly with a \`\u25B0\u25B1\` gauge, % used, $ spent against the cap, resets countdown, and the per-resource breakdown. Never raw "credits". But apply the silence gate first: when \`quota\` is null, \`quota_error\` is set, or the org has \`unlimited_credits\`, say **nothing** about quota \u2014 never mention a 401, never suggest logging in again (the token is fine, the same response just read their account), and never announce "unlimited". And **never volunteer the lens**: the response withholds it unless the user asked, so there is nothing to report and no other tool to reach for. Both are pinned regressions (WORKFLOWS #30 / #31).
3498
3507
 
3499
- **Step 3 drafts, and spends nothing.** Call \`leadbay_prepare_outreach\` with \`leadId\` alone \u2014 **never \`enrich: true\`**, which launches a paid contact reveal off the back of a *draft* click. \`recommended_contact\` returns with \`email\`/\`phone\` null; that is expected, and it is the hook for step 4. Render through \`message_compose_v1\` (2\u20133 strategy-labelled variants), address it to the job TITLE \u2014 no name exists yet, and inventing one is fabrication \u2014 and never send it or offer to.
3508
+ **Step 3 drafts, and uses no quota.** Call \`leadbay_prepare_outreach\` with \`leadId\` alone \u2014 **never \`enrich: true\`**, which launches a contact reveal off the back of a *draft* click. \`recommended_contact\` returns with \`email\`/\`phone\` null; that is expected, and it is the hook for step 4. Render through \`message_compose_v1\` (2\u20133 strategy-labelled variants), address it to the job TITLE \u2014 no name exists yet, and inventing one is fabrication \u2014 and never send it or offer to.
3500
3509
 
3501
- **Step 4 runs in two beats \u2014 free first, paid only on consent.** Scoped to the ONE lead step 3 drafted for. Beat 1 omits \`titles\` and returns \`mode:"discover"\`, the free list of job titles at that company; say plainly that nothing has been spent. Beat 2 names the title the draft is addressed to, states the cost BEFORE they decide (one contact, one credit), and only on confirmation calls again with \`titles\` + \`confirm:true\` + \`email:true\` \u2014 polled via \`leadbay_bulk_enrich_status\` until done, reporting only what actually resolved. The gate click bought the free look, not the reveal: never launch without an explicit confirm.
3510
+ **Step 4 runs in two beats \u2014 free first, the reveal only on consent.** Scoped to the ONE lead step 3 drafted for. Beat 1 omits \`titles\` and returns \`mode:"discover"\`, the free list of job titles at that company; say plainly that nothing has run yet. Beat 2 names the title the draft is addressed to, says BEFORE they decide that revealing one contact uses a little of their plan's quota (no amount, no price), and only on confirmation calls again with \`titles\` + \`confirm:true\` + \`email:true\` \u2014 polled via \`leadbay_bulk_enrich_status\` until done, reporting only what actually resolved. The gate click bought the free look, not the reveal: never launch without an explicit confirm.
3502
3511
 
3503
3512
  ## Empty first batch is normal, not an error
3504
3513
 
@@ -3535,7 +3544,7 @@ WHEN NOT TO USE: discovery (use leadbay_pull_leads); single-lead deep dive (use
3535
3544
  Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
3536
3545
  \`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
3537
3546
  running result, that work is queued on Leadbay and runs to completion, and the
3538
- quota it costs is already committed. A discovery, preview or \`dry_run\` result
3547
+ quota it uses is already committed. A discovery, preview or \`dry_run\` result
3539
3548
  launched nothing and is not covered here.
3540
3549
 
3541
3550
  The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
@@ -3543,7 +3552,7 @@ waiting, never the job. \`cancelled: true\` means we stopped watching, not that
3543
3552
  work stopped. What to do next depends on what you are holding:
3544
3553
 
3545
3554
  - **A handle.** Poll the status tool with it, and do not launch the work that
3546
- handle covers a second time \u2014 that spends the quota again on the same rows.
3555
+ handle covers a second time \u2014 that uses the quota again on the same rows.
3547
3556
  \`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
3548
3557
  under that name. A qualification started by \`leadbay_import_and_qualify\` has no
3549
3558
  notification of its own: resume it with
@@ -3557,7 +3566,7 @@ work stopped. What to do next depends on what you are holding:
3557
3566
  finished. Calling the same tool again with the same arguments will usually hand
3558
3567
  back the job already launched rather than starting a second one, but that guard
3559
3568
  is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
3560
- are about to re-run before you spend the user's quota on it.
3569
+ are about to re-run before you use the user's quota on it.
3561
3570
 
3562
3571
 
3563
3572
  Budgets: \`total_budget_ms\` caps wall-clock; \`per_lead_budget_ms\` caps each lead's poll. For short transport timeouts, pass \`wait_for_completion:false\` and poll \`leadbay_import_status\`. Outputs \`qualified[]\`, \`still_running[]\`, \`not_imported[]\`, plus the ids that resume it: \`lead_ids\` + \`lens_id\` for leadbay_qualify_status, \`import_ids\` for leadbay_import_status. There is no qualification \`notification_id\` \u2014 the qualify phase runs per-lead, so no job notification exists; \`notification_ids[]\` are the file-import ones. Idempotent within a 5-min window. \`dry_run:'preview'\` returns mapping hints + custom-field candidates without importing.
@@ -3780,7 +3789,7 @@ a launcher only for a subset that never started, never for the whole batch:
3780
3789
  that are the ones that never started. Only then call
3781
3790
  \`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
3782
3791
  that is merely slow looks identical to one that never launched over a few
3783
- polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
3792
+ polls, and re-launching it uses the user's quota twice \u2014 when unsure, tell the user
3784
3793
  rather than guess.
3785
3794
 
3786
3795
 
@@ -3847,18 +3856,18 @@ WHEN NOT TO USE: from agent flow \u2014 leadbay_enrich_titles handles selection
3847
3856
  ## A launched job cannot be stopped, and this tool has no retry guard
3848
3857
 
3849
3858
  Leadbay has no cancel. Once this call returns having actually launched, the work
3850
- is queued on Leadbay and runs to completion, and the quota it costs is already
3851
- committed. A \`dry_run\` result reached no backend and spent nothing. The user
3859
+ is queued on Leadbay and runs to completion, and the quota it uses is already
3860
+ committed. A \`dry_run\` result reached no backend and used nothing. The user
3852
3861
  cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
3853
3862
  never the job.
3854
3863
 
3855
3864
  Unlike the composite launchers, this tool has **no double-launch guard**: calling
3856
- it again always issues a new paid launch, even seconds later with identical
3865
+ it again always issues a new launch that uses quota again, even seconds later with identical
3857
3866
  arguments. So when a call returns nothing at all, do not simply retry. Read the
3858
3867
  record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
3859
3868
  lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
3860
3869
  see whether the launch already landed, and tell the user what you are about to
3861
- spend before spending it again.
3870
+ run before running it again.
3862
3871
 
3863
3872
 
3864
3873
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
@@ -3883,14 +3892,16 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
3883
3892
 
3884
3893
  Terminal job -> render the full delivery per the lead-delivery table +
3885
3894
  honest funnel line. Still running -> one progress line (examined /
3886
- delivered / spent so far) and offer to check again in ~1 min. Never
3895
+ delivered so far) and offer to check again in ~1 min. Never
3887
3896
  render UUIDs or cursors.
3888
3897
 
3889
3898
  ---
3890
3899
 
3891
3900
  Cumulative snapshot of a lead-delivery job: state, funnel counters, every
3892
- item emitted so far (full lead payloads for delivered/degraded, honest
3893
- status_reason for skipped), spend + breakdown, and the \`explain\` block
3901
+ item emitted so far (compact lead payloads for delivered/degraded, with
3902
+ one reason and the freshest signals; full evidence via
3903
+ \`leadbay_research_lead_by_id\`; honest status_reason for skipped), internal
3904
+ usage counters (never render them), and the \`explain\` block
3894
3905
  (basis, seed strategy, scope notes). Items are immutable once emitted \u2014
3895
3906
  polling never re-reads live data, so numbers only ever grow.
3896
3907
 
@@ -3899,7 +3910,7 @@ completion when the user asked for results "in this reply". \`since\` (from a
3899
3910
  prior poll's \`next_since\`) pages only the new items. Jobs terminalize
3900
3911
  server-side: past the 30-min wall clock a job reads \`completed_partial\`
3901
3912
  (time budget), past 30 days \`expired\` (items no longer listed \u2014 re-read
3902
- billed leads via leadbay_qualify_leads \`prior_deliveries\`). A 404 means
3913
+ delivered leads via leadbay_qualify_leads \`prior_deliveries\`). A 404 means
3903
3914
  unknown job or another org's job.
3904
3915
 
3905
3916
  ---
@@ -3934,7 +3945,7 @@ when nothing was delivered.
3934
3945
  **Column 3 \u2014 Contact**
3935
3946
 
3936
3947
  - \`[Name](linkedin) \xB7 role\` (linked name mandatory when a LinkedIn URL
3937
- exists; plain name otherwise). Below it, the PURCHASED channels only:
3948
+ exists; plain name otherwise). Below it, the FOUND channels only:
3938
3949
  \`\u2709 value\` / \`\u260E value\` inline as plain text (they auto-linkify).
3939
3950
  - Channel statuses: \`delivered\` \u2192 show value; \`already_owned\` \u2192 value +
3940
3951
  *(already yours)*; \`masked\` \u2192 "on file \u2014 reveal via channels";
@@ -3945,16 +3956,17 @@ when nothing was delivered.
3945
3956
 
3946
3957
  **The funnel line (mandatory, after the table):**
3947
3958
 
3948
- One short line narrating the delivery honestly, from \`funnel\` + \`cost\` +
3959
+ One short line narrating the delivery honestly, from \`funnel\` +
3949
3960
  \`explain.scope_notes\`:
3950
3961
 
3951
3962
  > Matched N \xB7 examined E \xB7 qualified Q \xB7 disqualified D \u2192 **delivered X of
3952
- > the Y asked** \xB7 stopped: <stop_reason in plain words> \xB7 spent C.CC.
3963
+ > the Y asked** \xB7 stopped: <stop_reason in plain words>.
3953
3964
 
3954
- **Money: divide, then symbol.** Every amount (\`cost.spent\`,
3955
- \`estimated_cost.max\`, quotes) is \`cost_cents\` \u2014 divide by 100, two decimals,
3956
- so \`165\` renders \`1.65\`, NEVER \`165.00\`. Symbol from the account region: US
3957
- \`$\`, France \`\u20AC\`, unknown \u2192 bare. Never hard-code \`$\`: it misstates a charge.
3965
+ **No money, anywhere.** \`cost.*\`, \`estimated_cost.max\` and quotes are internal
3966
+ usage units. Never render them, never convert them to a currency, never call
3967
+ them a charge: the user's plan or top-up covers this work, and a price reads as
3968
+ a bill. If the user asks what a job used, show \`leadbay_account_status\`'s quota
3969
+ windows.
3958
3970
 
3959
3971
  "of the Y asked" needs \`summary.items_requested\`, which submits carry but a
3960
3972
  later \`leadbay_lead_job_status\` snapshot does not. Without it write **delivered
@@ -3962,7 +3974,7 @@ X** and stop \u2014 never back-fill Y from \`matched\`/\`examined\` (they count
3962
3974
  candidates), never guess it.
3963
3975
 
3964
3976
  Plain-word stop reasons: \`target_reached\` \u2192 omit (success), \`pool_exhausted\` \u2192
3965
- "ran out of matching candidates", \`max_cost\` \u2192 "hit the cost cap", \`quota\` \u2192
3977
+ "ran out of matching candidates", \`max_cost\` \u2192 "hit the job's usage cap", \`quota\` \u2192
3966
3978
  "hit an org quota", \`time_budget\` \u2192 "hit the 30-min time budget".
3967
3979
 
3968
3980
  **When \`delivered\` is 0**: NEVER say just "no results". Render no table; give
@@ -3987,7 +3999,7 @@ table \`Ref \u2192 Outcome\` translating \`status_reason\` to plain words:
3987
3999
  \`low_confidence_identity\` \u2192 "couldn't safely match \u2014 check \`resolution.alternatives\`",
3988
4000
  \`no_matching_contact\` \u2192 "no contact with the requested title",
3989
4001
  \`disqualified\` \u2192 "evaluated: does not fit" (evidence is in the item when owned),
3990
- \`enrichment_failed\` \u2192 "channel could not be sourced (not billed)".
4002
+ \`enrichment_failed\` \u2192 "channel could not be sourced (no quota used)".
3991
4003
 
3992
4004
  **\`items_truncated\`**: rows are a PREFIX, not the batch. Say so, and offer
3993
4005
  \`leadbay_lead_job_status(job_id, since: next_since)\` for the rest.
@@ -4054,7 +4066,7 @@ is a status tool, keep it terse:
4054
4066
  |---|---|---|
4055
4067
  | Still running | "Keep waiting (~1 min) or leave it \u2014 results are kept 30 days" | leadbay_lead_job_status(job_id, wait_seconds: 60) |
4056
4068
  | Terminal (completed / partial / failed) | Render the delivery per the RENDERING block, then offer the matching find_new_leads / qualify_leads NEXT STEPS | \u2014 |
4057
- | \`expired\` (past the 30-day window) | "Re-read the billed leads from your delivery ledger" \u2014 there is nothing left to render: the job terminalized and its items are no longer listed, so do NOT present an empty delivery as a result | leadbay_qualify_leads(prior_deliveries: {job_id}) |
4069
+ | \`expired\` (past the 30-day window) | "Re-read the delivered leads from your delivery ledger" \u2014 there is nothing left to render: the job terminalized and its items are no longer listed, so do NOT present an empty delivery as a result | leadbay_qualify_leads(prior_deliveries: {job_id}) |
4058
4070
  `;
4059
4071
  leadbay_like_lead = `## WHEN TO USE
4060
4072
 
@@ -4730,9 +4742,9 @@ Offer 2\u20133 follow-ups. Choose based on enrichment state + available channels
4730
4742
  | After a successful exchange | "Update qualification answers based on what you learned" | leadbay_answer_clarification |
4731
4743
  The "log outreach" step is the most-important follow-up \u2014 it closes the loop and populates history for the next \`leadbay_prepare_outreach\` call. Detect intent from natural language: "I sent the email", "she didn't pick up", "left a voicemail", "they responded yes/no", etc.
4732
4744
  `;
4733
- leadbay_preview_bulk_enrichment = `Preview a bulk-enrichment cost given a set of job titles applied to the current selection. Returns \`{selected_leads, enriched_contacts, enrichable_contacts, title_suggestions, auto_included_titles, previously_enriched_titles}\`. \`previously_enriched_titles\` is a newer field (in prod soon) \u2014 when present, the agent can recommend repeating those titles for new leads.
4745
+ leadbay_preview_bulk_enrichment = `Preview a bulk enrichment given a set of job titles applied to the current selection. Returns \`{selected_leads, enriched_contacts, enrichable_contacts, title_suggestions, auto_included_titles, previously_enriched_titles}\`. \`previously_enriched_titles\` is a newer field (in prod soon) \u2014 when present, the agent can recommend repeating those titles for new leads.
4734
4746
 
4735
- WHEN TO USE: between selecting leads and launching, to know what the enrichment will cost.
4747
+ WHEN TO USE: between selecting leads and launching, to know how many contacts the enrichment will reveal.
4736
4748
 
4737
4749
  WHEN NOT TO USE: from agent flow \u2014 leadbay_enrich_titles wraps preview + launch with the right safety checks.
4738
4750
  `;
@@ -5127,18 +5139,18 @@ WHEN NOT TO USE: as the agent's bulk-qualify path \u2014 use leadbay_bulk_qualif
5127
5139
  ## A launched job cannot be stopped, and this tool has no retry guard
5128
5140
 
5129
5141
  Leadbay has no cancel. Once this call returns having actually launched, the work
5130
- is queued on Leadbay and runs to completion, and the quota it costs is already
5131
- committed. A \`dry_run\` result reached no backend and spent nothing. The user
5142
+ is queued on Leadbay and runs to completion, and the quota it uses is already
5143
+ committed. A \`dry_run\` result reached no backend and used nothing. The user
5132
5144
  cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
5133
5145
  never the job.
5134
5146
 
5135
5147
  Unlike the composite launchers, this tool has **no double-launch guard**: calling
5136
- it again always issues a new paid launch, even seconds later with identical
5148
+ it again always issues a new launch that uses quota again, even seconds later with identical
5137
5149
  arguments. So when a call returns nothing at all, do not simply retry. Read the
5138
5150
  record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
5139
5151
  lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
5140
5152
  see whether the launch already landed, and tell the user what you are about to
5141
- spend before spending it again.
5153
+ run before running it again.
5142
5154
 
5143
5155
 
5144
5156
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
@@ -5166,7 +5178,7 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
5166
5178
  3-col table for delivered items (fit bar + company / why-fits \u226420 words /
5167
5179
  contact + channels) in returned order, then a compact Ref \u2192 Outcome table
5168
5180
  for skipped refs (not_in_universe, low_confidence_identity, ... in plain
5169
- words), then the honest funnel + cost line. Full algorithm below.
5181
+ words), then the honest funnel line. Full algorithm below.
5170
5182
 
5171
5183
  ---
5172
5184
 
@@ -5181,29 +5193,29 @@ job; this tool polls up to \`wait_seconds\` (default 45) and hands off to
5181
5193
  **Refs are flexible; outcomes are per-item.** \`lead_refs\` accepts any mix of
5182
5194
  \`lead_id\`, \`website\`, \`name\`(+\`location\`), or a stable \`contact_id\` from a
5183
5195
  prior result (enrichment then targets exactly that person, never a re-match).
5184
- \`prior_deliveries\` expands past MCP deliveries into refs \u2014 billed leads stay
5196
+ \`prior_deliveries\` expands past MCP deliveries into refs \u2014 delivered leads stay
5185
5197
  re-readable this way even after the 30-day result window. Duplicates collapse.
5186
5198
  A ref that can't be served comes back \`skipped\` with an honest
5187
5199
  \`status_reason\` (\`not_in_universe\`, \`low_confidence_identity\` with the
5188
5200
  \`resolution.alternatives\` to choose from, \`no_matching_contact\`, ...) \u2014 that
5189
- is an ANSWER about the ref, not an error, and it costs nothing.
5201
+ is an ANSWER about the ref, not an error, and it uses no quota.
5190
5202
 
5191
5203
  **Disqualified \u2260 dropped.** Companies the org owns that fail qualification
5192
5204
  are DELIVERED with their negative evidence (question verdicts, tag misses,
5193
5205
  IBP reasoning) \u2014 "here's why to skip this account" is a deliverable.
5194
5206
 
5195
- **Cost \u2014 never spend silently.** Resolution and identity are free.
5196
- \`qualify: true\` (the default) costs ~94 cost_cents per lead needing FRESH
5197
- research+scoring \u2014 but repeat calls reuse every fresh cached stage
5198
- (\`from_cache\` flags on the items) and converge to near-zero cost. \`channels\`
5199
- purchase verified email (25c) / phone (250c) on success only;
5200
- \`already_owned\` values cost nothing.
5207
+ **Usage quota \u2014 never use it silently.** Resolution and identity are free.
5208
+ \`qualify: true\` (the default) draws on the org's usage quota for each lead
5209
+ needing FRESH research+scoring \u2014 but repeat calls reuse every fresh cached stage
5210
+ (\`from_cache\` flags on the items) and converge to near-zero. \`channels\` draw on
5211
+ it only when a verified email / phone is found; \`already_owned\` values are
5212
+ free. Nothing is invoiced.
5201
5213
 
5202
- The gate is enforced in code, not just here: a PAID call (\`qualify\` left at
5214
+ The gate is enforced in code, not just here: a call that uses quota (\`qualify\` left at
5203
5215
  its default or set true, and/or any \`channels\`) is WITHHELD unless it carries
5204
5216
  \`confirm: true\`. Without it the tool submits nothing and returns
5205
5217
  \`mode: "needs_confirmation"\` with a real backend quote \u2014 show that quote to
5206
- the user, get the go-ahead (an explicit "spend / get their emails" in their
5218
+ the user, get the go-ahead (an explicit "go ahead / get their emails" in their
5207
5219
  message counts), then re-call with \`confirm: true\`. \`confirm: false\` is a
5208
5220
  veto: nothing is submitted and no quote round-trip is made. A fully FREE
5209
5221
  call (\`qualify: false\`, no \`channels\`) needs no \`confirm\` and passes straight
@@ -5244,7 +5256,7 @@ when nothing was delivered.
5244
5256
  **Column 3 \u2014 Contact**
5245
5257
 
5246
5258
  - \`[Name](linkedin) \xB7 role\` (linked name mandatory when a LinkedIn URL
5247
- exists; plain name otherwise). Below it, the PURCHASED channels only:
5259
+ exists; plain name otherwise). Below it, the FOUND channels only:
5248
5260
  \`\u2709 value\` / \`\u260E value\` inline as plain text (they auto-linkify).
5249
5261
  - Channel statuses: \`delivered\` \u2192 show value; \`already_owned\` \u2192 value +
5250
5262
  *(already yours)*; \`masked\` \u2192 "on file \u2014 reveal via channels";
@@ -5255,16 +5267,17 @@ when nothing was delivered.
5255
5267
 
5256
5268
  **The funnel line (mandatory, after the table):**
5257
5269
 
5258
- One short line narrating the delivery honestly, from \`funnel\` + \`cost\` +
5270
+ One short line narrating the delivery honestly, from \`funnel\` +
5259
5271
  \`explain.scope_notes\`:
5260
5272
 
5261
5273
  > Matched N \xB7 examined E \xB7 qualified Q \xB7 disqualified D \u2192 **delivered X of
5262
- > the Y asked** \xB7 stopped: <stop_reason in plain words> \xB7 spent C.CC.
5274
+ > the Y asked** \xB7 stopped: <stop_reason in plain words>.
5263
5275
 
5264
- **Money: divide, then symbol.** Every amount (\`cost.spent\`,
5265
- \`estimated_cost.max\`, quotes) is \`cost_cents\` \u2014 divide by 100, two decimals,
5266
- so \`165\` renders \`1.65\`, NEVER \`165.00\`. Symbol from the account region: US
5267
- \`$\`, France \`\u20AC\`, unknown \u2192 bare. Never hard-code \`$\`: it misstates a charge.
5276
+ **No money, anywhere.** \`cost.*\`, \`estimated_cost.max\` and quotes are internal
5277
+ usage units. Never render them, never convert them to a currency, never call
5278
+ them a charge: the user's plan or top-up covers this work, and a price reads as
5279
+ a bill. If the user asks what a job used, show \`leadbay_account_status\`'s quota
5280
+ windows.
5268
5281
 
5269
5282
  "of the Y asked" needs \`summary.items_requested\`, which submits carry but a
5270
5283
  later \`leadbay_lead_job_status\` snapshot does not. Without it write **delivered
@@ -5272,7 +5285,7 @@ X** and stop \u2014 never back-fill Y from \`matched\`/\`examined\` (they count
5272
5285
  candidates), never guess it.
5273
5286
 
5274
5287
  Plain-word stop reasons: \`target_reached\` \u2192 omit (success), \`pool_exhausted\` \u2192
5275
- "ran out of matching candidates", \`max_cost\` \u2192 "hit the cost cap", \`quota\` \u2192
5288
+ "ran out of matching candidates", \`max_cost\` \u2192 "hit the job's usage cap", \`quota\` \u2192
5276
5289
  "hit an org quota", \`time_budget\` \u2192 "hit the 30-min time budget".
5277
5290
 
5278
5291
  **When \`delivered\` is 0**: NEVER say just "no results". Render no table; give
@@ -5297,7 +5310,7 @@ table \`Ref \u2192 Outcome\` translating \`status_reason\` to plain words:
5297
5310
  \`low_confidence_identity\` \u2192 "couldn't safely match \u2014 check \`resolution.alternatives\`",
5298
5311
  \`no_matching_contact\` \u2192 "no contact with the requested title",
5299
5312
  \`disqualified\` \u2192 "evaluated: does not fit" (evidence is in the item when owned),
5300
- \`enrichment_failed\` \u2192 "channel could not be sourced (not billed)".
5313
+ \`enrichment_failed\` \u2192 "channel could not be sourced (no quota used)".
5301
5314
 
5302
5315
  **\`items_truncated\`**: rows are a PREFIX, not the batch. Say so, and offer
5303
5316
  \`leadbay_lead_job_status(job_id, since: next_since)\` for the rest.
@@ -5365,7 +5378,7 @@ Pick the 2-3 options that match what actually happened:
5365
5378
  | Fit leads with contacts delivered | "Draft outreach for the qualified ones" | leadbay_prepare_outreach |
5366
5379
  | Items skipped \`not_in_universe\` | "Import those companies first, then re-qualify" | leadbay_import_leads \u2192 leadbay_qualify_leads |
5367
5380
  | Items skipped \`low_confidence_identity\` | "Pick the right match" (show \`resolution.alternatives\`) | leadbay_qualify_leads with the chosen lead_id |
5368
- | Contacts delivered without channels | "Purchase verified emails/phones for the keepers (state cost first)" | leadbay_qualify_leads(lead_refs with contact_id, channels) |
5381
+ | Contacts delivered without channels | "Get verified emails/phones for the keepers (uses quota \u2014 say so first)" | leadbay_qualify_leads(lead_refs with contact_id, channels) |
5369
5382
  | Disqualified with evidence | "Review why \u2014 adjust qualification questions if the criteria are off" | leadbay_get_qualification_questions |
5370
5383
  `;
5371
5384
  leadbay_qualify_status = `Retrieve the current state of a bulk_qualify_leads or import_and_qualify launch. Which ids to pass depends on which tool launched it, because only one of them creates a qualification job on the backend:
@@ -5411,7 +5424,7 @@ a launcher only for a subset that never started, never for the whole batch:
5411
5424
  that are the ones that never started. Only then call
5412
5425
  \`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
5413
5426
  that is merely slow looks identical to one that never launched over a few
5414
- polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
5427
+ polls, and re-launching it uses the user's quota twice \u2014 when unsure, tell the user
5415
5428
  rather than guess.
5416
5429
  `;
5417
5430
  leadbay_recall_ordered_titles = `Show job titles the org has previously enriched, so the agent can repeat the same titles for new leads (or skip already-saturated ones). Two implementation paths: (1) PREFERRED \u2014 a selection-scoped preview call that reads \`previously_enriched_titles\` from the backend (newer prod field). (2) FALLBACK \u2014 live aggregation across each lead's enriched contacts. The composite picks transparently.
@@ -6946,7 +6959,7 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
6946
6959
  NO_COMMERCE_TOOL_DESCRIPTIONS = {
6947
6960
  leadbay_account_status: `## WHEN TO USE
6948
6961
 
6949
- Trigger phrases: "what's my account status", "how much quota do I have", "what lens am I on", "I topped up / I bought credits / I added credits".
6962
+ Trigger phrases: "what's my account status", "how much quota do I have", "what lens am I on", "I topped up / I bought credits / I added credits", "what version of Leadbay am I running".
6950
6963
 
6951
6964
  Do NOT use for: "show me leads" \u2192 \`leadbay_pull_leads\`.
6952
6965
 
@@ -6955,6 +6968,7 @@ Prefer when: meta question about account, quota, active lens, or top-up recovery
6955
6968
  Examples that SHOULD invoke this tool:
6956
6969
  - "What's my account status?"
6957
6970
  - "How much quota do I have left this week?"
6971
+ - "Which version of the Leadbay connector is this?"
6958
6972
 
6959
6973
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
6960
6974
  - "Show me today's leads."
@@ -6976,6 +6990,8 @@ Show the user's account state \u2014 admin rights, language, last-active lens, q
6976
6990
 
6977
6991
  **After a user tops up, do NOT keep refusing \u2014 RETRY.** If the user signals they topped up / bought credits / added credits, the previous QUOTA_EXCEEDED is invalidated the moment the Stripe webhook lands. RE-CALL \`leadbay_account_status\` to pick up the new state AND retry the originally failed call. The retry itself does not require a successful account_status check first \u2014 a topped-up user has cleared the throttle whether or not your cached snapshot reflects it yet. **A stale quota snapshot is never a reason to gate-keep a topped-up user.**
6978
6992
 
6993
+ **\`mcp_version\`** is the version of the Leadbay MCP server answering the call. When the user asks which Leadbay version they are running, answer with it.
6994
+
6979
6995
  **\`notifications\` block.** The response now includes a top-level \`notifications\` array listing background work the user (or agent) initiated that has since completed (\`bulk_enrich\`, \`bulk_qualify\`, \`import\`). These are signals to revise prior agent outputs the just-finished work might have made stale \u2014 they're NOT a pending-task list for the user. After revising (or confirming nothing is affected), call \`leadbay_acknowledge_notification(notification_id)\`. Full handling protocol below.
6980
6996
 
6981
6997
  ## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
@@ -7004,7 +7020,7 @@ Some Leadbay tool responses include a \`_meta.notifications\` array listing **ba
7004
7020
 
7005
7021
  - **Previous turn / before an MCP restart, and the user has NOT asked about it** \u2014 don't poll for it in the background. Simply continue the conversation; the next time you call any tool, the completed-work entry appears in \`_meta.notifications\` (also on \`leadbay_account_status.notifications\`). This is the ambient push path \u2014 leave it to do its job. **But if the user explicitly asks for status or to "wait for it to finish"** (e.g. a multi-turn flow where a job was launched in a prior turn and this turn says "wait for enrichment to finish, then \u2026"), DO poll its status tool now until done, exactly as for a this-turn job below \u2014 the ambient push only surfaces *completed* work, so it can't answer a live "is it done / wait for it" request while the job is still running.
7006
7022
  - **This turn (you just launched it)** \u2014 the DEFAULT is: do NOT end your turn on the "launched" ack; stay active and poll the job's status tool in a loop until it reports done, then report the finished result yourself, rather than spinning forever or deferring the result to a later turn. (Two exceptions, detailed below: the user explicitly asked NOT to wait / to run it in the background; or it's a large qualification/import that's async by design \u2014 in those cases hand back the handle instead of looping.) Each status tool has its OWN terminal signal \u2014 poll until:
7007
- - \`leadbay_bulk_enrich_status\` \u2192 \`all_done:true\` \u2014 OR \`overall_progress.done\` holds steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (some contacts are unresolvable, so \`all_done\` can stay false forever). Don't call a plateau from the first few back-to-back reads \u2014 early on \`done\` sits flat while the backend spins up. Once the plateau is real, report what resolved and name what didn't.
7023
+ - \`leadbay_bulk_enrich_status\` \u2192 \`all_done:true\` \u2014 OR \`overall_progress.done\` holds steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (a reservation the provider never answers stays \`done:false\`, so \`all_done\` can stay false forever). Don't call a plateau from the first few back-to-back reads \u2014 early on \`done\` sits flat while the backend spins up. Once the plateau is real, report what resolved and name what didn't.
7008
7024
  - \`leadbay_qualify_status\` \u2192 \`still_running\` is empty: every launched lead has finished or failed. (\`in_progress\` also reads \`false\` on the fast path, but it can be \`null\` on the legacy/fallback read \u2014 so treat an empty \`still_running\` as terminal on its own; only require \`in_progress:false\` when that field is actually present.) LIKE imports, large qualification runs are async by design: \`leadbay_bulk_qualify_leads\` defaults to \`wait_for_completion:false\` for \`count > 5\` or chained workflows because blocking can time out, and \`leadbay_qualify_status\` may take minutes/hours. So don't force a long polling loop on a big run \u2014 return the handle/progress and let completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked to wait, or it's a small run that finishes quickly. A small \`wait_for_completion:true\` run you can poll to \`still_running\` empty inline.
7009
7025
  - \`leadbay_import_status\` \u2192 \`status:"complete"\` (or \`"failed"\`). BUT imports are the exception to the stay-active loop: a large \`leadbay_import_leads({wait_for_completion:false})\` is meant to return a handle and resolve over minutes, and the tool does ONE refresh pass per call. Don't block the conversation looping on it \u2014 surface the returned progress/handle and let the completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked you to wait for the import, or it's a small import that finishes quickly.
7010
7026
 
@@ -7085,7 +7101,7 @@ days"), computed against now \u2014 mirroring the widget's "r\xE9initialis\xE9 d
7085
7101
  raw value is an ISO-8601 timestamp.
7086
7102
 
7087
7103
  **Top-up (optional, subordinate).** When \`quota.topup\` is present, you MAY add one
7088
- small line below the windows: \`Top-up: $<remaining_cents/100> of $<total_credit_cents/100> left\`.
7104
+ small line below the windows: \`Top-up: $<(remaining_cents / 100).toFixed(2)> of $<(total_credit_cents / 100).toFixed(2)> left\`.
7089
7105
  Keep it secondary \u2014 the three window gauges are the headline. Omit when null.
7090
7106
 
7091
7107
  **Legend** (once, below): \`\` \`\u25B0\` used \xB7 \`\u25B1\` remaining \`\`.
@@ -7119,8 +7135,8 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
7119
7135
 
7120
7136
  3-col table of delivered leads in returned order: col 1 = 10-segment fit
7121
7137
  bar + linked company \xB7 location \xB7 size; col 2 = why-fits \u226420 words; col 3
7122
- = contact + purchased channels. ALWAYS close with the honest funnel line
7123
- (matched/examined/delivered/stop reason/spend) \u2014 especially on 0
7138
+ = contact + found channels. ALWAYS close with the honest funnel line
7139
+ (matched/examined/delivered/stop reason) \u2014 especially on 0
7124
7140
  delivered. Full algorithm below.
7125
7141
 
7126
7142
  ---
@@ -7128,19 +7144,19 @@ delivered. Full algorithm below.
7128
7144
  Submit a net-new lead search: the backend matches an ICP seed against the full
7129
7145
  company universe, applies hard filters, skips what the org already knows
7130
7146
  (\`novelty: org\`), optionally qualifies against the org's own intelligence
7131
- (questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally buys
7147
+ (questions, tags, ideal buyer profile \u2014 frozen at submit), and optionally reveals
7132
7148
  contact channels. Polls up to \`wait_seconds\` (default 45); a longer job returns
7133
7149
  \`still_running\` + \`next_poll\` \u2014 hand off to \`leadbay_lead_job_status\`. Jobs run
7134
7150
  \u226430 min, results kept 30 days.
7135
7151
 
7136
- **Free vs paid \u2014 never spend silently.** Default (\`qualify: false\`,
7152
+ **Free vs usage quota \u2014 never use quota silently.** Default (\`qualify: false\`,
7137
7153
  \`channels: []\`) is FREE: company profile + fit score + cached research +
7138
- contact identity. Paid: \`qualify: true\` (~94 cost_cents per candidate
7139
- EXAMINED, capped by \`exploration_cap\`/\`max_cost\`) and \`channels\` (email 25c /
7140
- phone 250c, success-only). Enforced in code: a paid call is WITHHELD unless it
7154
+ contact identity. \`qualify: true\` (per candidate EXAMINED, capped by
7155
+ \`exploration_cap\`/\`max_cost\`) and \`channels\` (only when a value is found) draw
7156
+ on the org's usage quota; nothing is invoiced. Enforced in code: such a call is WITHHELD unless it
7141
7157
  carries \`confirm: true\` \u2014 nothing is submitted and you get
7142
7158
  \`mode: "needs_confirmation"\` with a real quote to show the user. Re-call with
7143
- \`confirm: true\` on their go-ahead ("spend / get their emails" counts).
7159
+ \`confirm: true\` on their go-ahead ("go ahead / get their emails" counts).
7144
7160
  \`confirm: false\` vetoes. Free needs no consent. **Preview free first** \u2014
7145
7161
  reshaping an off-profile seed is free, exploring it with \`qualify: true\` is
7146
7162
  not.
@@ -7148,7 +7164,7 @@ not.
7148
7164
  **Ad-hoc exclusions ("no chains") are enforced by NO tier** \u2014 \`filters\` has no
7149
7165
  exclusion key, and \`qualify\` scores against the org's FROZEN questions and IBP,
7150
7166
  which need not mention chains; the seed's inverse only shifts ranking.
7151
- Violators can survive, be paid for and be delivered \u2014 post-filter them yourself
7167
+ Violators can survive, use quota and be delivered \u2014 post-filter them yourself
7152
7168
  and say the tier didn't enforce it. Durable enforcement \u2192
7153
7169
  \`leadbay_refine_prompt\`.
7154
7170
 
@@ -7195,9 +7211,9 @@ measured:
7195
7211
 
7196
7212
  **Parameter notes**
7197
7213
  - \`request_id\` (REQUIRED) is the retry contract: SAME value retrying the same
7198
- ask (same live job, no double spend); NEW for a changed ask. Derive from ask
7214
+ ask (same live job, no double launch); NEW for a changed ask. Derive from ask
7199
7215
  + archetype + date: \`gyms-dallas-2026-07-28\`.
7200
- - Never lower \`min_ai_score\` together with \`channels\` \u2014 that buys emails for
7216
+ - Never lower \`min_ai_score\` together with \`channels\` \u2014 that reveals emails for
7201
7217
  leads the AI just scored as junk.
7202
7218
  - \`count\` \u2264 50; \u22643 active jobs/org; \u226410 submits/hour (429 + Retry-After \u2014
7203
7219
  wait, don't hammer).
@@ -7237,7 +7253,7 @@ when nothing was delivered.
7237
7253
  **Column 3 \u2014 Contact**
7238
7254
 
7239
7255
  - \`[Name](linkedin) \xB7 role\` (linked name mandatory when a LinkedIn URL
7240
- exists; plain name otherwise). Below it, the PURCHASED channels only:
7256
+ exists; plain name otherwise). Below it, the FOUND channels only:
7241
7257
  \`\u2709 value\` / \`\u260E value\` inline as plain text (they auto-linkify).
7242
7258
  - Channel statuses: \`delivered\` \u2192 show value; \`already_owned\` \u2192 value +
7243
7259
  *(already yours)*; \`masked\` \u2192 "on file \u2014 reveal via channels";
@@ -7248,16 +7264,17 @@ when nothing was delivered.
7248
7264
 
7249
7265
  **The funnel line (mandatory, after the table):**
7250
7266
 
7251
- One short line narrating the delivery honestly, from \`funnel\` + \`cost\` +
7267
+ One short line narrating the delivery honestly, from \`funnel\` +
7252
7268
  \`explain.scope_notes\`:
7253
7269
 
7254
7270
  > Matched N \xB7 examined E \xB7 qualified Q \xB7 disqualified D \u2192 **delivered X of
7255
- > the Y asked** \xB7 stopped: <stop_reason in plain words> \xB7 spent C.CC.
7271
+ > the Y asked** \xB7 stopped: <stop_reason in plain words>.
7256
7272
 
7257
- **Money: divide, then symbol.** Every amount (\`cost.spent\`,
7258
- \`estimated_cost.max\`, quotes) is \`cost_cents\` \u2014 divide by 100, two decimals,
7259
- so \`165\` renders \`1.65\`, NEVER \`165.00\`. Symbol from the account region: US
7260
- \`$\`, France \`\u20AC\`, unknown \u2192 bare. Never hard-code \`$\`: it misstates a charge.
7273
+ **No money, anywhere.** \`cost.*\`, \`estimated_cost.max\` and quotes are internal
7274
+ usage units. Never render them, never convert them to a currency, never call
7275
+ them a charge: the user's plan or top-up covers this work, and a price reads as
7276
+ a bill. If the user asks what a job used, show \`leadbay_account_status\`'s quota
7277
+ windows.
7261
7278
 
7262
7279
  "of the Y asked" needs \`summary.items_requested\`, which submits carry but a
7263
7280
  later \`leadbay_lead_job_status\` snapshot does not. Without it write **delivered
@@ -7265,7 +7282,7 @@ X** and stop \u2014 never back-fill Y from \`matched\`/\`examined\` (they count
7265
7282
  candidates), never guess it.
7266
7283
 
7267
7284
  Plain-word stop reasons: \`target_reached\` \u2192 omit (success), \`pool_exhausted\` \u2192
7268
- "ran out of matching candidates", \`max_cost\` \u2192 "hit the cost cap", \`quota\` \u2192
7285
+ "ran out of matching candidates", \`max_cost\` \u2192 "hit the job's usage cap", \`quota\` \u2192
7269
7286
  "hit an org quota", \`time_budget\` \u2192 "hit the 30-min time budget".
7270
7287
 
7271
7288
  **When \`delivered\` is 0**: NEVER say just "no results". Render no table; give
@@ -7290,7 +7307,7 @@ table \`Ref \u2192 Outcome\` translating \`status_reason\` to plain words:
7290
7307
  \`low_confidence_identity\` \u2192 "couldn't safely match \u2014 check \`resolution.alternatives\`",
7291
7308
  \`no_matching_contact\` \u2192 "no contact with the requested title",
7292
7309
  \`disqualified\` \u2192 "evaluated: does not fit" (evidence is in the item when owned),
7293
- \`enrichment_failed\` \u2192 "channel could not be sourced (not billed)".
7310
+ \`enrichment_failed\` \u2192 "channel could not be sourced (no quota used)".
7294
7311
 
7295
7312
  **\`items_truncated\`**: rows are a PREFIX, not the batch. Say so, and offer
7296
7313
  \`leadbay_lead_job_status(job_id, since: next_since)\` for the rest.
@@ -7339,10 +7356,10 @@ Pick the 2-3 options that match what actually happened \u2014 never all seven:
7339
7356
  | Observation | Suggest | Calls |
7340
7357
  |---|---|---|
7341
7358
  | Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds: 60) |
7342
- | Free run delivered on-profile leads | "Qualify these N against your criteria (paid \u2014 \`dry_run\` first)" | leadbay_qualify_leads(prior_deliveries: {job_id}) |
7359
+ | Free run delivered on-profile leads | "Qualify these N against your criteria (uses quota \u2014 \`dry_run\` first)" | leadbay_qualify_leads(prior_deliveries: {job_id}) |
7343
7360
  | Delivered leads look right | "Draft outreach for the top ones" | leadbay_prepare_outreach |
7344
7361
  | Delivered 0 or off-profile | "Reshape the example and retry" (name the fix from funnel + scope_notes) | leadbay_find_new_leads (NEW request_id) |
7345
- | Stopped at cost cap (\`stop_reason: max_cost\`) | "Raise the cap to X and get the remaining N" \u2014 X in the account's currency per the funnel-line rule, never a hard-coded \`$\` | leadbay_find_new_leads, NEW request_id (same-id only dedupes onto a LIVE job) + higher max_cost + \`count\` = the SHORTFALL (\`items_requested\` \u2212 delivered), not the original + \`exclude_lead_ids\` = the examined-but-REJECTED ids (novelty covers delivered; these are what it misses \u2014 without them the rerun re-buys the same losers) |
7362
+ | Stopped at the job's usage cap (\`stop_reason: max_cost\`) | "Raise the job's cap and get the remaining N" \u2014 no amount, no currency | leadbay_find_new_leads, NEW request_id (same-id only dedupes onto a LIVE job) + higher max_cost + \`count\` = the SHORTFALL (\`items_requested\` \u2212 delivered), not the original + \`exclude_lead_ids\` = the examined-but-REJECTED ids (novelty covers delivered; these are what it misses \u2014 without them the rerun re-buys the same losers) |
7346
7363
  | Stopped on org quota (\`stop_reason: quota\`) | "Check which window is exhausted and when it resets" \u2014 never a re-run: it cannot clear an org quota and burns a submit slot to stop in the same place | leadbay_account_status |
7347
7364
  | User wants these tracked in Leadbay | "Add the keepers to a campaign" | leadbay_create_campaign / leadbay_add_leads_to_campaign |
7348
7365
  `,
@@ -7941,7 +7958,7 @@ var init_get_contacts = __esm({
7941
7958
  },
7942
7959
  credits_used: {
7943
7960
  type: "number",
7944
- description: "Credits charged for this reveal. Only meaningful when done:true. An explicit 0 alongside done:true means the provider returned nothing. Optional \u2014 when absent the cost is unknown and terminal-empty must NOT be inferred."
7961
+ description: "Credits used by this reveal. Only meaningful when done:true. An explicit 0 alongside done:true means the provider returned nothing. Optional \u2014 when absent the cost is unknown and terminal-empty must NOT be inferred."
7945
7962
  },
7946
7963
  email_requested: { type: "boolean" },
7947
7964
  phone_requested: { type: "boolean" }
@@ -12806,7 +12823,7 @@ var init_preview_bulk_enrichment = __esm({
12806
12823
  previewBulkEnrichment = {
12807
12824
  name: "leadbay_preview_bulk_enrichment",
12808
12825
  annotations: {
12809
- title: "Preview bulk enrichment cost",
12826
+ title: "Preview bulk enrichment",
12810
12827
  readOnlyHint: true,
12811
12828
  destructiveHint: false,
12812
12829
  idempotentHint: true,
@@ -16749,9 +16766,9 @@ var init_getting_started = __esm({
16749
16766
  leadId: "<the highest-scoring lead id from step 2>"
16750
16767
  },
16751
16768
  forbidden_args: [
16752
- "enrich \u2014 enrich:true launches a PAID contact reveal off the back of a DRAFT click. They agreed to see an email written, not to spend. Gate 4 is where the reveal gets asked for, explicitly and on its own terms."
16769
+ "enrich \u2014 enrich:true launches a contact reveal, which uses quota, off the back of a DRAFT click. They agreed to see an email written, not to reveal anyone. Gate 4 is where the reveal gets asked for, explicitly and on its own terms."
16753
16770
  ],
16754
- spend: "This gate spends NOTHING. Call leadbay_prepare_outreach with leadId and nothing else. `recommended_contact` comes back in its post-enrichment shape with email and phone still null \u2014 that is EXPECTED, not a failure, and it is precisely the hook for gate 4: an email written, and nobody to send it to yet. Do not apologise for the missing contact, and do not reach for another tool to fill it in.",
16771
+ spend: "This gate uses NO quota. Call leadbay_prepare_outreach with leadId and nothing else. `recommended_contact` comes back in its post-enrichment shape with email and phone still null \u2014 that is EXPECTED, not a failure, and it is precisely the hook for gate 4: an email written, and nobody to send it to yet. Do not apologise for the missing contact, and do not reach for another tool to fill it in.",
16755
16772
  branches: [
16756
16773
  {
16757
16774
  when: "always",
@@ -16767,7 +16784,7 @@ var init_getting_started = __esm({
16767
16784
  n: 4,
16768
16785
  gate_label: "Find who to email",
16769
16786
  gate_description: "Reveal the person at that company to send the draft to.",
16770
- explain: "Point straight at the gap the draft just opened: they have an email ready and nobody to send it to \u2014 it's addressed to a job title, not a person. That's what this step fixes. Explain what enrichment IS: Leadbay can find which roles exist at that company, then reveal the actual human and how to reach them. WHY IT'S USEFUL: they ask for the operations director by name instead of pitching whoever answers the switchboard \u2014 the difference between a conversation and a dead end. Say plainly that the first look is free, and that revealing the contact costs credits and needs their say-so.",
16787
+ explain: "Point straight at the gap the draft just opened: they have an email ready and nobody to send it to \u2014 it's addressed to a job title, not a person. That's what this step fixes. Explain what enrichment IS: Leadbay can find which roles exist at that company, then reveal the actual human and how to reach them. WHY IT'S USEFUL: they ask for the operations director by name instead of pitching whoever answers the switchboard \u2014 the difference between a conversation and a dead end. Say plainly that the first look is free, and that revealing the contact uses a little of their plan's quota and needs their say-so.",
16771
16788
  next_steps: {
16772
16789
  question: "Want to find out who to send that email to?",
16773
16790
  options: [
@@ -16790,8 +16807,8 @@ var init_getting_started = __esm({
16790
16807
  leadIds: "[<the ONE lead you drafted for at step 3>] \u2014 an ARRAY, always",
16791
16808
  lensId: "<the pinned lens id from step 2>"
16792
16809
  },
16793
- spend: "TWO BEATS \u2014 free preview FIRST, the real reveal only after the user confirms. Beat 1: call leadbay_enrich_titles with the drafted lead's id + lensId and NO titles / NO confirm / NO email / NO phone. That returns mode:'discover' \u2014 the FREE list of job titles at that company. Say plainly that nothing has been spent yet. Beat 2: name the title the draft is addressed to, tell them BEFORE they decide what it costs (one credit per contact revealed \u2014 here that is ONE contact, one credit), and ask them to confirm. Only then call leadbay_enrich_titles AGAIN with leadIds: [<that lead id>] \u2014 ALWAYS the array, even for a single lead: `leadId` singular is not a key this tool reads, so it is dropped and the paid call falls back to the default wishlist selection, charging for the whole batch \u2014 plus the chosen title, confirm:true and email:true. Poll leadbay_bulk_enrich_status with the returned notification_id and lead_ids until all_done (or the count plateaus), and report the contact that actually resolved. NEVER launch the reveal without an explicit confirm: silence is not consent, and neither is 'they clicked the gate'. If they decline, keep the draft and the title and move on \u2014 that is a normal outcome, not a failure.",
16794
- quota_note: "After the reveal, close the loop on gate 1 in one line: one credit per contact revealed, so this cost one. Then say the thing that makes it land \u2014 the draft from gate 3 now has a real person and a real address to go to. Re-check leadbay_account_status if you want to show the moved windows. This is where gate 1's numbers stop being abstract: they just watched them move, and got something for it. Keep it to a line; no pricing pitch."
16810
+ spend: "TWO BEATS \u2014 free preview FIRST, the real reveal only after the user confirms. Beat 1: call leadbay_enrich_titles with the drafted lead's id + lensId and NO titles / NO confirm / NO email / NO phone. That returns mode:'discover' \u2014 the FREE list of job titles at that company. Say plainly that nothing has run yet. Beat 2: name the title the draft is addressed to, tell them BEFORE they decide that revealing it uses their plan's quota (here ONE contact; no amount, no price), and ask them to confirm. Only then call leadbay_enrich_titles AGAIN with leadIds: [<that lead id>] \u2014 ALWAYS the array, even for a single lead: `leadId` singular is not a key this tool reads, so it is dropped and the reveal falls back to the default wishlist selection, revealing the whole batch \u2014 plus the chosen title, confirm:true and email:true. Poll leadbay_bulk_enrich_status with the returned notification_id and lead_ids until all_done (or the count plateaus), and report the contact that actually resolved. NEVER launch the reveal without an explicit confirm: silence is not consent, and neither is 'they clicked the gate'. If they decline, keep the draft and the title and move on \u2014 that is a normal outcome, not a failure.",
16811
+ quota_note: "After the reveal, close the loop on gate 1 in one line: the reveal used a little of their plan's quota, never a price. Then say the thing that makes it land \u2014 the draft from gate 3 now has a real person and a real address to go to. Re-check leadbay_account_status if you want to show the moved windows. This is where gate 1's numbers stop being abstract: they just watched them move, and got something for it. Keep it to a line; no pricing pitch."
16795
16812
  }
16796
16813
  ],
16797
16814
  keep_going: KEEP_GOING,
@@ -17796,6 +17813,11 @@ var init_account_status = __esm({
17796
17813
  agent_memory: { type: "object" }
17797
17814
  }
17798
17815
  },
17816
+ // Set by the MCP server wrapper (NOT this composite) on every call.
17817
+ mcp_version: {
17818
+ type: "string",
17819
+ description: "Version of the Leadbay MCP server answering this call. Answer 'what version of Leadbay are you running' with this value."
17820
+ },
17799
17821
  // Auto-update block. Populated by the MCP server wrapper (NOT this
17800
17822
  // composite) when a newer release is published on GitHub AND the
17801
17823
  // user hasn't suppressed it. When present, the agent should prompt
@@ -19771,7 +19793,7 @@ async function launchOnSelection(client, args, ctx) {
19771
19793
  preview,
19772
19794
  lead_ids: leadIds,
19773
19795
  titles,
19774
- message: `An identical enrichment was started ${claim.seconds_since}s ago and has not returned its job id yet. Nothing was launched twice and no quota was spent.`,
19796
+ message: `An identical enrichment was started ${claim.seconds_since}s ago and has not returned its job id yet. Nothing was launched twice and no quota was used.`,
19775
19797
  next_action: "Wait a few seconds and call leadbay_enrich_titles again with the same arguments \u2014 it will hand back the job id once the first call settles. Do not treat this as a running job; there is no id to poll yet."
19776
19798
  };
19777
19799
  }
@@ -19789,8 +19811,8 @@ async function launchOnSelection(client, args, ctx) {
19789
19811
  phone,
19790
19812
  notification_id: already.notification_id,
19791
19813
  launched_at: already.launched_at,
19792
- message: `An identical enrichment was launched ${already.seconds_since}s ago; this call did NOT spend quota again. Poll the original job rather than relaunching.`,
19793
- next_action: "Poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone, include_contacts: true}) until all_done, or until overall_progress.done holds steady across spaced polls (~15-30s apart) \u2014 unresolvable contacts never flip. Pass titles/email/phone every time: they scope counting to the roles and channel THIS run asked for, so a contact enriched earlier cannot report the run as finished. Then report what landed and name what didn't."
19814
+ message: `An identical enrichment was launched ${already.seconds_since}s ago; this call did NOT use quota again. Poll the original job rather than relaunching.`,
19815
+ next_action: "Poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone, include_contacts: true}) until all_done, or until overall_progress.done holds steady across spaced polls (~15-30s apart) \u2014 a reservation the provider never answers never flips. Pass titles/email/phone every time: they scope counting to the roles and channel THIS run asked for, so a contact enriched earlier cannot report the run as finished. Then report what landed and name what didn't."
19794
19816
  };
19795
19817
  }
19796
19818
  ctx?.progress?.({
@@ -19827,8 +19849,8 @@ async function launchOnSelection(client, args, ctx) {
19827
19849
  lead_ids: leadIds,
19828
19850
  notification_id: notificationId,
19829
19851
  launched_at: remembered.launched_at,
19830
- message: notificationId ? "Enrichment job launched (runs async). Unless the user asked NOT to wait, do NOT end your turn here \u2014 poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone}) until all_done OR until progress plateaus (overall_progress.done stops climbing across spaced polls \u2014 unresolvable contacts keep all_done:false forever), then report the finished contacts yourself. The notification_id keeps working across conversations and days; completion also surfaces via _meta.notifications / leadbay_account_status.notifications." : "Enrichment job launched, but the backend returned no notification_id, so there is no job id to poll. Poll leadbay_bulk_enrich_status({lead_ids, titles, email, phone}) instead \u2014 it answers per lead without a job id.",
19831
- next_action: notificationId ? "Unless the user explicitly asked NOT to wait, poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone, include_contacts: true}) until all_done \u2014 OR until overall_progress.done holds steady across several SPACED polls (~15-30s apart, ~90s-2min elapsed; don't call a plateau from the first back-to-back reads, and not while partial_failures is present \u2014 that's transient, respect retry_after). Carry titles/email/phone on every poll \u2014 they scope counting to the roles and channel THIS run asked for, and without them a contact enriched months ago counts as done and all_done flips true before anything landed. Then report the resolved enrichment in THIS turn, naming what landed and what didn't. If the user DID ask not to wait, hand back the notification_id \u2014 it resolves later from any conversation." : "Poll leadbay_bulk_enrich_status({lead_ids, titles, email, phone, include_contacts: true}) every ~30s. It scopes counting to these titles and treats a contact as done only when the REQUESTED channel landed, so you do not have to do that yourself. Stop once overall_progress.done stops growing across a couple of spaced re-checks (~90s-2min) \u2014 unresolvable contacts never flip."
19852
+ message: notificationId ? "Enrichment job launched (runs async). Unless the user asked NOT to wait, do NOT end your turn here \u2014 poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone}) until all_done OR until progress plateaus (overall_progress.done stops climbing across spaced polls \u2014 a reservation the provider never answers keeps all_done:false forever), then report the finished contacts yourself. The notification_id keeps working across conversations and days; completion also surfaces via _meta.notifications / leadbay_account_status.notifications." : "Enrichment job launched, but the backend returned no notification_id, so there is no job id to poll. Poll leadbay_bulk_enrich_status({lead_ids, titles, email, phone}) instead \u2014 it answers per lead without a job id.",
19853
+ next_action: notificationId ? "Unless the user explicitly asked NOT to wait, poll leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone, include_contacts: true}) until all_done \u2014 OR until overall_progress.done holds steady across several SPACED polls (~15-30s apart, ~90s-2min elapsed; don't call a plateau from the first back-to-back reads, and not while partial_failures is present \u2014 that's transient, respect retry_after). Carry titles/email/phone on every poll \u2014 they scope counting to the roles and channel THIS run asked for, and without them a contact enriched months ago counts as done and all_done flips true before anything landed. Then report the resolved enrichment in THIS turn, naming what landed and what didn't. If the user DID ask not to wait, hand back the notification_id \u2014 it resolves later from any conversation." : "Poll leadbay_bulk_enrich_status({lead_ids, titles, email, phone, include_contacts: true}) every ~30s. It scopes counting to these titles and to the channels this run asked for, and counts a reservation once it has settled, so you do not have to do that yourself. Stop once overall_progress.done stops growing across a couple of spaced re-checks (~90s-2min) \u2014 a reservation the provider never answers never flips."
19832
19854
  };
19833
19855
  }
19834
19856
  async function launchEnrichment(client, args, ctx) {
@@ -19901,14 +19923,14 @@ var init_enrich_titles = __esm({
19901
19923
  },
19902
19924
  confirm: {
19903
19925
  type: "boolean",
19904
- description: "Explicit spend decision for the paid enrichment. true = go ahead and launch. false = do NOT spend (a veto: returns mode:'needs_confirmation' and launches nothing, even on hosts without elicitation, and even if an email/phone channel was set). Omitted (and no explicit email/phone channel) \u2192 an elicitation-capable host asks the user before launching; a decline returns mode:'needs_confirmation'. Passing email:true/phone:true also counts as consent."
19926
+ description: "Explicit go-ahead for the email/phone reveal (uses quota). true = go ahead and launch. false = do NOT launch (a veto: returns mode:'needs_confirmation' and launches nothing, even on hosts without elicitation, and even if an email/phone channel was set). Omitted (and no explicit email/phone channel) \u2192 an elicitation-capable host asks the user before launching; a decline returns mode:'needs_confirmation'. Passing email:true/phone:true also counts as consent."
19905
19927
  }
19906
19928
  },
19907
19929
  additionalProperties: false
19908
19930
  },
19909
19931
  outputSchema: {
19910
19932
  type: "object",
19911
- description: "Branchy return shape; the `mode` (or `status`) field tells the agent which branch it got. Modes: 'discover' (no titles passed), 'preview_only' (no enrichable contacts), 'dry_run', 'needs_confirmation' (paid launch withheld pending user consent), 'already_launched' (idempotent reuse), 'launch_in_flight' (an identical launch is mid-flight and has no id yet), 'launched' (happy path). Status: 'quota_exceeded' (429).",
19933
+ description: "Branchy return shape; the `mode` (or `status`) field tells the agent which branch it got. Modes: 'discover' (no titles passed), 'preview_only' (no enrichable contacts), 'dry_run', 'needs_confirmation' (launch withheld pending user consent), 'already_launched' (idempotent reuse), 'launch_in_flight' (an identical launch is mid-flight and has no id yet), 'launched' (happy path). Status: 'quota_exceeded' (429).",
19912
19934
  properties: {
19913
19935
  mode: {
19914
19936
  type: "string",
@@ -19944,7 +19966,7 @@ var init_enrich_titles = __esm({
19944
19966
  },
19945
19967
  credits_remaining: {
19946
19968
  type: ["number", "string", "null"],
19947
- description: `Advisory-only balance (billing.ai_credits), present in discover / preview_only / dry_run modes. Enrichment is gated by QUOTA (email + phone reveals consume the per-window allowance in leadbay_account_status), NOT by this number \u2014 do NOT present it as a spend gate, do NOT say 'you have N credits', and NEVER refuse enrichment because it's low or zero. Confirm the run by naming enrichable_contacts + the channels, not a credit figure. Null = billing unavailable. The string "unlimited" = an internal/unlimited account: proceed freely and say nothing about credits or quota.`
19969
+ description: `Advisory-only balance (billing.ai_credits), present in discover / preview_only / dry_run modes. Enrichment is gated by QUOTA (email + phone reveals consume the per-window allowance in leadbay_account_status), NOT by this number \u2014 do NOT present it as a gate, do NOT say 'you have N credits', and NEVER refuse enrichment because it's low or zero. Confirm the run by naming enrichable_contacts + the channels, not a credit figure. Null = billing unavailable. The string "unlimited" = an internal/unlimited account: proceed freely and say nothing about credits or quota.`
19948
19970
  },
19949
19971
  selected_lead_count: {
19950
19972
  type: "number",
@@ -20138,8 +20160,8 @@ var init_enrich_titles = __esm({
20138
20160
  enrichable_contacts: preview2.enrichable_contacts,
20139
20161
  credits_remaining: await readCreditsRemaining(client),
20140
20162
  available_titles: availableTitles2,
20141
- message: "Enrichment not launched \u2014 confirm:false was passed (spend declined). Title & LinkedIn are already on the contact (free); enrichment is the PAID email/phone reveal. Re-call with confirm:true (or email:true) to spend.",
20142
- next_action: "Re-call leadbay_enrich_titles with confirm:true once the user approves the spend."
20163
+ message: "Enrichment not launched \u2014 confirm:false was passed (declined). Title & LinkedIn are already on the contact (free); enrichment is the email/phone reveal, which uses quota. Re-call with confirm:true (or email:true) to launch.",
20164
+ next_action: "Re-call leadbay_enrich_titles with confirm:true once the user approves the reveal."
20143
20165
  }
20144
20166
  };
20145
20167
  } else if (!willElicit) {
@@ -20218,8 +20240,8 @@ var init_enrich_titles = __esm({
20218
20240
  enrichable_contacts: preview.enrichable_contacts,
20219
20241
  credits_remaining: creditsRemaining,
20220
20242
  available_titles: availableTitles,
20221
- message: "Enrichment not launched \u2014 awaiting confirmation. Title & LinkedIn are already on the contact (free); enrichment is the PAID email/phone reveal. Re-call with confirm:true (or email:true) to spend.",
20222
- next_action: "Confirm the spend with the user, then call leadbay_enrich_titles again with confirm:true."
20243
+ message: "Enrichment not launched \u2014 awaiting confirmation. Title & LinkedIn are already on the contact (free); enrichment is the email/phone reveal, which uses quota. Re-call with confirm:true (or email:true) to launch.",
20244
+ next_action: "Confirm the reveal with the user, then call leadbay_enrich_titles again with confirm:true."
20223
20245
  };
20224
20246
  }
20225
20247
  return await launchEnrichment(client, {
@@ -20290,11 +20312,11 @@ var init_bulk_enrich_status = __esm({
20290
20312
  },
20291
20313
  email: {
20292
20314
  type: "boolean",
20293
- description: "The `email` flag the launch returned. A contact counts as done only once the requested channel has landed."
20315
+ description: "The `email` flag the launch returned. Scopes counting to reservations that asked for email (`enrichment.email_requested`), so a contact reserved by an earlier phone-only run is not counted in this one."
20294
20316
  },
20295
20317
  phone: {
20296
20318
  type: "boolean",
20297
- description: "The `phone` flag the launch returned. Same rule as `email`."
20319
+ description: "The `phone` flag the launch returned. Same rule as `email`, on `enrichment.phone_requested`."
20298
20320
  },
20299
20321
  include_contacts: {
20300
20322
  type: "boolean",
@@ -20379,22 +20401,21 @@ var init_bulk_enrich_status = __esm({
20379
20401
  }
20380
20402
  if (leadIds.length > 0) {
20381
20403
  const wantTitles = new Set((params.titles ?? []).map((t) => t.trim().toLowerCase()));
20382
- const channelResolved = (c) => {
20383
- if (c?.enrichment?.done !== true)
20384
- return false;
20385
- if (params.email && !c.email)
20404
+ const inThisRun = (c) => {
20405
+ if (params.email && c.enrichment.email_requested === false)
20386
20406
  return false;
20387
- if (params.phone && !c.phone_number)
20407
+ if (params.phone && c.enrichment.phone_requested === false)
20388
20408
  return false;
20389
20409
  return true;
20390
20410
  };
20411
+ const settled = (c) => c.enrichment.done === true;
20391
20412
  let doneSoFar = 0;
20392
20413
  const totalLeads = leadIds.length;
20393
20414
  const results = await pMap(leadIds, async (leadId) => {
20394
20415
  try {
20395
20416
  const out = await getContacts.execute(client, { leadId });
20396
20417
  const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
20397
- const enrichable = contacts.filter((c) => c && c.enrichment && (wantTitles.size === 0 || typeof c.job_title === "string" && wantTitles.has(c.job_title.trim().toLowerCase())));
20418
+ const enrichable = contacts.filter((c) => c && c.enrichment && inThisRun(c) && (wantTitles.size === 0 || typeof c.job_title === "string" && wantTitles.has(c.job_title.trim().toLowerCase())));
20398
20419
  const fe = Array.isArray(out?._fetch_errors) ? out._fetch_errors : [];
20399
20420
  doneSoFar += 1;
20400
20421
  ctx?.progress?.({
@@ -20413,7 +20434,7 @@ var init_bulk_enrich_status = __esm({
20413
20434
  return {
20414
20435
  kind: "ok",
20415
20436
  lead_id: leadId,
20416
- done: enrichable.filter(channelResolved).length,
20437
+ done: enrichable.filter(settled).length,
20417
20438
  total: enrichable.length,
20418
20439
  ...includeContacts ? { contacts } : {}
20419
20440
  };
@@ -21552,7 +21573,7 @@ function mockedSubmitPreview(submit, tool, region) {
21552
21573
  error: true,
21553
21574
  code: "MALFORMED_SUBMIT_RESPONSE",
21554
21575
  message: `${tool}: the submit succeeded but the response carried no job_id, so the job cannot be polled.`,
21555
- hint: "The job may still be running server-side. Do not re-submit blindly \u2014 reuse the same request_id so a retry dedupes instead of double-spending."
21576
+ hint: "The job may still be running server-side. Do not re-submit blindly \u2014 reuse the same request_id so a retry dedupes instead of launching twice."
21556
21577
  };
21557
21578
  }
21558
21579
  return {
@@ -21564,15 +21585,88 @@ function mockedSubmitPreview(submit, tool, region) {
21564
21585
  region
21565
21586
  };
21566
21587
  }
21567
- function splitItems(snapshot) {
21568
- const leads = [];
21569
- const skipped = [];
21570
- for (const item of snapshot.items) {
21571
- if (item.status === "skipped")
21572
- skipped.push(item);
21573
- else
21574
- leads.push(item);
21588
+ function clip(s, n) {
21589
+ if (typeof s !== "string" || s.length === 0)
21590
+ return void 0;
21591
+ return s.length <= n ? s : s.slice(0, n - 1).trimEnd() + "\u2026";
21592
+ }
21593
+ function compactContact(c) {
21594
+ if (!c || typeof c !== "object")
21595
+ return c ?? null;
21596
+ const channels = {};
21597
+ for (const [name, state] of Object.entries(c.channels ?? {})) {
21598
+ const s = state;
21599
+ channels[name] = s?.value != null ? { status: s.status, value: s.value } : { status: s?.status };
21575
21600
  }
21601
+ return {
21602
+ lead_contact_id: c.lead_contact_id,
21603
+ name: c.name,
21604
+ role: clip(c.role, 80),
21605
+ linkedin: c.linkedin,
21606
+ channels
21607
+ };
21608
+ }
21609
+ function compactLead(lead, lean = false) {
21610
+ const company = lead.company ?? {};
21611
+ const fit = lead.fit ?? {};
21612
+ const q = fit.components?.qualification ?? {};
21613
+ const answers = Array.isArray(q.questions) ? q.questions : [];
21614
+ const best = answers.filter((a) => a?.verdict === "yes").sort((a, b) => (b?.score ?? 0) - (a?.score ?? 0))[0];
21615
+ const signals = (Array.isArray(lead.signals) ? lead.signals : []).filter((s) => s?.summary).sort((a, b) => Number(!!b.hot) - Number(!!a.hot) || String(b.date ?? "").localeCompare(String(a.date ?? ""))).slice(0, MAX_SIGNALS).map((s) => ({ summary: clip(s.summary, SIGNAL_CHARS), date: s.date, source_url: s.source_url }));
21616
+ const location = company.location ? { city: company.location.city, region: company.location.region, country: company.location.country } : void 0;
21617
+ return {
21618
+ lead_id: lead.lead_id,
21619
+ company: {
21620
+ name: company.name,
21621
+ website: company.website,
21622
+ location,
21623
+ employees: company.employees,
21624
+ // Kept on trimmed rows too: it is the rendering rules' last fallback for
21625
+ // "why it fits" once the reason and the tags are gone.
21626
+ description: clip(company.description ?? company.short_description, lean ? 80 : 140)
21627
+ },
21628
+ fit: {
21629
+ available: fit.available,
21630
+ score: fit.score,
21631
+ reasoning: lean ? void 0 : clip(best?.reasoning ?? q.ibp?.reasoning, LEAD_REASON_CHARS),
21632
+ components: {
21633
+ qualification: {
21634
+ available: q.available,
21635
+ ai_score: q.ai_score,
21636
+ matched_tags: lean ? void 0 : (Array.isArray(q.matched_tags) ? q.matched_tags : []).map((t) => typeof t === "string" ? t : t?.tag).filter(Boolean)
21637
+ }
21638
+ }
21639
+ },
21640
+ signals: lean ? void 0 : signals,
21641
+ contact: compactContact(lead.contact),
21642
+ alternative_contacts: lean ? void 0 : (Array.isArray(lead.alternative_contacts) ? lead.alternative_contacts : []).slice(0, MAX_ALTERNATIVE_CONTACTS).map(compactContact),
21643
+ // Unbounded, so only full rows carry it: they are measured, trimmed rows
21644
+ // are only reserved for.
21645
+ custom_fields: !lean && Array.isArray(lead.custom_fields) && lead.custom_fields.length > 0 ? lead.custom_fields : void 0,
21646
+ evidence_trimmed: lean ? true : void 0
21647
+ };
21648
+ }
21649
+ function splitItems(snapshot) {
21650
+ const leanRow = (item) => ({
21651
+ ref: item.ref,
21652
+ status: item.status,
21653
+ status_reason: item.status_reason,
21654
+ ...item.resolution ? { resolution: item.resolution } : {},
21655
+ seq: item.seq,
21656
+ ...item.lead ? { lead: compactLead(item.lead, true) } : {}
21657
+ });
21658
+ const skipped = snapshot.items.filter((i) => i.status === "skipped").map(leanRow);
21659
+ const rows = snapshot.items.filter((i) => i.status !== "skipped");
21660
+ let room = RESULT_CHARS - JSON.stringify(skipped).length;
21661
+ const leads = rows.map((item, i) => {
21662
+ const { from_cache: _fromCache, ...rest } = item;
21663
+ const full = item.lead ? { ...rest, lead: compactLead(item.lead) } : rest;
21664
+ const fullChars = JSON.stringify(full).length;
21665
+ const reserve = (rows.length - i - 1) * LEAN_ROW_CHARS;
21666
+ const out = fullChars + reserve <= room ? full : leanRow(item);
21667
+ room -= out === full ? fullChars : JSON.stringify(out).length;
21668
+ return out;
21669
+ });
21576
21670
  return { leads, skipped };
21577
21671
  }
21578
21672
  function compactBody(body) {
@@ -21623,7 +21717,7 @@ function rejectMalformedExclusions(ids) {
21623
21717
  error: true,
21624
21718
  code: "INVALID_EXCLUDE_LEAD_ID",
21625
21719
  message: `exclude_lead_ids has ${bad.length} entr${bad.length === 1 ? "y" : "ies"} that is not a lead id: ${bad.join(", ")}.`,
21626
- hint: "Drop or fix those entries and re-call \u2014 every entry must be a non-blank lead id string. Silently skipping them would run the search without an exclusion you asked for, and could re-deliver and charge for that exact lead."
21720
+ hint: "Drop or fix those entries and re-call \u2014 every entry must be a non-blank lead id string. Silently skipping them would run the search without an exclusion you asked for, and could re-deliver that exact lead and use quota on it again."
21627
21721
  };
21628
21722
  }
21629
21723
  function rejectOversizedExclusions(ids) {
@@ -21678,7 +21772,7 @@ function readSpendFlag(value, field) {
21678
21772
  error: true,
21679
21773
  code: "BAD_INPUT",
21680
21774
  message: `${field} must be a boolean (got ${Array.isArray(value) ? "array" : typeof value}: ${JSON.stringify(value)}).`,
21681
- hint: `Re-call the tool with ${field}: true or ${field}: false as a JSON boolean, not a string or a number. This flag decides whether the user is charged, so an unrecognised value is refused rather than guessed.`
21775
+ hint: `Re-call the tool with ${field}: true or ${field}: false as a JSON boolean, not a string or a number. This flag decides whether the run uses the org's quota, so an unrecognised value is refused rather than guessed.`
21682
21776
  };
21683
21777
  }
21684
21778
  function rejectCountryLocations(locations, region) {
@@ -21729,7 +21823,7 @@ function clampWaitSeconds(requested, fallback) {
21729
21823
  return fallback;
21730
21824
  return Math.min(Math.max(requested, 0), 180);
21731
21825
  }
21732
- var TERMINAL_JOB_STATES, MCP_JOB_POLL, SNAPSHOT_TIMEOUT_MS, PAGE_LIMIT, MAX_JOB_ITEMS, MIN_PAGES, maxPagesFor, JOB_ID_CHARSET, MAX_JOB_ID_LENGTH, UUID_RE2, COUNTRY_ALIASES, SUBNATIONAL_EXEMPTIONS, ALL_EXEMPTIONS, COUNTRY_LOCATION_VALUES, MAX_EXCLUDE_LEAD_IDS, MAX_LEAD_REFS;
21826
+ var TERMINAL_JOB_STATES, MCP_JOB_POLL, SNAPSHOT_TIMEOUT_MS, PAGE_LIMIT, MAX_JOB_ITEMS, MIN_PAGES, maxPagesFor, JOB_ID_CHARSET, MAX_JOB_ID_LENGTH, UUID_RE2, LEAD_REASON_CHARS, SIGNAL_CHARS, MAX_SIGNALS, MAX_ALTERNATIVE_CONTACTS, RESULT_CHARS, LEAN_ROW_CHARS, COUNTRY_ALIASES, SUBNATIONAL_EXEMPTIONS, ALL_EXEMPTIONS, COUNTRY_LOCATION_VALUES, MAX_EXCLUDE_LEAD_IDS, MAX_LEAD_REFS;
21733
21827
  var init_mcp_job_helpers = __esm({
21734
21828
  "../core/dist/composite/_mcp-job-helpers.js"() {
21735
21829
  "use strict";
@@ -21749,6 +21843,12 @@ var init_mcp_job_helpers = __esm({
21749
21843
  JOB_ID_CHARSET = /^[A-Za-z0-9._~-]+$/;
21750
21844
  MAX_JOB_ID_LENGTH = 200;
21751
21845
  UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21846
+ LEAD_REASON_CHARS = 180;
21847
+ SIGNAL_CHARS = 140;
21848
+ MAX_SIGNALS = 2;
21849
+ MAX_ALTERNATIVE_CONTACTS = 1;
21850
+ RESULT_CHARS = 52e3;
21851
+ LEAN_ROW_CHARS = 700;
21752
21852
  COUNTRY_ALIASES = [
21753
21853
  "united states",
21754
21854
  "united states of america",
@@ -21862,7 +21962,7 @@ var init_find_new_leads = __esm({
21862
21962
  },
21863
21963
  qualify: {
21864
21964
  type: "boolean",
21865
- description: "Run fresh AI qualification and drop candidates scoring below min_ai_score. PAID: ~94 cost_cents per candidate EXAMINED (survivor or not). Default false (free)."
21965
+ description: "Run fresh AI qualification and drop candidates scoring below min_ai_score. Uses the org's usage quota for every candidate EXAMINED (survivor or not). Default false (free)."
21866
21966
  },
21867
21967
  min_ai_score: {
21868
21968
  type: "number",
@@ -21881,7 +21981,7 @@ var init_find_new_leads = __esm({
21881
21981
  channels: {
21882
21982
  type: "array",
21883
21983
  items: { type: "string", enum: ["email", "phone"] },
21884
- description: "Contact channels to PURCHASE (email 25c, phone 250c, billed on success only). Empty = free identity tier."
21984
+ description: "Contact channels to find (email, phone). Uses the org's usage quota only when a value is found. Empty = free identity tier."
21885
21985
  },
21886
21986
  exclude_lead_ids: {
21887
21987
  type: "array",
@@ -21895,7 +21995,7 @@ var init_find_new_leads = __esm({
21895
21995
  },
21896
21996
  max_cost: {
21897
21997
  type: "number",
21898
- description: "Spend cap for the whole job in cost_cents. Defaults by plan tier (500/2000/5000). The job stops honestly at the cap (stop_reason max_cost)."
21998
+ description: "Usage cap for the whole job, in internal units. Leave it unset: the default (100000) covers any job. What the requested channels need is kept for them; a cap below that is refused, naming the minimum. Never show it to the user as money."
21899
21999
  },
21900
22000
  exploration_cap: {
21901
22001
  type: "number",
@@ -21903,20 +22003,20 @@ var init_find_new_leads = __esm({
21903
22003
  },
21904
22004
  request_id: {
21905
22005
  type: "string",
21906
- description: "REQUIRED idempotency key. Derive it from the ask (e.g. 'gyms-texas-2026-07-28'); REUSE the exact same value when retrying the same ask \u2014 a duplicate returns the SAME job instead of double-spending. Use a NEW value only for a genuinely new ask."
22006
+ description: "REQUIRED idempotency key. Derive it from the ask (e.g. 'gyms-texas-2026-07-28'); REUSE the exact same value when retrying the same ask \u2014 a duplicate returns the SAME job instead of launching twice. Use a NEW value only for a genuinely new ask."
21907
22007
  },
21908
22008
  lang: { type: "string", description: "Output language (default: user's language)." },
21909
22009
  confirm: {
21910
22010
  type: "boolean",
21911
- description: "Explicit spend decision, required only for a PAID search (qualify:true and/or channels). true = the user approved the quote, go ahead. false = a veto (returns mode:'needs_confirmation', spends nothing). Omitted on a paid call \u2192 the tool withholds the submit and returns a free quote to show the user first. The default FREE search (no qualify, no channels) needs no confirm."
22011
+ description: "Explicit go-ahead, required only for a search that uses quota (qualify:true and/or channels). true = the user approved the quote, go ahead. false = a veto (returns mode:'needs_confirmation', uses nothing). Omitted on such a call \u2192 the tool withholds the submit and returns a free quote to show the user first. The default FREE search (no qualify, no channels) needs no confirm."
21912
22012
  },
21913
22013
  dry_run: {
21914
22014
  type: "boolean",
21915
- description: "Validate + worst-case cost estimate + quota forecast. No job, no spend. Use before the first PAID run of a session."
22015
+ description: "Validate + worst-case usage estimate + quota forecast. No job, uses nothing. Use before the first quota-using run of a session."
21916
22016
  },
21917
22017
  wait_seconds: {
21918
22018
  type: "number",
21919
- description: "How long to poll before returning (default 45, max 180, 0 = submit + one snapshot). Free searches usually finish inside the window; paid exploration can take minutes \u2014 the result then carries still_running:true and the job_id to check with leadbay_lead_job_status."
22019
+ description: "How long to poll before returning (default 45, max 180, 0 = submit + one snapshot). Free searches usually finish inside the window; qualified exploration can take minutes \u2014 the result then carries still_running:true and the job_id to check with leadbay_lead_job_status."
21920
22020
  }
21921
22021
  },
21922
22022
  required: ["count", "request_id"],
@@ -22018,13 +22118,13 @@ var init_find_new_leads = __esm({
22018
22118
  submitted: false,
22019
22119
  vetoed,
22020
22120
  paid_because: [
22021
- buysQualification ? "qualify: true (~94 cost_cents per candidate EXAMINED)" : null,
22121
+ buysQualification ? "qualify: true (uses quota per candidate examined)" : null,
22022
22122
  buysChannels ? `channels requested: ${params.channels.join(", ")}` : null
22023
22123
  ].filter(Boolean),
22024
22124
  quote: forecast,
22025
22125
  estimated_cost: forecast?.estimated_cost ?? null,
22026
22126
  items_requested: forecast?.items_requested ?? null,
22027
- hint: vetoed ? "confirm:false vetoed the spend \u2014 nothing was submitted. Re-call with confirm:true to proceed, or drop qualify/channels for a free search." : "Show the user this worst-case quote and get an explicit go-ahead, then re-call with confirm:true. For a free search instead: omit qualify and channels.",
22127
+ hint: vetoed ? "confirm:false vetoed the run \u2014 nothing was submitted. Re-call with confirm:true to proceed, or drop qualify/channels for a free search." : "Tell the user what will run and that it uses their plan's quota (no amounts, no money), get an explicit go-ahead, then re-call with confirm:true. For a free search instead: omit qualify and channels.",
22028
22128
  region: client.region
22029
22129
  };
22030
22130
  }
@@ -22240,7 +22340,7 @@ var init_qualify_leads = __esm({
22240
22340
  },
22241
22341
  prior_deliveries: {
22242
22342
  type: "object",
22243
- description: "Selector expanding the org's past MCP deliveries into refs \u2014 billed leads stay re-readable after result expiry. Combine with lead_refs or use alone.",
22343
+ description: "Selector expanding the org's past MCP deliveries into refs \u2014 delivered leads stay re-readable after result expiry. Combine with lead_refs or use alone.",
22244
22344
  properties: {
22245
22345
  job_id: { type: "string" },
22246
22346
  since: { type: "string", description: "ISO instant lower bound." },
@@ -22250,7 +22350,7 @@ var init_qualify_leads = __esm({
22250
22350
  },
22251
22351
  qualify: {
22252
22352
  type: "boolean",
22253
- description: "Fresh AI qualification (default true; ~94 cost_cents per lead needing fresh research+scoring, cache-free when a fresh dossier exists). Owned disqualified leads come back WITH their negative evidence."
22353
+ description: "Fresh AI qualification (default true; uses the org's usage quota for each lead needing fresh research+scoring, none when a fresh dossier exists). Owned disqualified leads come back WITH their negative evidence."
22254
22354
  },
22255
22355
  contact_titles: {
22256
22356
  type: "array",
@@ -22265,24 +22365,24 @@ var init_qualify_leads = __esm({
22265
22365
  channels: {
22266
22366
  type: "array",
22267
22367
  items: { type: "string", enum: ["email", "phone"] },
22268
- description: "Channels to PURCHASE (email 25c, phone 250c, success-only, already-owned values are free). Empty = free identity tier."
22368
+ description: "Channels to find (email, phone). Uses the org's usage quota only when a value is found; already-owned values are free. Empty = free identity tier."
22269
22369
  },
22270
22370
  max_cost: {
22271
22371
  type: "number",
22272
- description: "Spend cap in cost_cents (plan-tier default when unset)."
22372
+ description: "Usage cap in internal units; leave it unset (default 100000). What the requested channels need is kept for them; a cap below that is refused, naming the minimum. Never show it as money."
22273
22373
  },
22274
22374
  request_id: {
22275
22375
  type: "string",
22276
- description: "Recommended idempotency key \u2014 REUSE the same value when retrying the same batch so a retry returns the SAME job instead of re-spending."
22376
+ description: "Recommended idempotency key \u2014 REUSE the same value when retrying the same batch so a retry returns the SAME job instead of running it again."
22277
22377
  },
22278
22378
  lang: { type: "string", description: "Output language (default: user's language)." },
22279
22379
  confirm: {
22280
22380
  type: "boolean",
22281
- description: "Explicit spend decision for the PAID work (fresh qualification and/or channel purchases). true = the user approved the quote, go ahead. false = a veto (returns mode:'needs_confirmation', spends nothing). Omitted on a paid call \u2192 the tool withholds the submit and returns a free quote to show the user first. A fully FREE call (qualify:false and no channels) needs no confirm."
22381
+ description: "Explicit go-ahead for the work that uses quota (fresh qualification and/or channels). true = the user approved, go ahead. false = a veto (returns mode:'needs_confirmation', uses nothing). Omitted on such a call \u2192 the tool withholds the submit and returns a free quote to show the user first. A fully FREE call (qualify:false and no channels) needs no confirm."
22282
22382
  },
22283
22383
  dry_run: {
22284
22384
  type: "boolean",
22285
- description: "Validate + worst-case cost + quota forecast. No job, no spend."
22385
+ description: "Validate + worst-case usage + quota forecast. No job, uses nothing."
22286
22386
  },
22287
22387
  wait_seconds: {
22288
22388
  type: "number",
@@ -22342,7 +22442,7 @@ var init_qualify_leads = __esm({
22342
22442
  quote: forecast,
22343
22443
  estimated_cost: forecast?.estimated_cost ?? null,
22344
22444
  items_requested: forecast?.items_requested ?? null,
22345
- hint: vetoed ? "confirm:false vetoed the spend \u2014 nothing was submitted. Re-call with confirm:true to proceed, or qualify:false with no channels for a free pass." : "Show the user this worst-case quote and get an explicit go-ahead, then re-call with confirm:true. For a free pass instead: qualify:false and no channels.",
22445
+ hint: vetoed ? "confirm:false vetoed the run \u2014 nothing was submitted. Re-call with confirm:true to proceed, or qualify:false with no channels for a free pass." : "Tell the user what will run and that it uses their plan's quota (no amounts, no money), get an explicit go-ahead, then re-call with confirm:true. For a free pass instead: qualify:false and no channels.",
22346
22446
  region: client.region
22347
22447
  };
22348
22448
  }
@@ -24542,7 +24642,7 @@ What Leadbay does need is anything that changes **who it should find**. When the
24542
24642
 
24543
24643
  Build me a Leadbay campaign from scratch{{arg:campaign_name_paren}} \u2014 a cohort of **{{arg:count_or_default}}** fully-actionable leads: each in-ICP, high \`ai_agent_lead_score\`, AND with a reachable buyer contact. {{arg:audience_block}} {{arg:job_titles_block}}
24544
24644
 
24545
- **Run this end-to-end, autonomously, without pausing.** Do NOT stop to confirm the audience, do NOT stop to confirm the enrichment spend, do NOT ask me to pick, and do NOT stop to hand off \u2014 just keep discovering, qualifying, enriching, and swapping until the cohort holds **{{arg:count_or_default}}** leads that each meet EVERY requirement (in-ICP, high \`ai_agent_lead_score\`, and a reachable target-title contact whose email/phone actually landed). The ONLY reasons to stop short: the lens genuinely can't supply that many buyer-ready in-ICP leads, or enrichment quota is exhausted (a backend 429). In those cases, finish with whatever you locked and tell me plainly how many you got and why it stopped. Enrichment consumes quota, not credits \u2014 never pre-refuse on a credit balance.
24645
+ **Run this end-to-end, autonomously, without pausing.** Do NOT stop to confirm the audience, do NOT stop to confirm the enrichment, do NOT ask me to pick, and do NOT stop to hand off \u2014 just keep discovering, qualifying, enriching, and swapping until the cohort holds **{{arg:count_or_default}}** leads that each meet EVERY requirement (in-ICP, high \`ai_agent_lead_score\`, and a reachable target-title contact whose email/phone actually landed). The ONLY reasons to stop short: the lens genuinely can't supply that many buyer-ready in-ICP leads, or enrichment quota is exhausted (a backend 429). In those cases, finish with whatever you locked and tell me plainly how many you got and why it stopped. Enrichment consumes quota, not credits \u2014 never pre-refuse on a credit balance.
24546
24646
 
24547
24647
  GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
24548
24648
 
@@ -24580,7 +24680,7 @@ If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retr
24580
24680
  Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
24581
24681
  \`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
24582
24682
  running result, that work is queued on Leadbay and runs to completion, and the
24583
- quota it costs is already committed. A discovery, preview or \`dry_run\` result
24683
+ quota it uses is already committed. A discovery, preview or \`dry_run\` result
24584
24684
  launched nothing and is not covered here.
24585
24685
 
24586
24686
  The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
@@ -24588,7 +24688,7 @@ waiting, never the job. \`cancelled: true\` means we stopped watching, not that
24588
24688
  work stopped. What to do next depends on what you are holding:
24589
24689
 
24590
24690
  - **A handle.** Poll the status tool with it, and do not launch the work that
24591
- handle covers a second time \u2014 that spends the quota again on the same rows.
24691
+ handle covers a second time \u2014 that uses the quota again on the same rows.
24592
24692
  \`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
24593
24693
  under that name. A qualification started by \`leadbay_import_and_qualify\` has no
24594
24694
  notification of its own: resume it with
@@ -24602,7 +24702,7 @@ work stopped. What to do next depends on what you are holding:
24602
24702
  finished. Calling the same tool again with the same arguments will usually hand
24603
24703
  back the job already launched rather than starting a second one, but that guard
24604
24704
  is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
24605
- are about to re-run before you spend the user's quota on it.
24705
+ are about to re-run before you use the user's quota on it.
24606
24706
 
24607
24707
 
24608
24708
 
@@ -24701,7 +24801,7 @@ If I named specific leads, seed with those (still apply the Phase 3 buyer-covera
24701
24801
 
24702
24802
  # PHASE 3 \u2014 ENRICH THE RIGHT CONTACTS (load-bearing)
24703
24803
 
24704
- This is the phase that decides whether the campaign is worth a salesperson's time. Contacts aren't attached by default and enrichment is paid \u2014 so spend it ONLY on the people who would actually **buy what I sell**, at the target titles, not on whoever is most senior.
24804
+ This is the phase that decides whether the campaign is worth a salesperson's time. Contacts aren't attached by default and enrichment uses quota \u2014 so use it ONLY on the people who would actually **buy what I sell**, at the target titles, not on whoever is most senior.
24705
24805
 
24706
24806
  **Step A \u2014 settle the target titles / buyer persona.**
24707
24807
 
@@ -24727,7 +24827,7 @@ Call \`leadbay_recall_ordered_titles({leadIds, lensId})\` and \`leadbay_enrich_t
24727
24827
 
24728
24828
  Tell me what you swapped in one line ("dropped Corbett + RBS \u2014 ops-only; swapped in Acme + Globex which have Sales VPs").
24729
24829
 
24730
- **Step C \u2014 enrich (NO confirm gate \u2014 just spend).** You do NOT need my permission: I authorized this spend by asking for the campaign. Do NOT call \`ask_user_input_v0\`, do NOT ask "enrich these N now?", do NOT wait. State the persona + titles + "enriching {enrichable_contacts} contacts (email + phone, consumes quota)" in one line for the record, then immediately launch: \`leadbay_enrich_titles({leadIds, lensId, titles:[...chosen], email:true, phone:true})\`. Enrich up to {{arg:count_or_default}} best target-title contacts. Do NOT quote a "credits" figure or refuse on a credit balance \u2014 the only real limit is quota (a backend 429). If a 429 stops you mid-run, keep the leads already enriched, note how many landed, and continue to Phase 4 with those.
24830
+ **Step C \u2014 enrich (NO confirm gate \u2014 just launch).** You do NOT need my permission: I authorized this enrichment by asking for the campaign. Do NOT call \`ask_user_input_v0\`, do NOT ask "enrich these N now?", do NOT wait. State the persona + titles + "enriching {enrichable_contacts} contacts (email + phone, consumes quota)" in one line for the record, then immediately launch: \`leadbay_enrich_titles({leadIds, lensId, titles:[...chosen], email:true, phone:true})\`. Enrich up to {{arg:count_or_default}} best target-title contacts. Do NOT quote a "credits" figure or refuse on a credit balance \u2014 the only real limit is quota (a backend 429). If a 429 stops you mid-run, keep the leads already enriched, note how many landed, and continue to Phase 4 with those.
24731
24831
 
24732
24832
  **Step D \u2014 poll + count only landed.** Poll \`leadbay_bulk_enrich_status\` until done (enrichment can take several minutes \u2014 keep polling, don't render an empty sheet prematurely). Once \`all_done\`, call \`leadbay_account_status\` and show my refreshed quota so I see what the run consumed. A lead only counts toward the {{arg:count_or_default}} once its target-title contact actually landed (email/phone present); if some came back empty, swap + enrich replacements (loop back to Step B.5) until the cohort is genuinely {{arg:count_or_default}} deep or the lens is exhausted.
24733
24833
 
@@ -24752,7 +24852,7 @@ Building a campaign is NOT outreaching \u2014 do not send anything and do not ca
24752
24852
  # Iron laws
24753
24853
 
24754
24854
  - **Run to the goal, autonomously.** Keep discovering \u2192 qualifying \u2192 enriching \u2192 swapping until the cohort holds {{arg:count_or_default}} leads that are ALL in-ICP, high-score, and buyer-covered \u2014 or the lens is genuinely exhausted. Do NOT stop early, do NOT ask me to pick, do NOT hand off mid-flow.
24755
- - **No confirm gates. No pauses.** Do NOT confirm the audience switch, and do NOT confirm the enrichment spend (no \`ask_user_input_v0\` before enriching) \u2014 asking for the campaign IS the authorization. The only acceptable stops are lens exhaustion or a backend 429.
24855
+ - **No confirm gates. No pauses.** Do NOT confirm the audience switch, and do NOT confirm the enrichment (no \`ask_user_input_v0\` before enriching) \u2014 asking for the campaign IS the authorization. The only acceptable stops are lens exhaustion or a backend 429.
24756
24856
  - Enrichment targets MY buyer titles \u2014 the people who would actually buy what *I* sell (my given titles, or the persona derived from my product/ICP) \u2014 NOT generic seniority. For a sales/prospecting tool that means the revenue org; a Director of Operations, COO, or logistics manager is useless no matter how senior.
24757
24857
  - Selection is DATA-DRIVEN (\`leadbay_recall_ordered_titles\` + \`leadbay_enrich_titles\` discovery) but FILTERED to the target titles \u2014 never blindly repeat past-enriched or suggested titles that don't match who buys my product.
24758
24858
  - The FINAL cohort must be all buyer-ready: a lead counts only once its target-title contact actually landed. Drop/swap + re-enrich any lead with no reachable buyer rather than shipping it empty.
@@ -24802,7 +24902,7 @@ If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retr
24802
24902
  Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
24803
24903
  \`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
24804
24904
  running result, that work is queued on Leadbay and runs to completion, and the
24805
- quota it costs is already committed. A discovery, preview or \`dry_run\` result
24905
+ quota it uses is already committed. A discovery, preview or \`dry_run\` result
24806
24906
  launched nothing and is not covered here.
24807
24907
 
24808
24908
  The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
@@ -24810,7 +24910,7 @@ waiting, never the job. \`cancelled: true\` means we stopped watching, not that
24810
24910
  work stopped. What to do next depends on what you are holding:
24811
24911
 
24812
24912
  - **A handle.** Poll the status tool with it, and do not launch the work that
24813
- handle covers a second time \u2014 that spends the quota again on the same rows.
24913
+ handle covers a second time \u2014 that uses the quota again on the same rows.
24814
24914
  \`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
24815
24915
  under that name. A qualification started by \`leadbay_import_and_qualify\` has no
24816
24916
  notification of its own: resume it with
@@ -24824,7 +24924,7 @@ work stopped. What to do next depends on what you are holding:
24824
24924
  finished. Calling the same tool again with the same arguments will usually hand
24825
24925
  back the job already launched rather than starting a second one, but that guard
24826
24926
  is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
24827
- are about to re-run before you spend the user's quota on it.
24927
+ are about to re-run before you use the user's quota on it.
24828
24928
 
24829
24929
 
24830
24930
 
@@ -25064,7 +25164,7 @@ If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retr
25064
25164
  Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
25065
25165
  \`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
25066
25166
  running result, that work is queued on Leadbay and runs to completion, and the
25067
- quota it costs is already committed. A discovery, preview or \`dry_run\` result
25167
+ quota it uses is already committed. A discovery, preview or \`dry_run\` result
25068
25168
  launched nothing and is not covered here.
25069
25169
 
25070
25170
  The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
@@ -25072,7 +25172,7 @@ waiting, never the job. \`cancelled: true\` means we stopped watching, not that
25072
25172
  work stopped. What to do next depends on what you are holding:
25073
25173
 
25074
25174
  - **A handle.** Poll the status tool with it, and do not launch the work that
25075
- handle covers a second time \u2014 that spends the quota again on the same rows.
25175
+ handle covers a second time \u2014 that uses the quota again on the same rows.
25076
25176
  \`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
25077
25177
  under that name. A qualification started by \`leadbay_import_and_qualify\` has no
25078
25178
  notification of its own: resume it with
@@ -25086,7 +25186,7 @@ work stopped. What to do next depends on what you are holding:
25086
25186
  finished. Calling the same tool again with the same arguments will usually hand
25087
25187
  back the job already launched rather than starting a second one, but that guard
25088
25188
  is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
25089
- are about to re-run before you spend the user's quota on it.
25189
+ are about to re-run before you use the user's quota on it.
25090
25190
 
25091
25191
 
25092
25192
 
@@ -25273,7 +25373,7 @@ days"), computed against now \u2014 mirroring the widget's "r\xE9initialis\xE9 d
25273
25373
  raw value is an ISO-8601 timestamp.
25274
25374
 
25275
25375
  **Top-up (optional, subordinate).** When \`quota.topup\` is present, you MAY add one
25276
- small line below the windows: \`Top-up: $<remaining_cents/100> of $<total_credit_cents/100> left\`.
25376
+ small line below the windows: \`Top-up: $<(remaining_cents / 100).toFixed(2)> of $<(total_credit_cents / 100).toFixed(2)> left\`.
25277
25377
  Keep it secondary \u2014 the three window gauges are the headline. Omit when null.
25278
25378
 
25279
25379
  **Legend** (once, below): \`\` \`\u25B0\` used \xB7 \`\u25B1\` remaining \`\`.
@@ -25442,9 +25542,9 @@ Say plainly that this only **drafts** \u2014 nothing is sent, and they see it fi
25442
25542
  On click: call \`leadbay_prepare_outreach\` with \`leadId\` = the top lead's id,
25443
25543
  **and nothing else**.
25444
25544
 
25445
- **This gate spends NOTHING. Never pass \`enrich: true\`** \u2014 that launches a paid
25545
+ **This gate uses NO quota. Never pass \`enrich: true\`** \u2014 that launches a
25446
25546
  contact reveal off the back of a *draft* click. They agreed to see an email
25447
- written, not to spend. GATE 4 is where the reveal gets asked for, on its own
25547
+ written, not to reveal anyone. GATE 4 is where the reveal gets asked for, on its own
25448
25548
  terms.
25449
25549
 
25450
25550
  \`recommended_contact\` comes back in its post-enrichment shape with \`email\` and
@@ -25478,7 +25578,7 @@ pitching whoever answers the switchboard \u2014 the difference between a
25478
25578
  conversation and a dead end.
25479
25579
 
25480
25580
  Say plainly that the first look is **free**, and that revealing the contact
25481
- costs credits and needs their say-so.
25581
+ uses a little of their plan's quota and needs their say-so.
25482
25582
 
25483
25583
  **First, check \`leadbay_enrich_titles\` is in your tool set.** On a read-only
25484
25584
  deployment it is not registered, and a gate whose tool cannot run is a dead
@@ -25491,23 +25591,23 @@ does nothing.
25491
25591
 
25492
25592
  This gate runs in **TWO BEATS**. Do not collapse them.
25493
25593
 
25494
- ## BEAT 1 \u2014 the free look (spends nothing)
25594
+ ## BEAT 1 \u2014 the free look (uses no quota)
25495
25595
 
25496
25596
  On click: call \`leadbay_enrich_titles\` with \`leadIds\` = **the one lead you
25497
25597
  drafted for at GATE 3** and \`lensId\` = the pinned lens id.
25498
25598
 
25499
- **This call must spend NOTHING.** Omit \`titles\` entirely: that returns
25599
+ **This call must use NO quota.** Omit \`titles\` entirely: that returns
25500
25600
  \`mode:"discover"\`, the free preview of which job titles exist at that company.
25501
25601
  Do NOT pass \`titles\`, \`confirm=true\`, \`email=true\` or \`phone=true\` on this call
25502
- \u2014 any one of them launches the paid reveal before the user has chosen anything.
25602
+ \u2014 any one of them launches the reveal before the user has chosen anything.
25503
25603
 
25504
- Present the discovered titles and say plainly: "nothing spent yet."
25604
+ Present the discovered titles and say plainly: "nothing revealed yet."
25505
25605
 
25506
- ## BEAT 2 \u2014 reveal the person the draft is for (spends credits)
25606
+ ## BEAT 2 \u2014 reveal the person the draft is for (uses quota)
25507
25607
 
25508
- Name the title the GATE 3 draft is addressed to, and tell them the cost
25509
- **before** they decide: one credit per contact revealed \u2014 here that's **one
25510
- contact, one credit**. Then ask them to confirm.
25608
+ Name the title the GATE 3 draft is addressed to, and tell them **before** they
25609
+ decide that revealing **one contact** uses a little of their plan's quota. No
25610
+ amount, no price. Then ask them to confirm.
25511
25611
 
25512
25612
  **Wait for an explicit confirmation.** Silence is not consent, and neither is
25513
25613
  "they clicked the gate earlier" \u2014 the gate click bought the free look, not the
@@ -25515,13 +25615,13 @@ reveal.
25515
25615
 
25516
25616
  Once confirmed, call \`leadbay_enrich_titles\` AGAIN with
25517
25617
  \`leadIds: [<the drafted lead's id>]\` \u2014 **the array, always, even for one lead**
25518
- \u2014 plus the chosen \`titles\`, \`confirm: true\` and \`email: true\`. That's the real,
25519
- paid reveal.
25618
+ \u2014 plus the chosen \`titles\`, \`confirm: true\` and \`email: true\`. That's the real
25619
+ reveal.
25520
25620
 
25521
25621
  \`leadIds\` is the only key this tool reads for scope. A singular \`leadId\` is not
25522
25622
  a parameter: it is silently ignored, and the call then falls back to the
25523
25623
  account's **default wishlist selection** while \`confirm\`/\`email\` are set \u2014 so
25524
- it would reveal and charge for the whole batch instead of the one lead the user
25624
+ it would reveal the whole batch instead of the one lead the user
25525
25625
  agreed to.
25526
25626
 
25527
25627
  It returns a \`notification_id\` and runs async \u2014 poll \`leadbay_bulk_enrich_status\`
@@ -25530,8 +25630,8 @@ count plateaus across a few spaced polls. Then report the contact that actually
25530
25630
  resolved: name, title, and the email/phone that came back. Contacts sometimes
25531
25631
  don't resolve; say so honestly rather than implying success.
25532
25632
 
25533
- **Then close the loop** \u2014 one line: one credit per contact revealed, so this
25534
- cost one. And say the thing that makes it land: the draft from GATE 3 now has a
25633
+ **Then close the loop** \u2014 one line: the reveal used a little of their plan's
25634
+ quota, never a price. And say the thing that makes it land: the draft from GATE 3 now has a
25535
25635
  real person and a real address to go to. This is the moment GATE 1's quota
25536
25636
  numbers stop being abstract, because they just watched them move and got
25537
25637
  something for it. Don't turn it into a pricing pitch.
@@ -25891,12 +25991,12 @@ Call \`leadbay_find_new_leads\` with the seed, \`filters\`, \`count\`,
25891
25991
  from the ask + the ARCHETYPE + today's date. \`count\` is the TOTAL I asked
25892
25992
  for, not a per-search number: with two archetypes and a request for 10,
25893
25993
  split it (5 + 5, or whatever weighting fits my ask) rather than sending 10
25894
- to each \u2014 otherwise I get 20 leads and, on the paid pass, pay for 20.
25994
+ to each \u2014 otherwise I get 20 leads and, on the qualifying pass, use quota on 20.
25895
25995
 
25896
25996
  When you RETRY a search \u2014 it timed out, or the job is still live \u2014 reuse the
25897
25997
  \`request_id\` you already sent, verbatim. Do not recompute it: rederiving from
25898
25998
  "today's date" after midnight yields a new key, the backend cannot dedupe, and
25899
- a second paid, novelty-claiming search launches. Roll the date only when I am
25999
+ a second quota-using, novelty-claiming search launches. Roll the date only when I am
25900
26000
  genuinely asking for a new batch. The archetype component is not
25901
26001
  optional: \`request_id\` is the idempotency key, so two archetype searches
25902
26002
  sharing one id dedupe to the same job and the second archetype is never
@@ -25911,52 +26011,54 @@ kind of companies I asked for?
25911
26011
  - **Off-profile or empty** (terminal) \u2192 read \`funnel\` +
25912
26012
  \`explain.scope_notes\`, tell me what went wrong in one line (wrong
25913
26013
  archetype? too narrow a filter? thin universe?), reshape the seed or
25914
- filters, and retry under a NEW request_id. Reshaping is free; do not pay
25915
- to explore a bad seed.
26014
+ filters, and retry under a NEW request_id. Reshaping is free; do not use
26015
+ quota to explore a bad seed.
25916
26016
 
25917
- # PHASE 4 \u2014 PAID DEPTH (only with my explicit go-ahead)
26017
+ # PHASE 4 \u2014 QUALIFIED DEPTH (uses my plan's quota; only with my explicit go-ahead)
25918
26018
 
25919
26019
  When I want qualification evidence and/or reachable contacts:
25920
- 1. Quote first: \`dry_run: true\` on the tool you will actually run, with the
25921
- exact flags I asked for, and tell me the worst-case cost in plain money.
26020
+ 1. Check first: \`dry_run: true\` on the tool you will actually run, with the
26021
+ exact flags I asked for. Tell me what will run (how many companies, which
26022
+ channels) and that it uses my plan's quota. No amounts and no money: my plan
26023
+ or top-up covers this, and a price reads as a bill.
25922
26024
  The two tools take DIFFERENT flags \u2014 passing the wrong one is rejected
25923
26025
  outright (\`additionalProperties: false\`):
25924
26026
  - \`leadbay_qualify_leads\`: \`qualify: true\`, \`contact_titles\`,
25925
26027
  \`title_gate\`, \`channels\`, \`max_cost\`. **No \`min_ai_score\`.**
25926
26028
  - \`leadbay_find_new_leads\`: the same, PLUS \`min_ai_score\` and \`count\`.
25927
26029
  2. On my go-ahead, prefer feeding the free preview's deliveries to
25928
- \`leadbay_qualify_leads\` (\`prior_deliveries: {job_id}\`) \u2014 one paid pass PER
26030
+ \`leadbay_qualify_leads\` (\`prior_deliveries: {job_id}\`) \u2014 one qualifying pass PER
25929
26031
  preview job when Phase 3 ran several archetypes, or merge their delivered
25930
26032
  refs into a single \`lead_refs\` call. Never qualify just the first job and
25931
26033
  call it done: the other archetypes are part of what I asked for. It only
25932
- spends on
25933
- companies already known to match. Paid calls need \`confirm: true\`; without
26034
+ uses quota on
26035
+ companies already known to match. Calls that use quota need \`confirm: true\`; without
25934
26036
  it the tool withholds the submit and hands back a quote instead of
25935
- spending. That applies to \`leadbay_find_new_leads\` too whenever you set
26037
+ running. That applies to \`leadbay_find_new_leads\` too whenever you set
25936
26038
  \`qualify: true\` or ask for channels.
25937
26039
 
25938
26040
  If the preview delivered FEWER than I asked for, do both halves and do not
25939
26041
  conflate them: qualify what the preview already found, and run the fresh
25940
26042
  search only for the SHORTFALL \u2014 \`count\` = what is still missing, never the
25941
26043
  original number, under a NEW \`request_id\`. Reusing the preview's id dedupes
25942
- the paid submit back into the free job; keeping the original count buys a
26044
+ the qualifying submit back into the free job; keeping the original count runs a
25943
26045
  whole second batch, because \`novelty: org\` already excludes everything the
25944
26046
  preview delivered.
25945
26047
 
25946
- The same arithmetic applies AFTER the paid pass. A full-count preview can
26048
+ The same arithmetic applies AFTER the qualifying pass. A full-count preview can
25947
26049
  still end short once qualification disqualifies rows or a strict title /
25948
26050
  channel match misses: what I asked for is n QUALIFIED, CONTACTABLE leads,
25949
26051
  not n examined. Count the delivered-and-callable rows; if they fall short,
25950
- tell me the gap in one line and offer to top it up \u2014 another shortfall-sized
25951
- search under a NEW \`request_id\`, quoted first like any paid run. Never
25952
- silently hand back fewer than I asked for and paid toward.
26052
+ tell me the gap in one line and offer to fill the gap \u2014 another shortfall-sized
26053
+ search under a NEW \`request_id\`, confirmed first like any run that uses
26054
+ quota. Never silently hand back fewer than I asked for.
25953
26055
 
25954
- Pass the leads already EXAMINED-AND-REJECTED into that top-up's
26056
+ Pass the leads already EXAMINED-AND-REJECTED into that search's
25955
26057
  \`exclude_lead_ids\` \u2014 disqualified and skipped, from both the preview and
25956
- the paid pass. \`novelty: org\` already excludes prior DELIVERIES, so
26058
+ the qualifying pass. \`novelty: org\` already excludes prior DELIVERIES, so
25957
26059
  delivered ids are redundant there; the rejected ones are exactly what it
25958
- misses, and without them the top-up re-picks the same misses and charges
25959
- again to close no gap. **\`exclude_lead_ids\` caps at 500** \u2014 a wide
26060
+ misses, and without them that search re-picks the same misses and uses
26061
+ quota again to close no gap. **\`exclude_lead_ids\` caps at 500** \u2014 a wide
25960
26062
  \`exploration_cap\` can examine more than that, so send the most recent 500
25961
26063
  rejects rather than an over-long list the tool refuses outright.
25962
26064
  3. While the job runs, poll with \`leadbay_lead_job_status\`
@@ -25972,7 +26074,7 @@ best fit is under 30, say "weak matches only" and propose reshaping before
25972
26074
  showing more than 3.
25973
26075
 
25974
26076
  Render per the lead-delivery table, then ALWAYS the funnel line: matched /
25975
- examined / qualified / disqualified / delivered / stop reason / spend. Zero
26077
+ examined / qualified / disqualified / delivered / stop reason. Zero
25976
26078
  delivered gets a diagnosis and a concrete next move, never a shrug. Close
25977
26079
  with NEXT STEPS from the tool description \u2014 and STOP; take no further action
25978
26080
  without my say-so.
@@ -26127,7 +26229,7 @@ If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retr
26127
26229
  Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
26128
26230
  \`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
26129
26231
  running result, that work is queued on Leadbay and runs to completion, and the
26130
- quota it costs is already committed. A discovery, preview or \`dry_run\` result
26232
+ quota it uses is already committed. A discovery, preview or \`dry_run\` result
26131
26233
  launched nothing and is not covered here.
26132
26234
 
26133
26235
  The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
@@ -26135,7 +26237,7 @@ waiting, never the job. \`cancelled: true\` means we stopped watching, not that
26135
26237
  work stopped. What to do next depends on what you are holding:
26136
26238
 
26137
26239
  - **A handle.** Poll the status tool with it, and do not launch the work that
26138
- handle covers a second time \u2014 that spends the quota again on the same rows.
26240
+ handle covers a second time \u2014 that uses the quota again on the same rows.
26139
26241
  \`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
26140
26242
  under that name. A qualification started by \`leadbay_import_and_qualify\` has no
26141
26243
  notification of its own: resume it with
@@ -26149,7 +26251,7 @@ work stopped. What to do next depends on what you are holding:
26149
26251
  finished. Calling the same tool again with the same arguments will usually hand
26150
26252
  back the job already launched rather than starting a second one, but that guard
26151
26253
  is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
26152
- are about to re-run before you spend the user's quota on it.
26254
+ are about to re-run before you use the user's quota on it.
26153
26255
 
26154
26256
 
26155
26257
 
@@ -26840,7 +26942,7 @@ If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retr
26840
26942
  Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
26841
26943
  \`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
26842
26944
  running result, that work is queued on Leadbay and runs to completion, and the
26843
- quota it costs is already committed. A discovery, preview or \`dry_run\` result
26945
+ quota it uses is already committed. A discovery, preview or \`dry_run\` result
26844
26946
  launched nothing and is not covered here.
26845
26947
 
26846
26948
  The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
@@ -26848,7 +26950,7 @@ waiting, never the job. \`cancelled: true\` means we stopped watching, not that
26848
26950
  work stopped. What to do next depends on what you are holding:
26849
26951
 
26850
26952
  - **A handle.** Poll the status tool with it, and do not launch the work that
26851
- handle covers a second time \u2014 that spends the quota again on the same rows.
26953
+ handle covers a second time \u2014 that uses the quota again on the same rows.
26852
26954
  \`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
26853
26955
  under that name. A qualification started by \`leadbay_import_and_qualify\` has no
26854
26956
  notification of its own: resume it with
@@ -26862,7 +26964,7 @@ work stopped. What to do next depends on what you are holding:
26862
26964
  finished. Calling the same tool again with the same arguments will usually hand
26863
26965
  back the job already launched rather than starting a second one, but that guard
26864
26966
  is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
26865
- are about to re-run before you spend the user's quota on it.
26967
+ are about to re-run before you use the user's quota on it.
26866
26968
 
26867
26969
 
26868
26970
 
@@ -27083,11 +27185,11 @@ Cash-to-capture is not available: it needs \`ca12\` from my invoicing system, wh
27083
27185
 
27084
27186
  # PHASE 5 \u2014 CONTACTS (consent-gated)
27085
27187
 
27086
- Each card needs a reachable decision-maker. \`leadbay_enrich_titles({leadIds, lensId})\` in discovery mode first \u2014 that reveals what's enrichable and spends nothing. Render whatever contact detail is already on the record; many accounts already carry a named contact.
27188
+ Each card needs a reachable decision-maker. \`leadbay_enrich_titles({leadIds, lensId})\` in discovery mode first \u2014 that reveals what's enrichable and uses no quota. Render whatever contact detail is already on the record; many accounts already carry a named contact.
27087
27189
 
27088
- **Do NOT stop and wait for enrichment consent before delivering.** Asking for a plan is not authorization to spend quota on {{arg:count_or_default}} accounts \u2014 but neither is it a reason to end the turn on a spending question with no plan attached. Ship the ranked plan (Phase 6), then **offer** the paid reveal alongside it. The discovery call returned no \`titles\`, so it only told you what's *available* \u2014 **the offer must therefore carry the titles you propose to enrich AND the channels**, not just a volume: "enrich N contacts at these titles (\`<the titles you picked from available_titles / title_suggestions>\`), email only / email + phone \u2014 reveals consume quota". A bare "yes" to a volume-only question is not a mandate to pick titles yourself, and re-running discovery instead of launching wastes a turn.
27190
+ **Do NOT stop and wait for enrichment consent before delivering.** Asking for a plan is not authorization to use quota on {{arg:count_or_default}} accounts \u2014 but neither is it a reason to end the turn on a consent question with no plan attached. Ship the ranked plan (Phase 6), then **offer** the reveal alongside it. The discovery call returned no \`titles\`, so it only told you what's *available* \u2014 **the offer must therefore carry the titles you propose to enrich AND the channels**, not just a volume: "enrich N contacts at these titles (\`<the titles you picked from available_titles / title_suggestions>\`), email only / email + phone \u2014 reveals consume quota". A bare "yes" to a volume-only question is not a mandate to pick titles yourself, and re-running discovery instead of launching wastes a turn.
27089
27191
 
27090
- \u26A0 **Do NOT quote a cost or a credits figure.** The per-reveal rate is backend-side and enrichment is gated by quota, not a credit balance; \`credits_remaining\` is advisory context only. A spend number invented to make the offer concrete is the same failure as an invented euro on a card.
27192
+ \u26A0 **Do NOT quote a cost or a credits figure.** The per-reveal rate is backend-side and enrichment is gated by quota, not a credit balance; \`credits_remaining\` is advisory context only. A usage number invented to make the offer concrete is the same failure as an invented euro on a card.
27091
27193
 
27092
27194
  On an explicit yes, launch with the agreed \`titles\` + channels, then poll \`leadbay_bulk_enrich_status\` until done and **keep the \`notification_id\` handles** for the deck.
27093
27195
 
@@ -27235,9 +27337,9 @@ ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We
27235
27337
  - One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
27236
27338
 
27237
27339
 
27238
- \u26A0 **The deck's contact layer depends on what actually happened in Phase 5.** Bind a \`leadbay_bulk_enrich_status\` resource ONLY if a paid reveal was launched and you hold a \`notification_id\`. If the user accepted the deck but not the reveal, render the contacts already on record and carry the paid-reveal offer inside the deck \u2014 never wire a status resource with no handle (it renders permanently empty) and never launch enrichment from the deck to manufacture one.
27340
+ \u26A0 **The deck's contact layer depends on what actually happened in Phase 5.** Bind a \`leadbay_bulk_enrich_status\` resource ONLY if a reveal was launched and you hold a \`notification_id\`. If the user accepted the deck but not the reveal, render the contacts already on record and carry the reveal offer inside the deck \u2014 never wire a status resource with no handle (it renders permanently empty) and never launch enrichment from the deck to manufacture one.
27239
27341
 
27240
- On acceptance, call \`leadbay_artifact_kit\`, read its \`usage_guide\` before writing any code, and build a single-file deck. Wire the live layer from the handles you kept: a poll-until-done resource per \`notification_id\` for the qualification pills, and one over \`leadbay_bulk_enrich_status\` for the contacts. \u26A0 **If enrichment already ran this session, bind the existing \`notification_id\` \u2014 re-launching enrichment from the deck double-spends my quota.** Per-card notes and outcomes go through the pre-wired note/outreach view-models (they carry the required verification and \`_triggered_by\` fields; hand-rolling those is where it breaks). Keep the checklists in local storage, and always wire a Refresh \u2014 auto-poll is host-dependent. List every tool the deck calls in its \`mcp_tools\`, and render the bridge-unavailable branch, or the pills silently show empty.
27342
+ On acceptance, call \`leadbay_artifact_kit\`, read its \`usage_guide\` before writing any code, and build a single-file deck. Wire the live layer from the handles you kept: a poll-until-done resource per \`notification_id\` for the qualification pills, and one over \`leadbay_bulk_enrich_status\` for the contacts. \u26A0 **If enrichment already ran this session, bind the existing \`notification_id\` \u2014 re-launching enrichment from the deck uses my quota twice.** Per-card notes and outcomes go through the pre-wired note/outreach view-models (they carry the required verification and \`_triggered_by\` fields; hand-rolling those is where it breaks). Keep the checklists in local storage, and always wire a Refresh \u2014 auto-poll is host-dependent. List every tool the deck calls in its \`mcp_tools\`, and render the bridge-unavailable branch, or the pills silently show empty.
27241
27343
 
27242
27344
  # Iron laws
27243
27345
 
@@ -27247,7 +27349,7 @@ On acceptance, call \`leadbay_artifact_kit\`, read its \`usage_guide\` before wr
27247
27349
  - **Deliver first, ask alongside.** Do not end a turn without a ranked list of real accounts. The benchmark, the Tier-1 threshold, the territory, a missing lens, a short question set and an unanswered enrichment offer are all NON-blocking \u2014 carry them next to the plan. Only an unresolvable identity mismatch (whose plan is this?) may stop delivery.
27248
27350
  - **One motif per account, from the closed set of six**, with its deciding evidence stated.
27249
27351
  - **The org's real qualification questions**, read from Leadbay \u2014 never invented.
27250
- - **Consent before any paid enrichment**, and never re-launch a bulk that already exists.
27352
+ - **Consent before any enrichment**, and never re-launch a bulk that already exists.
27251
27353
  - **Offer the deck; don't force it.** The chat answer must stand alone as useful.
27252
27354
  - Carry the captured \`lensId\` on the calls whose schema **accepts** it (\`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_enrich_titles\`). Do NOT add it to \`leadbay_pull_followups\`, \`leadbay_scan_portfolio_signals\`, \`leadbay_qualify_status\` or \`leadbay_bulk_enrich_status\` \u2014 they declare no such argument and reject unknown properties.
27253
27355
  - Building a plan is not outreaching \u2014 do not send anything and do not call \`leadbay_report_outreach\`.
@@ -27299,7 +27401,7 @@ Call \`leadbay_campaign_call_sheet({campaign_id})\`. The response carries \`summ
27299
27401
 
27300
27402
  - "\u{1F4DE} Start calling now" \u2014 IF \`ready_for_calling\`. Top option when phones are there.
27301
27403
  - "\u2709 Email session instead" \u2014 IF \`ready_for_emailing\` AND \`email_ratio > phone_ratio\`. Don't surface this when calling is more obvious.
27302
- - "\u{1F527} Enrich titles first" \u2014 IF \`needs_enrichment\`. Top option when most leads have no contacts. Phrase as "<N> leads have no reachable contact yet \u2014 enrich titles before we start?" so the user understands the cost.
27404
+ - "\u{1F527} Enrich titles first" \u2014 IF \`needs_enrichment\`. Top option when most leads have no contacts. Phrase as "<N> leads have no reachable contact yet \u2014 enrich titles before we start?" so the user knows it uses quota.
27303
27405
  - "\u{1F5FA} View on a map" \u2014 IF \`travel_friendly\` **AND** the user hasn't previously signaled disinterest in maps (check your conversation memory; if you've seen the user dismiss map renders before in this session or saved a "no maps" preference, drop this option).
27304
27406
 
27305
27407
  If the MCP prompt argument \`mode\` was actually supplied, skip the proposal and jump to the matching mode below. If \`mode\` was omitted, do not treat \`call_sheet\` as implicit user consent \u2014 propose first.
@@ -27357,7 +27459,7 @@ Optional: offer to review the \`leadbay_campaign_progression\` for the same camp
27357
27459
  - If the user dictates an outcome that doesn't cleanly map to one of the four epilogue values, ASK ONCE before guessing.
27358
27460
  `;
27359
27461
  var PROMPT_META = {
27360
- leadbay_build_campaign: { "name": "leadbay_build_campaign", "short_description": 'Build a sales campaign from scratch, autonomously, to a target size:\ndiscover on the lens, qualify, and enrich the buyer titles until `count`\nleads each have a reachable target-title contact \u2014 no pauses, no confirm\ngates. Saves via `leadbay_create_campaign` and renders a one-tap\ncall/email view via `leadbay_campaign_call_sheet`. Trigger on "build me a\ncampaign", "build N leads", "create a campaign from scratch". Work an\nexisting one with `leadbay_work_campaign`.\n', "arguments": [{ "name": "audience", "description": "Optional: a fresh audience to target (e.g. 'dental clinics in Texas'). Omit to build from your ACTIVE lens \u2014 the default.", "required": false }, { "name": "campaign_name", "description": "Optional: a name for the campaign. Omit and one is derived from the lens/audience + date (or the backend AI-names it).", "required": false }, { "name": "count", "description": "Optional: how many fully-actionable leads to build (default 20). The loop keeps discovering, qualifying and enriching until this many in-ICP leads each have a reachable target-title contact \u2014 or the lens is exhausted. Higher counts take longer and consume more quota.", "required": false }, { "name": "job_titles", "description": "Optional: the exact buyer job titles to enrich, comma-separated (e.g. 'VP Sales, Head of Growth, Director of Business Development'). Omit and the buyer persona is derived from what you sell. A lead only counts toward the target when it has a reachable contact matching one of these titles.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_recall_ordered_titles", "leadbay_enrich_titles", "leadbay_bulk_enrich_status", "leadbay_create_campaign", "leadbay_add_leads_to_campaign", "leadbay_campaign_call_sheet", "leadbay_campaign_progression", "leadbay_new_lens", "leadbay_adjust_audience"], "failure_modes": ["Pauses to confirm before enriching (or asks 'enrich these N now?' via ask_user_input_v0) \u2014 this prompt runs to goal with NO confirm gate; asking for the campaign IS the authorization. Never stop for a spend confirmation.", "Stops to confirm a lens switch when the user named a fresh audience \u2014 naming the audience IS the authorization; switch, state which lens in one line, and don't ask.", "Pauses at any point to ask the user to choose, confirm, or hand off \u2014 the only acceptable stops are lens exhaustion (can't supply the count) or a backend 429 (quota out). Anything else is purpose drift.", "Stops at fewer than the target `count` of actionable leads without looping back to pull / qualify / enrich more \u2014 must run to the target, or honestly report the lens is exhausted and offer to widen.", "Counts a lead toward `count` before its target-title contact actually landed (email/phone present) \u2014 an empty enrichment doesn't count; swap and re-enrich until the cohort is genuinely `count` deep.", "Enriches by seniority instead of by buyer persona \u2014 picks COO / Director of Operations / Mgr of Logistics / CFO / CTO because they sound senior, when the user sells a SALES tool whose buyer is the revenue org (VP/Head/Director of Sales, BD, growth, marketing). Operations people never buy a sales tool; this hands the salesperson a useless list.", "When no titles are given, fails to derive the user's buyer persona from their product/ICP before choosing titles \u2014 jumps straight to generic exec titles instead of working out who buys what THIS user sells. (When titles ARE given, use them verbatim \u2014 don't substitute 'more senior' ones.)", "Blindly repeats leadbay_recall_ordered_titles / discovery suggestions even when they are off-persona (e.g. operations roles a prior session wrongly enriched) \u2014 recall is a filtered input, not the answer.", "Poor coverage \u2014 leaves picked leads with no target-title contact (or 0 enrichments on some leads) and ships them anyway, so the salesperson opens the campaign to half-empty rows. Swap them out and refill to the count instead.", "Creates the campaign before qualifying / picking \u2014 seeds a campaign with unvetted leads. Qualify and lock the buyer-covered cohort FIRST, then leadbay_create_campaign.", "Ends at 'campaign created' without rendering the leadbay_campaign_call_sheet view \u2014 the ready-to-work view IS the deliverable; stopping short is purpose drift.", "Runs the calling / outcome / follow-up loop, or calls leadbay_work_campaign itself, instead of stopping at the call sheet \u2014 this prompt BUILDS and stops; work_campaign is a separate session the user starts later.", "Auto-sends outreach or calls leadbay_report_outreach \u2014 building a campaign is not outreaching. No send, no log.", "Re-pulls leadbay_pull_leads without the captured lensId \u2014 a mid-session lens shift discards the cohort being built.", "Renders the picked leads or the call sheet as prose instead of the canonical per-tool RENDERING layout."] },
27462
+ leadbay_build_campaign: { "name": "leadbay_build_campaign", "short_description": 'Build a sales campaign from scratch, autonomously, to a target size:\ndiscover on the lens, qualify, and enrich the buyer titles until `count`\nleads each have a reachable target-title contact \u2014 no pauses, no confirm\ngates. Saves via `leadbay_create_campaign` and renders a one-tap\ncall/email view via `leadbay_campaign_call_sheet`. Trigger on "build me a\ncampaign", "build N leads", "create a campaign from scratch". Work an\nexisting one with `leadbay_work_campaign`.\n', "arguments": [{ "name": "audience", "description": "Optional: a fresh audience to target (e.g. 'dental clinics in Texas'). Omit to build from your ACTIVE lens \u2014 the default.", "required": false }, { "name": "campaign_name", "description": "Optional: a name for the campaign. Omit and one is derived from the lens/audience + date (or the backend AI-names it).", "required": false }, { "name": "count", "description": "Optional: how many fully-actionable leads to build (default 20). The loop keeps discovering, qualifying and enriching until this many in-ICP leads each have a reachable target-title contact \u2014 or the lens is exhausted. Higher counts take longer and consume more quota.", "required": false }, { "name": "job_titles", "description": "Optional: the exact buyer job titles to enrich, comma-separated (e.g. 'VP Sales, Head of Growth, Director of Business Development'). Omit and the buyer persona is derived from what you sell. A lead only counts toward the target when it has a reachable contact matching one of these titles.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_recall_ordered_titles", "leadbay_enrich_titles", "leadbay_bulk_enrich_status", "leadbay_create_campaign", "leadbay_add_leads_to_campaign", "leadbay_campaign_call_sheet", "leadbay_campaign_progression", "leadbay_new_lens", "leadbay_adjust_audience"], "failure_modes": ["Pauses to confirm before enriching (or asks 'enrich these N now?' via ask_user_input_v0) \u2014 this prompt runs to goal with NO confirm gate; asking for the campaign IS the authorization. Never stop for an enrichment confirmation.", "Stops to confirm a lens switch when the user named a fresh audience \u2014 naming the audience IS the authorization; switch, state which lens in one line, and don't ask.", "Pauses at any point to ask the user to choose, confirm, or hand off \u2014 the only acceptable stops are lens exhaustion (can't supply the count) or a backend 429 (quota out). Anything else is purpose drift.", "Stops at fewer than the target `count` of actionable leads without looping back to pull / qualify / enrich more \u2014 must run to the target, or honestly report the lens is exhausted and offer to widen.", "Counts a lead toward `count` before its target-title contact actually landed (email/phone present) \u2014 an empty enrichment doesn't count; swap and re-enrich until the cohort is genuinely `count` deep.", "Enriches by seniority instead of by buyer persona \u2014 picks COO / Director of Operations / Mgr of Logistics / CFO / CTO because they sound senior, when the user sells a SALES tool whose buyer is the revenue org (VP/Head/Director of Sales, BD, growth, marketing). Operations people never buy a sales tool; this hands the salesperson a useless list.", "When no titles are given, fails to derive the user's buyer persona from their product/ICP before choosing titles \u2014 jumps straight to generic exec titles instead of working out who buys what THIS user sells. (When titles ARE given, use them verbatim \u2014 don't substitute 'more senior' ones.)", "Blindly repeats leadbay_recall_ordered_titles / discovery suggestions even when they are off-persona (e.g. operations roles a prior session wrongly enriched) \u2014 recall is a filtered input, not the answer.", "Poor coverage \u2014 leaves picked leads with no target-title contact (or 0 enrichments on some leads) and ships them anyway, so the salesperson opens the campaign to half-empty rows. Swap them out and refill to the count instead.", "Creates the campaign before qualifying / picking \u2014 seeds a campaign with unvetted leads. Qualify and lock the buyer-covered cohort FIRST, then leadbay_create_campaign.", "Ends at 'campaign created' without rendering the leadbay_campaign_call_sheet view \u2014 the ready-to-work view IS the deliverable; stopping short is purpose drift.", "Runs the calling / outcome / follow-up loop, or calls leadbay_work_campaign itself, instead of stopping at the call sheet \u2014 this prompt BUILDS and stops; work_campaign is a separate session the user starts later.", "Auto-sends outreach or calls leadbay_report_outreach \u2014 building a campaign is not outreaching. No send, no log.", "Re-pulls leadbay_pull_leads without the captured lensId \u2014 a mid-session lens shift discards the cohort being built.", "Renders the picked leads or the call sheet as prose instead of the canonical per-tool RENDERING layout."] },
27361
27463
  leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": 'Morning DISCOVERY workflow \u2014 new leads from the lens wishlist. Trigger\non "show me leads", "what\'s new today", "let\'s prospect", "run my check-in",\n"my morning check-in", "I do this every day", "every morning". Recurrence\nlanguage always means this prompt. Do NOT trigger on follow-up phrasings\n("follow up", "before my trip") \u2014 those go to `leadbay_followup_check_in`.\n', "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_bulk_qualify_leads", "leadbay_enrich_contacts"], "failure_modes": ["Calls leadbay_report_outreach without explicit user authorization", "Surfaces fewer than 10 leads when more are available, or fails to top up via leadbay_qualify_top_n when the batch is short", `Replaces the canonical pull_leads table layout with prose per row (the per-tool RENDERING block is the structural contract; "Today's nudges" goes above it, not in place of it)`, "Skips the nudge paragraph entirely \u2014 the table alone is fine but adding the nudge is the value-add", `Skips deep research on promising leads (Phase 4) \u2014 the agent must call leadbay_research_lead_by_id on each when the user's intent is to research specific leads; Phase 4 is intentionally skipped for batch-view requests ("show me today's leads", "run my morning check-in") per the Phase 4 skip gate`, "Triggers contact enrichment without asking the user first (it consumes quota)", "Skips the STOP byproduct and proposes next actions on its own", 'Fires 10 parallel leadbay_research_lead_by_id calls and treats "stream closed" errors as terminal \u2014 must serialize and retry singletons', "Re-pulls leadbay_pull_leads without passing the captured lensId, allowing a backend lens shift to discard the Phase 2 batch", 'Treats a "Request timed out" from leadbay_bulk_qualify_leads as terminal instead of retrying with wait_for_completion:false + qualify_status polling', 'Triggers on a follow-up query (e.g., "leads I should follow up with") that should have routed to `leadbay_followup_check_in` \u2014 the two entry points are different data sources (Discover wishlist vs Monitor view) per \xA71.6'] },
27362
27464
  leadbay_extend_my_lens: { "name": "leadbay_extend_my_lens", "short_description": "Add more leads to the current lens on demand \u2014 for users whose appetite\nexceeds the standard daily fill. The agent picks seeds silently from\nwhat's already on the lens, fires the extra refill, and surfaces the\nqueue confirmation. The user never reviews the seed list.\n", "arguments": [{ "name": "extra_count", "description": "How many extra leads to add. Optional. Omit to use the backend default.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_seed_candidates", "leadbay_extend_lens", "leadbay_pull_leads"], "failure_modes": ["Surfaces the seed candidate list to the user instead of picking silently \u2014 the user asked for MORE LEADS, not a candidate review meeting", "Skips the seeded path and calls `leadbay_extend_lens` with no `seed_lead_ids`, losing the bias signal the recommender needs", "On 429, silently retries instead of surfacing the three options (smaller / wait / upgrade) via your host's choice widget (`ask_user_input_v0` or `AskUserQuestion`)", "Forgets to pre-check `LENS_EXTRA_REFILL` quota in `leadbay_account_status` and burns a wasted API call", "Skips the post-queue pull-leads suggestion, so the user doesn't see what just got added"] },
27363
27465
  leadbay_followup_check_in: { "name": "leadbay_followup_check_in", "short_description": 'Follow-up check-in: surface KNOWN leads from the Monitor view needing\nre-engagement. Trigger on "follow up", "already known leads", "what\'s\noverdue", "before my trip", "who should I re-engage". Do NOT trigger on\n"show me today\'s leads", "my morning check-in", "run my check-in",\n"I do this every day", "every morning" \u2014 those go to\n`leadbay_daily_check_in`.\n', "arguments": [], "expected_calls": ["leadbay_pull_followups", "leadbay_research_lead_by_id", "leadbay_prepare_outreach"], "failure_modes": ["Calls leadbay_pull_leads (the Discover entry point) instead of leadbay_pull_followups \u2014 these are different data sources; the Discover queue does NOT contain Monitor's known-but-cold pipeline", 'Iterates pages of leadbay_pull_leads filtering by engagement_count to "fake" a follow-up view (a real bug observed in 0.9.0 \u2014 the right move is to call pull_followups directly)', "Replaces the canonical pull_followups table layout with prose per row (the per-tool RENDERING block is the structural contract; commentary belongs above or below)", 'Skips the cross-mode pivot offer at the end ("Want to see NEW leads from your wishlist instead?" routes to leadbay_pull_leads)'] },
@@ -27367,7 +27469,7 @@ reveal who to send it to. Use when the user is new or asks to be SHOWN how
27367
27469
  Leadbay works ("walk me through Leadbay", "I'm new", "how do I use this",
27368
27470
  "give me a tour"). Don't use it for orientation prose with no clicking \u2014
27369
27471
  that's leadbay_prospecting_overview.
27370
- `, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_prepare_outreach", "leadbay_enrich_titles", "leadbay_bulk_enrich_status"], "failure_modes": ['Presents a gate as prose ("let me know if you want me to pull your leads") instead of CALLING the host choice widget \u2014 the click IS the lesson, and prose turns the walkthrough into a lecture', "Runs a step's tool WITHOUT firing that step's widget first and waiting for the click \u2014 the walkthrough becomes an automated demo the user only watches, which is the exact opposite of learning by doing", "Fires the widget without the EXPLAIN beat, so the user gets an unexplained button and learns nothing about what a lens or an enrichment actually is", 'Answers gate 1 with a bare "you\'re connected as X at Y" when the quota IS readable \u2014 the user clicked a button labelled `check my account status`, so the quota windows (Daily/Weekly/Monthly gauges, % used, $ spent, resets) ARE the answer, not an optional extra', 'Renders quota as raw "credits" instead of the web app\'s percentage + dollar-spend gauges, or dumps raw `resource_type` strings the user has never seen', "Opens with a wall of text \u2014 previewing all four steps, explaining lenses up front, or writing several paragraphs before the first widget. The opening is TWO lines then the button; a first-run user wants to see it work, not read a syllabus", `Ends the first message without firing gate 1's widget, leaving the user to reply "ok" before anything happens`, "Rewrites the gate's own `next_steps` payload (its `question`, `label` or `description`) instead of mapping it into the widget verbatim, or merges two gates into a single multi-option widget", 'Fires a THIRD option, or turns the exit into an alternative route ("show me my lenses instead") \u2014 each gate carries exactly one forward action plus the `I\'m done for now` exit, never a menu of paths', `Fires a single-option widget \u2014 the host requires 2\u20134 options, so a lone option is rejected or silently degrades to prose ("say the word and I'll check it"), which is the exact defect this rule exists to prevent`, 'Launches the PAID reveal at gate 4 BEFORE the user has picked leads and confirmed \u2014 beat 1 must be the free `mode:"discover"` preview (no `titles`, no `confirm`, no `email`, no `phone`); the gate click bought the free look, not the reveal, and silence is never consent', "Stops at the free preview after the user DID pick leads and confirm \u2014 they asked for real contact details, so the second call must actually run with `confirm:true` and the chosen titles", "Reports the enrichment without polling `leadbay_bulk_enrich_status` to completion, so it claims contacts it never actually saw resolve", `Reveals contacts and never says what it cost \u2014 the user just spent credits and deserves the one-line "N contacts = N credits", which is also what makes gate 1's quota numbers concrete`, 'Reports "no leads" on an empty batch while `computing_wishlist` / `computing_scores` is true \u2014 the lens is still building; render the tool\'s own two-option warm-up widget verbatim and pause', "Rewords, reorders or prose-ifies the `next_steps` payload from `leadbay_pull_leads` instead of mapping `options[]` into the widget verbatim", "Runs all four steps in one turn without waiting for the user's click between gates \u2014 the walkthrough is a sequence of gates, not a script to recite", "Skips `leadbay_pull_leads` and jumps straight to enrichment, leaving gate 4 with no `leadIds` to scope", "Passes a singular `leadId` to `leadbay_enrich_titles` on the confirmed reveal \u2014 that key does not exist on this tool, so it is dropped and the paid call falls back to the whole default wishlist selection, charging for far more than the one lead the user agreed to. it is always the `leadIds` ARRAY, even for a single lead", "Drops the pinned `lens.id` between gates, so gate 4 enriches against a different lens than the one the user just saw", "Ends the completed walkthrough without the `keep_going` cheat-sheet \u2014 the buttons disappear with the tour, so a user who was never told what to TYPE learned to click a tutorial and nothing about using Leadbay tomorrow", "Invents phrases for the cheat-sheet, or rewords them into something that sounds nicer but doesn't match the tool's real triggers \u2014 teaching a phrase that doesn't route is worse than teaching none", "SENDS the gate 3 draft, or offers to send it \u2014 the walkthrough drafts and stops there; the email is the user's to judge, and nothing leaves the chat", "Passes `enrich:true` to `leadbay_prepare_outreach` at gate 3 \u2014 that launches a PAID contact reveal off the back of a DRAFT click, spending credits the user never agreed to", "Invents a contact NAME for the gate 3 draft \u2014 `recommended_contact` still has null email/name at that point, so the draft is addressed to the job TITLE; a fabricated name is the one thing that makes the whole draft untrustworthy", "Treats the null email at gate 3 as a failure \u2014 apologising for it, retrying, or calling another tool to fill it in. It is the setup for gate 4 \u2014 an email written, nobody to send it to yet", "Pastes the drafted email into chat prose alongside `message_compose_v1` instead of letting the composer BE the answer", "Enriches leads other than the one it drafted for at gate 3 \u2014 gate 4 reveals the person that email is going to, so it is scoped to that ONE lead, one contact, one credit", "Renders the cheat-sheet on the exit and stops there, dropping the 1:1 offer \u2014 the observed failure is that the agent feels finished once the table is on screen, so the user who just stepped out never hears about the help that would bring them back. ENDING B is not complete without the offer, and the offer goes LAST", "Treats the exit click as ENDING C (typed off-script) and closes in silence, or treats a typed request as ENDING B and buries their real answer under a cheat-sheet and a booking link", "Turns the exit offer into a pitch \u2014 several sentences, a re-opened gate, or an argument for finishing the tour. They said they were done; it is one line and a link", "Fires the 1:1 offer mid-tour, or at a user who left by TYPING a different request \u2014 a booking link on top of their real question is an interruption, not an offer", "Runs the four gates at a user whose actual problem is SETUP \u2014 the connector isn't installed, they can't sign in, or their Leadbay tools aren't appearing. The tour assumes a working connection and cannot fix any of it; the setup guide can", "Pastes the setup-guide link mid-tour, between gates, instead of once at the closing \u2014 a link in the middle of the walkthrough invites the user to leave the thing they're doing"] },
27472
+ `, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_prepare_outreach", "leadbay_enrich_titles", "leadbay_bulk_enrich_status"], "failure_modes": ['Presents a gate as prose ("let me know if you want me to pull your leads") instead of CALLING the host choice widget \u2014 the click IS the lesson, and prose turns the walkthrough into a lecture', "Runs a step's tool WITHOUT firing that step's widget first and waiting for the click \u2014 the walkthrough becomes an automated demo the user only watches, which is the exact opposite of learning by doing", "Fires the widget without the EXPLAIN beat, so the user gets an unexplained button and learns nothing about what a lens or an enrichment actually is", 'Answers gate 1 with a bare "you\'re connected as X at Y" when the quota IS readable \u2014 the user clicked a button labelled `check my account status`, so the quota windows (Daily/Weekly/Monthly gauges, % used, $ spent, resets) ARE the answer, not an optional extra', 'Renders quota as raw "credits" instead of the web app\'s percentage + dollar-spend gauges, or dumps raw `resource_type` strings the user has never seen', "Opens with a wall of text \u2014 previewing all four steps, explaining lenses up front, or writing several paragraphs before the first widget. The opening is TWO lines then the button; a first-run user wants to see it work, not read a syllabus", `Ends the first message without firing gate 1's widget, leaving the user to reply "ok" before anything happens`, "Rewrites the gate's own `next_steps` payload (its `question`, `label` or `description`) instead of mapping it into the widget verbatim, or merges two gates into a single multi-option widget", 'Fires a THIRD option, or turns the exit into an alternative route ("show me my lenses instead") \u2014 each gate carries exactly one forward action plus the `I\'m done for now` exit, never a menu of paths', `Fires a single-option widget \u2014 the host requires 2\u20134 options, so a lone option is rejected or silently degrades to prose ("say the word and I'll check it"), which is the exact defect this rule exists to prevent`, 'Launches the contact reveal at gate 4 BEFORE the user has picked leads and confirmed \u2014 beat 1 must be the free `mode:"discover"` preview (no `titles`, no `confirm`, no `email`, no `phone`); the gate click bought the free look, not the reveal, and silence is never consent', "Stops at the free preview after the user DID pick leads and confirm \u2014 they asked for real contact details, so the second call must actually run with `confirm:true` and the chosen titles", "Reports the enrichment without polling `leadbay_bulk_enrich_status` to completion, so it claims contacts it never actually saw resolve", "Reveals contacts and never says the reveal used a little of their plan's quota \u2014 that one line, with no amount and no price, is what makes gate 1's quota numbers concrete", 'Reports "no leads" on an empty batch while `computing_wishlist` / `computing_scores` is true \u2014 the lens is still building; render the tool\'s own two-option warm-up widget verbatim and pause', "Rewords, reorders or prose-ifies the `next_steps` payload from `leadbay_pull_leads` instead of mapping `options[]` into the widget verbatim", "Runs all four steps in one turn without waiting for the user's click between gates \u2014 the walkthrough is a sequence of gates, not a script to recite", "Skips `leadbay_pull_leads` and jumps straight to enrichment, leaving gate 4 with no `leadIds` to scope", "Passes a singular `leadId` to `leadbay_enrich_titles` on the confirmed reveal \u2014 that key does not exist on this tool, so it is dropped and the reveal falls back to the whole default wishlist selection, revealing far more than the one lead the user agreed to. it is always the `leadIds` ARRAY, even for a single lead", "Drops the pinned `lens.id` between gates, so gate 4 enriches against a different lens than the one the user just saw", "Ends the completed walkthrough without the `keep_going` cheat-sheet \u2014 the buttons disappear with the tour, so a user who was never told what to TYPE learned to click a tutorial and nothing about using Leadbay tomorrow", "Invents phrases for the cheat-sheet, or rewords them into something that sounds nicer but doesn't match the tool's real triggers \u2014 teaching a phrase that doesn't route is worse than teaching none", "SENDS the gate 3 draft, or offers to send it \u2014 the walkthrough drafts and stops there; the email is the user's to judge, and nothing leaves the chat", "Passes `enrich:true` to `leadbay_prepare_outreach` at gate 3 \u2014 that launches a contact reveal off the back of a DRAFT click, using quota the user never agreed to", "Invents a contact NAME for the gate 3 draft \u2014 `recommended_contact` still has null email/name at that point, so the draft is addressed to the job TITLE; a fabricated name is the one thing that makes the whole draft untrustworthy", "Treats the null email at gate 3 as a failure \u2014 apologising for it, retrying, or calling another tool to fill it in. It is the setup for gate 4 \u2014 an email written, nobody to send it to yet", "Pastes the drafted email into chat prose alongside `message_compose_v1` instead of letting the composer BE the answer", "Enriches leads other than the one it drafted for at gate 3 \u2014 gate 4 reveals the person that email is going to, so it is scoped to that ONE lead, one contact", "Renders the cheat-sheet on the exit and stops there, dropping the 1:1 offer \u2014 the observed failure is that the agent feels finished once the table is on screen, so the user who just stepped out never hears about the help that would bring them back. ENDING B is not complete without the offer, and the offer goes LAST", "Treats the exit click as ENDING C (typed off-script) and closes in silence, or treats a typed request as ENDING B and buries their real answer under a cheat-sheet and a booking link", "Turns the exit offer into a pitch \u2014 several sentences, a re-opened gate, or an argument for finishing the tour. They said they were done; it is one line and a link", "Fires the 1:1 offer mid-tour, or at a user who left by TYPING a different request \u2014 a booking link on top of their real question is an interruption, not an offer", "Runs the four gates at a user whose actual problem is SETUP \u2014 the connector isn't installed, they can't sign in, or their Leadbay tools aren't appearing. The tour assumes a working connection and cannot fix any of it; the setup guide can", "Pastes the setup-guide link mid-tour, between gates, instead of once at the closing \u2014 a link in the middle of the walkthrough invites the user to leave the thing they're doing"] },
27371
27473
  leadbay_import_file: { "name": "leadbay_import_file", "short_description": "Import a user-supplied CSV/file into Leadbay through five phases with\nevidence gates \u2014 scan, derive, resolve identities, preserve & commit,\nthen optionally qualify and report. The job is to maximize how many\nrows the Leadbay system actually ingests and matches.\n", "arguments": [{ "name": "file", "description": "Path or user-visible name of the CSV/file to import. If omitted, use the file the user attached or referenced.", "required": false }, { "name": "instruction", "description": 'Additional user goal, e.g. "then qualify the leads", "preserve owner phone as a custom field", or "only import restaurants in Manhattan".', "required": false }], "expected_calls": ["leadbay_resolve_import_rows", "leadbay_list_mappable_fields", "leadbay_create_custom_field", "leadbay_import_leads", "leadbay_import_and_qualify", "leadbay_add_note", "leadbay_import_status"], "failure_modes": ["Picks LEADBAY_ID from score alone, name-only, fuzzy-name-only, root-domain-only, brand-only, postcode-only, or city-only evidence", "Drops meaningful business notes or CRM record links instead of preserving them as custom fields or lead notes", "Treats a consumer mailbox domain (gmail.com, hotmail.com, ...) as the company domain", "Skips deriving company_domain from a business email when no website column exists (this kills match rate)", "Skips the COLUMN PRESERVATION PLAN byproduct before importing", "Skips the DECISION LOG byproduct before writing LEADBAY_ID", "Returns the imported records WITHOUT writing LEADBAY_ID values back into the user's file (leaves the user no audit trail of what matched)", "Fabricates leadIds, contact emails, or mapping IDs not present in the file or a tool response"] },
27372
27474
  leadbay_log_outreach: { "name": "leadbay_log_outreach", "short_description": "Log outreach (an email I sent, a call I made, a meeting I had) on a\nspecific lead. Captures verification so the SDR pipeline trusts the entry.\n", "arguments": [{ "name": "lead_id", "description": "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead_by_id.", "required": true }, { "name": "summary", "description": "1-2 sentences describing what I did (e.g. 'Sent intro email to CTO citing recent Hornsea contract').", "required": true }], "expected_calls": ["leadbay_report_outreach"], "failure_modes": ["Calls leadbay_report_outreach without first collecting a verification source", "Fabricates a gmail_message_id or calendar_event_id (the human team treats verification as canonical)", "Records outreach to a different lead_id than the one the user supplied", "Skips the dry_run step when the user is unsure what would be sent"] },
27373
27475
  leadbay_new_leads: { "name": "leadbay_new_leads", "short_description": `Guided net-new lead delivery \u2014 turn a described need ("gyms around Dallas
@@ -27377,7 +27479,7 @@ Trigger when the user DESCRIBES who they want: "get me N companies that
27377
27479
  <profile>", "we're entering <market>". A bare "find me new leads" with no
27378
27480
  profile, and "today's leads", are the daily lens batch \u2014 leadbay_pull_leads.
27379
27481
  "Qualify these companies I have" is leadbay_qualify_leads.
27380
- `, "arguments": [{ "name": "need", "description": "What the user is looking for, in their own words (e.g. '10 gyms around Dallas that would buy modular flooring, with phone numbers'). Optional \u2014 the session starts by asking when absent.", "required": false }], "expected_calls": ["leadbay_find_new_leads", "leadbay_lead_job_status", "leadbay_qualify_leads"], "failure_modes": ["Passes the user's raw sentence as `query` instead of crafting an example_lead description (vendor-vocabulary trap \u2014 measured 0 delivered from a raw query vs on-profile results from a crafted example)", "Invents a distinctive brand name in example_lead.name (pulls matching toward name-lookalikes)", 'Puts event language ("hiring", "expanding", "just raised") into the seed description', "Launches qualify:true or channels without a dry_run quote and the user's explicit go-ahead", "Retries a failed/timed-out submit with a NEW request_id (double-spend) \u2014 the same ask must reuse the same request_id", 'Reports "no results" without narrating the funnel + scope_notes and proposing a concrete fix', "Renders delivered leads as freeform prose instead of the canonical lead-delivery table", "Blends two distinct buyer archetypes into one seed description instead of running one search per archetype", "Passes a country name in filters.locations (silently matches a same-named town \u2014 whole-country intent means OMITTING locations) or a nested employees object instead of the flat employees_min/employees_max", "Renders rows that visibly violate the user's exclusions, or presents a best-fit-under-30 table as an answer instead of flagging weak matches"] },
27482
+ `, "arguments": [{ "name": "need", "description": "What the user is looking for, in their own words (e.g. '10 gyms around Dallas that would buy modular flooring, with phone numbers'). Optional \u2014 the session starts by asking when absent.", "required": false }], "expected_calls": ["leadbay_find_new_leads", "leadbay_lead_job_status", "leadbay_qualify_leads"], "failure_modes": ["Passes the user's raw sentence as `query` instead of crafting an example_lead description (vendor-vocabulary trap \u2014 measured 0 delivered from a raw query vs on-profile results from a crafted example)", "Invents a distinctive brand name in example_lead.name (pulls matching toward name-lookalikes)", 'Puts event language ("hiring", "expanding", "just raised") into the seed description', "Launches qualify:true or channels without a dry_run quote and the user's explicit go-ahead", "Retries a failed/timed-out submit with a NEW request_id (double launch) \u2014 the same ask must reuse the same request_id", 'Reports "no results" without narrating the funnel + scope_notes and proposing a concrete fix', "Renders delivered leads as freeform prose instead of the canonical lead-delivery table", "Blends two distinct buyer archetypes into one seed description instead of running one search per archetype", "Passes a country name in filters.locations (silently matches a same-named town \u2014 whole-country intent means OMITTING locations) or a nested employees object instead of the flat employees_min/employees_max", "Renders rows that visibly violate the user's exclusions, or presents a best-fit-under-30 table as an answer instead of flagging weak matches"] },
27381
27483
  leadbay_plan_tour_in_city: { "name": "leadbay_plan_tour_in_city", "short_description": 'Use whenever the user names a city they\'ll be in and asks who to see\n\u2014 "I\'m in SF next Tuesday, who\'s worth meeting?", "I\'m going to Berlin\n\u2014 who should I visit?", "plan my <city> tour". Any in-person/visit\nintent tied to a place routes here, NOT to `leadbay_pull_leads`. It\nsurfaces follow-ups + fresh Discover leads in the city via\n`leadbay_tour_plan`, ALWAYS offers to plot them on a map (rendering it\non yes), then offers outreach drafts + campaign persistence.\n', "arguments": [{ "name": "city", "description": "City or region the user is visiting (e.g. 'Limoges', 'Bay Area'). Used as the geo filter for both Monitor and Discover lookups. A country is not a city: this workspace already covers exactly one country, and a country name here silently fences the tour to a same-named village. Do NOT omit the argument to recover \u2014 a city-less tour returns arbitrary leads from across the whole workspace, which is not an itinerary. Ask which city or region the visit is to.", "required": true }, { "name": "date", "description": "When the visit is (e.g. 'May 24', 'next Thursday'). Surfaced in the outreach drafts as 'I'll be in <city> on <date>'.", "required": false }], "expected_calls": ["leadbay_tour_plan", "leadbay_research_lead_by_id", "leadbay_prepare_outreach", "leadbay_create_campaign"], "failure_modes": ["Calls leadbay_followups_map (Monitor-only) instead of leadbay_tour_plan \u2014 loses the Discover (fresh-lead) half that the user explicitly asked for", "Calls leadbay_pull_leads then drops the geo filter \u2014 returns the lens-wide wishlist instead of city-relevant fresh leads", 'Skips the campaign-persist step ("would you like to save these as a tour?") \u2014 leaves the rep with a one-shot map but no follow-up artifact', "Creates a campaign WITHOUT asking the user first \u2014 the persist step is high-intent; offer it, don't assume", "Fabricates lead_ids when seeding the campaign instead of using the ids returned by tour_plan"] },
27382
27484
  leadbay_prospecting_overview: { "name": "leadbay_prospecting_overview", "short_description": `Orientation for working with Leadbay from any host \u2014 discovery vs.
27383
27485
  follow-up, the outreach loop, outcome recording, imports, pushback /
@@ -27390,7 +27492,7 @@ should I follow up on" to "I'll send via lemlist".
27390
27492
  leadbay_refine_audience: { "name": "leadbay_refine_audience", "short_description": "Refine the kind of leads Leadbay surfaces beyond firmographics, with a\nfree-text instruction. Handles the clarification round-trip if the new\nprompt is ambiguous.\n", "arguments": [{ "name": "instruction", "description": "The refinement (e.g. 'focus on hospitals running their own IT'). Set to plain English.", "required": true }], "expected_calls": ["leadbay_refine_prompt", "leadbay_account_status"], "failure_modes": ["Calls leadbay_answer_clarification on the user's behalf instead of surfacing the clarification verbatim", "Glosses over the clarification options instead of presenting them as offered", "Promises immediate effect when status='applied' actually triggers an async intelligence recompute"] },
27391
27493
  leadbay_research_a_domain: { "name": "leadbay_research_a_domain", "short_description": "Resolve a company by name or domain across the user's visible Discover,\nMonitor, and Activate corpus, then return everything Leadbay knows about it.\n", "arguments": [{ "name": "domain", "description": "Company name or domain (for example 'Acme Corporation' or 'acme.com'). The legacy argument key remains `domain` for client compatibility.", "required": true }], "expected_calls": ["leadbay_research_lead_by_name_fuzzy"], "failure_modes": ["Fabricates qualification answers not present in any tool response", "Calls leadbay_import_and_qualify before searching the existing visible corpus", "Treats the active lens as the entire search universe when the user did not request a lens scope", "Imports a missing company without the user's explicit permission", "Renders the research result as a freeform narrative instead of the canonical research-company-card layout (the card with header score bar, pill row, signal sections, contacts table is the structural contract; commentary belongs ABOVE or BELOW it)"] },
27392
27494
  leadbay_setup_team_prospecting: { "name": "leadbay_setup_team_prospecting", "short_description": "Manager-led prospecting setup: conversationally turn a natural-language\naudience ask into a Leadbay lens, validate the candidate leads, and\npersist them as one or more named campaigns the rep(s) can work\nthrough. Closes #3630 US3 end-to-end (within the current\ncreator-scoped campaign visibility model).\n", "arguments": [{ "name": "audience", "description": "Natural-language audience description (e.g. 'plumbing companies with 10-50 employees in Seine-Maritime'). The lens-creation step (`leadbay_refine_prompt` \u2192 `leadbay_create_lens`) interprets it. A country name is not a scope here \u2014 this workspace already covers exactly one country, so drop it and keep the rest of the description; a DIFFERENT country cannot be targeted at all.", "required": true }, { "name": "rep_split", "description": "Optional: how to split the validated leads into per-rep campaigns. Free text \u2014 e.g. 'split by city' or 'one campaign per rep: John gets Tulsa, Sarah gets OKC'. Splitting by country is not a split \u2014 the workspace is single-country.", "required": false }], "expected_calls": ["leadbay_refine_prompt", "leadbay_create_lens", "leadbay_promote_lens", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_create_campaign", "leadbay_add_leads_to_campaign"], "failure_modes": ["Skips the validation step \u2014 creates a campaign of unvetted leads from a freshly-created lens without giving the manager a chance to drop weak fits", "Creates ONE campaign for all reps without asking about the split \u2014 the user explicitly mentioned per-rep distribution and the prompt should honor it", "Pretends the backend supports cross-user assignment \u2014 campaigns are owned by the caller (creator-scoped). Surface this honestly instead of fabricating an assignment model", "Asks ALL clarifying questions inline before tool calls \u2014 instead, run the lens refinement loop with `leadbay_refine_prompt` which handles the clarification protocol natively"] },
27393
- leadbay_top_accounts_to_activate: { "name": "leadbay_top_accounts_to_activate", "short_description": 'Build a ranked account-conquest plan from Leadbay data \u2014 the accounts worth\nactivating, each with a motif, a pitch and a checklist, ranked by the\nstrongest Leadbay signal. Every figure carries its source, and anything\nLeadbay can\'t measure is shown as OMITTED rather than estimated. Uses\n`leadbay_bulk_qualify_leads` and `leadbay_enrich_titles`. Trigger on\n"top 50 accounts to activate", "who should we go after".\n', "arguments": [{ "name": "count", "description": "Optional: how many accounts the plan should hold (default 50).", "required": false }, { "name": "territory", "description": "Optional: restrict the plan to a territory (e.g. 'Indre-et-Loire', 'R\xE9gion Ouest'). Sets geography on the Discover lens. A country is not a territory \u2014 this workspace already covers exactly one country.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_get_qualification_questions", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_scan_portfolio_signals", "leadbay_enrich_titles", "leadbay_bulk_enrich_status", "leadbay_account_history", "leadbay_artifact_kit", "leadbay_new_lens", "leadbay_adjust_audience"], "failure_modes": ["Invents, estimates or proxies a revenue-realized figure \u2014 the single worst failure. Leadbay does not hold what an account buys, and headcount, sector and lead score are NOT proxies for it.", "Sorts by cash-to-capture, synthesizing a revenue figure per account purely to make that ranking work. Leadbay has no revenue data: rank by the Leadbay signal, say so in the header, and never invent the key.", "Emits \u20AC figures with no provenance class, so modelled numbers read as measured fact in front of a paying client.", "Skips the PROVENANCE LEDGER, or drops un-sourceable fields from it instead of rendering them as OMITTED \u2014 which hides the gap.", "Fabricates registry/TAM counts (France or regional company counts) instead of querying the registry or marking the figure NOT COMPUTED. Leadbay does not proxy SIRENE.", "Invents the five qualification questions from this prompt's own recommendations instead of reading the org's actual questions via leadbay_get_qualification_questions.", "Leaves the deck's live layer dead \u2014 qualification and enrichment handles never wired in, so the pills and contacts stay empty while the deck still looks finished.", "Invents lead ids to make the qualification pills appear populated.", "Fabricates a plausible-sounding signal ('just won a public tender') for an account whose signals were never read. No signal read means an explicit dash.", "Assigns a motif outside the closed set of six, or assigns SAUVETAGE / PLAN DE COMPTE / MONT\xC9E EN GAMME / R\xC9VEIL from a lead score or sector when order history was never available.", "Labels Monitor membership as 'is a client' \u2014 Monitor is a Leadbay view whose membership is decided by lens scoring, not by whether the company ever bought anything.", "Launches paid enrichment on the whole plan without consent. Asking for a plan is not authorization to spend on 50 accounts.", "Re-launches enrichment from inside the built deck when a bulk handle already exists this session \u2014 double-spends the user's quota.", "Forces the interactive deck without offering it first, or ships the deck INSTEAD of a chat answer that stands on its own.", "Refuses the task because revenue data is missing, instead of delivering the conquest plan and naming what a cash-ranked version would need.", "Ends the turn without a ranked list of real accounts \u2014 gating the whole plan on a NON-blocking question (a MISSING territory, a missing lens, or a 3-vs-5 qualification-question gap) so the user gets a plan-of-a-plan. Only two things may stop delivery: an unresolvable company-identity mismatch, and a territory naming a foreign or supra-national scope. Every other open question rides alongside the delivered plan.", "Stops after the discovery contact preview to wait for enrichment consent, delivering no plan that turn \u2014 the ranked plan ships first; the paid reveal is offered alongside it.", "Renders a contact channel enrichment never returned (e.g. a phone link when only email was approved and revealed) instead of showing the returned channels and marking the rest omitted."] },
27495
+ leadbay_top_accounts_to_activate: { "name": "leadbay_top_accounts_to_activate", "short_description": 'Build a ranked account-conquest plan from Leadbay data \u2014 the accounts worth\nactivating, each with a motif, a pitch and a checklist, ranked by the\nstrongest Leadbay signal. Every figure carries its source, and anything\nLeadbay can\'t measure is shown as OMITTED rather than estimated. Uses\n`leadbay_bulk_qualify_leads` and `leadbay_enrich_titles`. Trigger on\n"top 50 accounts to activate", "who should we go after".\n', "arguments": [{ "name": "count", "description": "Optional: how many accounts the plan should hold (default 50).", "required": false }, { "name": "territory", "description": "Optional: restrict the plan to a territory (e.g. 'Indre-et-Loire', 'R\xE9gion Ouest'). Sets geography on the Discover lens. A country is not a territory \u2014 this workspace already covers exactly one country.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_get_qualification_questions", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_scan_portfolio_signals", "leadbay_enrich_titles", "leadbay_bulk_enrich_status", "leadbay_account_history", "leadbay_artifact_kit", "leadbay_new_lens", "leadbay_adjust_audience"], "failure_modes": ["Invents, estimates or proxies a revenue-realized figure \u2014 the single worst failure. Leadbay does not hold what an account buys, and headcount, sector and lead score are NOT proxies for it.", "Sorts by cash-to-capture, synthesizing a revenue figure per account purely to make that ranking work. Leadbay has no revenue data: rank by the Leadbay signal, say so in the header, and never invent the key.", "Emits \u20AC figures with no provenance class, so modelled numbers read as measured fact in front of a paying client.", "Skips the PROVENANCE LEDGER, or drops un-sourceable fields from it instead of rendering them as OMITTED \u2014 which hides the gap.", "Fabricates registry/TAM counts (France or regional company counts) instead of querying the registry or marking the figure NOT COMPUTED. Leadbay does not proxy SIRENE.", "Invents the five qualification questions from this prompt's own recommendations instead of reading the org's actual questions via leadbay_get_qualification_questions.", "Leaves the deck's live layer dead \u2014 qualification and enrichment handles never wired in, so the pills and contacts stay empty while the deck still looks finished.", "Invents lead ids to make the qualification pills appear populated.", "Fabricates a plausible-sounding signal ('just won a public tender') for an account whose signals were never read. No signal read means an explicit dash.", "Assigns a motif outside the closed set of six, or assigns SAUVETAGE / PLAN DE COMPTE / MONT\xC9E EN GAMME / R\xC9VEIL from a lead score or sector when order history was never available.", "Labels Monitor membership as 'is a client' \u2014 Monitor is a Leadbay view whose membership is decided by lens scoring, not by whether the company ever bought anything.", "Launches enrichment on the whole plan without consent. Asking for a plan is not authorization to use quota on 50 accounts.", "Re-launches enrichment from inside the built deck when a bulk handle already exists this session \u2014 uses the user's quota twice.", "Forces the interactive deck without offering it first, or ships the deck INSTEAD of a chat answer that stands on its own.", "Refuses the task because revenue data is missing, instead of delivering the conquest plan and naming what a cash-ranked version would need.", "Ends the turn without a ranked list of real accounts \u2014 gating the whole plan on a NON-blocking question (a MISSING territory, a missing lens, or a 3-vs-5 qualification-question gap) so the user gets a plan-of-a-plan. Only two things may stop delivery: an unresolvable company-identity mismatch, and a territory naming a foreign or supra-national scope. Every other open question rides alongside the delivered plan.", "Stops after the discovery contact preview to wait for enrichment consent, delivering no plan that turn \u2014 the ranked plan ships first; the reveal is offered alongside it.", "Renders a contact channel enrichment never returned (e.g. a phone link when only email was approved and revealed) instead of showing the returned channels and marking the rest omitted."] },
27394
27496
  leadbay_work_campaign: { "name": "leadbay_work_campaign", "short_description": "Work a campaign as a real outreach session: pick the campaign,\nassess what the user has (phones / emails / coords), then PROPOSE\nthe right session mode (call sheet, email sheet, enrich titles\nfirst, map). After they pick, render \u2014 and as they dictate\noutcomes per lead, record both note + epilogue via\n`leadbay_report_outreach` in one round trip.\n", "arguments": [{ "name": "campaign", "description": "Campaign name (fuzzy match against your own campaigns) or campaign UUID. Omit to list and pick interactively.", "required": false }, { "name": "mode", "description": "Optional: skip the readiness-assessment proposal and jump directly into 'call_sheet' / 'email_sheet' / 'map' / 'enrich_first'. Omit (recommended) and let the prompt propose based on the data.", "required": false }], "expected_calls": ["leadbay_list_campaigns", "leadbay_campaign_call_sheet", "leadbay_enrich_titles", "leadbay_report_outreach"], "failure_modes": ["Renders the call sheet immediately without proposing the right mode \u2014 if 60% of leads have no contacts, calling is futile; enrich first. Always assess `readiness` first.", "Auto-renders the map widget without asking \u2014 maps are intrusive when the user just wants to scroll a list. Map mode is a proposed option, not a default.", "Proposes map mode after the user has previously said they don't like maps \u2014 check conversation memory before adding 'View on a map' to the options list.", "Calls `leadbay_campaign_progression` instead of `leadbay_campaign_call_sheet` \u2014 progression has counts but no phones / LinkedIn / call-ready data; the user can't actually dial from progression rows.", "Renders contacts WITHOUT making the phone number a `[bare](tel:URL)` link \u2014 on mobile that breaks one-tap calling, which is the whole point of the cheat sheet.", "Records outreach WITHOUT epilogue_status \u2014 leaves the lead's pipeline state unchanged; the rep then sees the same lead surfaced again next session.", "Records outreach WITHOUT verification \u2014 verification.source/ref is REQUIRED. For calls, pass `{source: 'user_confirmed', ref: <user's exact words>}`.", "Loops through ALL leads in a 50-lead campaign before recording any outreach \u2014 the call-then-record loop must be per-lead, not batched."] }
27395
27497
  };
27396
27498
  var PROMPT_CATALOG_HEADER = `This server exposes the following workflow prompts via \`prompts/list\` and \`prompts/get\`. Some MCP clients render them as slash commands; if your client does not, you (the agent) should invoke them directly via \`prompts/get\` when the user's request matches one of the triggers described below.`;
@@ -28436,7 +28538,7 @@ function buildAcknowledgeUpdateTool(opts) {
28436
28538
  var ENRICHMENT_TERMINAL = `A settled-empty enrichment is TERMINAL \u2014 do not re-attempt it on a later run. On any contact record, \`enrichment.done: true\` together with \`enrichment.credits_used: 0\` means the reveal completed and the provider returned nothing. Roughly 29% of all enrichments land there, so treat it as an ordinary outcome, not an anomaly. The answer will not change tomorrow: a scheduled task that keeps calling leadbay_enrich_titles on the same contacts every run is buying nothing and hiding its own lack of progress. Read the two fields TOGETHER \u2014 \`credits_used: 0\` is also what an IN-FLIGHT reservation reports (\`done: false\`), and an ABSENT \`credits_used\` means the cost is unknown, not zero; neither is a verdict on its own. \`enrichment: null\` is a different state again \u2014 that contact was never requested and IS enrichable. The one retryable exception is a launch that errored in this same session: its reservation settles as a zero-credit failure, so retry it once, then treat it as terminal. When you report to the user, say what actually happened \u2014 "we looked for a contact at <company> and there isn't one we can reach" \u2014 instead of presenting it as still pending or silently retrying it.`;
28437
28539
  var FRICTION = `Problem reports: when the user asks you to report a Leadbay problem ("report this", "tell the team this didn't work"), call leadbay_report_friction with {category, message (the user's own words), tool_called?, severity?}. If they stated the problem in the same breath as the request, those words ARE the message \u2014 send it in that turn rather than asking them to confirm wording they just gave you, and never stall on optional fields (omit what you don't know). If you notice a problem worth reporting but the user hasn't asked, OFFER once \u2014 "Want me to report this to the Leadbay team?" \u2014 and call it only if they agree. Never call it unprompted. Always tell the user the outcome the tool returns: if \`reported\` is false the report was NOT delivered and you must say so rather than implying it was sent. Frustration alone is not a reason to call it: keep solving their ask.`;
28438
28540
  var MENTAL_MODEL = `How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.`;
28439
- var QUOTA_REFRESH = `Show the refreshed quota AFTER a paid action has actually COMPLETED: when leadbay_bulk_enrich_status reports the job done \u2014 all_done, OR a plateau you've decided is terminal (overall_progress.done stopped climbing across spaced polls, so some contacts are unresolvable and all_done stays false) \u2014 OR a top-up the user confirmed landed, call leadbay_account_status once and render the refreshed quota \u2014 the per-window %/$ gauge (Daily/Weekly/Monthly) it returns \u2014 so the user sees where they now stand. Wait for genuine completion: leadbay_enrich_contacts only LAUNCHES an async reveal (it returns a hint to check back in ~60s), so do NOT refresh quota right after it \u2014 the usage isn't reflected yet. For that single-contact flow, refresh only once a re-read of the lead's contacts (leadbay_research_lead_by_id; leadbay_get_contacts where exposed) shows the REQUESTED channel actually landed \u2014 the requested email and/or phone_number present \u2014 NOT enrichment.done alone (that flag is already true for a contact enriched on the other channel earlier, so a phone reveal could otherwise trigger the refresh before phone_number arrives). This is the canonical quota surface; do NOT hand-roll a 'credits' line in its place. Skip it only when account_status reports unlimited_credits, quota_error, or a null quota (nothing to show), or when billing is genuinely unavailable. Do it ONCE per completed action \u2014 not after every poll while work is still in progress.`;
28541
+ var QUOTA_REFRESH = `Show the refreshed quota AFTER a quota-using action has actually COMPLETED: when leadbay_bulk_enrich_status reports the job done \u2014 all_done, OR a plateau you've decided is terminal (overall_progress.done stopped climbing across spaced polls, so some contacts are unresolvable and all_done stays false) \u2014 OR a top-up the user confirmed landed, call leadbay_account_status once and render the refreshed quota \u2014 the per-window %/$ gauge (Daily/Weekly/Monthly) it returns \u2014 so the user sees where they now stand. Wait for genuine completion: leadbay_enrich_contacts only LAUNCHES an async reveal (it returns a hint to check back in ~60s), so do NOT refresh quota right after it \u2014 the usage isn't reflected yet. For that single-contact flow, refresh only once a re-read of the lead's contacts (leadbay_research_lead_by_id; leadbay_get_contacts where exposed) shows the REQUESTED channel actually landed \u2014 the requested email and/or phone_number present \u2014 NOT enrichment.done alone (that flag is already true for a contact enriched on the other channel earlier, so a phone reveal could otherwise trigger the refresh before phone_number arrives). This is the canonical quota surface; do NOT hand-roll a 'credits' line in its place. Skip it only when account_status reports unlimited_credits, quota_error, or a null quota (nothing to show), or when billing is genuinely unavailable. Do it ONCE per completed action \u2014 not after every poll while work is still in progress.`;
28440
28542
  var QUOTA_TOPUP = `Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, the user has TWO options \u2014 wait for the window reset (daily / weekly / monthly resets shown in leadbay_account_status), OR top up AI credits (top-ups clear the throttle IMMEDIATELY \u2014 they are not subject to the same window). Always offer BOTH options; default-recommending 'wait until tomorrow' is wrong when a 30-second top-up unblocks the same call. If the host exposes leadbay_create_topup_link, OFFER it on every quota wall: 'Want me to generate a top-up link?' \u2014 when the user says yes, call leadbay_create_topup_link and surface the returned Stripe URL as a clickable link for the user to open in their browser. (Sibling leadbay_open_billing_portal is for ongoing subscription changes, not one-shot top-ups.) AFTER the user has topped up: do NOT keep refusing operations. A top-up invalidates every prior 429 and every stale 'you're at your quota' snapshot. The moment the user signals they topped up / bought credits / added credits \u2014 even WITHOUT re-calling account_status \u2014 treat the previous quota state as void and RETRY the originally failed call. (Best practice: re-call leadbay_account_status to surface the fresh state to the user, then retry; but the retry itself does NOT require a successful account_status check first. If the retry hits the wall again, THEN you have evidence the top-up didn't land; only then re-offer top-up / wait.) The agent's job after a top-up is to RESUME the workflow the user was on, not gate-keep.`;
28441
28543
  var TRANSIENT_401 = `Transient 401s (DO NOT narrate as an auth problem): Leadbay bearer tokens do NOT expire on a timer, and the client already auto-retried this call once before surfacing it. So a lone AUTH_EXPIRED / 401 from a tool is almost always a brief Leadbay-side hiccup, NOT a sign the user must sign in again. When you see ONE, just silently retry the operation once more and carry on \u2014 do NOT tell the user to log in again, reconnect the connector, reauthorize, or 're-load Leadbay', and do NOT surface the bare '401' to them at all. Only if calls keep failing across several attempts should you mention it \u2014 and then frame it as a possible Leadbay-side issue or a possible logout (you cannot tell which from here), and offer to report it to the team. Never turn a one-off 401 into a 'your connection needs re-authenticating' message; that is the wrong read on a connection that is actually working.`;
28442
28544
  var TRIGGERED_BY = `Trigger provenance (MANDATORY): every Leadbay composite-tool call MUST carry a non-empty \`_triggered_by\` argument \u2014 the verbatim slice of the user's most recent message that this call is acting upon. Quote literally; do NOT paraphrase, summarize, or substitute a one-word label like "leads" or "request" (those are rejected). If you are acting WITHOUT a fresh user message (a memory recall, a scheduled run, a self-initiated retry), pass the actual instruction you are acting on \u2014 the recalled directive, the schedule's intent, or the original request being retried \u2014 so the value is always a real, auditable trace. Strip any secrets the user pasted (API keys, passwords, card numbers, full home addresses) \u2014 replace with [REDACTED]. A composite call missing or blanking this field is rejected with LAST_PROMPT_REQUIRED; just re-call with the field set. This is a protocol requirement on EVERY composite invocation (not just the first), independent of any telemetry setting.`;
@@ -28575,7 +28677,7 @@ function buildProtocolPrimitivesParagraph(has) {
28575
28677
  }
28576
28678
  if (deliveryRunners.length > 0) {
28577
28679
  clauses.push(
28578
- "On " + deliveryRunners.map((n) => `leadbay_${n}`).join(", ") + " the job is BACKEND-owned and likewise keeps running. Any work already paid for still completes; poll `leadbay_lead_job_status` with the `job_id` later to collect it."
28680
+ "On " + deliveryRunners.map((n) => `leadbay_${n}`).join(", ") + " the job is BACKEND-owned and likewise keeps running. Any work already launched still completes; poll `leadbay_lead_job_status` with the `job_id` later to collect it."
28579
28681
  );
28580
28682
  }
28581
28683
  parts.push(
@@ -29136,6 +29238,9 @@ ${url}
29136
29238
  }) === true
29137
29239
  }));
29138
29240
  await maybeAttachUpdate(name, result);
29241
+ if (name === "leadbay_account_status" && result !== null && typeof result === "object" && !Array.isArray(result) && result.error !== true) {
29242
+ result.mcp_version = serverVersion;
29243
+ }
29139
29244
  maybeAttachNotifications(result);
29140
29245
  if (result && typeof result === "object" && result.error === true) {
29141
29246
  const envText = formatErrorForLLM(result);
@@ -30787,7 +30892,7 @@ var OAUTH_BASE_URLS = {
30787
30892
  fr: "https://staging.api.leadbay.app"
30788
30893
  }
30789
30894
  };
30790
- var VERSION = "0.37.0";
30895
+ var VERSION = "0.38.1";
30791
30896
  var HELP = `
30792
30897
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
30793
30898