@leadbay/mcp 0.34.1 → 0.35.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/CHANGELOG.md +137 -0
- package/MIGRATION.md +51 -0
- package/README.md +1 -1
- package/dist/bin.js +1200 -1558
- package/dist/http-server.js +1061 -816
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/http-server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/http-server.ts
|
|
4
|
-
import { randomUUID as
|
|
4
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5
5
|
import { realpathSync } from "fs";
|
|
6
6
|
import { basename } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
@@ -47,7 +47,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
47
47
|
|
|
48
48
|
# Resilience rules for Leadbay long-running tools
|
|
49
49
|
|
|
50
|
-
These
|
|
50
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
51
51
|
|
|
52
52
|
## Rule 1 \u2014 Pin the lens
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
55
55
|
|
|
56
56
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
57
57
|
|
|
58
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
58
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
59
59
|
|
|
60
60
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
61
61
|
|
|
@@ -71,6 +71,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
71
71
|
|
|
72
72
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
73
73
|
|
|
74
|
+
## A launched job cannot be stopped
|
|
75
|
+
|
|
76
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
77
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
78
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
79
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
80
|
+
launched nothing and is not covered here.
|
|
81
|
+
|
|
82
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
83
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
84
|
+
work stopped. What to do next depends on what you are holding:
|
|
85
|
+
|
|
86
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
87
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
88
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
89
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
90
|
+
notification of its own: resume it with
|
|
91
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
92
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
93
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
94
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
95
|
+
batch.
|
|
96
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
97
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
98
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
99
|
+
back the job already launched rather than starting a second one, but that guard
|
|
100
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
101
|
+
are about to re-run before you spend the user's quota on it.
|
|
102
|
+
|
|
103
|
+
|
|
74
104
|
|
|
75
105
|
# PHASE 0 \u2014 STATE + AUDIENCE
|
|
76
106
|
|
|
@@ -239,7 +269,7 @@ Run the Leadbay daily check-in for me. Treat this prompt the same way for any eq
|
|
|
239
269
|
|
|
240
270
|
# Resilience rules for Leadbay long-running tools
|
|
241
271
|
|
|
242
|
-
These
|
|
272
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
243
273
|
|
|
244
274
|
## Rule 1 \u2014 Pin the lens
|
|
245
275
|
|
|
@@ -247,7 +277,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
247
277
|
|
|
248
278
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
249
279
|
|
|
250
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
280
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
251
281
|
|
|
252
282
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
253
283
|
|
|
@@ -263,6 +293,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
263
293
|
|
|
264
294
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
265
295
|
|
|
296
|
+
## A launched job cannot be stopped
|
|
297
|
+
|
|
298
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
299
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
300
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
301
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
302
|
+
launched nothing and is not covered here.
|
|
303
|
+
|
|
304
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
305
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
306
|
+
work stopped. What to do next depends on what you are holding:
|
|
307
|
+
|
|
308
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
309
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
310
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
311
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
312
|
+
notification of its own: resume it with
|
|
313
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
314
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
315
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
316
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
317
|
+
batch.
|
|
318
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
319
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
320
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
321
|
+
back the job already launched rather than starting a second one, but that guard
|
|
322
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
323
|
+
are about to re-run before you spend the user's quota on it.
|
|
324
|
+
|
|
325
|
+
|
|
266
326
|
|
|
267
327
|
# PHASE 0 \u2014 RESUME CHECK
|
|
268
328
|
|
|
@@ -357,7 +417,7 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
357
417
|
|
|
358
418
|
ABOVE the table, add a 2\u20134 sentence "Today's nudges" paragraph for the 3 most-promising rows. The nudges speak to urgency / opportunity / freshness \u2014 what makes acting on these RIGHT NOW the right call. Do NOT repeat the "why it fits" column from the table; the nudges should add fresh framing the table doesn't carry (e.g., recent news from the \`qualification_summary\` excerpt, a window closing, a competitor activity the user mentioned earlier in the session). One sentence per nudge, salesperson voice, not coachspeak.
|
|
359
419
|
|
|
360
|
-
If the batch returns fewer than 10 qualified leads, top it up: call \`leadbay_bulk_qualify_leads\` with \`lensId:<captured>\`, \`count:<1.5x deficit, capped at 25>\`, and **\`wait_for_completion:false\`**. Capture \`
|
|
420
|
+
If the batch returns fewer than 10 qualified leads, top it up: call \`leadbay_bulk_qualify_leads\` with \`lensId:<captured>\`, \`count:<1.5x deficit, capped at 25>\`, and **\`wait_for_completion:false\`**. Capture \`notification_id\` from the response and poll \`leadbay_qualify_status\` every ~10s until \`status:'done'\`. Then re-pull with the same \`lensId\` to pick up the newly qualified leads. **Never re-pull without \`lensId\` \u2014 you will lose your batch to a lens shift.** (The \`leadbay_qualify_top_n\` slash-prompt wraps this same tool with a friendlier surface for users; agents should call the underlying tool directly here.)
|
|
361
421
|
|
|
362
422
|
# PHASE 4 \u2014 DEEP DIVE (every promising lead)
|
|
363
423
|
|
|
@@ -471,7 +531,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
471
531
|
|
|
472
532
|
# Resilience rules for Leadbay long-running tools
|
|
473
533
|
|
|
474
|
-
These
|
|
534
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
475
535
|
|
|
476
536
|
## Rule 1 \u2014 Pin the lens
|
|
477
537
|
|
|
@@ -479,7 +539,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
479
539
|
|
|
480
540
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
481
541
|
|
|
482
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
542
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
483
543
|
|
|
484
544
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
485
545
|
|
|
@@ -495,6 +555,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
495
555
|
|
|
496
556
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
497
557
|
|
|
558
|
+
## A launched job cannot be stopped
|
|
559
|
+
|
|
560
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
561
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
562
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
563
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
564
|
+
launched nothing and is not covered here.
|
|
565
|
+
|
|
566
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
567
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
568
|
+
work stopped. What to do next depends on what you are holding:
|
|
569
|
+
|
|
570
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
571
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
572
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
573
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
574
|
+
notification of its own: resume it with
|
|
575
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
576
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
577
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
578
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
579
|
+
batch.
|
|
580
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
581
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
582
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
583
|
+
back the job already launched rather than starting a second one, but that guard
|
|
584
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
585
|
+
are about to re-run before you spend the user's quota on it.
|
|
586
|
+
|
|
587
|
+
|
|
498
588
|
|
|
499
589
|
# THE ONE-FORWARD-OPTION RULE \u2014 the structural contract of this walkthrough
|
|
500
590
|
|
|
@@ -930,7 +1020,7 @@ account's **default wishlist selection** while \`confirm\`/\`email\` are set \u2
|
|
|
930
1020
|
it would reveal and charge for the whole batch instead of the one lead the user
|
|
931
1021
|
agreed to.
|
|
932
1022
|
|
|
933
|
-
It returns a \`
|
|
1023
|
+
It returns a \`notification_id\` and runs async \u2014 poll \`leadbay_bulk_enrich_status\`
|
|
934
1024
|
with that id (\`include_contacts=true\`) until \`all_done\`, or until the resolved
|
|
935
1025
|
count plateaus across a few spaced polls. Then report the contact that actually
|
|
936
1026
|
resolved: name, title, and the email/phone that came back. Contacts sometimes
|
|
@@ -1168,7 +1258,7 @@ Build the final mappings yourself. Start from \`leadbay_resolve_import_rows.mapp
|
|
|
1168
1258
|
|
|
1169
1259
|
# PHASE 5 \u2014 QUALIFY (optional) + REPORT
|
|
1170
1260
|
|
|
1171
|
-
Prefer \`leadbay_import_and_qualify\` when the user asks to qualify/research after import; otherwise use \`leadbay_import_leads\`. For large files or short client timeouts, pass \`wait_for_completion=false\` and poll \`leadbay_import_status\`. After import, qualify only lead IDs returned by the import. Rows that came back \`uncrawled\` are pending a background crawl (not failures); the leads Leadbay adds for them populate in the user's Leadbay account as the crawl completes \u2014 tell the user that, not that a tool call will fetch them (\`import_status\`
|
|
1261
|
+
Prefer \`leadbay_import_and_qualify\` when the user asks to qualify/research after import; otherwise use \`leadbay_import_leads\`. For large files or short client timeouts, pass \`wait_for_completion=false\` and poll \`leadbay_import_status\`. After import, qualify only lead IDs returned by the import. Rows that came back \`uncrawled\` are pending a background crawl (not failures); the leads Leadbay adds for them populate in the user's Leadbay account as the crawl completes \u2014 tell the user that, not that a tool call will fetch them (\`import_status\` refreshes status/progress only; \`pull_leads\` reads the active lens, so an imported lead outside it may not appear; re-running the import later re-reconciles those companies).
|
|
1172
1262
|
|
|
1173
1263
|
**Deliver the augmented file back to the user**: the original file plus a new \`LEADBAY_ID\` column populated from the resolution step. This is the second deliverable of a job well done.
|
|
1174
1264
|
|
|
@@ -1340,7 +1430,7 @@ You are working with Leadbay through the \`leadbay_*\` MCP tools. This prompt or
|
|
|
1340
1430
|
|
|
1341
1431
|
# Resilience rules for Leadbay long-running tools
|
|
1342
1432
|
|
|
1343
|
-
These
|
|
1433
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
1344
1434
|
|
|
1345
1435
|
## Rule 1 \u2014 Pin the lens
|
|
1346
1436
|
|
|
@@ -1348,7 +1438,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
1348
1438
|
|
|
1349
1439
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
1350
1440
|
|
|
1351
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
1441
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
1352
1442
|
|
|
1353
1443
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
1354
1444
|
|
|
@@ -1364,6 +1454,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
1364
1454
|
|
|
1365
1455
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
1366
1456
|
|
|
1457
|
+
## A launched job cannot be stopped
|
|
1458
|
+
|
|
1459
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
1460
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
1461
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
1462
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
1463
|
+
launched nothing and is not covered here.
|
|
1464
|
+
|
|
1465
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
1466
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
1467
|
+
work stopped. What to do next depends on what you are holding:
|
|
1468
|
+
|
|
1469
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
1470
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
1471
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
1472
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
1473
|
+
notification of its own: resume it with
|
|
1474
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
1475
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
1476
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
1477
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
1478
|
+
batch.
|
|
1479
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
1480
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
1481
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
1482
|
+
back the job already launched rather than starting a second one, but that guard
|
|
1483
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
1484
|
+
are about to re-run before you spend the user's quota on it.
|
|
1485
|
+
|
|
1486
|
+
|
|
1367
1487
|
|
|
1368
1488
|
## The two entry points
|
|
1369
1489
|
|
|
@@ -1524,7 +1644,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
1524
1644
|
# PHASE 1 \u2014 LAUNCH
|
|
1525
1645
|
Call \`leadbay_bulk_qualify_leads\` with \`count={{arg:count_or_default}}\` and \`wait_for_completion=true\` (synchronous mode \u2014 waits for results before returning).
|
|
1526
1646
|
|
|
1527
|
-
**Resilience rule:** If \`leadbay_bulk_qualify_leads\` returns
|
|
1647
|
+
**Resilience rule:** If \`leadbay_bulk_qualify_leads\` returns an infrastructure error, do NOT retry with \`wait_for_completion=false\`. Instead, proceed directly to Phase 3 and call \`leadbay_pull_leads\` to surface the already-qualified leads in the current batch.
|
|
1528
1648
|
|
|
1529
1649
|
# PHASE 2 \u2014 POLL
|
|
1530
1650
|
While it polls, expect notifications / progress events showing per-lead transitions. Surface meaningful ones (e.g. "lead X just finished") to me as they arrive \u2014 one inline status sentence per check, never expanded into a card:
|
|
@@ -2023,7 +2143,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
2023
2143
|
|
|
2024
2144
|
# Resilience rules for Leadbay long-running tools
|
|
2025
2145
|
|
|
2026
|
-
These
|
|
2146
|
+
These rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
2027
2147
|
|
|
2028
2148
|
## Rule 1 \u2014 Pin the lens
|
|
2029
2149
|
|
|
@@ -2031,7 +2151,7 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
|
|
|
2031
2151
|
|
|
2032
2152
|
## Rule 2 \u2014 Prefer async for bulk operations
|
|
2033
2153
|
|
|
2034
|
-
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false
|
|
2154
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\` and return immediately. They hand back different ids: \`bulk_qualify_leads\` returns \`{status:'running', notification_id, lead_ids, lens_id}\` \u2014 poll \`leadbay_qualify_status\` with those. \`import_and_qualify\` returns \`{status:'running', import_ids}\` and no \`notification_id\` at all \u2014 poll \`leadbay_import_status({importIds, dry_run})\` with those. Poll every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
2035
2155
|
|
|
2036
2156
|
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
2037
2157
|
|
|
@@ -2047,6 +2167,36 @@ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any o
|
|
|
2047
2167
|
|
|
2048
2168
|
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
2049
2169
|
|
|
2170
|
+
## A launched job cannot be stopped
|
|
2171
|
+
|
|
2172
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
2173
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
2174
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
2175
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
2176
|
+
launched nothing and is not covered here.
|
|
2177
|
+
|
|
2178
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
2179
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
2180
|
+
work stopped. What to do next depends on what you are holding:
|
|
2181
|
+
|
|
2182
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
2183
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
2184
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
2185
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
2186
|
+
notification of its own: resume it with
|
|
2187
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
2188
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
2189
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
2190
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
2191
|
+
batch.
|
|
2192
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
2193
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
2194
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
2195
|
+
back the job already launched rather than starting a second one, but that guard
|
|
2196
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
2197
|
+
are about to re-run before you spend the user's quota on it.
|
|
2198
|
+
|
|
2199
|
+
|
|
2050
2200
|
|
|
2051
2201
|
# PHASE 0 \u2014 SCOPE + STATE
|
|
2052
2202
|
|
|
@@ -2144,7 +2294,7 @@ So: **read the persisted filter first** (the response reports \`active_filters\`
|
|
|
2144
2294
|
|
|
2145
2295
|
\u26A0 **Pass explicit \`leadIds\` whenever the cohort isn't simply "the next N on the lens"** \u2014 e.g. after you've selected a shortlist, or when the plan mixes Monitor and Discover rows. The \`count\`-based path selects the next *unqualified leads from the lens wishlist*, so on any other cohort it qualifies unrelated leads and hands you handles whose pills belong to different companies. Use \`leadbay_bulk_qualify_leads({leadIds:[\u2026\u226425 of the cohort], wait_for_completion:false})\` and chunk through the cohort's own ids. The \`{lensId, count}\` form is only right when the cohort genuinely *is* the lens's top N.
|
|
2146
2296
|
|
|
2147
|
-
**Qualify the plan cohort, not the whole base.** Select your ~{{arg:count_or_default}} candidates (plus a modest buffer for drop-outs) BEFORE qualifying \u2014 qualification is async and quota-bearing, so running it across an entire portfolio to produce a top-{{arg:count_or_default}} burns the user's quota for rows that will never appear. **Keep every returned \`
|
|
2297
|
+
**Qualify the plan cohort, not the whole base.** Select your ~{{arg:count_or_default}} candidates (plus a modest buffer for drop-outs) BEFORE qualifying \u2014 qualification is async and quota-bearing, so running it across an entire portfolio to produce a top-{{arg:count_or_default}} burns the user's quota for rows that will never appear. **Keep every returned \`notification_id\`** \u2014 the deck's live qualification layer is wired from those handles, and a deck with none is a dead deck that still looks finished. Never ship a plan whose lower ranks have empty qualification pills because only the first 25 were ever qualified.
|
|
2148
2298
|
|
|
2149
2299
|
**Signals \u2014 scoped to the cohort.** \u26A0 **Always pass the selected \`leadIds\`.** With \`leadIds\` omitted, \`leadbay_scan_portfolio_signals\` builds its own portfolio by paging \`/monitor\` \u2014 so on an imported cohort or a freshly-pulled Discover set it would scan a *different population* and you'd render dashes for accounts whose signals were never read.
|
|
2150
2300
|
|
|
@@ -2271,7 +2421,7 @@ Each card needs a reachable decision-maker. \`leadbay_enrich_titles({leadIds, le
|
|
|
2271
2421
|
|
|
2272
2422
|
\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.
|
|
2273
2423
|
|
|
2274
|
-
On an explicit yes, launch with the agreed \`titles\` + channels, then poll \`leadbay_bulk_enrich_status\` until done and **keep the \`
|
|
2424
|
+
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.
|
|
2275
2425
|
|
|
2276
2426
|
\u26A0 **Render only the channels that actually came back.** The default reveal is email-only unless phone was explicitly requested, so never emit a \`tel:\` link for a contact whose phone was never revealed \u2014 show the channels enrichment returned and mark the rest omitted. A fabricated phone link is the same failure as a fabricated euro.
|
|
2277
2427
|
|
|
@@ -2417,9 +2567,9 @@ ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We
|
|
|
2417
2567
|
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
2418
2568
|
|
|
2419
2569
|
|
|
2420
|
-
\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 \`
|
|
2570
|
+
\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.
|
|
2421
2571
|
|
|
2422
|
-
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 \`
|
|
2572
|
+
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.
|
|
2423
2573
|
|
|
2424
2574
|
# Iron laws
|
|
2425
2575
|
|
|
@@ -4104,7 +4254,7 @@ Some Leadbay tool responses include a \`_meta.notifications\` array listing **ba
|
|
|
4104
4254
|
- \`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.
|
|
4105
4255
|
- \`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.
|
|
4106
4256
|
|
|
4107
|
-
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the
|
|
4257
|
+
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the notification_id and let completion arrive via \`_meta.notifications\` (only when a notification id exists; if none was returned, tell the user to ask again / that you'll poll later, since nothing will auto-surface). For qualification and imports, poll inline only for small/quick runs or when the user explicitly asked you to wait; otherwise return the handle and let \`_meta.notifications\` deliver it. Either way, the user should never have to ask "is it done yet?" for work you kicked off in the same turn \u2014 you either report it or hand back a clear in-progress handle.
|
|
4108
4258
|
|
|
4109
4259
|
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
4110
4260
|
|
|
@@ -4411,23 +4561,57 @@ WHEN TO USE: the user asks for a clickable / interactive artifact, dashboard, or
|
|
|
4411
4561
|
|
|
4412
4562
|
WHEN NOT TO USE: the user wants a plain data answer (route to leadbay_pull_leads / leadbay_pull_followups) or to log a single real outreach you just did (leadbay_report_outreach).
|
|
4413
4563
|
`;
|
|
4414
|
-
var leadbay_bulk_enrich_status = `Check status + per-lead contacts for a bulk enrichment you previously launched via leadbay_enrich_titles.
|
|
4564
|
+
var 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.
|
|
4415
4565
|
|
|
4416
|
-
WHEN TO USE: poll this REPEATEDLY after leadbay_enrich_titles returns a \`
|
|
4566
|
+
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.
|
|
4417
4567
|
|
|
4418
4568
|
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that already includes enriched contacts for a single lead.
|
|
4419
4569
|
|
|
4570
|
+
## A launched job cannot be stopped
|
|
4571
|
+
|
|
4572
|
+
Leadbay has no cancel. A job started by \`leadbay_enrich_titles\`,
|
|
4573
|
+
\`leadbay_bulk_qualify_leads\`, \`leadbay_import_leads\` or
|
|
4574
|
+
\`leadbay_import_and_qualify\` runs to completion on Leadbay. The user cancelling
|
|
4575
|
+
in the chat, a request timeout, or a closed stream stops YOUR waiting, never the
|
|
4576
|
+
job, and \`cancelled: true\` on an earlier result means we stopped watching, not
|
|
4577
|
+
that the work stopped.
|
|
4578
|
+
|
|
4579
|
+
**This tool only reads.** Calling it again launches nothing and spends no quota,
|
|
4580
|
+
so poll it as often as the job needs \u2014 a timeout here is a reason to call it
|
|
4581
|
+
again, not a reason to stop.
|
|
4582
|
+
|
|
4583
|
+
One import state does NOT progress: a chunk cancelled before its mappings were
|
|
4584
|
+
committed reads \`running\` / \`committing\` forever. If the counts hold flat across
|
|
4585
|
+
several spaced polls, say so and stop, rather than polling on.
|
|
4586
|
+
|
|
4587
|
+
What must not be repeated is the LAUNCH \u2014 for work that actually launched. Re-run
|
|
4588
|
+
a launcher only for a subset that never started, never for the whole batch:
|
|
4589
|
+
|
|
4590
|
+
- \`failed[]\` entries with \`error:"not_queued"\`;
|
|
4591
|
+
- a \`rows_pending_upload\` count;
|
|
4592
|
+
- leads in \`still_running\` after a CANCELLED \`leadbay_import_and_qualify\`. Its
|
|
4593
|
+
fan-out is sequential, so an interruption leaves the remainder unlaunched and
|
|
4594
|
+
folds them in with the ones that did launch. Nothing in the result tells the
|
|
4595
|
+
two apart, and this tool cannot start either. Wait until the REST of the batch
|
|
4596
|
+
has settled: what launched settles in order, so leads still unanswered after
|
|
4597
|
+
that are the ones that never started. Only then call
|
|
4598
|
+
\`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
|
|
4599
|
+
that is merely slow looks identical to one that never launched over a few
|
|
4600
|
+
polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
|
|
4601
|
+
rather than guess.
|
|
4602
|
+
|
|
4603
|
+
|
|
4420
4604
|
## QUOTA \u2014 show where the user stands after the spend
|
|
4421
4605
|
|
|
4422
4606
|
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.
|
|
4423
4607
|
|
|
4424
4608
|
## COMPLETION REPORT \u2014 what to tell the user when the job is done
|
|
4425
4609
|
|
|
4426
|
-
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\`.
|
|
4610
|
+
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.
|
|
4427
4611
|
`;
|
|
4428
|
-
var 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',
|
|
4612
|
+
var 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.
|
|
4429
4613
|
|
|
4430
|
-
**Default to \`wait_for_completion:false\`** for any \`count > 5\` or when chained inside a multi-phase workflow \u2014 the blocking default can hit the MCP per-call timeout and surface as \`"Request timed out"\` even when the server is still working fine. The async pattern (capture \`
|
|
4614
|
+
**Default to \`wait_for_completion:false\`** for any \`count > 5\` or when chained inside a multi-phase workflow \u2014 the blocking default can hit the MCP per-call timeout and surface as \`"Request timed out"\` even when the server is still working fine. The async pattern (capture \`notification_id\`, poll \`leadbay_qualify_status\` every ~10s) is timeout-proof. Reserve the blocking form for tiny single-digit counts in interactive use.
|
|
4431
4615
|
|
|
4432
4616
|
Context: Leadbay auto-qualifies roughly the top 10 of each daily batch. Leads below the top ~10 are NOT worse \u2014 the system is saving resources. This tool is how the agent spends more resources to go deeper on promising-looking leads the user hasn't had time to surface yet.
|
|
4433
4617
|
|
|
@@ -4435,6 +4619,36 @@ WHEN TO USE: when the user wants more qualified leads than what's currently show
|
|
|
4435
4619
|
|
|
4436
4620
|
WHEN NOT TO USE: to qualify a single specific lead \u2014 that's leadbay_qualify_lead (granular, advanced).
|
|
4437
4621
|
|
|
4622
|
+
## A launched job cannot be stopped
|
|
4623
|
+
|
|
4624
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
4625
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
4626
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
4627
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
4628
|
+
launched nothing and is not covered here.
|
|
4629
|
+
|
|
4630
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
4631
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
4632
|
+
work stopped. What to do next depends on what you are holding:
|
|
4633
|
+
|
|
4634
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
4635
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
4636
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
4637
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
4638
|
+
notification of its own: resume it with
|
|
4639
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
4640
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
4641
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
4642
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
4643
|
+
batch.
|
|
4644
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
4645
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
4646
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
4647
|
+
back the job already launched rather than starting a second one, but that guard
|
|
4648
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
4649
|
+
are about to re-run before you spend the user's quota on it.
|
|
4650
|
+
|
|
4651
|
+
|
|
4438
4652
|
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\`.
|
|
4439
4653
|
|
|
4440
4654
|
|
|
@@ -4456,7 +4670,7 @@ After the status line, propose the obvious refresh / progress-check / recovery a
|
|
|
4456
4670
|
|
|
4457
4671
|
Specifically for bulk qualify:
|
|
4458
4672
|
|
|
4459
|
-
- Kicked off async \u2192 \`"\u2713 Qualifying N lead(s) (
|
|
4673
|
+
- Kicked off async \u2192 \`"\u2713 Qualifying N lead(s) (notification_id <id>) \u2014 typically ~M minutes. I'll refresh your leads view when it's done."\`
|
|
4460
4674
|
- Blocking call returned with answers \u2192 \`"\u2713 Qualified N lead(s). Refresh your leads to see the new \u2756 caps."\`
|
|
4461
4675
|
- Already-qualified short-circuit \u2192 \`"All N leads are already qualified \u2014 no work to do."\`
|
|
4462
4676
|
- 429 mid-fanout \u2192 \`"\u26A0 Rate-limited after launching M of N \u2014 already-launched leads will complete; re-call later for the rest."\`
|
|
@@ -4919,6 +5133,23 @@ WHEN TO USE: when the user has already picked WHO they want on a company and you
|
|
|
4919
5133
|
|
|
4920
5134
|
WHEN NOT TO USE: for bulk enrichment by job title across many leads \u2014 use leadbay_enrich_titles, which handles the selection lifecycle and returns a clean preview/launch flow. Not to mark someone as the priority contact \u2014 that is leadbay_pin_contact, and pinning does not enrich anyone.
|
|
4921
5135
|
|
|
5136
|
+
## A launched job cannot be stopped, and this tool has no retry guard
|
|
5137
|
+
|
|
5138
|
+
Leadbay has no cancel. Once this call returns having actually launched, the work
|
|
5139
|
+
is queued on Leadbay and runs to completion, and the quota it costs is already
|
|
5140
|
+
committed. A \`dry_run\` result reached no backend and spent nothing. The user
|
|
5141
|
+
cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
|
|
5142
|
+
never the job.
|
|
5143
|
+
|
|
5144
|
+
Unlike the composite launchers, this tool has **no double-launch guard**: calling
|
|
5145
|
+
it again always issues a new paid launch, even seconds later with identical
|
|
5146
|
+
arguments. So when a call returns nothing at all, do not simply retry. Read the
|
|
5147
|
+
record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
|
|
5148
|
+
lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
|
|
5149
|
+
see whether the launch already landed, and tell the user what you are about to
|
|
5150
|
+
spend before spending it again.
|
|
5151
|
+
|
|
5152
|
+
|
|
4922
5153
|
## QUOTA, NOT CREDITS
|
|
4923
5154
|
|
|
4924
5155
|
Enrichment is gated by QUOTA (the per-window allowance in \`leadbay_account_status\`), not a credit balance. **Never pre-refuse because a credit number looks low or zero** \u2014 a freemium/fresh account with quota left can enrich even when its credit counter reads 0. The reveal either fits the remaining quota or the backend returns 429 (\`quota_exceeded\`); only THEN surface the exhausted window + wait-or-top-up choice. The \`credits_remaining\` field on the result is **advisory internal context only \u2014 do NOT display it**. Because it can read \`0\` on an account that still has quota, printing \`_(N credits remaining)_\` would falsely tell the user they're out. Do not render a credits balance at all; if the user asks where they stand, call \`leadbay_account_status\` and show the quota gauge instead. The actual per-contact cost (\`enrichment.credits_used\`) appears on the contact after enrichment.
|
|
@@ -4933,6 +5164,36 @@ WHEN TO USE: as the agent's go-to enrichment entry point, immediately before pro
|
|
|
4933
5164
|
|
|
4934
5165
|
WHEN NOT TO USE: to enrich a single named contact \u2014 that's leadbay_enrich_contacts. Speculatively, before the user has committed to outreaching \u2014 enrichment consumes quota. **NOT to add "titles" or "LinkedIn" to a list** \u2014 a contact's \`job_title\` and \`linkedin_page\` already ride on the contact record; they are FREE and need no enrichment. If the user asks for "title and LinkedIn only", read those fields directly (e.g. leadbay_get_contacts / leadbay_research_lead_by_id); do NOT launch a job here. This tool is strictly the email / phone reveal, which consumes quota.
|
|
4935
5166
|
|
|
5167
|
+
## A launched job cannot be stopped
|
|
5168
|
+
|
|
5169
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
5170
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
5171
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
5172
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
5173
|
+
launched nothing and is not covered here.
|
|
5174
|
+
|
|
5175
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
5176
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
5177
|
+
work stopped. What to do next depends on what you are holding:
|
|
5178
|
+
|
|
5179
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
5180
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
5181
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
5182
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
5183
|
+
notification of its own: resume it with
|
|
5184
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
5185
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
5186
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
5187
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
5188
|
+
batch.
|
|
5189
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
5190
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
5191
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
5192
|
+
back the job already launched rather than starting a second one, but that guard
|
|
5193
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
5194
|
+
are about to re-run before you spend the user's quota on it.
|
|
5195
|
+
|
|
5196
|
+
|
|
4936
5197
|
## ENRICHMENT CONSUMES QUOTA \u2014 the model to reason with
|
|
4937
5198
|
|
|
4938
5199
|
Each email reveal and each phone reveal **consumes quota** (the per-window daily / weekly / monthly allowance shown in \`leadbay_account_status\`). That is the ONLY thing that gates enrichment. Do **NOT** reason about, mention, or block on "credits": there is no separate credit wall the user must clear first \u2014 enrichment either fits the user's remaining quota or the backend returns 429 (\`{status:'quota_exceeded'}\`) when a window is actually exhausted. **Never pre-refuse enrichment because a credit number looks low or zero** \u2014 a fresh/freemium account with quota still available can enrich even when its credit counter reads 0. If and only if the backend returns \`quota_exceeded\`, tell the user which window is exhausted and offer the wait-or-top-up choice (see \`leadbay_account_status\`).
|
|
@@ -4955,7 +5216,7 @@ Do NOT rely on a bare call (no \`confirm\`, no \`dry_run\`, no channels) as a "s
|
|
|
4955
5216
|
|
|
4956
5217
|
## AFTER LAUNCH \u2014 STAY ACTIVE UNTIL DONE
|
|
4957
5218
|
|
|
4958
|
-
When a launch returns \`mode:"launched"\` with a \`
|
|
5219
|
+
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).
|
|
4959
5220
|
|
|
4960
5221
|
**"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.
|
|
4961
5222
|
|
|
@@ -5608,9 +5869,39 @@ WHEN TO USE: agent has a list of companies (domains, or CSV-shaped rows from the
|
|
|
5608
5869
|
|
|
5609
5870
|
WHEN NOT TO USE: discovery (use leadbay_pull_leads); single-lead deep dive (use leadbay_research_lead_by_id); high-cadence or untrusted automation \u2014 this mutates user state and consumes ai_rescore + web_fetch quota.
|
|
5610
5871
|
|
|
5611
|
-
|
|
5872
|
+
## A launched job cannot be stopped
|
|
5873
|
+
|
|
5874
|
+
Leadbay has no cancel. Once \`leadbay_enrich_titles\`, \`leadbay_bulk_qualify_leads\`,
|
|
5875
|
+
\`leadbay_import_leads\` or \`leadbay_import_and_qualify\` has returned a launched or
|
|
5876
|
+
running result, that work is queued on Leadbay and runs to completion, and the
|
|
5877
|
+
quota it costs is already committed. A discovery, preview or \`dry_run\` result
|
|
5878
|
+
launched nothing and is not covered here.
|
|
5879
|
+
|
|
5880
|
+
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR
|
|
5881
|
+
waiting, never the job. \`cancelled: true\` means we stopped watching, not that the
|
|
5882
|
+
work stopped. What to do next depends on what you are holding:
|
|
5883
|
+
|
|
5884
|
+
- **A handle.** Poll the status tool with it, and do not launch the work that
|
|
5885
|
+
handle covers a second time \u2014 that spends the quota again on the same rows.
|
|
5886
|
+
\`leadbay_import_status\` takes \`importIds\`, so pass the values of \`import_ids\`
|
|
5887
|
+
under that name. A qualification started by \`leadbay_import_and_qualify\` has no
|
|
5888
|
+
notification of its own: resume it with
|
|
5889
|
+
\`leadbay_qualify_status({lead_ids, lens_id})\`.
|
|
5890
|
+
- **A handle AND a subset the result says never started** \u2014 \`failed[]\` entries
|
|
5891
|
+
with \`error:"not_queued"\`, or a \`rows_pending_upload\` count. Poll the handle
|
|
5892
|
+
for what was launched and re-run for that subset only, never for the whole
|
|
5893
|
+
batch.
|
|
5894
|
+
- **No result at all**, because the call timed out or the stream closed before it
|
|
5895
|
+
returned. Check \`leadbay_account_status\` first: the launch may have landed and
|
|
5896
|
+
finished. Calling the same tool again with the same arguments will usually hand
|
|
5897
|
+
back the job already launched rather than starting a second one, but that guard
|
|
5898
|
+
is in-memory, five minutes, and per process, so it is best-effort \u2014 say what you
|
|
5899
|
+
are about to re-run before you spend the user's quota on it.
|
|
5612
5900
|
|
|
5613
|
-
|
|
5901
|
+
|
|
5902
|
+
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.
|
|
5903
|
+
|
|
5904
|
+
\`not_imported\` rows with \`reason:"uncrawled"\` are **pending a background crawl**, NOT failures: Leadbay just hasn't matched/crawled that domain yet and will add the lead asynchronously (the label doesn't verify the URL resolves \u2014 don't call the site bad, but don't certify it valid either). Surface them as pending; the leads populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 \`leadbay_import_status\` returns status/progress only, and \`leadbay_pull_leads\` reads the active lens's wishlist so an imported lead outside that lens may not appear). To pull those specific companies back through the MCP, re-run the import later. A large \`uncrawled\` share on a fresh list is normal.
|
|
5614
5905
|
|
|
5615
5906
|
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\`.
|
|
5616
5907
|
|
|
@@ -5635,9 +5926,9 @@ Otherwise, partition \`not_imported\` by \`reason\` into these buckets before yo
|
|
|
5635
5926
|
**Header \u2014 single line, choose by status:**
|
|
5636
5927
|
|
|
5637
5928
|
- Completed: \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` (drop any segment whose count is 0)
|
|
5638
|
-
- Running
|
|
5639
|
-
- Running with \`timed_out:true\` (blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
5640
|
-
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014
|
|
5929
|
+
- Running: \`"\u23F3 Import running \u2014 importIds <ids>; poll leadbay_import_status"\`
|
|
5930
|
+
- Running with \`timed_out:true\` (the blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
5931
|
+
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 I'll pick it up with leadbay_qualify_status"\` (its resume ids are \`lead_ids\` + \`lens_id\`; there is no qualification notification_id to quote)
|
|
5641
5932
|
|
|
5642
5933
|
Count \`uncrawled\` rows as **pending**, never as failures \u2014 never say "M failed" when the M is mostly/entirely uncrawled rows.
|
|
5643
5934
|
|
|
@@ -5685,7 +5976,7 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
5685
5976
|
|
|
5686
5977
|
| Observation | Suggest | Calls |
|
|
5687
5978
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
5688
|
-
| Status: running
|
|
5979
|
+
| Status: running | "Check progress" | leadbay_import_status(importIds) |
|
|
5689
5980
|
| Status: running with \`timed_out:true\` | "Check progress" \u2014 NOT "retry the import" | leadbay_import_status(importIds, dry_run if the result carried it) after ~30s; \`result.leads\` carries the leadIds once complete |
|
|
5690
5981
|
| \`rows_pending_upload\` present | "Import the rows that never got submitted" | leadbay_import_leads (that subset only) |
|
|
5691
5982
|
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
@@ -5695,9 +5986,9 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
5695
5986
|
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
5696
5987
|
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
5697
5988
|
`;
|
|
5698
|
-
var leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead_by_id. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\` to return quickly with \`{status:'running',
|
|
5989
|
+
var leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead_by_id. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\` to return quickly with \`{status:'running', importIds}\`; poll leadbay_import_status with that handle. For end-to-end import+qualify in one call, prefer leadbay_import_and_qualify. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
|
|
5699
5990
|
|
|
5700
|
-
SLOW BACKEND \u21D2 \`{status:'running', timed_out:true, importIds}\`. The wizard is sometimes slow; when the poll budget runs out this tool returns that SUCCESS result, not an error. The import is still running server-side. **Do NOT call leadbay_import_leads again** \u2014 that re-uploads the file and leaves a duplicate CRM-imports row. Call \`leadbay_import_status({importIds})\` after ~30s \u2014 and pass \`dry_run:true\` too if the result carried it; on \`complete\` it returns \`result.leads\` with the leadIds, while \`phase:"committing"\` just means keep polling. Tell the user it's running and you'll check back \u2014 it is not a problem to report. Exception: \`rows_pending_upload\` rows never reached the backend and DO need a fresh call for that subset only. In records mode the result also carries \`row_ids\` \u2014 the synthetic id of each input row, in your \`records[]\` order \u2014 because \`leadbay_import_status\` reports recovered leads by that id; keep it to map them back to your source rows.
|
|
5991
|
+
SLOW BACKEND \u21D2 \`{status:'running', timed_out:true, importIds}\`. The wizard is sometimes slow; when the poll budget runs out this tool returns that SUCCESS result, not an error. The import is still running server-side. **Do NOT call leadbay_import_leads again** \u2014 that re-uploads the file and leaves a duplicate CRM-imports row. Leadbay has no cancel, so a Cancel or timeout is no reason to call it either. Sole exception: a \`wait_for_completion:false\` call that returned NOTHING \u2014 and even that can re-upload, so check CRM-imports. Call \`leadbay_import_status({importIds})\` after ~30s \u2014 and pass \`dry_run:true\` too if the result carried it; on \`complete\` it returns \`result.leads\` with the leadIds, while \`phase:"committing"\` just means keep polling. Tell the user it's running and you'll check back \u2014 it is not a problem to report. Exception: \`rows_pending_upload\` rows never reached the backend and DO need a fresh call for that subset only. In records mode the result also carries \`row_ids\` \u2014 the synthetic id of each input row, in your \`records[]\` order \u2014 because \`leadbay_import_status\` reports recovered leads by that id; keep it to map them back to your source rows.
|
|
5701
5992
|
|
|
5702
5993
|
TWO MODES: (A) Domain-list shortcut \u2014 pass \`domains: [{domain, name?}]\`. The tool builds a 2-column CSV (LEAD_NAME, LEAD_WEBSITE) and imports with the default mapping. (B) Custom records + mapping \u2014 pass \`records: [{Col1, Col2, ...}]\` plus \`mappings.fields: {Col1: 'LEAD_NAME', ...}\`. \`mappings.fields\` must include LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE (resolver needs at least one identity key). Pass exactly one of \`domains\` / \`records\`. Reserved column \`MCP_ROW_ID\` cannot appear in records/mappings \u2014 the tool injects it for stable reconciliation.
|
|
5703
5994
|
|
|
@@ -5732,9 +6023,9 @@ Otherwise, partition \`not_imported\` by \`reason\` into these buckets before yo
|
|
|
5732
6023
|
**Header \u2014 single line, choose by status:**
|
|
5733
6024
|
|
|
5734
6025
|
- Completed: \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` (drop any segment whose count is 0)
|
|
5735
|
-
- Running
|
|
5736
|
-
- Running with \`timed_out:true\` (blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
5737
|
-
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014
|
|
6026
|
+
- Running: \`"\u23F3 Import running \u2014 importIds <ids>; poll leadbay_import_status"\`
|
|
6027
|
+
- Running with \`timed_out:true\` (the blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
6028
|
+
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 I'll pick it up with leadbay_qualify_status"\` (its resume ids are \`lead_ids\` + \`lens_id\`; there is no qualification notification_id to quote)
|
|
5738
6029
|
|
|
5739
6030
|
Count \`uncrawled\` rows as **pending**, never as failures \u2014 never say "M failed" when the M is mostly/entirely uncrawled rows.
|
|
5740
6031
|
|
|
@@ -5782,7 +6073,7 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
5782
6073
|
|
|
5783
6074
|
| Observation | Suggest | Calls |
|
|
5784
6075
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
5785
|
-
| Status: running
|
|
6076
|
+
| Status: running | "Check progress" | leadbay_import_status(importIds) |
|
|
5786
6077
|
| Status: running with \`timed_out:true\` | "Check progress" \u2014 NOT "retry the import" | leadbay_import_status(importIds, dry_run if the result carried it) after ~30s; \`result.leads\` carries the leadIds once complete |
|
|
5787
6078
|
| \`rows_pending_upload\` present | "Import the rows that never got submitted" | leadbay_import_leads (that subset only) |
|
|
5788
6079
|
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
@@ -5792,11 +6083,45 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
5792
6083
|
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
5793
6084
|
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
5794
6085
|
`;
|
|
5795
|
-
var leadbay_import_status = `Retrieve the current **status/progress** of a lead import, and its leadIds once it finishes. Pass
|
|
6086
|
+
var leadbay_import_status = `Retrieve the current **status/progress** of a lead import, and its leadIds once it finishes. Pass the \`importIds\` the launch returned \u2014 \`leadbay_import_leads\` returns \`importIds\`, \`leadbay_import_and_qualify\` returns \`import_ids\`. These are the backend's own import ids, so they resolve from a later message, a later conversation, or the next day; nothing is stored on the MCP side. Also pass the \`dry_run\` the import was launched with, so completion is judged against the right phase (a dry run finishes at preprocess, a real import at processing). This status call performs a single refresh pass and never polls in a loop.
|
|
6087
|
+
|
|
6088
|
+
WHEN TO USE: after an async import returns its ids \u2014 \`leadbay_import_leads\` as \`{status:'running', importIds}\`, \`leadbay_import_and_qualify\` as \`import_ids\` \u2014 poll with those; OR to check whether a finished import is still processing. This tool does NOT surface the leads Leadbay adds later for pending-crawl (\`uncrawled\`) rows \u2014 those populate in the user's Leadbay account as the crawl completes; no tool here fetches them on demand (re-run the import to pull them back through the MCP).
|
|
6089
|
+
|
|
6090
|
+
WHEN NOT TO USE: for the qualification half \u2014 use leadbay_qualify_status, with the \`lead_ids\` + \`lens_id\` an \`leadbay_import_and_qualify\` launch returned (it has no qualification \`notification_id\`; its \`notification_ids[]\` are these same file imports); or when you still want the legacy blocking behavior from leadbay_import_leads with \`wait_for_completion=true\`.
|
|
6091
|
+
|
|
6092
|
+
## A launched job cannot be stopped
|
|
6093
|
+
|
|
6094
|
+
Leadbay has no cancel. A job started by \`leadbay_enrich_titles\`,
|
|
6095
|
+
\`leadbay_bulk_qualify_leads\`, \`leadbay_import_leads\` or
|
|
6096
|
+
\`leadbay_import_and_qualify\` runs to completion on Leadbay. The user cancelling
|
|
6097
|
+
in the chat, a request timeout, or a closed stream stops YOUR waiting, never the
|
|
6098
|
+
job, and \`cancelled: true\` on an earlier result means we stopped watching, not
|
|
6099
|
+
that the work stopped.
|
|
6100
|
+
|
|
6101
|
+
**This tool only reads.** Calling it again launches nothing and spends no quota,
|
|
6102
|
+
so poll it as often as the job needs \u2014 a timeout here is a reason to call it
|
|
6103
|
+
again, not a reason to stop.
|
|
5796
6104
|
|
|
5797
|
-
|
|
6105
|
+
One import state does NOT progress: a chunk cancelled before its mappings were
|
|
6106
|
+
committed reads \`running\` / \`committing\` forever. If the counts hold flat across
|
|
6107
|
+
several spaced polls, say so and stop, rather than polling on.
|
|
6108
|
+
|
|
6109
|
+
What must not be repeated is the LAUNCH \u2014 for work that actually launched. Re-run
|
|
6110
|
+
a launcher only for a subset that never started, never for the whole batch:
|
|
6111
|
+
|
|
6112
|
+
- \`failed[]\` entries with \`error:"not_queued"\`;
|
|
6113
|
+
- a \`rows_pending_upload\` count;
|
|
6114
|
+
- leads in \`still_running\` after a CANCELLED \`leadbay_import_and_qualify\`. Its
|
|
6115
|
+
fan-out is sequential, so an interruption leaves the remainder unlaunched and
|
|
6116
|
+
folds them in with the ones that did launch. Nothing in the result tells the
|
|
6117
|
+
two apart, and this tool cannot start either. Wait until the REST of the batch
|
|
6118
|
+
has settled: what launched settles in order, so leads still unanswered after
|
|
6119
|
+
that are the ones that never started. Only then call
|
|
6120
|
+
\`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
|
|
6121
|
+
that is merely slow looks identical to one that never launched over a few
|
|
6122
|
+
polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
|
|
6123
|
+
rather than guess.
|
|
5798
6124
|
|
|
5799
|
-
WHEN NOT TO USE: for qualification handles returned as \`qualify_id\` \u2014 use leadbay_qualify_status for those; or when you still want the legacy blocking behavior from leadbay_import_leads with \`wait_for_completion=true\`.
|
|
5800
6125
|
|
|
5801
6126
|
---
|
|
5802
6127
|
|
|
@@ -5816,12 +6141,11 @@ After the status line, propose the obvious refresh / progress-check / recovery a
|
|
|
5816
6141
|
|
|
5817
6142
|
Specifically for import status:
|
|
5818
6143
|
|
|
5819
|
-
This tool returns \`status\`, \`importIds\`, and \`progress\` ({phase, records_processed, records_total}).
|
|
6144
|
+
This tool returns \`status\`, \`importIds\`, and \`progress\` ({phase, records_processed, records_total}). Once every named import is \`complete\` and it wasn't a dry run, it also reconciles the wizard's records and carries \`result\` ({leads, not_imported, importIds, still_settling?}) \u2014 that is how you recover the leadIds of an import you stopped watching, without re-importing. \`result.still_settling\` counts rows the wizard hasn't finished placing; they are neither imported nor failed, so poll again rather than reporting them. If \`result\` is absent on a \`complete\` import the records weren't readable \u2014 report completion without inventing counts. **Render only from the fields actually present; never invent counts.**
|
|
5820
6145
|
|
|
5821
6146
|
Caveat on \`progress\`: \`records_processed\` counts only the rows that MATCHED an existing lead (backend \`imported_records\`), not every row that finished processing \u2014 so for a complete import whose rows are mostly/all \`uncrawled\` (pending crawl), \`records_processed\` is legitimately low or 0. Never read a low \`records_processed\` on a \`complete\` import as "stuck" or "failed": once \`status:"complete"\`, processing is done; the pending-crawl rows just matched no existing lead yet.
|
|
5822
6147
|
|
|
5823
|
-
- Running \u2192 \`"\u23F3 Import still running \u2014 phase <phase>; check back in ~M minutes."\` (use the phase; don't turn the matched-count into an "X/Y processed" progress bar). \`phase:"committing"\`
|
|
5824
|
-
- Complete with **\`dry_run:true\`** on the response \u2192 a VALIDATION pass; nothing was committed. \`"\u{1F50E} Dry run complete \u2014 input validated, nothing imported. Re-run without dry_run to commit."\` Never render this as a completed import, and never quote a lead count.
|
|
6148
|
+
- Running \u2192 \`"\u23F3 Import still running \u2014 phase <phase>; check back in ~M minutes."\` (use the phase; don't turn the matched-count into an "X/Y processed" progress bar). \`phase:"committing"\` means the mappings are still being committed \u2014 say it's still being committed, never that it failed or finished empty.
|
|
5825
6149
|
- Complete, **no \`result\`** \u2192 \`"\u2713 Import complete."\` Do NOT append a \`records_processed/records_total\` fraction (it undercounts pending-crawl rows and looks stuck) and do NOT report pending-crawl / need-attention bucket counts \u2014 the row-level \`not_imported\` breakdown isn't in this response.
|
|
5826
6150
|
- Complete with \`result.still_settling > 0\` \u2192 say \`"\u2713 Import complete \u2014 N imported, S rows still being placed."\` Never count \`still_settling\` rows as failures.
|
|
5827
6151
|
- Complete, **\`result\` present AND it was a dry run** (\`result.dry_run:true\`, or every \`result.not_imported\` row has \`reason:"dry_run"\`) \u2192 this resolved handle was a VALIDATION pass, nothing committed. Render \`"\u{1F50E} Dry run complete \u2014 V rows validated, nothing imported. Re-run without dry_run to commit."\` \u2014 do NOT render it as a real import completion or use the pending/attention buckets.
|
|
@@ -5853,12 +6177,29 @@ How the OTHER reasons map to the "Need attention" bucket (see the render block a
|
|
|
5853
6177
|
| Status: running (incl. \`committing\`) | "Check again in N minutes" | leadbay_import_status \u2014 re-call (pass \`dry_run:true\` if the ids came from a dry run) |
|
|
5854
6178
|
| Status: error / failed (true error) | "Diagnose the failure" | leadbay_resolve_import_rows |
|
|
5855
6179
|
`;
|
|
5856
|
-
var leadbay_launch_bulk_enrichment = `Launch a bulk-enrichment job against the current selection. The backend requires \`email=true\` OR \`phone=true\` (both can be true). Returns 204 with no body \u2014 there is no
|
|
6180
|
+
var leadbay_launch_bulk_enrichment = `Launch a bulk-enrichment job against the current selection. The backend requires \`email=true\` OR \`phone=true\` (both can be true). Returns 204 with no body \u2014 there is no notification_id and no per-job status endpoint. Track results by polling individual leads via leadbay_get_contacts after ~60s; a contact is done for this run only when the REQUESTED channel landed (requested \`email\` and/or \`phone_number\` present), not \`contact.enrichment.done\` alone (that flag is already true for a contact enriched on the other channel earlier). \`dry_run:true\` returns the call shape without contacting the backend.
|
|
5857
6181
|
|
|
5858
6182
|
WHEN TO USE: low-level.
|
|
5859
6183
|
|
|
5860
6184
|
WHEN NOT TO USE: from agent flow \u2014 leadbay_enrich_titles handles selection lifecycle, preview, launch, and cleanup.
|
|
5861
6185
|
|
|
6186
|
+
## A launched job cannot be stopped, and this tool has no retry guard
|
|
6187
|
+
|
|
6188
|
+
Leadbay has no cancel. Once this call returns having actually launched, the work
|
|
6189
|
+
is queued on Leadbay and runs to completion, and the quota it costs is already
|
|
6190
|
+
committed. A \`dry_run\` result reached no backend and spent nothing. The user
|
|
6191
|
+
cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
|
|
6192
|
+
never the job.
|
|
6193
|
+
|
|
6194
|
+
Unlike the composite launchers, this tool has **no double-launch guard**: calling
|
|
6195
|
+
it again always issues a new paid launch, even seconds later with identical
|
|
6196
|
+
arguments. So when a call returns nothing at all, do not simply retry. Read the
|
|
6197
|
+
record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
|
|
6198
|
+
lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
|
|
6199
|
+
see whether the launch already landed, and tell the user what you are about to
|
|
6200
|
+
spend before spending it again.
|
|
6201
|
+
|
|
6202
|
+
|
|
5862
6203
|
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\`.
|
|
5863
6204
|
`;
|
|
5864
6205
|
var leadbay_like_lead = `## WHEN TO USE
|
|
@@ -6928,13 +7269,70 @@ WHEN TO USE: low-level \u2014 when you need to kick qualification on exactly one
|
|
|
6928
7269
|
|
|
6929
7270
|
WHEN NOT TO USE: as the agent's bulk-qualify path \u2014 use leadbay_bulk_qualify_leads, which paginates past already-qualified leads, fans out, polls, and bails out cleanly on 429.
|
|
6930
7271
|
|
|
7272
|
+
## A launched job cannot be stopped, and this tool has no retry guard
|
|
7273
|
+
|
|
7274
|
+
Leadbay has no cancel. Once this call returns having actually launched, the work
|
|
7275
|
+
is queued on Leadbay and runs to completion, and the quota it costs is already
|
|
7276
|
+
committed. A \`dry_run\` result reached no backend and spent nothing. The user
|
|
7277
|
+
cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting,
|
|
7278
|
+
never the job.
|
|
7279
|
+
|
|
7280
|
+
Unlike the composite launchers, this tool has **no double-launch guard**: calling
|
|
7281
|
+
it again always issues a new paid launch, even seconds later with identical
|
|
7282
|
+
arguments. So when a call returns nothing at all, do not simply retry. Read the
|
|
7283
|
+
record back first \u2014 \`leadbay_research_lead_by_id\` or \`leadbay_get_contacts\` for a
|
|
7284
|
+
lead, \`leadbay_account_status\` for background work that has since finished \u2014 to
|
|
7285
|
+
see whether the launch already landed, and tell the user what you are about to
|
|
7286
|
+
spend before spending it again.
|
|
7287
|
+
|
|
7288
|
+
|
|
6931
7289
|
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\`.
|
|
6932
7290
|
`;
|
|
6933
|
-
var leadbay_qualify_status = `Retrieve the current state of
|
|
7291
|
+
var 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:
|
|
7292
|
+
|
|
7293
|
+
- **\`leadbay_bulk_qualify_leads\`** returns a \`notification_id\`. Pass it for progress in ONE call, and add the \`lead_ids\` + \`lens_id\` it also returned for per-lead detail (which settled, which are still running).
|
|
7294
|
+
- **\`leadbay_import_and_qualify\`** returns NO qualification \`notification_id\` \u2014 its qualify phase runs per-lead, so no job notification exists. Pass the \`lead_ids\` + \`lens_id\` it returned. Its \`notification_ids[]\` are the FILE-IMPORT notifications; handing one of those to this tool is rejected as the wrong kind, and \`leadbay_import_status({importIds})\` is where the import half is polled.
|
|
6934
7295
|
|
|
6935
|
-
|
|
7296
|
+
**When it is finished:** \`status\` is always \`"launched"\` \u2014 it is not a progress field. On the \`notification_id\` path the job is done when \`in_progress\` is false (or \`bulk_progress.success_count + failure_count\` reaches \`total_count\`); \`still_running[]\` is empty on that path from the very first poll and must NOT be read as "done". On the \`lead_ids\` path the job is done when \`still_running[]\` is empty. Pass both and you get both signals in one call.
|
|
7297
|
+
|
|
7298
|
+
Everything comes straight out of the launch response \u2014 nothing is stored on the MCP side. A backend job is scoped to the user who launched it, so a \`notification_id\` resolves from a later message, a later conversation, or the next day.
|
|
7299
|
+
|
|
7300
|
+
WHEN TO USE: after leadbay_bulk_qualify_leads or leadbay_import_and_qualify came back with a non-empty \`still_running[]\`, call this tool a few minutes later (or hours) with those ids to retrieve the now-completed qualifications without re-running the import or re-spending qualify quota.
|
|
6936
7301
|
|
|
6937
7302
|
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that's a deeper per-lead profile and includes contacts. This tool is purely the qualification answers + signals_count.
|
|
7303
|
+
|
|
7304
|
+
## A launched job cannot be stopped
|
|
7305
|
+
|
|
7306
|
+
Leadbay has no cancel. A job started by \`leadbay_enrich_titles\`,
|
|
7307
|
+
\`leadbay_bulk_qualify_leads\`, \`leadbay_import_leads\` or
|
|
7308
|
+
\`leadbay_import_and_qualify\` runs to completion on Leadbay. The user cancelling
|
|
7309
|
+
in the chat, a request timeout, or a closed stream stops YOUR waiting, never the
|
|
7310
|
+
job, and \`cancelled: true\` on an earlier result means we stopped watching, not
|
|
7311
|
+
that the work stopped.
|
|
7312
|
+
|
|
7313
|
+
**This tool only reads.** Calling it again launches nothing and spends no quota,
|
|
7314
|
+
so poll it as often as the job needs \u2014 a timeout here is a reason to call it
|
|
7315
|
+
again, not a reason to stop.
|
|
7316
|
+
|
|
7317
|
+
One import state does NOT progress: a chunk cancelled before its mappings were
|
|
7318
|
+
committed reads \`running\` / \`committing\` forever. If the counts hold flat across
|
|
7319
|
+
several spaced polls, say so and stop, rather than polling on.
|
|
7320
|
+
|
|
7321
|
+
What must not be repeated is the LAUNCH \u2014 for work that actually launched. Re-run
|
|
7322
|
+
a launcher only for a subset that never started, never for the whole batch:
|
|
7323
|
+
|
|
7324
|
+
- \`failed[]\` entries with \`error:"not_queued"\`;
|
|
7325
|
+
- a \`rows_pending_upload\` count;
|
|
7326
|
+
- leads in \`still_running\` after a CANCELLED \`leadbay_import_and_qualify\`. Its
|
|
7327
|
+
fan-out is sequential, so an interruption leaves the remainder unlaunched and
|
|
7328
|
+
folds them in with the ones that did launch. Nothing in the result tells the
|
|
7329
|
+
two apart, and this tool cannot start either. Wait until the REST of the batch
|
|
7330
|
+
has settled: what launched settles in order, so leads still unanswered after
|
|
7331
|
+
that are the ones that never started. Only then call
|
|
7332
|
+
\`leadbay_bulk_qualify_leads({leadIds, lensId})\` for exactly those ids. A lead
|
|
7333
|
+
that is merely slow looks identical to one that never launched over a few
|
|
7334
|
+
polls, and re-launching it charges the user twice \u2014 when unsure, tell the user
|
|
7335
|
+
rather than guess.
|
|
6938
7336
|
`;
|
|
6939
7337
|
var 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.
|
|
6940
7338
|
|
|
@@ -8530,7 +8928,7 @@ Some Leadbay tool responses include a \`_meta.notifications\` array listing **ba
|
|
|
8530
8928
|
- \`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.
|
|
8531
8929
|
- \`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.
|
|
8532
8930
|
|
|
8533
|
-
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the
|
|
8931
|
+
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the notification_id and let completion arrive via \`_meta.notifications\` (only when a notification id exists; if none was returned, tell the user to ask again / that you'll poll later, since nothing will auto-surface). For qualification and imports, poll inline only for small/quick runs or when the user explicitly asked you to wait; otherwise return the handle and let \`_meta.notifications\` deliver it. Either way, the user should never have to ask "is it done yet?" for work you kicked off in the same turn \u2014 you either report it or hand back a clear in-progress handle.
|
|
8534
8932
|
|
|
8535
8933
|
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
8536
8934
|
|
|
@@ -9386,7 +9784,9 @@ var qualifyLead = {
|
|
|
9386
9784
|
title: "Qualify a single lead",
|
|
9387
9785
|
readOnlyHint: false,
|
|
9388
9786
|
destructiveHint: true,
|
|
9389
|
-
|
|
9787
|
+
// No double-launch guard: this POSTs straight through, so an identical
|
|
9788
|
+
// repeat is a second paid launch, not a no-op (product#4039).
|
|
9789
|
+
idempotentHint: false,
|
|
9390
9790
|
openWorldHint: true
|
|
9391
9791
|
},
|
|
9392
9792
|
description: leadbay_qualify_lead,
|
|
@@ -9448,7 +9848,9 @@ var enrichContacts = {
|
|
|
9448
9848
|
title: "Enrich contacts for a lead",
|
|
9449
9849
|
readOnlyHint: false,
|
|
9450
9850
|
destructiveHint: true,
|
|
9451
|
-
|
|
9851
|
+
// No double-launch guard: this POSTs straight through, so an identical
|
|
9852
|
+
// repeat is a second paid launch, not a no-op (product#4039).
|
|
9853
|
+
idempotentHint: false,
|
|
9452
9854
|
openWorldHint: true
|
|
9453
9855
|
},
|
|
9454
9856
|
description: leadbay_enrich_contacts,
|
|
@@ -11413,7 +11815,66 @@ async function refreshLeadStates(client, leadIds, questionOrder) {
|
|
|
11413
11815
|
}
|
|
11414
11816
|
|
|
11415
11817
|
// ../core/dist/composite/import-leads.js
|
|
11416
|
-
import { createHash as
|
|
11818
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
11819
|
+
|
|
11820
|
+
// ../core/dist/jobs/launch-guard.js
|
|
11821
|
+
import { createHash as createHash2 } from "crypto";
|
|
11822
|
+
var WINDOW_MS = 5 * 60 * 1e3;
|
|
11823
|
+
var MAX_ENTRIES = 1e3;
|
|
11824
|
+
var recent = /* @__PURE__ */ new Map();
|
|
11825
|
+
function launchFingerprint(parts) {
|
|
11826
|
+
const flat = parts.map((p) => Array.isArray(p) ? [...p].sort().join(",") : String(p)).join("|");
|
|
11827
|
+
return createHash2("sha256").update(flat).digest("hex");
|
|
11828
|
+
}
|
|
11829
|
+
function sweep(now) {
|
|
11830
|
+
for (const [k, v] of recent) {
|
|
11831
|
+
if (now - v.at >= WINDOW_MS)
|
|
11832
|
+
recent.delete(k);
|
|
11833
|
+
}
|
|
11834
|
+
}
|
|
11835
|
+
function beginLaunch(fingerprint, now = Date.now()) {
|
|
11836
|
+
const prior = recallLaunch(fingerprint, now);
|
|
11837
|
+
if (prior) {
|
|
11838
|
+
return prior.in_flight ? { state: "in_flight", seconds_since: prior.seconds_since } : { state: "settled", record: prior };
|
|
11839
|
+
}
|
|
11840
|
+
recent.set(fingerprint, {
|
|
11841
|
+
notification_id: null,
|
|
11842
|
+
in_flight: true,
|
|
11843
|
+
launched_at: new Date(now).toISOString(),
|
|
11844
|
+
at: now
|
|
11845
|
+
});
|
|
11846
|
+
return { state: "owned" };
|
|
11847
|
+
}
|
|
11848
|
+
function abandonLaunch(fingerprint) {
|
|
11849
|
+
const held = recent.get(fingerprint);
|
|
11850
|
+
if (held?.in_flight)
|
|
11851
|
+
recent.delete(fingerprint);
|
|
11852
|
+
}
|
|
11853
|
+
function recallLaunch(fingerprint, now = Date.now()) {
|
|
11854
|
+
sweep(now);
|
|
11855
|
+
const hit = recent.get(fingerprint);
|
|
11856
|
+
if (!hit)
|
|
11857
|
+
return void 0;
|
|
11858
|
+
return { ...hit, seconds_since: Math.round((now - hit.at) / 1e3) };
|
|
11859
|
+
}
|
|
11860
|
+
function rememberLaunch(fingerprint, notificationId, now = Date.now(), importIds) {
|
|
11861
|
+
sweep(now);
|
|
11862
|
+
while (recent.size >= MAX_ENTRIES) {
|
|
11863
|
+
const oldest = recent.keys().next();
|
|
11864
|
+
if (oldest.done)
|
|
11865
|
+
break;
|
|
11866
|
+
recent.delete(oldest.value);
|
|
11867
|
+
}
|
|
11868
|
+
const rec = {
|
|
11869
|
+
notification_id: notificationId,
|
|
11870
|
+
in_flight: false,
|
|
11871
|
+
...importIds ? { import_ids: importIds } : {},
|
|
11872
|
+
launched_at: new Date(now).toISOString(),
|
|
11873
|
+
at: now
|
|
11874
|
+
};
|
|
11875
|
+
recent.set(fingerprint, rec);
|
|
11876
|
+
return rec;
|
|
11877
|
+
}
|
|
11417
11878
|
|
|
11418
11879
|
// ../core/dist/composite/_import-records.js
|
|
11419
11880
|
function normalizeDomain(input) {
|
|
@@ -11611,10 +12072,10 @@ function reconcileRecords(records) {
|
|
|
11611
12072
|
}
|
|
11612
12073
|
|
|
11613
12074
|
// ../core/dist/composite/_import-commit-log.js
|
|
11614
|
-
var
|
|
12075
|
+
var MAX_ENTRIES2 = 500;
|
|
11615
12076
|
var failures = /* @__PURE__ */ new Map();
|
|
11616
12077
|
function recordCommitFailure(importId, reason) {
|
|
11617
|
-
if (failures.size >=
|
|
12078
|
+
if (failures.size >= MAX_ENTRIES2) {
|
|
11618
12079
|
const oldest = failures.keys().next().value;
|
|
11619
12080
|
if (oldest !== void 0)
|
|
11620
12081
|
failures.delete(oldest);
|
|
@@ -11707,7 +12168,7 @@ function importFingerprint(params, prep) {
|
|
|
11707
12168
|
mappings: prep.mappings,
|
|
11708
12169
|
dry_run: Boolean(params.dry_run)
|
|
11709
12170
|
};
|
|
11710
|
-
return
|
|
12171
|
+
return createHash3("sha256").update(stableStringify(payload)).digest("hex");
|
|
11711
12172
|
}
|
|
11712
12173
|
var ImportPhaseTimeout = class extends Error {
|
|
11713
12174
|
phase;
|
|
@@ -12104,7 +12565,7 @@ async function commitMappings(client, importId, mappings, ctx) {
|
|
|
12104
12565
|
throw err;
|
|
12105
12566
|
}
|
|
12106
12567
|
}
|
|
12107
|
-
async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onNotificationId) {
|
|
12568
|
+
async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onNotificationId, onCommitSent) {
|
|
12108
12569
|
const { importId, chunk } = upload;
|
|
12109
12570
|
const phaseBudget = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
12110
12571
|
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
@@ -12113,6 +12574,7 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
12113
12574
|
return { importId, records: [], notification_id: null };
|
|
12114
12575
|
}
|
|
12115
12576
|
const importNotificationId = await commitMappings(client, importId, mappings, ctx);
|
|
12577
|
+
onCommitSent?.();
|
|
12116
12578
|
if (importNotificationId) {
|
|
12117
12579
|
onNotificationId?.(importNotificationId);
|
|
12118
12580
|
ctx?.logger?.info?.(`import-leads: notification_id=${importNotificationId} importId=${importId}`);
|
|
@@ -12339,7 +12801,7 @@ var importLeads = {
|
|
|
12339
12801
|
},
|
|
12340
12802
|
wait_for_completion: {
|
|
12341
12803
|
type: "boolean",
|
|
12342
|
-
description: "When false,
|
|
12804
|
+
description: "When false, upload the rows and return `{status:'running', importIds}` immediately. Poll leadbay_import_status({importIds, dry_run}) \u2014 those importIds are the backend's own and keep working from any conversation. Default is true."
|
|
12343
12805
|
}
|
|
12344
12806
|
},
|
|
12345
12807
|
// Neither field is "required" at the schema level; xor + presence is
|
|
@@ -12358,10 +12820,6 @@ var importLeads = {
|
|
|
12358
12820
|
type: "string",
|
|
12359
12821
|
description: "`running` when wait_for_completion=false; absent on the legacy blocking result."
|
|
12360
12822
|
},
|
|
12361
|
-
handle_id: {
|
|
12362
|
-
type: "string",
|
|
12363
|
-
description: "Persisted UUID handle to pass to leadbay_import_status. Only on the wait_for_completion=false path; absent when a blocking call timed out (use importIds then)."
|
|
12364
|
-
},
|
|
12365
12823
|
timed_out: {
|
|
12366
12824
|
type: "boolean",
|
|
12367
12825
|
description: "True when a blocking call ran out of poll budget. The import is still running server-side \u2014 poll leadbay_import_status(importIds). Do NOT re-issue the import."
|
|
@@ -12392,7 +12850,7 @@ var importLeads = {
|
|
|
12392
12850
|
region: { type: "string" },
|
|
12393
12851
|
cancelled: {
|
|
12394
12852
|
type: "boolean",
|
|
12395
|
-
description: "True when
|
|
12853
|
+
description: "True when the HOST cancelled the call (a user Cancel, or the host's own request timeout); a Leadbay-side or phase-budget timeout reports `timed_out` instead. Rows already uploaded keep importing on Leadbay \u2014 poll leadbay_import_status with importIds, but a chunk cancelled before its mappings were committed can read `running` without ever progressing; if the counts stop moving, say so rather than polling on. Rows past the interruption may never have been uploaded; re-run the import for those rows only."
|
|
12396
12854
|
},
|
|
12397
12855
|
dry_run: {
|
|
12398
12856
|
type: "boolean",
|
|
@@ -12460,62 +12918,72 @@ var importLeads = {
|
|
|
12460
12918
|
}
|
|
12461
12919
|
const chunks = chunkAt100(prep.validInputs);
|
|
12462
12920
|
if (!waitForCompletion) {
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
const
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
}
|
|
12484
|
-
await ctx.bulkTracker.setImportProgress(reservation.record.bulk_id, {
|
|
12921
|
+
const fingerprint = launchFingerprint([
|
|
12922
|
+
"import",
|
|
12923
|
+
prep.mode,
|
|
12924
|
+
dryRun,
|
|
12925
|
+
// Cached /users/me — the admin gate above already read it, so this
|
|
12926
|
+
// costs no round trip. Scopes the fingerprint to the organization.
|
|
12927
|
+
await client.resolveOrgId(),
|
|
12928
|
+
importFingerprint(params, prep)
|
|
12929
|
+
]);
|
|
12930
|
+
const claim = beginLaunch(fingerprint);
|
|
12931
|
+
if (claim.state === "in_flight") {
|
|
12932
|
+
throw client.makeError("IMPORT_LAUNCH_IN_FLIGHT", `An identical import was started ${claim.seconds_since}s ago and has not returned its import ids yet`, "Nothing was uploaded twice. Call leadbay_import_leads again with the same arguments in a few seconds to receive the importIds.", "");
|
|
12933
|
+
}
|
|
12934
|
+
const already = claim.state === "settled" ? claim.record : void 0;
|
|
12935
|
+
if (already) {
|
|
12936
|
+
return {
|
|
12937
|
+
status: "running",
|
|
12938
|
+
importIds: already.import_ids ?? [],
|
|
12939
|
+
notification_ids: [],
|
|
12940
|
+
progress: {
|
|
12485
12941
|
phase: "preprocess",
|
|
12486
12942
|
records_processed: 0,
|
|
12487
12943
|
records_total: prep.validInputs.length
|
|
12944
|
+
},
|
|
12945
|
+
region: client.region,
|
|
12946
|
+
reused: true,
|
|
12947
|
+
seconds_since_original: already.seconds_since,
|
|
12948
|
+
_meta: client.lastMeta ?? {
|
|
12949
|
+
region: client.region,
|
|
12950
|
+
endpoint: "POST /imports",
|
|
12951
|
+
latency_ms: null,
|
|
12952
|
+
retry_after: null
|
|
12953
|
+
}
|
|
12954
|
+
};
|
|
12955
|
+
}
|
|
12956
|
+
const importIds2 = [];
|
|
12957
|
+
const uploadedChunks = [];
|
|
12958
|
+
try {
|
|
12959
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
12960
|
+
const upload = await uploadOneChunk(client, chunks[i], i, chunks.length, prep.header, ctx, (id) => {
|
|
12961
|
+
if (!importIds2.includes(id))
|
|
12962
|
+
importIds2.push(id);
|
|
12488
12963
|
});
|
|
12489
|
-
|
|
12490
|
-
await ctx.bulkTracker.markImportFailed(reservation.record.bulk_id, err?.message ?? err?.code ?? "unknown");
|
|
12491
|
-
throw err;
|
|
12964
|
+
uploadedChunks.push(upload);
|
|
12492
12965
|
}
|
|
12966
|
+
} catch (err) {
|
|
12967
|
+
abandonLaunch(fingerprint);
|
|
12968
|
+
throw err;
|
|
12493
12969
|
}
|
|
12970
|
+
rememberLaunch(fingerprint, null, void 0, importIds2);
|
|
12494
12971
|
if (uploadedChunks.length > 0) {
|
|
12495
|
-
void runImportInBackground(client, prep, uploadedChunks, {
|
|
12496
|
-
dryRun,
|
|
12497
|
-
perPhaseBudget,
|
|
12498
|
-
totalBudget
|
|
12499
|
-
}, ctx, reservation.record.bulk_id);
|
|
12972
|
+
void runImportInBackground(client, prep, uploadedChunks, { dryRun, perPhaseBudget, totalBudget }, ctx ?? {});
|
|
12500
12973
|
}
|
|
12501
12974
|
return {
|
|
12502
12975
|
status: "running",
|
|
12503
|
-
handle_id: reservation.record.bulk_id,
|
|
12504
12976
|
importIds: importIds2,
|
|
12505
|
-
// Notifications fire from update_mappings, which the background
|
|
12506
|
-
//
|
|
12507
|
-
//
|
|
12977
|
+
// Notifications fire from update_mappings, which the background task
|
|
12978
|
+
// hasn't called yet. They surface via the WS listener / catch-up REST
|
|
12979
|
+
// on subsequent turns.
|
|
12508
12980
|
notification_ids: [],
|
|
12509
12981
|
progress: {
|
|
12510
|
-
phase:
|
|
12511
|
-
records_processed:
|
|
12512
|
-
records_total:
|
|
12982
|
+
phase: importIds2.length > 0 ? "preprocess" : "queued",
|
|
12983
|
+
records_processed: 0,
|
|
12984
|
+
records_total: prep.validInputs.length
|
|
12513
12985
|
},
|
|
12514
12986
|
region: client.region,
|
|
12515
|
-
...reservation.reused ? {
|
|
12516
|
-
reused: true,
|
|
12517
|
-
seconds_since_original: reservation.seconds_since_original
|
|
12518
|
-
} : {},
|
|
12519
12987
|
_meta: client.lastMeta ?? {
|
|
12520
12988
|
region: client.region,
|
|
12521
12989
|
endpoint: "POST /imports",
|
|
@@ -12628,27 +13096,24 @@ function resumeParkedUpload(client, upload, mappings, ctx) {
|
|
|
12628
13096
|
});
|
|
12629
13097
|
}, 0);
|
|
12630
13098
|
}
|
|
12631
|
-
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx
|
|
12632
|
-
const tracker = ctx.bulkTracker;
|
|
12633
|
-
if (!tracker)
|
|
12634
|
-
return;
|
|
12635
|
-
void tracker.setImportProgress(handleId, {
|
|
12636
|
-
phase: "preprocess",
|
|
12637
|
-
records_processed: 0,
|
|
12638
|
-
records_total: prep.validInputs.length
|
|
12639
|
-
}).catch(() => {
|
|
12640
|
-
});
|
|
13099
|
+
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx) {
|
|
12641
13100
|
setTimeout(() => {
|
|
12642
13101
|
void (async () => {
|
|
12643
|
-
const bgCtx = { logger: ctx.logger
|
|
13102
|
+
const bgCtx = { logger: ctx.logger };
|
|
12644
13103
|
const importIds = uploadedChunks.map((chunk) => chunk.importId);
|
|
12645
13104
|
const notificationIds = [];
|
|
12646
13105
|
const matched = /* @__PURE__ */ new Map();
|
|
12647
13106
|
const notImported = /* @__PURE__ */ new Map();
|
|
13107
|
+
let inFlight = 0;
|
|
13108
|
+
let committed = false;
|
|
12648
13109
|
try {
|
|
12649
13110
|
const totalDeadline = Date.now() + opts.totalBudget;
|
|
12650
|
-
for (
|
|
12651
|
-
const
|
|
13111
|
+
for (inFlight = 0; inFlight < uploadedChunks.length; inFlight++) {
|
|
13112
|
+
const upload = uploadedChunks[inFlight];
|
|
13113
|
+
committed = false;
|
|
13114
|
+
const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0, void 0, () => {
|
|
13115
|
+
committed = true;
|
|
13116
|
+
});
|
|
12652
13117
|
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
12653
13118
|
notificationIds.push(out.notification_id);
|
|
12654
13119
|
}
|
|
@@ -12656,16 +13121,21 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
12656
13121
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
12657
13122
|
}
|
|
12658
13123
|
}
|
|
12659
|
-
|
|
12660
|
-
await tracker.markImportComplete(handleId, {
|
|
12661
|
-
leads: result.leads,
|
|
12662
|
-
not_imported: result.not_imported,
|
|
12663
|
-
importIds: result.importIds
|
|
12664
|
-
});
|
|
13124
|
+
buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false, notificationIds);
|
|
12665
13125
|
} catch (err) {
|
|
12666
|
-
|
|
13126
|
+
if (!opts.dryRun) {
|
|
13127
|
+
const parkedFrom = committed ? inFlight + 1 : inFlight;
|
|
13128
|
+
for (const parked of uploadedChunks.slice(parkedFrom)) {
|
|
13129
|
+
resumeParkedUpload(client, parked, prep.mappings, bgCtx);
|
|
13130
|
+
}
|
|
13131
|
+
}
|
|
13132
|
+
ctx?.logger?.warn?.(`import-leads: background import failed for ${importIds.join(",")}: ${err?.message ?? err?.code ?? err}`);
|
|
12667
13133
|
}
|
|
12668
|
-
})()
|
|
13134
|
+
})().catch((e) => (
|
|
13135
|
+
// Terminal guard. Nothing may escape a detached task — an unhandled
|
|
13136
|
+
// rejection would take the whole multi-tenant hosted process down.
|
|
13137
|
+
ctx?.logger?.error?.(`import-leads: background import crashed: ${e?.message ?? e}`)
|
|
13138
|
+
));
|
|
12669
13139
|
}, 0);
|
|
12670
13140
|
}
|
|
12671
13141
|
|
|
@@ -13665,7 +14135,9 @@ var launchBulkEnrichment = {
|
|
|
13665
14135
|
title: "Launch bulk enrichment",
|
|
13666
14136
|
readOnlyHint: false,
|
|
13667
14137
|
destructiveHint: true,
|
|
13668
|
-
|
|
14138
|
+
// No double-launch guard: this POSTs straight through, so an identical
|
|
14139
|
+
// repeat is a second paid launch, not a no-op (product#4039).
|
|
14140
|
+
idempotentHint: false,
|
|
13669
14141
|
openWorldHint: true
|
|
13670
14142
|
},
|
|
13671
14143
|
description: leadbay_launch_bulk_enrichment,
|
|
@@ -17367,7 +17839,7 @@ var GETTING_STARTED_MANIFEST = {
|
|
|
17367
17839
|
leadIds: "[<the ONE lead you drafted for at step 3>] \u2014 an ARRAY, always",
|
|
17368
17840
|
lensId: "<the pinned lens id from step 2>"
|
|
17369
17841
|
},
|
|
17370
|
-
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
|
|
17842
|
+
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.",
|
|
17371
17843
|
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."
|
|
17372
17844
|
}
|
|
17373
17845
|
],
|
|
@@ -18415,7 +18887,7 @@ var bulkQualifyLeads = {
|
|
|
18415
18887
|
},
|
|
18416
18888
|
wait_for_completion: {
|
|
18417
18889
|
type: "boolean",
|
|
18418
|
-
description: "When false, launch qualification and return `{status:'running',
|
|
18890
|
+
description: "When false, launch qualification and return `{status:'running', notification_id, lead_ids, lens_id}` immediately. Poll leadbay_qualify_status with them. Default is true for 0.6.x backwards compatibility."
|
|
18419
18891
|
}
|
|
18420
18892
|
},
|
|
18421
18893
|
additionalProperties: false
|
|
@@ -18432,13 +18904,15 @@ var bulkQualifyLeads = {
|
|
|
18432
18904
|
type: "string",
|
|
18433
18905
|
description: "`running` when wait_for_completion=false; absent on the legacy blocking result."
|
|
18434
18906
|
},
|
|
18435
|
-
|
|
18436
|
-
|
|
18907
|
+
notification_id: {
|
|
18908
|
+
type: ["string", "null"],
|
|
18909
|
+
description: "The backend's job id. Carry it to leadbay_qualify_status."
|
|
18910
|
+
},
|
|
18437
18911
|
lead_ids: { type: "array", items: { type: "string" } },
|
|
18438
18912
|
launched_count: { type: "number" },
|
|
18439
18913
|
still_running: {
|
|
18440
18914
|
type: "array",
|
|
18441
|
-
description: "Leads launched but whose qualification did not complete within budget. Re-poll via leadbay_qualify_status with
|
|
18915
|
+
description: "Leads launched but whose qualification did not complete within budget. Re-poll via leadbay_qualify_status with notification_id (when present) or lead_ids + lens_id.",
|
|
18442
18916
|
items: { type: "object" }
|
|
18443
18917
|
},
|
|
18444
18918
|
failed: {
|
|
@@ -18472,8 +18946,7 @@ var bulkQualifyLeads = {
|
|
|
18472
18946
|
{
|
|
18473
18947
|
required: [
|
|
18474
18948
|
"status",
|
|
18475
|
-
"
|
|
18476
|
-
"qualify_id",
|
|
18949
|
+
"notification_id",
|
|
18477
18950
|
"lead_ids",
|
|
18478
18951
|
"launched_count",
|
|
18479
18952
|
"failed",
|
|
@@ -18533,45 +19006,45 @@ var bulkQualifyLeads = {
|
|
|
18533
19006
|
};
|
|
18534
19007
|
}
|
|
18535
19008
|
if (!waitForCompletion) {
|
|
18536
|
-
|
|
18537
|
-
|
|
19009
|
+
const fingerprint = launchFingerprint(["qualify", candidates, lensId]);
|
|
19010
|
+
const claim = beginLaunch(fingerprint);
|
|
19011
|
+
if (claim.state === "in_flight") {
|
|
19012
|
+
throw client.makeError("QUALIFY_LAUNCH_IN_FLIGHT", `An identical qualification was started ${claim.seconds_since}s ago and has not returned its job id yet`, "Nothing was launched twice. Call leadbay_bulk_qualify_leads again with the same arguments in a few seconds to receive the notification_id.", "");
|
|
18538
19013
|
}
|
|
18539
|
-
const
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18551
|
-
|
|
18552
|
-
|
|
18553
|
-
quotaExceeded2 = launch.quotaExceeded;
|
|
18554
|
-
notificationId = launch.resp?.notification_id ?? null;
|
|
18555
|
-
const queuedIds = launch.resp?.queued_ids ?? [];
|
|
18556
|
-
const skippedIds = launch.resp?.skipped_ids ?? [];
|
|
18557
|
-
launchedCount = queuedIds.length;
|
|
18558
|
-
const seen = /* @__PURE__ */ new Set([...queuedIds, ...skippedIds]);
|
|
18559
|
-
failed2 = candidates.filter((id) => !seen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
18560
|
-
if (queuedIds.length > 0 || quotaExceeded2 || skippedIds.length > 0 || failed2.length === candidates.length) {
|
|
18561
|
-
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id, notificationId);
|
|
18562
|
-
}
|
|
18563
|
-
} else {
|
|
18564
|
-
notificationId = reservation.record.notification_id ?? null;
|
|
18565
|
-
launchedCount = reservation.record.lead_ids.length;
|
|
19014
|
+
const already = claim.state === "settled" ? claim.record : void 0;
|
|
19015
|
+
if (already) {
|
|
19016
|
+
return {
|
|
19017
|
+
status: "running",
|
|
19018
|
+
lead_ids: candidates,
|
|
19019
|
+
launched_count: candidates.length,
|
|
19020
|
+
failed: [],
|
|
19021
|
+
quota_exceeded: false,
|
|
19022
|
+
lens_id: lensId,
|
|
19023
|
+
notification_id: already.notification_id,
|
|
19024
|
+
reused: true,
|
|
19025
|
+
seconds_since_original_launch: already.seconds_since,
|
|
19026
|
+
_meta: { region: client.region }
|
|
19027
|
+
};
|
|
18566
19028
|
}
|
|
19029
|
+
let launch;
|
|
19030
|
+
try {
|
|
19031
|
+
launch = await launchBulkQualify(client, candidates, ctx);
|
|
19032
|
+
} catch (err) {
|
|
19033
|
+
abandonLaunch(fingerprint);
|
|
19034
|
+
throw err;
|
|
19035
|
+
}
|
|
19036
|
+
const notificationId = launch.resp?.notification_id ?? null;
|
|
19037
|
+
const queuedIds = launch.resp?.queued_ids ?? [];
|
|
19038
|
+
const skippedIds = launch.resp?.skipped_ids ?? [];
|
|
19039
|
+
const seen = /* @__PURE__ */ new Set([...queuedIds, ...skippedIds]);
|
|
19040
|
+
const failed2 = candidates.filter((id) => !seen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
19041
|
+
rememberLaunch(fingerprint, notificationId);
|
|
18567
19042
|
const out = {
|
|
18568
19043
|
status: "running",
|
|
18569
|
-
handle_id: reservation.record.bulk_id,
|
|
18570
|
-
qualify_id: reservation.record.bulk_id,
|
|
18571
19044
|
lead_ids: candidates,
|
|
18572
|
-
launched_count:
|
|
19045
|
+
launched_count: queuedIds.length,
|
|
18573
19046
|
failed: failed2,
|
|
18574
|
-
quota_exceeded:
|
|
19047
|
+
quota_exceeded: launch.quotaExceeded,
|
|
18575
19048
|
lens_id: lensId,
|
|
18576
19049
|
notification_id: notificationId,
|
|
18577
19050
|
_meta: { region: client.region }
|
|
@@ -19145,7 +19618,7 @@ var importAndQualify = {
|
|
|
19145
19618
|
},
|
|
19146
19619
|
total_budget_ms: {
|
|
19147
19620
|
type: "number",
|
|
19148
|
-
description: `Total wall-clock budget across import + qualify in ms (default ${DEFAULT_TOTAL_BUDGET_MS3}). When exhausted, the response returns
|
|
19621
|
+
description: `Total wall-clock budget across import + qualify in ms (default ${DEFAULT_TOTAL_BUDGET_MS3}). When exhausted, the response returns lead_ids + lens_id for resume via leadbay_qualify_status.`
|
|
19149
19622
|
},
|
|
19150
19623
|
per_phase_budget_ms: {
|
|
19151
19624
|
type: "number",
|
|
@@ -19153,7 +19626,7 @@ var importAndQualify = {
|
|
|
19153
19626
|
},
|
|
19154
19627
|
wait_for_completion: {
|
|
19155
19628
|
type: "boolean",
|
|
19156
|
-
description: "When false,
|
|
19629
|
+
description: "When false, upload the rows and return `{kind:'result', status:'running', import_ids}` immediately. Poll leadbay_import_status({importIds, dry_run}); the qualify phase does NOT run on this path \u2014 call leadbay_bulk_qualify_leads on the imported leads yourself. Default is true."
|
|
19157
19630
|
},
|
|
19158
19631
|
lensId: {
|
|
19159
19632
|
type: "number",
|
|
@@ -19176,7 +19649,7 @@ var importAndQualify = {
|
|
|
19176
19649
|
},
|
|
19177
19650
|
outputSchema: {
|
|
19178
19651
|
type: "object",
|
|
19179
|
-
description: "Two return shapes: kind:'preview' (when dry_run='preview') with mapping hints; kind:'result' (default) with imported + qualified leads +
|
|
19652
|
+
description: "Two return shapes: kind:'preview' (when dry_run='preview') with mapping hints; kind:'result' (default) with imported + qualified leads + lead_ids/lens_id to resume via leadbay_qualify_status.",
|
|
19180
19653
|
properties: {
|
|
19181
19654
|
kind: {
|
|
19182
19655
|
type: "string",
|
|
@@ -19186,10 +19659,6 @@ var importAndQualify = {
|
|
|
19186
19659
|
type: "string",
|
|
19187
19660
|
description: "`running` when wait_for_completion=false."
|
|
19188
19661
|
},
|
|
19189
|
-
handle_id: {
|
|
19190
|
-
type: "string",
|
|
19191
|
-
description: "Import handle to pass to leadbay_import_status when wait_for_completion=false."
|
|
19192
|
-
},
|
|
19193
19662
|
timed_out: {
|
|
19194
19663
|
type: "boolean",
|
|
19195
19664
|
description: "True when the underlying import ran out of poll budget. Still running server-side \u2014 poll leadbay_import_status(import_ids). Do NOT re-issue the import."
|
|
@@ -19234,10 +19703,12 @@ var importAndQualify = {
|
|
|
19234
19703
|
type: "object",
|
|
19235
19704
|
description: "Adaptive budgets the composite selected (when caller didn't override): {per_lead_budget_ms, total_budget_ms, per_phase_budget_ms, wall_clock_estimate_ms, strategy}."
|
|
19236
19705
|
},
|
|
19237
|
-
|
|
19238
|
-
type:
|
|
19239
|
-
description: "
|
|
19706
|
+
lead_ids: {
|
|
19707
|
+
type: "array",
|
|
19708
|
+
description: "Leads the qualify phase covers. Pass to leadbay_qualify_status for per-lead detail.",
|
|
19709
|
+
items: { type: "string" }
|
|
19240
19710
|
},
|
|
19711
|
+
lens_id: { type: "number", description: "Lens the qualification ran against." },
|
|
19241
19712
|
import_ids: {
|
|
19242
19713
|
type: "array",
|
|
19243
19714
|
description: "Backend file-import handles (one per chunk).",
|
|
@@ -19260,7 +19731,7 @@ var importAndQualify = {
|
|
|
19260
19731
|
},
|
|
19261
19732
|
still_running: {
|
|
19262
19733
|
type: "array",
|
|
19263
|
-
description: "Leads still being qualified at deadline; agent calls leadbay_qualify_status with
|
|
19734
|
+
description: "Leads still being qualified at deadline; agent calls leadbay_qualify_status with lead_ids + lens_id.",
|
|
19264
19735
|
items: { type: "object" }
|
|
19265
19736
|
},
|
|
19266
19737
|
failed: {
|
|
@@ -19281,10 +19752,13 @@ var importAndQualify = {
|
|
|
19281
19752
|
},
|
|
19282
19753
|
reused: {
|
|
19283
19754
|
type: "boolean",
|
|
19284
|
-
description: "True when an identical
|
|
19755
|
+
description: "True when an identical launch was reused within the idempotency window."
|
|
19285
19756
|
},
|
|
19286
19757
|
seconds_since_original: { type: "number" },
|
|
19287
|
-
cancelled: {
|
|
19758
|
+
cancelled: {
|
|
19759
|
+
type: "boolean",
|
|
19760
|
+
description: "True when the HOST cancelled the call (a user Cancel, or the host's own request timeout); a budget timeout reports `budget_exhausted` instead. The import and the qualifications already launched keep running on Leadbay, except a chunk cancelled before its mappings were committed, which can read `running` without ever progressing \u2014 if the counts stop moving, say so rather than polling on. Poll leadbay_import_status with the `import_ids` values passed as `importIds`, and leadbay_qualify_status with lead_ids + lens_id. `still_running` can also hold leads whose qualification was never launched; those need a fresh qualification, which qualify_status cannot start."
|
|
19761
|
+
},
|
|
19288
19762
|
budget_exhausted: { type: "boolean", description: "True when total_budget_ms hit before all leads finished." },
|
|
19289
19763
|
quota_blocked: { type: "boolean", description: "True when quota was exhausted before launching all leads." },
|
|
19290
19764
|
region: { type: "string" },
|
|
@@ -19306,9 +19780,6 @@ var importAndQualify = {
|
|
|
19306
19780
|
if (params.dry_run === "preview") {
|
|
19307
19781
|
return await runPreview(client, params, ctx, perPhaseBudget, totalBudget);
|
|
19308
19782
|
}
|
|
19309
|
-
if (!ctx?.bulkTracker) {
|
|
19310
|
-
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_and_qualify needs a BulkTracker (qualify_id persistence). Upgrade to @leadbay/mcp \u22650.5.0 or set LEADBAY_BULK_STORE_ALLOW_MEMORY=1.", "");
|
|
19311
|
-
}
|
|
19312
19783
|
if (params.wait_for_completion === false) {
|
|
19313
19784
|
const queued = await importLeads.execute(client, {
|
|
19314
19785
|
domains: params.domains,
|
|
@@ -19323,7 +19794,8 @@ var importAndQualify = {
|
|
|
19323
19794
|
return {
|
|
19324
19795
|
kind: "result",
|
|
19325
19796
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
19326
|
-
|
|
19797
|
+
lead_ids: [],
|
|
19798
|
+
lens_id: 0,
|
|
19327
19799
|
import_ids: queued.importIds,
|
|
19328
19800
|
notification_ids: queued.notification_ids ?? [],
|
|
19329
19801
|
imported: queued.leads.map((l) => ({
|
|
@@ -19346,9 +19818,9 @@ var importAndQualify = {
|
|
|
19346
19818
|
return {
|
|
19347
19819
|
kind: "result",
|
|
19348
19820
|
status: "running",
|
|
19349
|
-
...queued.handle_id ? { handle_id: queued.handle_id } : {},
|
|
19350
19821
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
19351
|
-
|
|
19822
|
+
lead_ids: [],
|
|
19823
|
+
lens_id: 0,
|
|
19352
19824
|
import_ids: queued.importIds,
|
|
19353
19825
|
notification_ids: queued.notification_ids ?? [],
|
|
19354
19826
|
imported: [],
|
|
@@ -19381,7 +19853,6 @@ var importAndQualify = {
|
|
|
19381
19853
|
return {
|
|
19382
19854
|
kind: "result",
|
|
19383
19855
|
status: "running",
|
|
19384
|
-
...importResultRaw.handle_id ? { handle_id: importResultRaw.handle_id } : {},
|
|
19385
19856
|
// Everything the rendering contract keys off has to survive the
|
|
19386
19857
|
// wrapper. Without `timed_out` the agent can't tell this from a
|
|
19387
19858
|
// deliberate async launch; without `rows_pending_upload` a >100-row
|
|
@@ -19393,7 +19864,8 @@ var importAndQualify = {
|
|
|
19393
19864
|
...importResultRaw.dry_run ? { dry_run: true } : {},
|
|
19394
19865
|
...importResultRaw.row_ids ? { row_ids: importResultRaw.row_ids } : {},
|
|
19395
19866
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
19396
|
-
|
|
19867
|
+
lead_ids: [],
|
|
19868
|
+
lens_id: 0,
|
|
19397
19869
|
import_ids: importResultRaw.importIds,
|
|
19398
19870
|
notification_ids: importResultRaw.notification_ids ?? [],
|
|
19399
19871
|
imported: [],
|
|
@@ -19414,7 +19886,8 @@ var importAndQualify = {
|
|
|
19414
19886
|
kind: "result",
|
|
19415
19887
|
...params.dry_run === true ? { dry_run: true } : {},
|
|
19416
19888
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
19417
|
-
|
|
19889
|
+
lead_ids: [],
|
|
19890
|
+
lens_id: 0,
|
|
19418
19891
|
import_ids: importResult.importIds,
|
|
19419
19892
|
notification_ids: importResult.notification_ids ?? [],
|
|
19420
19893
|
imported: [],
|
|
@@ -19452,7 +19925,8 @@ var importAndQualify = {
|
|
|
19452
19925
|
kind: "result",
|
|
19453
19926
|
...params.dry_run === true ? { dry_run: true } : {},
|
|
19454
19927
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
19455
|
-
|
|
19928
|
+
lead_ids: [],
|
|
19929
|
+
lens_id: 0,
|
|
19456
19930
|
import_ids: importResult.importIds,
|
|
19457
19931
|
notification_ids: importResult.notification_ids ?? [],
|
|
19458
19932
|
imported,
|
|
@@ -19498,29 +19972,17 @@ var importAndQualify = {
|
|
|
19498
19972
|
// targets do NOT collide on the same qualify_id.
|
|
19499
19973
|
buildFingerprintInput(params.mappings)
|
|
19500
19974
|
);
|
|
19501
|
-
const
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
19505
|
-
|
|
19506
|
-
|
|
19507
|
-
|
|
19508
|
-
|
|
19509
|
-
|
|
19510
|
-
|
|
19511
|
-
|
|
19512
|
-
let launchMarked = false;
|
|
19513
|
-
for (const attempt of [1, 2]) {
|
|
19514
|
-
try {
|
|
19515
|
-
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
|
|
19516
|
-
launchMarked = true;
|
|
19517
|
-
break;
|
|
19518
|
-
} catch (err) {
|
|
19519
|
-
ctx?.logger?.warn?.(`import_and_qualify: markLaunched attempt ${attempt} failed: ${err?.message ?? err}`);
|
|
19520
|
-
}
|
|
19521
|
-
}
|
|
19522
|
-
if (!launchMarked) {
|
|
19523
|
-
ctx?.logger?.warn?.(`import_and_qualify: markLaunched failed twice \u2014 qualify_status may BULK_PENDING-trap immediate retrieval; agent should poll, not relaunch`);
|
|
19975
|
+
const qualifyFingerprint = launchFingerprint([
|
|
19976
|
+
"import_qualify",
|
|
19977
|
+
leadIds,
|
|
19978
|
+
importResult.importIds,
|
|
19979
|
+
lensId,
|
|
19980
|
+
mappingFp
|
|
19981
|
+
]);
|
|
19982
|
+
const qualifyClaim = beginLaunch(qualifyFingerprint);
|
|
19983
|
+
const alreadyQualified = qualifyClaim.state === "settled" ? qualifyClaim.record : void 0;
|
|
19984
|
+
if (alreadyQualified) {
|
|
19985
|
+
ctx?.logger?.info?.(`import_and_qualify: identical qualify ran ${alreadyQualified.seconds_since}s ago; re-running the fan-out to report current per-lead state (already-qualified leads are skipped inside it)`);
|
|
19524
19986
|
}
|
|
19525
19987
|
let questionOrder = void 0;
|
|
19526
19988
|
try {
|
|
@@ -19534,22 +19996,24 @@ var importAndQualify = {
|
|
|
19534
19996
|
total: 3,
|
|
19535
19997
|
message: `Qualifying ${leadIds.length} lead${leadIds.length === 1 ? "" : "s"} (phase 3/3)`
|
|
19536
19998
|
});
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
|
|
19541
|
-
|
|
19542
|
-
|
|
19543
|
-
|
|
19544
|
-
|
|
19545
|
-
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
19999
|
+
let fanOut;
|
|
20000
|
+
try {
|
|
20001
|
+
fanOut = await fanOutWebFetchAndPoll(client, leadIds, {
|
|
20002
|
+
perLeadBudgetMs: perLeadBudget,
|
|
20003
|
+
totalDeadlineMs: totalDeadline,
|
|
20004
|
+
signal,
|
|
20005
|
+
ctx,
|
|
20006
|
+
skipAlreadyQualifiedLensId: lensId,
|
|
20007
|
+
skipAlreadyQualifiedLaunch: skipAlreadyQualified,
|
|
20008
|
+
...questionOrder ? { questionOrder } : {}
|
|
20009
|
+
});
|
|
20010
|
+
} catch (err) {
|
|
20011
|
+
if (!alreadyQualified)
|
|
20012
|
+
abandonLaunch(qualifyFingerprint);
|
|
20013
|
+
throw err;
|
|
19552
20014
|
}
|
|
20015
|
+
if (!alreadyQualified)
|
|
20016
|
+
rememberLaunch(qualifyFingerprint, null);
|
|
19553
20017
|
const qualified = fanOut.results.filter((r) => !r._stillRunning).map(({ _stillRunning, ...rest }) => rest);
|
|
19554
20018
|
const notInLensSet = new Set(fanOut.not_in_lens);
|
|
19555
20019
|
const stillRunningIds = new Set([
|
|
@@ -19563,7 +20027,8 @@ var importAndQualify = {
|
|
|
19563
20027
|
return {
|
|
19564
20028
|
kind: "result",
|
|
19565
20029
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
19566
|
-
|
|
20030
|
+
lead_ids: leadIds,
|
|
20031
|
+
lens_id: lensId,
|
|
19567
20032
|
import_ids: importResult.importIds,
|
|
19568
20033
|
notification_ids: importResult.notification_ids ?? [],
|
|
19569
20034
|
imported,
|
|
@@ -19574,9 +20039,9 @@ var importAndQualify = {
|
|
|
19574
20039
|
quota_exceeded: fanOut.quota_exceeded,
|
|
19575
20040
|
skipped_already_qualified,
|
|
19576
20041
|
not_in_lens: fanOut.not_in_lens,
|
|
19577
|
-
...
|
|
20042
|
+
...alreadyQualified ? {
|
|
19578
20043
|
reused: true,
|
|
19579
|
-
seconds_since_original:
|
|
20044
|
+
seconds_since_original: alreadyQualified.seconds_since
|
|
19580
20045
|
} : {},
|
|
19581
20046
|
...fanOut.cancelled ? { cancelled: true } : {},
|
|
19582
20047
|
...budgetExhausted ? { budget_exhausted: true } : {},
|
|
@@ -19699,19 +20164,6 @@ async function runPreview(client, params, ctx, perPhaseBudget, _totalBudget) {
|
|
|
19699
20164
|
};
|
|
19700
20165
|
}
|
|
19701
20166
|
|
|
19702
|
-
// ../core/dist/jobs/bulk-store.js
|
|
19703
|
-
import { mkdir as mkdirAsync, lstat, open as fsOpen, readFile, rename, stat, unlink } from "fs/promises";
|
|
19704
|
-
import { constants as fsConstants } from "fs";
|
|
19705
|
-
import { dirname, resolve as resolvePath } from "path";
|
|
19706
|
-
import { homedir, platform } from "os";
|
|
19707
|
-
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
19708
|
-
var DEFAULT_IDEMPOTENCY_WINDOW_MS = 5 * 60 * 1e3;
|
|
19709
|
-
var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
19710
|
-
var UUIDV4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
19711
|
-
function isValidBulkId(v) {
|
|
19712
|
-
return typeof v === "string" && UUIDV4_RE.test(v);
|
|
19713
|
-
}
|
|
19714
|
-
|
|
19715
20167
|
// ../core/dist/composite/import-status.js
|
|
19716
20168
|
function summarizeImports(imports, dryRun) {
|
|
19717
20169
|
let recordsTotal = 0;
|
|
@@ -19828,10 +20280,6 @@ var importStatus = {
|
|
|
19828
20280
|
inputSchema: {
|
|
19829
20281
|
type: "object",
|
|
19830
20282
|
properties: {
|
|
19831
|
-
handle_id: {
|
|
19832
|
-
type: "string",
|
|
19833
|
-
description: "UUIDv4 handle returned by leadbay_import_leads when wait_for_completion=false."
|
|
19834
|
-
},
|
|
19835
20283
|
importIds: {
|
|
19836
20284
|
type: "array",
|
|
19837
20285
|
description: "Backend file-import ids to inspect directly \u2014 from a completed import's `importIds`, or from a `{status:'running', timed_out:true}` result.",
|
|
@@ -19848,12 +20296,11 @@ var importStatus = {
|
|
|
19848
20296
|
type: "object",
|
|
19849
20297
|
properties: {
|
|
19850
20298
|
status: { type: "string", description: "running, complete, or failed." },
|
|
19851
|
-
handle_id: { type: "string" },
|
|
19852
20299
|
importIds: { type: "array", items: { type: "string" } },
|
|
19853
20300
|
progress: { type: "object" },
|
|
19854
20301
|
result: {
|
|
19855
20302
|
type: "object",
|
|
19856
|
-
description: "Final import result: {leads, not_imported, importIds, still_settling?}. Present when
|
|
20303
|
+
description: "Final import result: {leads, not_imported, importIds, still_settling?}. Present when the importIds[] path finds every import complete and reconciles the wizard's records."
|
|
19857
20304
|
},
|
|
19858
20305
|
error: { type: "string" },
|
|
19859
20306
|
dry_run: {
|
|
@@ -19866,99 +20313,20 @@ var importStatus = {
|
|
|
19866
20313
|
required: ["status", "importIds", "progress", "region", "_meta"]
|
|
19867
20314
|
},
|
|
19868
20315
|
execute: async (client, params, ctx) => {
|
|
19869
|
-
let handleId = params.handle_id;
|
|
19870
20316
|
let importIds = params.importIds ?? [];
|
|
19871
|
-
let handleDryRun = params.dry_run;
|
|
19872
|
-
if (handleId) {
|
|
19873
|
-
if (!isValidBulkId(handleId)) {
|
|
19874
|
-
throw client.makeError("BULK_INVALID_ID", "handle_id is not a valid UUIDv4", "Pass the handle_id returned by leadbay_import_leads verbatim.", "");
|
|
19875
|
-
}
|
|
19876
|
-
if (!ctx?.bulkTracker) {
|
|
19877
|
-
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_status needs a BulkTracker to resolve handle_id. Pass importIds[] directly as a fallback.", "");
|
|
19878
|
-
}
|
|
19879
|
-
const record = await ctx.bulkTracker.getImport(handleId);
|
|
19880
|
-
if (!record) {
|
|
19881
|
-
const any = await ctx.bulkTracker.get(handleId);
|
|
19882
|
-
if (any && any.kind !== "import") {
|
|
19883
|
-
throw client.makeError("BULK_WRONG_KIND", "This handle was not created by leadbay_import_leads", "Use leadbay_qualify_status for qualify ids or leadbay_bulk_enrich_status for enrich ids.", "");
|
|
19884
|
-
}
|
|
19885
|
-
throw client.makeError("BULK_NOT_FOUND", "No import record for that handle_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence.", "");
|
|
19886
|
-
}
|
|
19887
|
-
importIds = record.import_ids;
|
|
19888
|
-
handleDryRun = record.dry_run ?? handleDryRun;
|
|
19889
|
-
if (record.status === "complete" && record.result) {
|
|
19890
|
-
return {
|
|
19891
|
-
status: "complete",
|
|
19892
|
-
handle_id: handleId,
|
|
19893
|
-
importIds,
|
|
19894
|
-
progress: record.progress ?? {
|
|
19895
|
-
phase: "complete",
|
|
19896
|
-
records_processed: record.records_total,
|
|
19897
|
-
records_total: record.records_total
|
|
19898
|
-
},
|
|
19899
|
-
result: record.result,
|
|
19900
|
-
region: client.region,
|
|
19901
|
-
_meta: client.lastMeta ?? {
|
|
19902
|
-
region: client.region,
|
|
19903
|
-
endpoint: "bulk-store",
|
|
19904
|
-
latency_ms: null,
|
|
19905
|
-
retry_after: null
|
|
19906
|
-
}
|
|
19907
|
-
};
|
|
19908
|
-
}
|
|
19909
|
-
if (record.status === "failed") {
|
|
19910
|
-
return {
|
|
19911
|
-
status: "failed",
|
|
19912
|
-
handle_id: handleId,
|
|
19913
|
-
importIds,
|
|
19914
|
-
progress: record.progress ?? {
|
|
19915
|
-
phase: "failed",
|
|
19916
|
-
records_processed: 0,
|
|
19917
|
-
records_total: record.records_total
|
|
19918
|
-
},
|
|
19919
|
-
error: record.error ?? "import failed",
|
|
19920
|
-
region: client.region,
|
|
19921
|
-
_meta: client.lastMeta ?? {
|
|
19922
|
-
region: client.region,
|
|
19923
|
-
endpoint: "bulk-store",
|
|
19924
|
-
latency_ms: null,
|
|
19925
|
-
retry_after: null
|
|
19926
|
-
}
|
|
19927
|
-
};
|
|
19928
|
-
}
|
|
19929
|
-
if (importIds.length === 0) {
|
|
19930
|
-
return {
|
|
19931
|
-
status: "running",
|
|
19932
|
-
handle_id: handleId,
|
|
19933
|
-
importIds,
|
|
19934
|
-
progress: record.progress ?? {
|
|
19935
|
-
phase: "queued",
|
|
19936
|
-
records_processed: 0,
|
|
19937
|
-
records_total: record.records_total
|
|
19938
|
-
},
|
|
19939
|
-
region: client.region,
|
|
19940
|
-
_meta: client.lastMeta ?? {
|
|
19941
|
-
region: client.region,
|
|
19942
|
-
endpoint: "bulk-store",
|
|
19943
|
-
latency_ms: null,
|
|
19944
|
-
retry_after: null
|
|
19945
|
-
}
|
|
19946
|
-
};
|
|
19947
|
-
}
|
|
19948
|
-
}
|
|
19949
20317
|
importIds = [...new Set(importIds)];
|
|
19950
20318
|
if (importIds.length === 0) {
|
|
19951
|
-
throw client.makeError("IMPORT_STATUS_INPUT_REQUIRED", "Pass
|
|
20319
|
+
throw client.makeError("IMPORT_STATUS_INPUT_REQUIRED", "Pass importIds[]", "Call leadbay_import_leads with wait_for_completion=false first, then pass back the importIds it returned.", "");
|
|
19952
20320
|
}
|
|
19953
20321
|
const imports = await Promise.all(importIds.map((id) => client.request("GET", `/imports/${id}`)));
|
|
19954
|
-
const progress = summarizeImports(imports,
|
|
20322
|
+
const progress = summarizeImports(imports, params.dry_run);
|
|
19955
20323
|
const failed = imports.find((i) => i.pre_processing?.error || i.processing?.error);
|
|
19956
20324
|
const complete = imports.every((i) => {
|
|
19957
20325
|
if (i.pre_processing?.error || i.processing?.error)
|
|
19958
20326
|
return false;
|
|
19959
|
-
if (
|
|
20327
|
+
if (params.dry_run === true)
|
|
19960
20328
|
return Boolean(i.pre_processing?.finished);
|
|
19961
|
-
if (
|
|
20329
|
+
if (params.dry_run === false)
|
|
19962
20330
|
return Boolean(i.processing?.finished);
|
|
19963
20331
|
return Boolean(i.processing?.finished || i.pre_processing?.finished && !i.processing);
|
|
19964
20332
|
});
|
|
@@ -19966,7 +20334,7 @@ var importStatus = {
|
|
|
19966
20334
|
const commitError = commitFailureFor(importIds);
|
|
19967
20335
|
let notReady = false;
|
|
19968
20336
|
const declaredTotal = imports.reduce((n, i) => n + Number(i.total_records ?? 0), 0);
|
|
19969
|
-
if (!failed && complete &&
|
|
20337
|
+
if (!failed && complete && params.dry_run !== true && importIds.length > 0) {
|
|
19970
20338
|
try {
|
|
19971
20339
|
reconciled = await fetchReconciledRecords(client, importIds, declaredTotal, ctx);
|
|
19972
20340
|
} catch (err) {
|
|
@@ -19981,9 +20349,8 @@ var importStatus = {
|
|
|
19981
20349
|
const settled = complete && !notReady;
|
|
19982
20350
|
return {
|
|
19983
20351
|
status: failed || commitError ? "failed" : settled ? "complete" : "running",
|
|
19984
|
-
...handleId ? { handle_id: handleId } : {},
|
|
19985
20352
|
importIds,
|
|
19986
|
-
...
|
|
20353
|
+
...params.dry_run === true ? { dry_run: true } : {},
|
|
19987
20354
|
progress: notReady ? { ...progress, phase: "committing" } : progress,
|
|
19988
20355
|
...reconciled ? {
|
|
19989
20356
|
result: {
|
|
@@ -20007,15 +20374,30 @@ var importStatus = {
|
|
|
20007
20374
|
}
|
|
20008
20375
|
};
|
|
20009
20376
|
|
|
20010
|
-
// ../core/dist/
|
|
20011
|
-
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20377
|
+
// ../core/dist/notifications/read-by-id.js
|
|
20378
|
+
var PAGE_SIZE2 = 50;
|
|
20379
|
+
var MAX_PAGES = 4;
|
|
20380
|
+
async function readNotificationById(client, notificationId) {
|
|
20381
|
+
for (const archived of [false, true]) {
|
|
20382
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
20383
|
+
let res;
|
|
20384
|
+
try {
|
|
20385
|
+
res = await client.listNotifications({ archived, page, count: PAGE_SIZE2 });
|
|
20386
|
+
} catch {
|
|
20387
|
+
return null;
|
|
20388
|
+
}
|
|
20389
|
+
const hit = res.items.find((n) => n.id === notificationId);
|
|
20390
|
+
if (hit)
|
|
20391
|
+
return hit;
|
|
20392
|
+
const pages = res.pagination?.pages ?? 1;
|
|
20393
|
+
if (page + 1 >= pages)
|
|
20394
|
+
break;
|
|
20395
|
+
}
|
|
20017
20396
|
}
|
|
20397
|
+
return null;
|
|
20018
20398
|
}
|
|
20399
|
+
|
|
20400
|
+
// ../core/dist/composite/qualify-status.js
|
|
20019
20401
|
var qualifyStatus = {
|
|
20020
20402
|
name: "leadbay_qualify_status",
|
|
20021
20403
|
annotations: {
|
|
@@ -20029,18 +20411,30 @@ var qualifyStatus = {
|
|
|
20029
20411
|
inputSchema: {
|
|
20030
20412
|
type: "object",
|
|
20031
20413
|
properties: {
|
|
20032
|
-
|
|
20414
|
+
notification_id: {
|
|
20033
20415
|
type: "string",
|
|
20034
|
-
description: "
|
|
20416
|
+
description: "The `notification_id` returned by leadbay_import_and_qualify / leadbay_bulk_qualify_leads. Answers progress in ONE call."
|
|
20417
|
+
},
|
|
20418
|
+
lead_ids: {
|
|
20419
|
+
type: "array",
|
|
20420
|
+
description: "The `lead_ids` the launch returned. Supply them for per-lead detail (which settled, which are still running). Progress alone needs only notification_id.",
|
|
20421
|
+
items: { type: "string" }
|
|
20422
|
+
},
|
|
20423
|
+
lens_id: {
|
|
20424
|
+
type: "number",
|
|
20425
|
+
description: "The `lens_id` the launch returned. Used to flag leads no longer in the lens."
|
|
20035
20426
|
}
|
|
20036
20427
|
},
|
|
20037
|
-
required: ["
|
|
20428
|
+
anyOf: [{ required: ["notification_id"] }, { required: ["lead_ids"] }],
|
|
20038
20429
|
additionalProperties: false
|
|
20039
20430
|
},
|
|
20040
20431
|
outputSchema: {
|
|
20041
20432
|
type: "object",
|
|
20042
20433
|
properties: {
|
|
20043
|
-
|
|
20434
|
+
notification_id: {
|
|
20435
|
+
type: ["string", "null"],
|
|
20436
|
+
description: "The backend job id this status is for; pass it back to poll again."
|
|
20437
|
+
},
|
|
20044
20438
|
launched_at: { type: "string", description: "ISO timestamp of original launch." },
|
|
20045
20439
|
status: { type: "string", description: "'launched' on success (other states surface as error envelopes)." },
|
|
20046
20440
|
import_ids: {
|
|
@@ -20051,7 +20445,7 @@ var qualifyStatus = {
|
|
|
20051
20445
|
lens_id: { type: "number", description: "Lens id the qualification ran against." },
|
|
20052
20446
|
lead_ids: {
|
|
20053
20447
|
type: "array",
|
|
20054
|
-
description: "Lead UUIDs covered by this
|
|
20448
|
+
description: "Lead UUIDs covered by this status (echoed from launch).",
|
|
20055
20449
|
items: { type: "string" }
|
|
20056
20450
|
},
|
|
20057
20451
|
qualified: {
|
|
@@ -20086,7 +20480,7 @@ var qualifyStatus = {
|
|
|
20086
20480
|
_meta: { type: "object" }
|
|
20087
20481
|
},
|
|
20088
20482
|
required: [
|
|
20089
|
-
"
|
|
20483
|
+
"notification_id",
|
|
20090
20484
|
"status",
|
|
20091
20485
|
"import_ids",
|
|
20092
20486
|
"lens_id",
|
|
@@ -20100,29 +20494,55 @@ var qualifyStatus = {
|
|
|
20100
20494
|
]
|
|
20101
20495
|
},
|
|
20102
20496
|
execute: async (client, params, ctx) => {
|
|
20103
|
-
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
|
|
20107
|
-
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_qualify_status needs a BulkTracker. Upgrade to @leadbay/mcp \u22650.5.0 or set LEADBAY_BULK_STORE_ALLOW_MEMORY=1.", "");
|
|
20497
|
+
const notifId = params.notification_id ?? null;
|
|
20498
|
+
const leadIds = params.lead_ids ?? [];
|
|
20499
|
+
if (!notifId && leadIds.length === 0) {
|
|
20500
|
+
throw client.makeError("QUALIFY_STATUS_INPUT_REQUIRED", "Pass notification_id (for progress) and/or lead_ids (for per-lead detail)", "Both are in the launch response from leadbay_bulk_qualify_leads / leadbay_import_and_qualify. Re-read that result and pass them back.", "");
|
|
20108
20501
|
}
|
|
20109
|
-
|
|
20110
|
-
|
|
20111
|
-
|
|
20112
|
-
|
|
20113
|
-
|
|
20114
|
-
|
|
20502
|
+
let bulkProgress = null;
|
|
20503
|
+
let inProgressFlag = null;
|
|
20504
|
+
let launchedAt = null;
|
|
20505
|
+
if (notifId) {
|
|
20506
|
+
const n = await readNotificationById(client, notifId);
|
|
20507
|
+
if (!n && leadIds.length === 0) {
|
|
20508
|
+
throw client.makeError("QUALIFY_JOB_NOT_FOUND", "No job for that notification_id", "The lookup scans this user's recent notifications (archived included); a job behind many newer ones will not be found. Re-call with the `lead_ids` + `lens_id` the launch returned \u2014 that answers per lead without the notification.", "");
|
|
20509
|
+
}
|
|
20510
|
+
if (n && inferKind(n) !== "bulk_qualify") {
|
|
20511
|
+
const kind = inferKind(n);
|
|
20512
|
+
throw client.makeError("QUALIFY_JOB_WRONG_KIND", `That notification_id is a ${kind === "bulk_enrich" ? "contact enrichment" : kind === "import" ? "file import" : "non-bulk"} notification, not a lead qualification`, kind === "bulk_enrich" ? "Poll it with leadbay_bulk_enrich_status({notification_id, lead_ids, titles, email, phone}) instead \u2014 carry the titles and channel the enrichment was launched with, or a contact enriched earlier counts as done and all_done flips true before anything landed." : kind === "import" ? "Poll it with leadbay_import_status({importIds}) instead." : "Pass the notification_id returned by leadbay_bulk_qualify_leads.", "");
|
|
20513
|
+
}
|
|
20514
|
+
if (n) {
|
|
20515
|
+
bulkProgress = n.bulk_progress;
|
|
20516
|
+
inProgressFlag = n.in_progress;
|
|
20517
|
+
launchedAt = n.created_at;
|
|
20115
20518
|
}
|
|
20116
|
-
throw client.makeError("BULK_NOT_FOUND", "No qualify record for that qualify_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence. Re-launch via leadbay_import_and_qualify.", "");
|
|
20117
|
-
}
|
|
20118
|
-
if (record.status === "pending") {
|
|
20119
|
-
throw client.makeError("BULK_PENDING", "Qualify record is in 'pending' state \u2014 the launch may be in flight or crashed before launch ack", "Retry leadbay_qualify_status in a few seconds. If it persists >60s, relaunch via leadbay_import_and_qualify.", "");
|
|
20120
|
-
}
|
|
20121
|
-
if (record.status === "failed") {
|
|
20122
|
-
throw client.makeError("BULK_LAUNCH_FAILED", "The original import_and_qualify launch failed; no qualifications were ordered", "Call leadbay_import_and_qualify again \u2014 the failed record won't block a fresh launch.", "");
|
|
20123
20519
|
}
|
|
20124
|
-
if (
|
|
20125
|
-
|
|
20520
|
+
if (leadIds.length === 0) {
|
|
20521
|
+
const out2 = {
|
|
20522
|
+
notification_id: notifId,
|
|
20523
|
+
launched_at: launchedAt ?? "",
|
|
20524
|
+
status: "launched",
|
|
20525
|
+
import_ids: [],
|
|
20526
|
+
lens_id: params.lens_id ?? 0,
|
|
20527
|
+
lead_ids: [],
|
|
20528
|
+
qualified: [],
|
|
20529
|
+
still_running: [],
|
|
20530
|
+
failed: [],
|
|
20531
|
+
not_in_lens: [],
|
|
20532
|
+
bulk_progress: bulkProgress,
|
|
20533
|
+
in_progress: inProgressFlag,
|
|
20534
|
+
region: client.region,
|
|
20535
|
+
_meta: client.lastMeta ?? {
|
|
20536
|
+
region: client.region,
|
|
20537
|
+
endpoint: "GET /notifications",
|
|
20538
|
+
latency_ms: null,
|
|
20539
|
+
retry_after: null
|
|
20540
|
+
}
|
|
20541
|
+
};
|
|
20542
|
+
if (bulkProgress && bulkProgress.quota_hit_count > 0) {
|
|
20543
|
+
out2.quota_hit_hint = "Some leads hit the AI-credits quota during qualification. Top up via leadbay_create_topup_link to clear the throttle immediately, or wait until the daily/weekly window resets.";
|
|
20544
|
+
}
|
|
20545
|
+
return out2;
|
|
20126
20546
|
}
|
|
20127
20547
|
ctx?.progress?.({
|
|
20128
20548
|
progress: 1,
|
|
@@ -20138,20 +20558,20 @@ var qualifyStatus = {
|
|
|
20138
20558
|
ctx?.progress?.({
|
|
20139
20559
|
progress: 2,
|
|
20140
20560
|
total: 3,
|
|
20141
|
-
message: `Checking lens membership for ${
|
|
20561
|
+
message: `Checking lens membership for ${leadIds.length} lead${leadIds.length === 1 ? "" : "s"}\u2026`
|
|
20142
20562
|
});
|
|
20143
20563
|
let notInLensSet = /* @__PURE__ */ new Set();
|
|
20144
20564
|
try {
|
|
20145
|
-
const pre = await prequalifiedLeads(client,
|
|
20565
|
+
const pre = await prequalifiedLeads(client, leadIds, params.lens_id ?? 0, ctx);
|
|
20146
20566
|
notInLensSet = pre.not_in_lens;
|
|
20147
20567
|
} catch {
|
|
20148
20568
|
}
|
|
20149
20569
|
ctx?.progress?.({
|
|
20150
20570
|
progress: 3,
|
|
20151
20571
|
total: 3,
|
|
20152
|
-
message: `Refreshing qualification state for ${
|
|
20572
|
+
message: `Refreshing qualification state for ${leadIds.length} lead${leadIds.length === 1 ? "" : "s"}\u2026`
|
|
20153
20573
|
});
|
|
20154
|
-
const fresh = await refreshLeadStates(client,
|
|
20574
|
+
const fresh = await refreshLeadStates(client, leadIds, questionOrder);
|
|
20155
20575
|
const failed = [];
|
|
20156
20576
|
const qualified = [];
|
|
20157
20577
|
const still_running = [];
|
|
@@ -20170,28 +20590,17 @@ var qualifyStatus = {
|
|
|
20170
20590
|
const { _stillRunning, _failedCode, ...rest } = r;
|
|
20171
20591
|
qualified.push(rest);
|
|
20172
20592
|
}
|
|
20173
|
-
let bulkProgress = null;
|
|
20174
|
-
let inProgressFlag = null;
|
|
20175
|
-
const notifId = record.notification_id ?? null;
|
|
20176
|
-
if (notifId) {
|
|
20177
|
-
const n = await readNotification(client, notifId);
|
|
20178
|
-
if (n) {
|
|
20179
|
-
bulkProgress = n.bulk_progress;
|
|
20180
|
-
inProgressFlag = n.in_progress;
|
|
20181
|
-
}
|
|
20182
|
-
}
|
|
20183
20593
|
const out = {
|
|
20184
|
-
|
|
20185
|
-
launched_at:
|
|
20186
|
-
status:
|
|
20187
|
-
import_ids:
|
|
20188
|
-
lens_id:
|
|
20189
|
-
lead_ids:
|
|
20594
|
+
notification_id: notifId,
|
|
20595
|
+
launched_at: launchedAt ?? "",
|
|
20596
|
+
status: "launched",
|
|
20597
|
+
import_ids: [],
|
|
20598
|
+
lens_id: params.lens_id ?? 0,
|
|
20599
|
+
lead_ids: leadIds,
|
|
20190
20600
|
qualified,
|
|
20191
20601
|
still_running,
|
|
20192
20602
|
failed,
|
|
20193
20603
|
not_in_lens: [...notInLensSet],
|
|
20194
|
-
notification_id: notifId,
|
|
20195
20604
|
bulk_progress: bulkProgress,
|
|
20196
20605
|
in_progress: inProgressFlag,
|
|
20197
20606
|
region: client.region,
|
|
@@ -20202,10 +20611,6 @@ var qualifyStatus = {
|
|
|
20202
20611
|
retry_after: null
|
|
20203
20612
|
}
|
|
20204
20613
|
};
|
|
20205
|
-
if (record.per_lead_budget_ms !== void 0)
|
|
20206
|
-
out.per_lead_budget_ms = record.per_lead_budget_ms;
|
|
20207
|
-
if (record.total_budget_ms !== void 0)
|
|
20208
|
-
out.total_budget_ms = record.total_budget_ms;
|
|
20209
20614
|
if (bulkProgress && bulkProgress.quota_hit_count > 0) {
|
|
20210
20615
|
out.quota_hit_hint = "Some leads hit the AI-credits quota during qualification. Top up via leadbay_create_topup_link to clear the throttle immediately, or wait until the daily/weekly window resets.";
|
|
20211
20616
|
}
|
|
@@ -20217,171 +20622,81 @@ var qualifyStatus = {
|
|
|
20217
20622
|
var DEFAULT_CANDIDATE_COUNT = 25;
|
|
20218
20623
|
async function launchOnSelection(client, args, ctx) {
|
|
20219
20624
|
const { leadIds, titles, email, phone, lensId, selectionSource, preview } = args;
|
|
20220
|
-
|
|
20221
|
-
|
|
20222
|
-
|
|
20223
|
-
|
|
20224
|
-
|
|
20225
|
-
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
|
|
20229
|
-
|
|
20230
|
-
|
|
20231
|
-
|
|
20232
|
-
|
|
20233
|
-
|
|
20234
|
-
|
|
20235
|
-
|
|
20236
|
-
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
|
|
20243
|
-
|
|
20244
|
-
|
|
20245
|
-
|
|
20246
|
-
|
|
20247
|
-
|
|
20248
|
-
|
|
20249
|
-
|
|
20250
|
-
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
|
|
20256
|
-
|
|
20257
|
-
|
|
20258
|
-
|
|
20259
|
-
|
|
20260
|
-
|
|
20261
|
-
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20267
|
-
|
|
20268
|
-
|
|
20269
|
-
lead_ids: leadIds,
|
|
20270
|
-
titles,
|
|
20271
|
-
email,
|
|
20272
|
-
phone,
|
|
20273
|
-
preview,
|
|
20274
|
-
message: "No new enrichment was ordered; quota not spent. A concurrent identical launch is already in flight. Unless the user asked NOT to wait, poll leadbay_bulk_enrich_status with this bulk_id for results (see next_action); if they asked not to wait, hand back the bulk_id.",
|
|
20275
|
-
next_action: "Unless the user explicitly asked NOT to wait, poll leadbay_bulk_enrich_status({bulk_id}) until all_done \u2014 OR until overall_progress.done plateaus across spaced polls (~90s\u20132min; unresolvable contacts never flip). include_contacts=true on the read you report from, then report the resolved enrichment in this turn. If the user asked not to wait, hand back the bulk_id instead."
|
|
20276
|
-
};
|
|
20277
|
-
}
|
|
20278
|
-
bulkRecord = {
|
|
20279
|
-
bulk_id: fresh.record.bulk_id,
|
|
20280
|
-
launched_at: fresh.record.launched_at,
|
|
20281
|
-
durability: fresh.record.durability
|
|
20282
|
-
};
|
|
20283
|
-
} else if (bulkReused && res.record.status !== "failed") {
|
|
20284
|
-
return {
|
|
20285
|
-
mode: "already_launched",
|
|
20286
|
-
re_used: true,
|
|
20287
|
-
bulk_id: res.record.bulk_id,
|
|
20288
|
-
launched_at: res.record.launched_at,
|
|
20289
|
-
durability: res.record.durability,
|
|
20290
|
-
notification_id: res.record.notification_id ?? null,
|
|
20291
|
-
seconds_since_original_launch: bulkSecondsSinceOriginal ?? 0,
|
|
20292
|
-
lead_ids: leadIds,
|
|
20293
|
-
titles,
|
|
20294
|
-
email,
|
|
20295
|
-
phone,
|
|
20296
|
-
preview,
|
|
20297
|
-
message: `No new enrichment was ordered; quota not spent. An identical bulk was launched ${bulkSecondsSinceOriginal ?? 0}s ago. Unless the user asked NOT to wait (background/'I'll check later'), poll leadbay_bulk_enrich_status with this bulk_id for results; if they DID ask not to wait, hand back the bulk_id instead.`,
|
|
20298
|
-
next_action: "Unless the user explicitly asked NOT to wait (background/'I'll check later'), poll leadbay_bulk_enrich_status({bulk_id}) until all_done \u2014 OR until overall_progress.done holds steady across several SPACED polls (~15\u201330s apart, ~90s\u20132min elapsed; unresolvable contacts never flip, so a reused bulk can stay all_done:false forever). include_contacts=true on the read you report from, then report the resolved enrichment in this turn \u2014 don't end your turn waiting or spin forever. If the user DID ask not to wait, hand back the bulk_id instead of polling."
|
|
20299
|
-
};
|
|
20300
|
-
}
|
|
20301
|
-
}
|
|
20302
|
-
ctx?.progress?.({
|
|
20303
|
-
progress: 3,
|
|
20304
|
-
total: 3,
|
|
20305
|
-
message: `Launching enrichment for ${titles.length} title${titles.length === 1 ? "" : "s"}\u2026`
|
|
20306
|
-
});
|
|
20307
|
-
let launchResp = null;
|
|
20308
|
-
try {
|
|
20309
|
-
launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles, email, phone });
|
|
20310
|
-
} catch (err) {
|
|
20311
|
-
const aborted = err?.name === "AbortError" || ctx?.signal?.aborted === true;
|
|
20312
|
-
if (bulkRecord && tracker) {
|
|
20313
|
-
try {
|
|
20314
|
-
if (aborted) {
|
|
20315
|
-
await tracker.markCancelled(bulkRecord.bulk_id);
|
|
20316
|
-
} else {
|
|
20317
|
-
await tracker.markFailed(bulkRecord.bulk_id);
|
|
20318
|
-
}
|
|
20319
|
-
} catch (e) {
|
|
20320
|
-
ctx?.logger?.warn?.(`enrich_titles: tracker.${aborted ? "markCancelled" : "markFailed"} failed: ${e?.message ?? e}`);
|
|
20321
|
-
}
|
|
20322
|
-
}
|
|
20323
|
-
if (err?.code === "QUOTA_EXCEEDED") {
|
|
20324
|
-
return {
|
|
20325
|
-
status: "quota_exceeded",
|
|
20326
|
-
preview,
|
|
20327
|
-
message: "Quota exceeded on launch",
|
|
20328
|
-
retry_after_seconds: err?._meta?.retry_after ?? null
|
|
20329
|
-
};
|
|
20330
|
-
}
|
|
20331
|
-
throw err;
|
|
20332
|
-
}
|
|
20333
|
-
const notificationId = launchResp?.notification_id ?? null;
|
|
20334
|
-
if (bulkRecord && tracker) {
|
|
20335
|
-
try {
|
|
20336
|
-
await tracker.markLaunched(bulkRecord.bulk_id, notificationId);
|
|
20337
|
-
} catch (e) {
|
|
20338
|
-
ctx?.logger?.warn?.(`enrich_titles: tracker.markLaunched failed: ${e?.message ?? e}`);
|
|
20339
|
-
return {
|
|
20340
|
-
mode: "launched_tracker_pending",
|
|
20341
|
-
launched: true,
|
|
20342
|
-
preview,
|
|
20343
|
-
bulk_id: bulkRecord.bulk_id,
|
|
20344
|
-
launched_at: bulkRecord.launched_at,
|
|
20345
|
-
durability: bulkRecord.durability,
|
|
20346
|
-
// Surface the resolved lead IDs so the agent can follow the backend
|
|
20347
|
-
// job per-lead — bulk_enrich_status is unusable for this stuck handle,
|
|
20348
|
-
// and the caller may have omitted leadIds (wishlist default), so
|
|
20349
|
-
// without these it has no identifiers to poll.
|
|
20350
|
-
lead_ids: leadIds,
|
|
20351
|
-
titles,
|
|
20352
|
-
email,
|
|
20353
|
-
phone,
|
|
20354
|
-
message: "Enrichment job launched on the backend, but the local tracker record could not be flipped to 'launched' and will NOT heal on its own this session. leadbay_bulk_enrich_status({bulk_id}) will keep returning status:'pending' (BULK_PENDING) \u2014 do NOT poll it in a loop expecting completion. The backend job is running regardless; track it per-lead instead.",
|
|
20355
|
-
next_action: "Do NOT poll leadbay_bulk_enrich_status \u2014 this bulk_id is stuck 'pending' and won't flip. If the user asked NOT to wait (background/'I'll check later'), just hand back the returned lead_ids and let them re-check later. Otherwise track results per lead via leadbay_get_contacts(leadId) / leadbay_research_lead_by_id for the returned lead_ids (re-check every ~30s). get_contacts returns each lead's FULL contact list, so only count/report contacts whose job_title matches the enriched titles (" + titles.join(", ") + ") \u2014 don't attribute a pre-existing CFO/Sales email to this run \u2014 and a contact is done only when the REQUESTED channel landed (requested email and/or phone_number present, not contact.enrichment.done alone). Stop once the done set plateaus (~90s\u20132min), then report the resolved contacts and name the rest. (The launch already succeeded \u2014 do not relaunch.)"
|
|
20356
|
-
};
|
|
20357
|
-
}
|
|
20358
|
-
}
|
|
20625
|
+
const fingerprint = launchFingerprint([
|
|
20626
|
+
"enrich",
|
|
20627
|
+
leadIds,
|
|
20628
|
+
titles,
|
|
20629
|
+
email,
|
|
20630
|
+
phone,
|
|
20631
|
+
lensId
|
|
20632
|
+
]);
|
|
20633
|
+
const claim = beginLaunch(fingerprint);
|
|
20634
|
+
if (claim.state === "in_flight") {
|
|
20635
|
+
return {
|
|
20636
|
+
mode: "launch_in_flight",
|
|
20637
|
+
launched: false,
|
|
20638
|
+
preview,
|
|
20639
|
+
lead_ids: leadIds,
|
|
20640
|
+
titles,
|
|
20641
|
+
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.`,
|
|
20642
|
+
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."
|
|
20643
|
+
};
|
|
20644
|
+
}
|
|
20645
|
+
const already = claim.state === "settled" ? claim.record : void 0;
|
|
20646
|
+
if (already) {
|
|
20647
|
+
return {
|
|
20648
|
+
mode: "already_launched",
|
|
20649
|
+
launched: true,
|
|
20650
|
+
preview,
|
|
20651
|
+
reused: true,
|
|
20652
|
+
seconds_since_original_launch: already.seconds_since,
|
|
20653
|
+
lead_ids: leadIds,
|
|
20654
|
+
titles,
|
|
20655
|
+
email,
|
|
20656
|
+
phone,
|
|
20657
|
+
notification_id: already.notification_id,
|
|
20658
|
+
launched_at: already.launched_at,
|
|
20659
|
+
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.`,
|
|
20660
|
+
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."
|
|
20661
|
+
};
|
|
20662
|
+
}
|
|
20663
|
+
ctx?.progress?.({
|
|
20664
|
+
progress: 3,
|
|
20665
|
+
total: 3,
|
|
20666
|
+
message: `Launching enrichment for ${titles.length} title${titles.length === 1 ? "" : "s"}\u2026`
|
|
20667
|
+
});
|
|
20668
|
+
let launchResp = null;
|
|
20669
|
+
try {
|
|
20670
|
+
launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles, email, phone });
|
|
20671
|
+
} catch (err) {
|
|
20672
|
+
abandonLaunch(fingerprint);
|
|
20673
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
20359
20674
|
return {
|
|
20360
|
-
|
|
20675
|
+
status: "quota_exceeded",
|
|
20361
20676
|
preview,
|
|
20362
|
-
|
|
20363
|
-
|
|
20364
|
-
email,
|
|
20365
|
-
phone,
|
|
20366
|
-
// Always surface the resolved lead IDs — in the no-tracker branch there's
|
|
20367
|
-
// no bulk_id to poll, and the caller may have omitted leadIds (wishlist
|
|
20368
|
-
// default), so without these the agent has no identifiers to follow the
|
|
20369
|
-
// job it just launched via leadbay_get_contacts / research_lead_by_id.
|
|
20370
|
-
lead_ids: leadIds,
|
|
20371
|
-
bulk_id: bulkRecord?.bulk_id,
|
|
20372
|
-
launched_at: bulkRecord?.launched_at,
|
|
20373
|
-
durability: bulkRecord?.durability,
|
|
20374
|
-
notification_id: notificationId,
|
|
20375
|
-
// Branch on bulkRecord FIRST: leadbay_bulk_enrich_status needs a real
|
|
20376
|
-
// bulk_id (tracker handle). A notification_id can come back even with no
|
|
20377
|
-
// tracker (legacy / OpenClaw raw-launch fall-through) — in that case
|
|
20378
|
-
// bulk_id is undefined, so the agent must use the per-lead fallback, not
|
|
20379
|
-
// poll a nonexistent bulk_id.
|
|
20380
|
-
message: bulkRecord ? notificationId ? "Enrichment job launched (runs async). Unless the user asked NOT to wait (background/'I'll check later'), do NOT end your turn here \u2014 poll leadbay_bulk_enrich_status({bulk_id}) 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. (If the user DID ask not to wait, hand back the bulk_id instead. Either way, if you leave the conversation the completion also surfaces later via _meta.notifications / leadbay_account_status.notifications \u2014 but for a job you launched this turn and were NOT told to background, poll it now.)" : "Enrichment job launched (runs async). Unless the user asked NOT to wait (background/'I'll check later'), do NOT end your turn here \u2014 poll leadbay_bulk_enrich_status({bulk_id}) 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. (No notification id was returned, so there is NO automatic _meta.notifications completion for this job \u2014 if you background it or don't finish this turn, you (or the user) must poll leadbay_bulk_enrich_status({bulk_id}) again later; it will NOT surface on its own.)" : "Enrichment job launched. No bulk_id tracker configured. Unless the user asked NOT to wait (background/'I'll check later' \u2014 in which case hand back the lead_ids and let them re-check later), poll leadbay_get_contacts per lead (re-check every ~30s). get_contacts returns each lead's FULL contact list, so only count/report contacts whose job_title matches the enriched titles (" + titles.join(", ") + ") \u2014 don't attribute a pre-existing email of an unrelated role to this run \u2014 and a contact is done only when the REQUESTED channel landed (requested email and/or phone_number present, not contact.enrichment.done alone). Then report the results. Stop once the set of done contacts stops growing across a couple of spaced re-checks (~90s\u20132min elapsed): some contacts are unresolvable and never flip, so report the resolved ones and name the rest rather than polling forever.",
|
|
20381
|
-
next_action: bulkRecord ? "Unless the user explicitly asked NOT to wait (background/'I'll check later'), poll leadbay_bulk_enrich_status({bulk_id}) in a loop until all_done \u2014 OR until overall_progress.done holds steady across several SPACED polls (~15\u201330s apart, ~90s\u20132min elapsed; don't call a plateau from the first back-to-back reads while the backend spins up, and don't call it a plateau while partial_failures is present \u2014 that's a transient fetch error, keep polling/respect retry_after). Pass include_contacts=true on the read you report from, then report the resolved enrichment in THIS turn (name what landed and what didn't). If the user DID ask not to wait, hand back the bulk_id instead of polling (and if notification_id is null, tell them to ask again later \u2014 nothing auto-surfaces)." : "Unless the user asked not to wait, re-check via leadbay_research_lead_by_id or leadbay_get_contacts for the returned lead_ids (every ~30s). get_contacts returns each lead's FULL contact list, so only count/report contacts whose job_title matches the enriched titles (" + titles.join(", ") + ") \u2014 don't attribute a pre-existing email of an unrelated role to this run. Treat a contact as done only when the REQUESTED channel landed \u2014 the requested email present and/or phone_number present \u2014 NOT contact.enrichment.done alone (it's already true for a contact enriched on the other channel earlier). Stop once the done set stops growing across a couple of spaced re-checks (~90s\u20132min elapsed) \u2014 unresolvable contacts never flip \u2014 then report the resolved ones and name the rest. Don't poll forever or end your turn waiting. If the user asked not to wait, hand back the lead_ids and let them re-check later."
|
|
20677
|
+
message: "Quota exceeded on launch",
|
|
20678
|
+
retry_after_seconds: err?._meta?.retry_after ?? null
|
|
20382
20679
|
};
|
|
20383
20680
|
}
|
|
20681
|
+
throw err;
|
|
20384
20682
|
}
|
|
20683
|
+
const notificationId = launchResp?.notification_id ?? null;
|
|
20684
|
+
const remembered = rememberLaunch(fingerprint, notificationId);
|
|
20685
|
+
return {
|
|
20686
|
+
mode: "launched",
|
|
20687
|
+
preview,
|
|
20688
|
+
launched: true,
|
|
20689
|
+
titles,
|
|
20690
|
+
email,
|
|
20691
|
+
phone,
|
|
20692
|
+
// Always surfaced: these are the coordinates the agent polls with. There is
|
|
20693
|
+
// no server-side handle to look them up from, by design.
|
|
20694
|
+
lead_ids: leadIds,
|
|
20695
|
+
notification_id: notificationId,
|
|
20696
|
+
launched_at: remembered.launched_at,
|
|
20697
|
+
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.",
|
|
20698
|
+
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."
|
|
20699
|
+
};
|
|
20385
20700
|
}
|
|
20386
20701
|
async function launchEnrichment(client, args, ctx) {
|
|
20387
20702
|
await client.acquireSelectionLock();
|
|
@@ -20411,7 +20726,7 @@ var enrichTitles = {
|
|
|
20411
20726
|
// destructive because the dominant flow mutates state.
|
|
20412
20727
|
destructiveHint: true,
|
|
20413
20728
|
// Idempotent against the same selection + titles set (same hash → same
|
|
20414
|
-
//
|
|
20729
|
+
// the launch; backend silently no-ops on already-enriched contacts).
|
|
20415
20730
|
idempotentHint: true,
|
|
20416
20731
|
openWorldHint: true
|
|
20417
20732
|
},
|
|
@@ -20452,11 +20767,11 @@ var enrichTitles = {
|
|
|
20452
20767
|
},
|
|
20453
20768
|
outputSchema: {
|
|
20454
20769
|
type: "object",
|
|
20455
|
-
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), '
|
|
20770
|
+
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).",
|
|
20456
20771
|
properties: {
|
|
20457
20772
|
mode: {
|
|
20458
20773
|
type: "string",
|
|
20459
|
-
description: "'discover' | 'preview_only' | 'dry_run' | 'needs_confirmation' | 'already_launched' | '
|
|
20774
|
+
description: "'discover' | 'preview_only' | 'dry_run' | 'needs_confirmation' | 'already_launched' | 'launch_in_flight' | 'launched'."
|
|
20460
20775
|
},
|
|
20461
20776
|
status: {
|
|
20462
20777
|
type: "string",
|
|
@@ -20506,22 +20821,23 @@ var enrichTitles = {
|
|
|
20506
20821
|
type: "object",
|
|
20507
20822
|
description: "What dry_run WOULD have launched (titles, email, phone)."
|
|
20508
20823
|
},
|
|
20509
|
-
|
|
20510
|
-
type: "
|
|
20511
|
-
description: "
|
|
20824
|
+
notification_id: {
|
|
20825
|
+
type: ["string", "null"],
|
|
20826
|
+
description: "The backend's job id. Carry it to leadbay_bulk_enrich_status. Null when the backend returned none \u2014 then poll by lead_ids instead."
|
|
20512
20827
|
},
|
|
20513
|
-
|
|
20514
|
-
type: "
|
|
20515
|
-
description: "
|
|
20828
|
+
lead_ids: {
|
|
20829
|
+
type: "array",
|
|
20830
|
+
description: "The leads this run enriched. Carry them to leadbay_bulk_enrich_status for per-lead progress; they also work when the notification is archived.",
|
|
20831
|
+
items: { type: "string" }
|
|
20832
|
+
},
|
|
20833
|
+
reused: {
|
|
20834
|
+
type: "boolean",
|
|
20835
|
+
description: "True when an identical launch inside the 5-minute window was reused instead of spending quota again."
|
|
20516
20836
|
},
|
|
20517
20837
|
launched_at: {
|
|
20518
20838
|
type: "string",
|
|
20519
20839
|
description: "ISO timestamp of the (re-used or fresh) launch."
|
|
20520
20840
|
},
|
|
20521
|
-
durability: {
|
|
20522
|
-
type: "string",
|
|
20523
|
-
description: "'file' (persisted bulks.json) or 'memory'."
|
|
20524
|
-
},
|
|
20525
20841
|
seconds_since_original_launch: {
|
|
20526
20842
|
type: "number",
|
|
20527
20843
|
description: "Age of the re-used bulk record (already_launched mode)."
|
|
@@ -20778,14 +21094,6 @@ var enrichTitles = {
|
|
|
20778
21094
|
};
|
|
20779
21095
|
|
|
20780
21096
|
// ../core/dist/composite/bulk-enrich-status.js
|
|
20781
|
-
async function readNotification2(client, notificationId) {
|
|
20782
|
-
try {
|
|
20783
|
-
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
20784
|
-
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
20785
|
-
} catch {
|
|
20786
|
-
return null;
|
|
20787
|
-
}
|
|
20788
|
-
}
|
|
20789
21097
|
var STATUS_FETCH_CONCURRENCY = 5;
|
|
20790
21098
|
async function pMap(items, fn, concurrency) {
|
|
20791
21099
|
const out = new Array(items.length);
|
|
@@ -20814,44 +21122,45 @@ var bulkEnrichStatus = {
|
|
|
20814
21122
|
inputSchema: {
|
|
20815
21123
|
type: "object",
|
|
20816
21124
|
properties: {
|
|
20817
|
-
|
|
21125
|
+
notification_id: {
|
|
20818
21126
|
type: "string",
|
|
20819
|
-
description: "
|
|
21127
|
+
description: "The `notification_id` returned by leadbay_enrich_titles. Gives the job-level counters in one call."
|
|
21128
|
+
},
|
|
21129
|
+
lead_ids: {
|
|
21130
|
+
type: "array",
|
|
21131
|
+
description: "The `lead_ids` the launch returned. Gives per-lead progress, and answers on its own if the notification is archived or has aged off page 1.",
|
|
21132
|
+
items: { type: "string" }
|
|
21133
|
+
},
|
|
21134
|
+
titles: {
|
|
21135
|
+
type: "array",
|
|
21136
|
+
description: "The `titles` the launch returned. Scopes progress to the roles THIS run enriched, so a lead's pre-existing CFO email cannot inflate a CEO run.",
|
|
21137
|
+
items: { type: "string" }
|
|
21138
|
+
},
|
|
21139
|
+
email: {
|
|
21140
|
+
type: "boolean",
|
|
21141
|
+
description: "The `email` flag the launch returned. A contact counts as done only once the requested channel has landed."
|
|
21142
|
+
},
|
|
21143
|
+
phone: {
|
|
21144
|
+
type: "boolean",
|
|
21145
|
+
description: "The `phone` flag the launch returned. Same rule as `email`."
|
|
20820
21146
|
},
|
|
20821
21147
|
include_contacts: {
|
|
20822
21148
|
type: "boolean",
|
|
20823
21149
|
description: "If true, return the full contact list per lead (email, phone, enrichment.done). Default false \u2014 cheap status polls."
|
|
20824
21150
|
}
|
|
20825
21151
|
},
|
|
20826
|
-
required: ["
|
|
21152
|
+
anyOf: [{ required: ["notification_id"] }, { required: ["lead_ids"] }],
|
|
20827
21153
|
additionalProperties: false
|
|
20828
21154
|
},
|
|
20829
21155
|
outputSchema: {
|
|
20830
21156
|
type: "object",
|
|
20831
21157
|
properties: {
|
|
20832
|
-
|
|
21158
|
+
notification_id: { type: "string", description: "The backend job id; pass it back to poll again." },
|
|
20833
21159
|
launched_at: { type: "string", description: "ISO timestamp of /enrichment/launch ack." },
|
|
20834
21160
|
status: {
|
|
20835
21161
|
type: "string",
|
|
20836
21162
|
description: "'launched' on success. Errors return error envelopes (handled separately)."
|
|
20837
21163
|
},
|
|
20838
|
-
durability: {
|
|
20839
|
-
type: "string",
|
|
20840
|
-
description: "'persistent' (file-backed bulks.json) or 'memory' (LEADBAY_BULK_STORE_ALLOW_MEMORY)."
|
|
20841
|
-
},
|
|
20842
|
-
titles: {
|
|
20843
|
-
type: "array",
|
|
20844
|
-
description: "Titles ordered at launch time (echoed from the original enrich_titles call).",
|
|
20845
|
-
items: { type: "string" }
|
|
20846
|
-
},
|
|
20847
|
-
email: { type: "boolean", description: "True if email enrichment was requested." },
|
|
20848
|
-
phone: { type: "boolean", description: "True if phone enrichment was requested." },
|
|
20849
|
-
lens_id: { type: "number", description: "Lens id used to scope the enrichment." },
|
|
20850
|
-
leads: {
|
|
20851
|
-
type: "array",
|
|
20852
|
-
description: "Per-lead rollup: {lead_id, enrichment_progress:{done,total}, contacts? (when include_contacts=true)}.",
|
|
20853
|
-
items: { type: "object" }
|
|
20854
|
-
},
|
|
20855
21164
|
overall_progress: {
|
|
20856
21165
|
type: "object",
|
|
20857
21166
|
description: "Aggregate progress across all leads.",
|
|
@@ -20875,243 +21184,180 @@ var bulkEnrichStatus = {
|
|
|
20875
21184
|
items: { type: "object" }
|
|
20876
21185
|
}
|
|
20877
21186
|
},
|
|
20878
|
-
required: ["
|
|
21187
|
+
required: ["status", "leads", "overall_progress", "all_done"]
|
|
20879
21188
|
},
|
|
20880
21189
|
execute: async (client, params, ctx) => {
|
|
20881
|
-
if (!isValidBulkId(params.bulk_id)) {
|
|
20882
|
-
return {
|
|
20883
|
-
error: true,
|
|
20884
|
-
code: "BULK_INVALID_ID",
|
|
20885
|
-
message: "bulk_id is not a valid UUIDv4",
|
|
20886
|
-
hint: "Pass the bulk_id returned by leadbay_enrich_titles verbatim."
|
|
20887
|
-
};
|
|
20888
|
-
}
|
|
20889
|
-
if (!ctx?.bulkTracker) {
|
|
20890
|
-
return {
|
|
20891
|
-
error: true,
|
|
20892
|
-
code: "BULK_TRACKER_UNAVAILABLE",
|
|
20893
|
-
message: "No BulkTracker configured on this MCP instance",
|
|
20894
|
-
hint: "This composite requires a BulkTracker in ToolContext. Upgrade to @leadbay/mcp \u22650.3.0 or run with LEADBAY_BULK_STORE_ALLOW_MEMORY=1."
|
|
20895
|
-
};
|
|
20896
|
-
}
|
|
20897
21190
|
const includeContacts = params.include_contacts ?? false;
|
|
21191
|
+
const leadIds = params.lead_ids ?? [];
|
|
20898
21192
|
const startMs = Date.now();
|
|
20899
|
-
|
|
20900
|
-
try {
|
|
20901
|
-
record = await ctx.bulkTracker.get(params.bulk_id);
|
|
20902
|
-
} catch (err) {
|
|
20903
|
-
return {
|
|
20904
|
-
error: true,
|
|
20905
|
-
code: "BULK_STORE_UNAVAILABLE",
|
|
20906
|
-
message: `Bulk store read failed: ${err?.message ?? err}`,
|
|
20907
|
-
hint: "Check the file at $LEADBAY_BULK_STORE_PATH (default ~/.leadbay/bulks.json). Set LEADBAY_BULK_STORE_ALLOW_MEMORY=1 to fall back to in-memory storage on startup (handles won't survive restart)."
|
|
20908
|
-
};
|
|
20909
|
-
}
|
|
20910
|
-
if (!record) {
|
|
20911
|
-
return {
|
|
20912
|
-
error: true,
|
|
20913
|
-
code: "BULK_NOT_FOUND",
|
|
20914
|
-
message: "No bulk record for that bulk_id",
|
|
20915
|
-
hint: "The record may have aged out (30-day TTL) or the MCP process was restarted without persistence. Launch a new enrichment via leadbay_enrich_titles."
|
|
20916
|
-
};
|
|
20917
|
-
}
|
|
20918
|
-
if (record.kind !== "enrich") {
|
|
20919
|
-
return {
|
|
20920
|
-
error: true,
|
|
20921
|
-
code: "BULK_WRONG_KIND",
|
|
20922
|
-
message: `This bulk_id was created by ${record.kind === "qualify" ? "leadbay_import_and_qualify" : "leadbay_import_leads"}, not leadbay_enrich_titles.`,
|
|
20923
|
-
hint: record.kind === "qualify" ? "Call leadbay_qualify_status with this id instead." : "Call leadbay_import_status with this id instead.",
|
|
20924
|
-
bulk_id: record.bulk_id
|
|
20925
|
-
};
|
|
20926
|
-
}
|
|
20927
|
-
if (record.status === "pending") {
|
|
20928
|
-
return {
|
|
20929
|
-
error: true,
|
|
20930
|
-
code: "BULK_PENDING",
|
|
20931
|
-
message: "Bulk is in 'pending' state \u2014 the launch is in flight or the MCP crashed between launch and ack.",
|
|
20932
|
-
hint: "Retry leadbay_bulk_enrich_status in a few seconds. If it persists >60s, relaunch via leadbay_enrich_titles.",
|
|
20933
|
-
bulk_id: record.bulk_id,
|
|
20934
|
-
launched_at: record.launched_at
|
|
20935
|
-
};
|
|
20936
|
-
}
|
|
20937
|
-
if (record.status === "failed") {
|
|
21193
|
+
if (!params.notification_id && leadIds.length === 0) {
|
|
20938
21194
|
return {
|
|
20939
21195
|
error: true,
|
|
20940
|
-
code: "
|
|
20941
|
-
message: "
|
|
20942
|
-
hint: "
|
|
20943
|
-
bulk_id: record.bulk_id,
|
|
20944
|
-
launched_at: record.launched_at
|
|
21196
|
+
code: "ENRICH_STATUS_INPUT_REQUIRED",
|
|
21197
|
+
message: "Pass notification_id and/or lead_ids",
|
|
21198
|
+
hint: "Both are in the leadbay_enrich_titles result. notification_id gives the job counters; lead_ids gives per-lead progress and works even when the notification has been archived."
|
|
20945
21199
|
};
|
|
20946
21200
|
}
|
|
20947
|
-
|
|
20948
|
-
|
|
20949
|
-
|
|
20950
|
-
|
|
20951
|
-
|
|
20952
|
-
|
|
20953
|
-
|
|
20954
|
-
launched_at: record.launched_at
|
|
20955
|
-
};
|
|
20956
|
-
}
|
|
20957
|
-
const notifId = record.notification_id ?? null;
|
|
20958
|
-
if (notifId) {
|
|
20959
|
-
const n = await readNotification2(client, notifId);
|
|
20960
|
-
if (n && n.bulk_progress) {
|
|
20961
|
-
const bp = n.bulk_progress;
|
|
20962
|
-
const inProgress = n.in_progress;
|
|
20963
|
-
let leads2 = [];
|
|
20964
|
-
const fastPartialFailures = [];
|
|
20965
|
-
if (includeContacts) {
|
|
20966
|
-
leads2 = await pMap(record.lead_ids, async (leadId) => {
|
|
20967
|
-
try {
|
|
20968
|
-
const out = await getContacts.execute(client, { leadId });
|
|
20969
|
-
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
20970
|
-
const fe = Array.isArray(out?._fetch_errors) ? out._fetch_errors : [];
|
|
20971
|
-
if (fe.length > 0) {
|
|
20972
|
-
fastPartialFailures.push({
|
|
20973
|
-
lead_id: leadId,
|
|
20974
|
-
code: fe[0]?.code ?? "FETCH_ERROR",
|
|
20975
|
-
...fe[0]?.retry_after !== void 0 ? { retry_after: fe[0].retry_after } : {}
|
|
20976
|
-
});
|
|
20977
|
-
}
|
|
20978
|
-
return { lead_id: leadId, contacts };
|
|
20979
|
-
} catch (err) {
|
|
20980
|
-
fastPartialFailures.push({
|
|
20981
|
-
lead_id: leadId,
|
|
20982
|
-
code: err?.code ?? "UNKNOWN",
|
|
20983
|
-
...err?._meta?.retry_after !== void 0 ? { retry_after: err._meta.retry_after } : {}
|
|
20984
|
-
});
|
|
20985
|
-
return { lead_id: leadId };
|
|
20986
|
-
}
|
|
20987
|
-
}, STATUS_FETCH_CONCURRENCY);
|
|
20988
|
-
} else {
|
|
20989
|
-
leads2 = record.lead_ids.map((id) => ({ lead_id: id }));
|
|
20990
|
-
}
|
|
20991
|
-
ctx?.logger?.info?.(`bulk.status_checked_via_notification bulk_id=${record.bulk_id} notification_id=${notifId} done=${bp.success_count}/${bp.total_count} in_progress=${inProgress} wall_ms=${Date.now() - startMs}`);
|
|
20992
|
-
const isReportRead = !inProgress || includeContacts;
|
|
20993
|
-
const creditsRemaining2 = isReportRead ? await readCreditsRemaining(client, true) : null;
|
|
21201
|
+
let bp = null;
|
|
21202
|
+
let inProgress = null;
|
|
21203
|
+
let launchedAt = null;
|
|
21204
|
+
if (params.notification_id) {
|
|
21205
|
+
const n = await readNotificationById(client, params.notification_id);
|
|
21206
|
+
if (n && inferKind(n) !== "bulk_enrich") {
|
|
21207
|
+
const kind = inferKind(n);
|
|
20994
21208
|
return {
|
|
20995
|
-
|
|
20996
|
-
|
|
20997
|
-
|
|
20998
|
-
|
|
20999
|
-
durability: record.durability,
|
|
21000
|
-
titles: record.titles,
|
|
21001
|
-
email: record.email,
|
|
21002
|
-
phone: record.phone,
|
|
21003
|
-
lens_id: record.lens_id,
|
|
21004
|
-
leads: leads2,
|
|
21005
|
-
overall_progress: {
|
|
21006
|
-
done: bp.success_count + bp.failure_count + bp.quota_hit_count,
|
|
21007
|
-
total: bp.total_count,
|
|
21008
|
-
done_ratio: bp.total_count === 0 ? 0 : (bp.success_count + bp.failure_count + bp.quota_hit_count) / bp.total_count
|
|
21009
|
-
},
|
|
21010
|
-
bulk_progress: bp,
|
|
21011
|
-
in_progress: inProgress,
|
|
21012
|
-
all_done: !inProgress,
|
|
21013
|
-
...fastPartialFailures.length > 0 ? { partial_failures: fastPartialFailures } : {},
|
|
21014
|
-
...isReportRead ? { credits_remaining: creditsRemaining2 } : {},
|
|
21015
|
-
...bp.quota_hit_count > 0 ? {
|
|
21016
|
-
quota_hit_hint: "Some contacts could not be enriched because the AI-credits quota was hit. Top up via leadbay_create_topup_link or wait for the window reset."
|
|
21017
|
-
} : {}
|
|
21209
|
+
error: true,
|
|
21210
|
+
code: "ENRICH_JOB_WRONG_KIND",
|
|
21211
|
+
message: `That notification_id is a ${kind === "bulk_qualify" ? "lead qualification" : kind === "import" ? "file import" : "non-bulk"} notification, not a contact enrichment`,
|
|
21212
|
+
hint: kind === "bulk_qualify" ? "Poll it with leadbay_qualify_status({notification_id}) instead." : kind === "import" ? "Poll it with leadbay_import_status({importIds}) instead \u2014 the import ids came back from the import launch." : "Pass the notification_id returned by leadbay_enrich_titles."
|
|
21018
21213
|
};
|
|
21019
21214
|
}
|
|
21020
|
-
|
|
21021
|
-
|
|
21022
|
-
|
|
21023
|
-
|
|
21024
|
-
|
|
21025
|
-
try {
|
|
21026
|
-
const out = await getContacts.execute(client, { leadId });
|
|
21027
|
-
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
21028
|
-
const wantTitles = new Set((record.titles ?? []).map((t) => t.trim().toLowerCase()));
|
|
21029
|
-
const enrichable = contacts.filter((c) => c && c.enrichment && (wantTitles.size === 0 || typeof c.job_title === "string" && wantTitles.has(c.job_title.trim().toLowerCase())));
|
|
21030
|
-
const channelResolved = (c) => {
|
|
21031
|
-
if (c.enrichment?.done !== true)
|
|
21032
|
-
return false;
|
|
21033
|
-
if (record.email && !c.email)
|
|
21034
|
-
return false;
|
|
21035
|
-
if (record.phone && !c.phone_number)
|
|
21036
|
-
return false;
|
|
21037
|
-
return true;
|
|
21038
|
-
};
|
|
21039
|
-
const done = enrichable.filter(channelResolved).length;
|
|
21040
|
-
const total = enrichable.length;
|
|
21041
|
-
doneSoFar += 1;
|
|
21042
|
-
ctx?.progress?.({
|
|
21043
|
-
progress: doneSoFar,
|
|
21044
|
-
total: totalLeads,
|
|
21045
|
-
message: `Fetched contacts for ${leadId} (${doneSoFar}/${totalLeads})`
|
|
21046
|
-
});
|
|
21047
|
-
return {
|
|
21048
|
-
kind: "ok",
|
|
21049
|
-
lead_id: leadId,
|
|
21050
|
-
done,
|
|
21051
|
-
total,
|
|
21052
|
-
contacts: includeContacts ? contacts : void 0
|
|
21053
|
-
};
|
|
21054
|
-
} catch (err) {
|
|
21055
|
-
doneSoFar += 1;
|
|
21056
|
-
ctx?.progress?.({
|
|
21057
|
-
progress: doneSoFar,
|
|
21058
|
-
total: totalLeads,
|
|
21059
|
-
message: `Fetch failed for ${leadId} (${doneSoFar}/${totalLeads}): ${err?.code ?? "UNKNOWN"}`
|
|
21060
|
-
});
|
|
21215
|
+
if (n) {
|
|
21216
|
+
bp = n.bulk_progress;
|
|
21217
|
+
inProgress = n.in_progress;
|
|
21218
|
+
launchedAt = n.created_at;
|
|
21219
|
+
} else if (leadIds.length === 0) {
|
|
21061
21220
|
return {
|
|
21062
|
-
|
|
21063
|
-
|
|
21064
|
-
|
|
21065
|
-
|
|
21221
|
+
error: true,
|
|
21222
|
+
code: "ENRICH_JOB_NOT_FOUND",
|
|
21223
|
+
message: "That notification_id is not in the recent notification list",
|
|
21224
|
+
hint: "The lookup scans your recent unarchived notifications; an archived job, or one behind many newer ones, will not be found. Re-call with the `lead_ids` the launch returned \u2014 that answers without the notification."
|
|
21066
21225
|
};
|
|
21067
21226
|
}
|
|
21068
|
-
}
|
|
21069
|
-
|
|
21070
|
-
|
|
21071
|
-
|
|
21072
|
-
|
|
21073
|
-
|
|
21074
|
-
|
|
21075
|
-
|
|
21227
|
+
}
|
|
21228
|
+
if (leadIds.length > 0) {
|
|
21229
|
+
const wantTitles = new Set((params.titles ?? []).map((t) => t.trim().toLowerCase()));
|
|
21230
|
+
const channelResolved = (c) => {
|
|
21231
|
+
if (c?.enrichment?.done !== true)
|
|
21232
|
+
return false;
|
|
21233
|
+
if (params.email && !c.email)
|
|
21234
|
+
return false;
|
|
21235
|
+
if (params.phone && !c.phone_number)
|
|
21236
|
+
return false;
|
|
21237
|
+
return true;
|
|
21238
|
+
};
|
|
21239
|
+
let doneSoFar = 0;
|
|
21240
|
+
const totalLeads = leadIds.length;
|
|
21241
|
+
const results = await pMap(leadIds, async (leadId) => {
|
|
21242
|
+
try {
|
|
21243
|
+
const out = await getContacts.execute(client, { leadId });
|
|
21244
|
+
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
21245
|
+
const enrichable = contacts.filter((c) => c && c.enrichment && (wantTitles.size === 0 || typeof c.job_title === "string" && wantTitles.has(c.job_title.trim().toLowerCase())));
|
|
21246
|
+
const fe = Array.isArray(out?._fetch_errors) ? out._fetch_errors : [];
|
|
21247
|
+
doneSoFar += 1;
|
|
21248
|
+
ctx?.progress?.({
|
|
21249
|
+
progress: doneSoFar,
|
|
21250
|
+
total: totalLeads,
|
|
21251
|
+
message: `Fetched contacts for ${leadId} (${doneSoFar}/${totalLeads})`
|
|
21252
|
+
});
|
|
21253
|
+
if (fe.length > 0) {
|
|
21254
|
+
return {
|
|
21255
|
+
kind: "fail",
|
|
21256
|
+
lead_id: leadId,
|
|
21257
|
+
code: fe[0]?.code ?? "FETCH_ERROR",
|
|
21258
|
+
...fe[0]?.retry_after !== void 0 ? { retry_after: fe[0].retry_after } : {}
|
|
21259
|
+
};
|
|
21260
|
+
}
|
|
21261
|
+
return {
|
|
21262
|
+
kind: "ok",
|
|
21263
|
+
lead_id: leadId,
|
|
21264
|
+
done: enrichable.filter(channelResolved).length,
|
|
21265
|
+
total: enrichable.length,
|
|
21266
|
+
...includeContacts ? { contacts } : {}
|
|
21267
|
+
};
|
|
21268
|
+
} catch (err) {
|
|
21269
|
+
doneSoFar += 1;
|
|
21270
|
+
ctx?.progress?.({
|
|
21271
|
+
progress: doneSoFar,
|
|
21272
|
+
total: totalLeads,
|
|
21273
|
+
message: `Fetch failed for ${leadId} (${doneSoFar}/${totalLeads}): ${err?.code ?? "UNKNOWN"}`
|
|
21274
|
+
});
|
|
21275
|
+
return {
|
|
21276
|
+
kind: "fail",
|
|
21277
|
+
lead_id: leadId,
|
|
21278
|
+
code: err?.code ?? "UNKNOWN",
|
|
21279
|
+
...err?._meta?.retry_after !== void 0 ? { retry_after: err._meta.retry_after } : {}
|
|
21280
|
+
};
|
|
21281
|
+
}
|
|
21282
|
+
}, STATUS_FETCH_CONCURRENCY);
|
|
21283
|
+
const leads = [];
|
|
21284
|
+
const partialFailures = [];
|
|
21285
|
+
let totalDone = 0;
|
|
21286
|
+
let totalAll = 0;
|
|
21287
|
+
for (const r of results) {
|
|
21288
|
+
if (r.kind === "fail") {
|
|
21289
|
+
partialFailures.push({
|
|
21290
|
+
lead_id: r.lead_id,
|
|
21291
|
+
code: r.code,
|
|
21292
|
+
...r.retry_after !== void 0 ? { retry_after: r.retry_after } : {}
|
|
21293
|
+
});
|
|
21294
|
+
continue;
|
|
21295
|
+
}
|
|
21296
|
+
leads.push({
|
|
21076
21297
|
lead_id: r.lead_id,
|
|
21077
|
-
|
|
21078
|
-
|
|
21298
|
+
...r.contacts ? { contacts: r.contacts } : {},
|
|
21299
|
+
enrichment_progress: { done: r.done, total: r.total }
|
|
21079
21300
|
});
|
|
21080
|
-
|
|
21301
|
+
totalDone += r.done;
|
|
21302
|
+
totalAll += r.total;
|
|
21081
21303
|
}
|
|
21082
|
-
|
|
21083
|
-
|
|
21084
|
-
|
|
21085
|
-
|
|
21086
|
-
|
|
21087
|
-
|
|
21088
|
-
|
|
21304
|
+
const allDone = totalAll > 0 && totalDone === totalAll && partialFailures.length === 0;
|
|
21305
|
+
ctx?.logger?.info?.(`bulk.status leads=${leadIds.length} done=${totalDone}/${totalAll} wall_ms=${Date.now() - startMs}`);
|
|
21306
|
+
const creditsRemaining2 = allDone ? await readCreditsRemaining(client, true) : null;
|
|
21307
|
+
return {
|
|
21308
|
+
...params.notification_id ? { notification_id: params.notification_id } : {},
|
|
21309
|
+
...launchedAt ? { launched_at: launchedAt } : {},
|
|
21310
|
+
status: allDone ? "complete" : "launched",
|
|
21311
|
+
// Echo what was asked for, so the reply states its own scope.
|
|
21312
|
+
...params.titles ? { titles: params.titles } : {},
|
|
21313
|
+
...params.email !== void 0 ? { email: params.email } : {},
|
|
21314
|
+
...params.phone !== void 0 ? { phone: params.phone } : {},
|
|
21315
|
+
leads,
|
|
21316
|
+
overall_progress: {
|
|
21317
|
+
done: totalDone,
|
|
21318
|
+
total: totalAll,
|
|
21319
|
+
done_ratio: totalAll === 0 ? 0 : totalDone / totalAll
|
|
21320
|
+
},
|
|
21321
|
+
...bp ? { bulk_progress: bp } : {},
|
|
21322
|
+
...inProgress !== null ? { in_progress: inProgress } : {},
|
|
21323
|
+
all_done: allDone,
|
|
21324
|
+
...partialFailures.length > 0 ? { partial_failures: partialFailures } : {},
|
|
21325
|
+
...allDone ? { credits_remaining: creditsRemaining2 } : {},
|
|
21326
|
+
...bp && bp.quota_hit_count > 0 ? {
|
|
21327
|
+
quota_hit_hint: "Some contacts could not be enriched because the AI-credits quota was hit. Top up via leadbay_create_topup_link or wait for the window reset."
|
|
21328
|
+
} : {}
|
|
21329
|
+
};
|
|
21089
21330
|
}
|
|
21090
|
-
|
|
21091
|
-
|
|
21092
|
-
|
|
21093
|
-
|
|
21094
|
-
|
|
21095
|
-
|
|
21096
|
-
|
|
21097
|
-
|
|
21098
|
-
|
|
21099
|
-
creditsRemaining = await readCreditsRemaining(client, true);
|
|
21331
|
+
if (!bp) {
|
|
21332
|
+
return {
|
|
21333
|
+
error: true,
|
|
21334
|
+
code: "ENRICH_JOB_NO_COUNTERS",
|
|
21335
|
+
message: `This enrichment notification carries no per-contact counters; the backend reports it as ${inProgress ? "still running" : "finished"}`,
|
|
21336
|
+
hint: "Re-call with the `lead_ids` returned by leadbay_enrich_titles (plus titles/email/phone) \u2014 that path counts contacts directly and works whether or not the notification has counters.",
|
|
21337
|
+
...inProgress !== null ? { in_progress: inProgress } : {},
|
|
21338
|
+
...launchedAt ? { launched_at: launchedAt } : {}
|
|
21339
|
+
};
|
|
21100
21340
|
}
|
|
21341
|
+
const done = bp.success_count + bp.failure_count + bp.quota_hit_count;
|
|
21342
|
+
const isReportRead = !inProgress;
|
|
21343
|
+
const creditsRemaining = isReportRead ? await readCreditsRemaining(client, true) : null;
|
|
21101
21344
|
return {
|
|
21102
|
-
|
|
21103
|
-
launched_at:
|
|
21104
|
-
status:
|
|
21105
|
-
|
|
21106
|
-
|
|
21107
|
-
|
|
21108
|
-
|
|
21109
|
-
|
|
21110
|
-
|
|
21111
|
-
|
|
21112
|
-
|
|
21113
|
-
|
|
21114
|
-
...
|
|
21345
|
+
notification_id: params.notification_id,
|
|
21346
|
+
launched_at: launchedAt,
|
|
21347
|
+
status: inProgress ? "launched" : "complete",
|
|
21348
|
+
leads: [],
|
|
21349
|
+
overall_progress: {
|
|
21350
|
+
done,
|
|
21351
|
+
total: bp.total_count,
|
|
21352
|
+
done_ratio: bp.total_count === 0 ? 0 : done / bp.total_count
|
|
21353
|
+
},
|
|
21354
|
+
bulk_progress: bp,
|
|
21355
|
+
in_progress: inProgress,
|
|
21356
|
+
all_done: !inProgress,
|
|
21357
|
+
...isReportRead ? { credits_remaining: creditsRemaining } : {},
|
|
21358
|
+
...bp.quota_hit_count > 0 ? {
|
|
21359
|
+
quota_hit_hint: "Some contacts could not be enriched because the AI-credits quota was hit. Top up via leadbay_create_topup_link or wait for the window reset."
|
|
21360
|
+
} : {}
|
|
21115
21361
|
};
|
|
21116
21362
|
}
|
|
21117
21363
|
};
|
|
@@ -23151,7 +23397,7 @@ var sendFeedback = {
|
|
|
23151
23397
|
|
|
23152
23398
|
// ../core/dist/artifact-runtime.generated.js
|
|
23153
23399
|
var ARTIFACT_KIT_VERSION = "0.5.0";
|
|
23154
|
-
var ARTIFACT_RUNTIME = '"use strict";(()=>{var L=Object.defineProperty;var A=(e,r,t)=>r in e?L(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var a=(e,r,t)=>A(e,typeof r!="symbol"?r+"":r,t);var S=`\n:root{\n--color-black:#191919;--color-white:#fff;\n--color-gray-1:#f9f9f9;--color-gray-2:#f0f0f0;--color-gray-3:#e0e0e0;--color-gray-4:#cecece;\n--color-gray-5:#c4c4c4;--color-gray-6:#8d8d8d;--color-gray-7:#787878;--color-gray-8:#646464;\n--color-gray-9:#202020;\n--color-linkedin:#0a66c2;\n--color-blue-background:oklch(0.947 0.029 251);--color-blue-foreground:oklch(0.564 0.181 251);\n--color-green-background:oklch(0.947 0.029 141);--color-green-foreground:oklch(0.564 0.181 141);\n--color-red-background:oklch(0.947 0.029 26);--color-red-foreground:oklch(0.564 0.191 26);\n--color-gold-background:oklch(0.972 0.049 91);--color-gold-foreground:oklch(0.667 0.177 91);\n--color-cherry-background:oklch(0.947 0.029 15);--color-cherry-foreground:oklch(0.44 0.146 15);\n--color-red-like:var(--color-cherry-foreground);\n--lb-font:"Nikkei Maru",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;\n--lb-radius:1rem;--lb-radius-sm:0.625rem;--lb-gap:0.75rem;\n--lb-surface:var(--color-gray-1);--lb-border:var(--color-gray-3);\n--lb-fg:var(--color-black);--lb-muted:var(--color-gray-8);--lb-field:var(--color-white);\n}\n:root[data-theme=dark],:root[data-lb-theme=dark]{\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}\n@media(prefers-color-scheme:dark){:root:not([data-theme=light]):not([data-lb-theme=light]){\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}}\n.lb-card{display:grid;gap:var(--lb-gap);padding:0.875rem;\nbackground-color:var(--lb-surface);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius);corner-shape:squircle;color:var(--lb-fg);\nfont-family:var(--lb-font);\nbox-shadow:0 1rem 2.5rem color-mix(in srgb,var(--color-gray-9) 6%,transparent),\n0 0.125rem 0.5rem color-mix(in srgb,var(--color-gray-9) 4%,transparent)}\n.lb-card-head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--lb-gap)}\n.lb-title{font-size:0.875rem;font-weight:600;line-height:1.25rem;color:var(--lb-fg)}\n.lb-sub{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-row{display:flex;align-items:center;gap:var(--lb-gap);flex-wrap:wrap}\n.lb-stack{display:grid;gap:var(--lb-gap)}\n.lb-select,.lb-input{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;color:var(--lb-fg);\nbackground-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.55rem;min-height:2.125rem}\n.lb-btn{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;font-weight:600;\ncolor:var(--lb-fg);background-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.85rem;min-height:2.125rem;\ncursor:pointer;transition:background-color .15s,border-color .15s,color .15s}\n.lb-btn:hover:not([disabled]){border-color:var(--color-gray-6)}\n.lb-btn:focus-visible,.lb-select:focus-visible,.lb-input:focus-visible{\noutline:2px solid var(--color-blue-foreground);outline-offset:1px}\n.lb-btn[data-lb-state=loading]{opacity:.55;cursor:progress}\n.lb-btn[data-lb-state=success]{background-color:var(--color-green-background);\nborder-color:var(--color-green-foreground);color:var(--color-green-foreground)}\n.lb-btn[data-lb-state=error],.lb-select[data-lb-state=error]{\nbackground-color:var(--color-red-background);border-color:var(--color-red-foreground);\ncolor:var(--color-red-foreground)}\n.lb-btn[data-lb-state=unavailable],.lb-btn[disabled]{opacity:.5;cursor:not-allowed}\n.lb-msg{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-msg[data-tone=error]{color:var(--color-red-foreground)}\n.lb-msg[data-tone=ok]{color:var(--color-green-foreground)}\n.lb-chip{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;\nfont-size:0.75rem;font-weight:600;line-height:1rem;padding:0.125rem 0.5rem;\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;\nbackground-color:var(--color-gray-2);color:var(--lb-muted)}\n.lb-chip[data-status=WANTED]{background-color:var(--color-blue-background);color:var(--color-blue-foreground)}\n.lb-chip[data-status=WON]{background-color:var(--color-green-background);color:var(--color-green-foreground)}\n.lb-chip[data-status=LOST]{background-color:var(--color-red-background);color:var(--color-red-foreground)}\n.lb-chip[data-status=UNWANTED]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chip[data-taste=liked]{background-color:var(--color-cherry-background);color:var(--color-red-like)}\n.lb-chip[data-taste=disliked]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chips{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}\n.lb-chip[hidden]{display:none}\n.lb-table{width:100%;border-collapse:collapse;font-family:var(--lb-font);color:var(--lb-fg)}\n.lb-table th,.lb-table td{text-align:left;padding:0.5rem 0.4rem;\nborder-bottom:1px solid var(--lb-border);vertical-align:middle;font-size:0.8125rem}\n.lb-table th{font-size:0.75rem;font-weight:600;color:var(--lb-muted);\ntext-transform:uppercase;letter-spacing:.04em}\n.lb-link{color:var(--color-blue-foreground);text-decoration:none}\n.lb-link:hover{text-decoration:underline}\n/* Quiet text link out of the artifact. Button-height so it shares the row\'s\n baseline; understated so it never competes with the actions beside it. The\n arrow is a bare diagonal stroke \\u2014 an escape-hatch marker, not an icon that\n asks to be read. */\n.lb-link-out{display:inline-flex;align-items:center;gap:.3rem;\nfont-size:0.75rem;line-height:1rem;min-height:2.125rem;\ncolor:var(--lb-fg);text-decoration:none;opacity:.65;transition:opacity .15s}\n.lb-link-out:hover{opacity:1;text-decoration:underline}\n.lb-link-out:focus-visible{outline:2px solid var(--color-blue-foreground);outline-offset:1px;\nborder-radius:var(--lb-radius-sm)}\n.lb-link-out svg{width:.85em;height:.85em;flex-shrink:0}\n/* Pushes whatever follows it to the right edge of an .lb-row, so a trailing\n link sits on the SAME baseline as the row\'s buttons instead of on its own\n line below them. */\n.lb-spacer{flex:1 1 auto}\n.lb-spinner{display:inline-block;width:.7em;height:.7em;border:2px solid var(--lb-border);\nborder-top-color:var(--color-blue-foreground);border-radius:50%;animation:lb-spin .8s linear infinite}\n@keyframes lb-spin{to{transform:rotate(1turn)}}\n@media(prefers-reduced-motion:reduce){.lb-spinner{animation:none}\n.lb-btn{transition-property:none}}\n`,p="lb-styles";var x="0.5.0",c=class extends Error{constructor(t,o={}){super(t);a(this,"code");a(this,"raw");this.name="LbError",this.code=o.code,this.raw=o.raw}},y=null,f=3e4;function C(){let e=globalThis.cowork;return e&&typeof e.callMcpTool=="function"?(r,t)=>e.callMcpTool(r,t):null}function _(e){if(e&&typeof e=="object"&&"content"in e){let r=e.content;if(Array.isArray(r)&&r[0]&&typeof r[0].text=="string")return r[0].text}return null}function E(e){if(!e||typeof e!="object")return e;let r=e;if(r.isError)throw new c(_(e)??"tool call failed",{raw:e});if("structuredContent"in r&&r.structuredContent!=null)return r.structuredContent;let t=_(e);if(t!=null)try{return JSON.parse(t)}catch{return t}return e}function I(e){return e instanceof Error?e.message:String(e)}function v(e){let r=e instanceof c?e.code:void 0;return{message:I(e),unavailable:r==="unavailable",code:r}}function O(e={}){y=e.call??null,f=e.timeoutMs??3e4}function R(){if(typeof document>"u"||!document.head)return null;let e=document.getElementById(p);if(e)return e;let r=document.createElement("style");return r.id=p,r.textContent=S,document.head.appendChild(r),r}async function T(e,r){if(!f||f<=0)return e;let t,o=new Promise((n,i)=>{t=setTimeout(()=>i(new c(`"${r}" timed out after ${f}ms`,{code:"timeout"})),f)});try{return await Promise.race([e,o])}finally{t&&clearTimeout(t)}}async function l(e,r={}){if(y)return E(await T(Promise.resolve(y(e,r)),e));let t=C();if(!t)throw new c("Leadbay bridge unavailable (window.cowork absent)",{code:"unavailable"});return E(await T(Promise.resolve(t(e,r)),e))}var g=class{constructor(){a(this,"subs",new Set)}subscribe(r){return this.subs.add(r),r(this),()=>this.subs.delete(r)}emit(){for(let r of this.subs)r(this)}};function N(e){return Array.isArray(e)?e.map(r=>r&&typeof r=="object"?r:{value:r,label:String(r)}):[]}var b=class extends g{constructor(t={}){super();a(this,"kind");a(this,"value");a(this,"options",[]);a(this,"loading",!1);a(this,"error",null);a(this,"ready",!1);a(this,"cfg");a(this,"depUnsubs",[]);a(this,"seq",0);this.cfg=t,this.kind=t.kind,this.value=t.value??"";for(let o of t.dependsOn??[]){let n=o.value;this.depUnsubs.push(o.subscribe(()=>{o.value!==n&&(n=o.value,this.cfg.load&&this.load())}))}t.load&&(t.autoLoad??!0)&&this.load()}async load(){if(!this.cfg.load)return;let t=++this.seq;this.loading=!0,this.error=null,this.emit();try{let o=await this.cfg.load();if(t!==this.seq)return;this.options=this.cfg.options?this.cfg.options(o):N(o),this.ready=!0;let n=this.value==null?"":String(this.value);this.options.length&&(n===""||!this.options.some(i=>String(i.value)===n))&&(this.value=this.options[0].value)}catch(o){if(t!==this.seq)return;this.options=[],this.error=v(o)}finally{t===this.seq&&(this.loading=!1,this.emit())}}setValue(t){this.value=t;let o=this.validate();this.error=o?{message:o,unavailable:!1}:null,this.emit()}validate(){return this.cfg.validate?this.cfg.validate(this.value):null}get valid(){return this.validate()==null}reset(){this.value=this.cfg.value??"",this.error=null,this.emit()}dispose(){for(let t of this.depUnsubs)t();this.depUnsubs=[]}};function P(e){if(!e||typeof e!="object")return null;let r=e;if(r.error!==!0)return null;let t=typeof r.message=="string"&&r.message?r.message:"tool call failed",o=typeof r.hint=="string"&&r.hint?` \\u2014 ${r.hint}`:"";return`${t}${o}`}var s=class extends g{constructor(t){super();a(this,"loading",!1);a(this,"error",null);a(this,"lastResult",null);a(this,"cfg");this.cfg=t}async run(){if(this.loading)return;for(let n of this.cfg.fields??[]){let i=n.validate();if(i!=null){this.error={message:i,unavailable:!1},this.emit();return}}if(this.cfg.confirm&&typeof globalThis.confirm=="function"&&!globalThis.confirm(this.cfg.confirm))return;this.loading=!0,this.error=null,this.emit();let t;try{let n=typeof this.cfg.args=="function"?this.cfg.args():this.cfg.args??{};t=await l(this.cfg.tool,n)}catch(n){this.error=v(n),this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}let o=P(t)??this.cfg.checkResult?.(t)??null;if(o!=null){this.error={message:o,unavailable:!1},this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}return this.lastResult=t,this.loading=!1,this.emit(),this.cfg.onSuccess?.(t),t}reset(){this.error=null,this.lastResult=null,this.emit()}},d=class extends g{constructor(t){super();a(this,"data",null);a(this,"loading",!1);a(this,"refreshing",!1);a(this,"error",null);a(this,"done",!1);a(this,"cfg");a(this,"timer",null);a(this,"seq",0);this.cfg=t,(t.autoLoad??!0)&&this.load()}async load(){this.clearTimer();let t=++this.seq;this.data==null?this.loading=!0:this.refreshing=!0,this.error=null,this.emit();try{let n=await this.cfg.load();if(t!==this.seq)return;this.data=n,this.done=this.cfg.until?this.cfg.until(n):!0,this.cfg.pollEvery&&!this.done&&(this.timer=setTimeout(()=>void this.load(),this.cfg.pollEvery))}catch(n){if(t!==this.seq)return;this.error=v(n)}finally{t===this.seq&&(this.loading=!1,this.refreshing=!1,this.emit())}}refresh(){return this.load()}stop(){this.clearTimer()}clearTimer(){this.timer&&(clearTimeout(this.timer),this.timer=null)}},h=class extends g{constructor(t){super();a(this,"items",[]);a(this,"page",0);a(this,"pageSize");a(this,"total",0);a(this,"loading",!1);a(this,"error",null);a(this,"cfg");a(this,"seq",0);this.cfg=t,this.pageSize=t.pageSize??20,(t.autoLoad??!0)&&this.loadPage(0)}async loadPage(t){let o=++this.seq;this.loading=!0,this.error=null,this.emit();try{let n=await this.cfg.load({page:t,pageSize:this.pageSize});if(o!==this.seq)return;this.items=n.items??[],this.total=n.total??this.items.length,this.page=t}catch(n){if(o!==this.seq)return;this.error=v(n)}finally{o===this.seq&&(this.loading=!1,this.emit())}}next(){return this.loadPage(this.page+1)}prev(){return this.loadPage(Math.max(0,this.page-1))}get hasMore(){return(this.page+1)*this.pageSize<this.total}};function M(e,r){let t=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":"ready";e.setAttribute("data-lb-state",t),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")}function D(e,r){let t=()=>r.setValue(e.value);e.addEventListener("change",t);let o=r.subscribe(()=>{M(e,r),e.disabled=r.loading,e.innerHTML="";for(let n of r.options){let i=document.createElement("option");i.value=String(n.value),i.textContent=n.label,e.appendChild(i)}e.value=r.value==null?"":String(r.value)});return()=>{e.removeEventListener("change",t),o()}}function z(e,r){let t=e.type==="checkbox",o=e.tagName==="SELECT"?"change":"input",n=()=>r.setValue(t?e.checked:e.value);e.addEventListener(o,n);let i=r.subscribe(()=>{if(t)e.checked=!!r.value;else{let u=r.value==null?"":String(r.value);e.value!==u&&(e.value=u)}e.setAttribute("data-lb-state",r.error?"error":"ready"),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener(o,n),i()}}function F(e,r){let t=n=>{n.preventDefault(),r.run()};e.addEventListener("click",t);let o=r.subscribe(()=>{let n=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":r.lastResult!=null?"success":"idle";e.setAttribute("data-lb-state",n),"disabled"in e&&(e.disabled=r.loading),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener("click",t),o()}}var q=["STILL_CHASING","COULD_NOT_REACH_STILL_TRYING","INTEREST_VALIDATED_OR_MEETING_PLANED","NOT_INTERESTED_LOST"],k=[{value:"",label:"Default ranking"},{value:"SCORE:DESC",label:"Score \\u2193"},{value:"SCORE:ASC",label:"Score \\u2191"},{value:"NAME:ASC",label:"Name A\\u2192Z"},{value:"NAME:DESC",label:"Name Z\\u2192A"},{value:"SIZE:DESC",label:"Size \\u2193"},{value:"SIZE:ASC",label:"Size \\u2191"},{value:"SECTOR:ASC",label:"Sector A\\u2192Z"},{value:"STATUS:ASC",label:"Status A\\u2192Z"},{value:"CONTACT_COUNT:DESC",label:"Contacts \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:DESC",label:"Last action \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:ASC",label:"Last action \\u2191"},{value:"EPILOGUE_STATUS_SET_AT:DESC",label:"Outcome set \\u2193"},{value:"LIKED:DESC",label:"Liked first"},{value:"DISLIKED:DESC",label:"Disliked first"}];function U(e){let r=String(e??"").trim().toUpperCase(),t=k.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",load:async()=>k.slice()})}var m=[{value:"WANTED",label:"Wanted"},{value:"WON",label:"Won"},{value:"LOST",label:"Lost"},{value:"UNWANTED",label:"Unwanted"}],H={value:"",label:"\\u2014 Not set \\u2014"};function W(e){let r=String(e??"").trim().toUpperCase(),t=m.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",validate:o=>String(o??"")===""?"Pick a status":null,load:async()=>t?m.slice():[H,...m]})}function $(e){let r=()=>{let t=typeof e.leadIds=="function"?e.leadIds():e.leadIds;return Array.isArray(t)?t:e.leadId?[e.leadId]:[]};return new s({tool:"leadbay_set_lead_status",fields:e.date?[e.status,e.date]:[e.status],confirm:e.confirm,args:()=>({lead_ids:r(),status:e.status.value,...e.date&&e.date.value?{status_date:e.date.value}:{},...e.ask?{_triggered_by:e.ask}:{}}),checkResult:t=>{let o=t?.failed;if(!Array.isArray(o)||o.length===0)return null;let n=r().length,i=o[0]?.message??"write rejected";return o.length===n?`Status not applied: ${i}`:`${o.length} of ${n} leads failed: ${i}`}})}function j(e){return new b({kind:"select",load:()=>l("leadbay_list_campaigns",{_triggered_by:e}),options:r=>(r?.campaigns??[]).map(o=>{let n=o?.campaign??o;return n?.id?{value:n.id,label:n.name??n.ai_generated_name??String(n.id)}:null}).filter(o=>o!=null)})}function V(e){return new s({tool:"leadbay_report_outreach",fields:e.note?[e.note]:[],args:()=>({lead_id:e.leadId,...e.status?{epilogue_status:e.status.value}:{},note:e.note?e.note.value:"",verification:{source:"user_confirmed",ref:e.ref??"logged from artifact"},_triggered_by:e.ask})})}function G(e){return new s({tool:"leadbay_add_note",fields:[e.note],args:()=>({leadId:e.leadId,note:e.note.value})})}function Z(e){return new s({tool:"leadbay_like_lead",args:{lead_id:e}})}function Y(e){return new s({tool:"leadbay_dislike_lead",args:{lead_id:e}})}function B(e,r){return new d({autoLoad:!1,load:()=>l("leadbay_account_history",{leadId:e,_triggered_by:r})})}function K(e,r){return new d({autoLoad:!1,load:()=>l("leadbay_research_lead_by_id",{leadId:e,_triggered_by:r})})}function J(e){let r=null;return new d({...e.autoLoad!==void 0?{autoLoad:e.autoLoad}:{},pollEvery:e.pollEvery??4e3,until:t=>!!t?.all_done,load:async()=>{if(!r){let t=await l("leadbay_enrich_titles",{...e.leadIds?{leadIds:e.leadIds}:{},titles:e.titles,...e.email!==void 0?{email:e.email}:{},...e.phone!==void 0?{phone:e.phone}:{},...e.confirm!==void 0?{confirm:e.confirm}:{},_triggered_by:e.ask});if(r=t?.bulk_id??null,!r)return{...t,all_done:!0,no_job:!0}}return l("leadbay_bulk_enrich_status",{bulk_id:r,_triggered_by:e.ask})}})}function Q(e){let r=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:t,pageSize:o})=>{let n=await l("leadbay_pull_leads",{page:t,count:o,...e.lensId?{lensId:e.lensId}:{},...r()?{order:r()}:{},_triggered_by:e.ask}),i=n.leads??[];return{items:i,total:n.pagination?.total??i.length}}})}function X(e){let r=e.source??"followups",t=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:o,pageSize:n})=>{let u=r==="campaign"?await l("leadbay_campaign_call_sheet",{campaign_id:e.campaignId,page:o,count:n,_triggered_by:e.ask}):await l("leadbay_pull_followups",{page:o,count:n,...e.city?{city:e.city}:{},...t()?{order:t()}:{},_triggered_by:e.ask}),w=u.leads??u.items??[];return{items:w,total:u.total_leads??u.pagination?.total??w.length}}})}function ee(e){return new d({load:()=>l("leadbay_team_activity",{weeks:e.weeks??4,_triggered_by:e.ask})})}var re={VERSION:x,configure:O,styles:R,call:l,field:e=>new b(e),action:e=>new s(e),resource:e=>new d(e),list:e=>new h(e),bindSelect:D,bindValue:z,bindAction:F,campaigns:j,outreach:V,note:G,like:Z,dislike:Y,leadStatus:W,setStatus:$,sortOrder:U,leadHistory:B,leadProfile:K,enrichment:J,callList:X,leadList:Q,teamActivity:ee,EPILOGUE_STATUSES:q,LEAD_STATUSES:m,SORT_ORDERS:k};typeof globalThis<"u"&&(globalThis.LeadbayArtifacts=re);})();';
|
|
23400
|
+
var ARTIFACT_RUNTIME = '"use strict";(()=>{var L=Object.defineProperty;var A=(e,r,t)=>r in e?L(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var a=(e,r,t)=>A(e,typeof r!="symbol"?r+"":r,t);var _=`\n:root{\n--color-black:#191919;--color-white:#fff;\n--color-gray-1:#f9f9f9;--color-gray-2:#f0f0f0;--color-gray-3:#e0e0e0;--color-gray-4:#cecece;\n--color-gray-5:#c4c4c4;--color-gray-6:#8d8d8d;--color-gray-7:#787878;--color-gray-8:#646464;\n--color-gray-9:#202020;\n--color-linkedin:#0a66c2;\n--color-blue-background:oklch(0.947 0.029 251);--color-blue-foreground:oklch(0.564 0.181 251);\n--color-green-background:oklch(0.947 0.029 141);--color-green-foreground:oklch(0.564 0.181 141);\n--color-red-background:oklch(0.947 0.029 26);--color-red-foreground:oklch(0.564 0.191 26);\n--color-gold-background:oklch(0.972 0.049 91);--color-gold-foreground:oklch(0.667 0.177 91);\n--color-cherry-background:oklch(0.947 0.029 15);--color-cherry-foreground:oklch(0.44 0.146 15);\n--color-red-like:var(--color-cherry-foreground);\n--lb-font:"Nikkei Maru",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;\n--lb-radius:1rem;--lb-radius-sm:0.625rem;--lb-gap:0.75rem;\n--lb-surface:var(--color-gray-1);--lb-border:var(--color-gray-3);\n--lb-fg:var(--color-black);--lb-muted:var(--color-gray-8);--lb-field:var(--color-white);\n}\n:root[data-theme=dark],:root[data-lb-theme=dark]{\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}\n@media(prefers-color-scheme:dark){:root:not([data-theme=light]):not([data-lb-theme=light]){\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}}\n.lb-card{display:grid;gap:var(--lb-gap);padding:0.875rem;\nbackground-color:var(--lb-surface);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius);corner-shape:squircle;color:var(--lb-fg);\nfont-family:var(--lb-font);\nbox-shadow:0 1rem 2.5rem color-mix(in srgb,var(--color-gray-9) 6%,transparent),\n0 0.125rem 0.5rem color-mix(in srgb,var(--color-gray-9) 4%,transparent)}\n.lb-card-head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--lb-gap)}\n.lb-title{font-size:0.875rem;font-weight:600;line-height:1.25rem;color:var(--lb-fg)}\n.lb-sub{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-row{display:flex;align-items:center;gap:var(--lb-gap);flex-wrap:wrap}\n.lb-stack{display:grid;gap:var(--lb-gap)}\n.lb-select,.lb-input{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;color:var(--lb-fg);\nbackground-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.55rem;min-height:2.125rem}\n.lb-btn{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;font-weight:600;\ncolor:var(--lb-fg);background-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.85rem;min-height:2.125rem;\ncursor:pointer;transition:background-color .15s,border-color .15s,color .15s}\n.lb-btn:hover:not([disabled]){border-color:var(--color-gray-6)}\n.lb-btn:focus-visible,.lb-select:focus-visible,.lb-input:focus-visible{\noutline:2px solid var(--color-blue-foreground);outline-offset:1px}\n.lb-btn[data-lb-state=loading]{opacity:.55;cursor:progress}\n.lb-btn[data-lb-state=success]{background-color:var(--color-green-background);\nborder-color:var(--color-green-foreground);color:var(--color-green-foreground)}\n.lb-btn[data-lb-state=error],.lb-select[data-lb-state=error]{\nbackground-color:var(--color-red-background);border-color:var(--color-red-foreground);\ncolor:var(--color-red-foreground)}\n.lb-btn[data-lb-state=unavailable],.lb-btn[disabled]{opacity:.5;cursor:not-allowed}\n.lb-msg{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-msg[data-tone=error]{color:var(--color-red-foreground)}\n.lb-msg[data-tone=ok]{color:var(--color-green-foreground)}\n.lb-chip{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;\nfont-size:0.75rem;font-weight:600;line-height:1rem;padding:0.125rem 0.5rem;\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;\nbackground-color:var(--color-gray-2);color:var(--lb-muted)}\n.lb-chip[data-status=WANTED]{background-color:var(--color-blue-background);color:var(--color-blue-foreground)}\n.lb-chip[data-status=WON]{background-color:var(--color-green-background);color:var(--color-green-foreground)}\n.lb-chip[data-status=LOST]{background-color:var(--color-red-background);color:var(--color-red-foreground)}\n.lb-chip[data-status=UNWANTED]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chip[data-taste=liked]{background-color:var(--color-cherry-background);color:var(--color-red-like)}\n.lb-chip[data-taste=disliked]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chips{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}\n.lb-chip[hidden]{display:none}\n.lb-table{width:100%;border-collapse:collapse;font-family:var(--lb-font);color:var(--lb-fg)}\n.lb-table th,.lb-table td{text-align:left;padding:0.5rem 0.4rem;\nborder-bottom:1px solid var(--lb-border);vertical-align:middle;font-size:0.8125rem}\n.lb-table th{font-size:0.75rem;font-weight:600;color:var(--lb-muted);\ntext-transform:uppercase;letter-spacing:.04em}\n.lb-link{color:var(--color-blue-foreground);text-decoration:none}\n.lb-link:hover{text-decoration:underline}\n/* Quiet text link out of the artifact. Button-height so it shares the row\'s\n baseline; understated so it never competes with the actions beside it. The\n arrow is a bare diagonal stroke \\u2014 an escape-hatch marker, not an icon that\n asks to be read. */\n.lb-link-out{display:inline-flex;align-items:center;gap:.3rem;\nfont-size:0.75rem;line-height:1rem;min-height:2.125rem;\ncolor:var(--lb-fg);text-decoration:none;opacity:.65;transition:opacity .15s}\n.lb-link-out:hover{opacity:1;text-decoration:underline}\n.lb-link-out:focus-visible{outline:2px solid var(--color-blue-foreground);outline-offset:1px;\nborder-radius:var(--lb-radius-sm)}\n.lb-link-out svg{width:.85em;height:.85em;flex-shrink:0}\n/* Pushes whatever follows it to the right edge of an .lb-row, so a trailing\n link sits on the SAME baseline as the row\'s buttons instead of on its own\n line below them. */\n.lb-spacer{flex:1 1 auto}\n.lb-spinner{display:inline-block;width:.7em;height:.7em;border:2px solid var(--lb-border);\nborder-top-color:var(--color-blue-foreground);border-radius:50%;animation:lb-spin .8s linear infinite}\n@keyframes lb-spin{to{transform:rotate(1turn)}}\n@media(prefers-reduced-motion:reduce){.lb-spinner{animation:none}\n.lb-btn{transition-property:none}}\n`,p="lb-styles";var x="0.5.0",c=class extends Error{constructor(t,o={}){super(t);a(this,"code");a(this,"raw");this.name="LbError",this.code=o.code,this.raw=o.raw}},y=null,f=3e4;function C(){let e=globalThis.cowork;return e&&typeof e.callMcpTool=="function"?(r,t)=>e.callMcpTool(r,t):null}function S(e){if(e&&typeof e=="object"&&"content"in e){let r=e.content;if(Array.isArray(r)&&r[0]&&typeof r[0].text=="string")return r[0].text}return null}function E(e){if(!e||typeof e!="object")return e;let r=e;if(r.isError)throw new c(S(e)??"tool call failed",{raw:e});if("structuredContent"in r&&r.structuredContent!=null)return r.structuredContent;let t=S(e);if(t!=null)try{return JSON.parse(t)}catch{return t}return e}function I(e){return e instanceof Error?e.message:String(e)}function v(e){let r=e instanceof c?e.code:void 0;return{message:I(e),unavailable:r==="unavailable",code:r}}function O(e={}){y=e.call??null,f=e.timeoutMs??3e4}function R(){if(typeof document>"u"||!document.head)return null;let e=document.getElementById(p);if(e)return e;let r=document.createElement("style");return r.id=p,r.textContent=_,document.head.appendChild(r),r}async function T(e,r){if(!f||f<=0)return e;let t,o=new Promise((n,i)=>{t=setTimeout(()=>i(new c(`"${r}" timed out after ${f}ms`,{code:"timeout"})),f)});try{return await Promise.race([e,o])}finally{t&&clearTimeout(t)}}async function l(e,r={}){if(y)return E(await T(Promise.resolve(y(e,r)),e));let t=C();if(!t)throw new c("Leadbay bridge unavailable (window.cowork absent)",{code:"unavailable"});return E(await T(Promise.resolve(t(e,r)),e))}var g=class{constructor(){a(this,"subs",new Set)}subscribe(r){return this.subs.add(r),r(this),()=>this.subs.delete(r)}emit(){for(let r of this.subs)r(this)}};function N(e){return Array.isArray(e)?e.map(r=>r&&typeof r=="object"?r:{value:r,label:String(r)}):[]}var b=class extends g{constructor(t={}){super();a(this,"kind");a(this,"value");a(this,"options",[]);a(this,"loading",!1);a(this,"error",null);a(this,"ready",!1);a(this,"cfg");a(this,"depUnsubs",[]);a(this,"seq",0);this.cfg=t,this.kind=t.kind,this.value=t.value??"";for(let o of t.dependsOn??[]){let n=o.value;this.depUnsubs.push(o.subscribe(()=>{o.value!==n&&(n=o.value,this.cfg.load&&this.load())}))}t.load&&(t.autoLoad??!0)&&this.load()}async load(){if(!this.cfg.load)return;let t=++this.seq;this.loading=!0,this.error=null,this.emit();try{let o=await this.cfg.load();if(t!==this.seq)return;this.options=this.cfg.options?this.cfg.options(o):N(o),this.ready=!0;let n=this.value==null?"":String(this.value);this.options.length&&(n===""||!this.options.some(i=>String(i.value)===n))&&(this.value=this.options[0].value)}catch(o){if(t!==this.seq)return;this.options=[],this.error=v(o)}finally{t===this.seq&&(this.loading=!1,this.emit())}}setValue(t){this.value=t;let o=this.validate();this.error=o?{message:o,unavailable:!1}:null,this.emit()}validate(){return this.cfg.validate?this.cfg.validate(this.value):null}get valid(){return this.validate()==null}reset(){this.value=this.cfg.value??"",this.error=null,this.emit()}dispose(){for(let t of this.depUnsubs)t();this.depUnsubs=[]}};function P(e){if(!e||typeof e!="object")return null;let r=e;if(r.error!==!0)return null;let t=typeof r.message=="string"&&r.message?r.message:"tool call failed",o=typeof r.hint=="string"&&r.hint?` \\u2014 ${r.hint}`:"";return`${t}${o}`}var s=class extends g{constructor(t){super();a(this,"loading",!1);a(this,"error",null);a(this,"lastResult",null);a(this,"cfg");this.cfg=t}async run(){if(this.loading)return;for(let n of this.cfg.fields??[]){let i=n.validate();if(i!=null){this.error={message:i,unavailable:!1},this.emit();return}}if(this.cfg.confirm&&typeof globalThis.confirm=="function"&&!globalThis.confirm(this.cfg.confirm))return;this.loading=!0,this.error=null,this.emit();let t;try{let n=typeof this.cfg.args=="function"?this.cfg.args():this.cfg.args??{};t=await l(this.cfg.tool,n)}catch(n){this.error=v(n),this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}let o=P(t)??this.cfg.checkResult?.(t)??null;if(o!=null){this.error={message:o,unavailable:!1},this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}return this.lastResult=t,this.loading=!1,this.emit(),this.cfg.onSuccess?.(t),t}reset(){this.error=null,this.lastResult=null,this.emit()}},u=class extends g{constructor(t){super();a(this,"data",null);a(this,"loading",!1);a(this,"refreshing",!1);a(this,"error",null);a(this,"done",!1);a(this,"cfg");a(this,"timer",null);a(this,"seq",0);this.cfg=t,(t.autoLoad??!0)&&this.load()}async load(){this.clearTimer();let t=++this.seq;this.data==null?this.loading=!0:this.refreshing=!0,this.error=null,this.emit();try{let n=await this.cfg.load();if(t!==this.seq)return;this.data=n,this.done=this.cfg.until?this.cfg.until(n):!0,this.cfg.pollEvery&&!this.done&&(this.timer=setTimeout(()=>void this.load(),this.cfg.pollEvery))}catch(n){if(t!==this.seq)return;this.error=v(n)}finally{t===this.seq&&(this.loading=!1,this.refreshing=!1,this.emit())}}refresh(){return this.load()}stop(){this.clearTimer()}clearTimer(){this.timer&&(clearTimeout(this.timer),this.timer=null)}},h=class extends g{constructor(t){super();a(this,"items",[]);a(this,"page",0);a(this,"pageSize");a(this,"total",0);a(this,"loading",!1);a(this,"error",null);a(this,"cfg");a(this,"seq",0);this.cfg=t,this.pageSize=t.pageSize??20,(t.autoLoad??!0)&&this.loadPage(0)}async loadPage(t){let o=++this.seq;this.loading=!0,this.error=null,this.emit();try{let n=await this.cfg.load({page:t,pageSize:this.pageSize});if(o!==this.seq)return;this.items=n.items??[],this.total=n.total??this.items.length,this.page=t}catch(n){if(o!==this.seq)return;this.error=v(n)}finally{o===this.seq&&(this.loading=!1,this.emit())}}next(){return this.loadPage(this.page+1)}prev(){return this.loadPage(Math.max(0,this.page-1))}get hasMore(){return(this.page+1)*this.pageSize<this.total}};function M(e,r){let t=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":"ready";e.setAttribute("data-lb-state",t),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")}function D(e,r){let t=()=>r.setValue(e.value);e.addEventListener("change",t);let o=r.subscribe(()=>{M(e,r),e.disabled=r.loading,e.innerHTML="";for(let n of r.options){let i=document.createElement("option");i.value=String(n.value),i.textContent=n.label,e.appendChild(i)}e.value=r.value==null?"":String(r.value)});return()=>{e.removeEventListener("change",t),o()}}function z(e,r){let t=e.type==="checkbox",o=e.tagName==="SELECT"?"change":"input",n=()=>r.setValue(t?e.checked:e.value);e.addEventListener(o,n);let i=r.subscribe(()=>{if(t)e.checked=!!r.value;else{let d=r.value==null?"":String(r.value);e.value!==d&&(e.value=d)}e.setAttribute("data-lb-state",r.error?"error":"ready"),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener(o,n),i()}}function F(e,r){let t=n=>{n.preventDefault(),r.run()};e.addEventListener("click",t);let o=r.subscribe(()=>{let n=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":r.lastResult!=null?"success":"idle";e.setAttribute("data-lb-state",n),"disabled"in e&&(e.disabled=r.loading),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener("click",t),o()}}var q=["STILL_CHASING","COULD_NOT_REACH_STILL_TRYING","INTEREST_VALIDATED_OR_MEETING_PLANED","NOT_INTERESTED_LOST"],k=[{value:"",label:"Default ranking"},{value:"SCORE:DESC",label:"Score \\u2193"},{value:"SCORE:ASC",label:"Score \\u2191"},{value:"NAME:ASC",label:"Name A\\u2192Z"},{value:"NAME:DESC",label:"Name Z\\u2192A"},{value:"SIZE:DESC",label:"Size \\u2193"},{value:"SIZE:ASC",label:"Size \\u2191"},{value:"SECTOR:ASC",label:"Sector A\\u2192Z"},{value:"STATUS:ASC",label:"Status A\\u2192Z"},{value:"CONTACT_COUNT:DESC",label:"Contacts \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:DESC",label:"Last action \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:ASC",label:"Last action \\u2191"},{value:"EPILOGUE_STATUS_SET_AT:DESC",label:"Outcome set \\u2193"},{value:"LIKED:DESC",label:"Liked first"},{value:"DISLIKED:DESC",label:"Disliked first"}];function U(e){let r=String(e??"").trim().toUpperCase(),t=k.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",load:async()=>k.slice()})}var m=[{value:"WANTED",label:"Wanted"},{value:"WON",label:"Won"},{value:"LOST",label:"Lost"},{value:"UNWANTED",label:"Unwanted"}],H={value:"",label:"\\u2014 Not set \\u2014"};function j(e){let r=String(e??"").trim().toUpperCase(),t=m.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",validate:o=>String(o??"")===""?"Pick a status":null,load:async()=>t?m.slice():[H,...m]})}function W(e){let r=()=>{let t=typeof e.leadIds=="function"?e.leadIds():e.leadIds;return Array.isArray(t)?t:e.leadId?[e.leadId]:[]};return new s({tool:"leadbay_set_lead_status",fields:e.date?[e.status,e.date]:[e.status],confirm:e.confirm,args:()=>({lead_ids:r(),status:e.status.value,...e.date&&e.date.value?{status_date:e.date.value}:{},...e.ask?{_triggered_by:e.ask}:{}}),checkResult:t=>{let o=t?.failed;if(!Array.isArray(o)||o.length===0)return null;let n=r().length,i=o[0]?.message??"write rejected";return o.length===n?`Status not applied: ${i}`:`${o.length} of ${n} leads failed: ${i}`}})}function $(e){return new b({kind:"select",load:()=>l("leadbay_list_campaigns",{_triggered_by:e}),options:r=>(r?.campaigns??[]).map(o=>{let n=o?.campaign??o;return n?.id?{value:n.id,label:n.name??n.ai_generated_name??String(n.id)}:null}).filter(o=>o!=null)})}function V(e){return new s({tool:"leadbay_report_outreach",fields:e.note?[e.note]:[],args:()=>({lead_id:e.leadId,...e.status?{epilogue_status:e.status.value}:{},note:e.note?e.note.value:"",verification:{source:"user_confirmed",ref:e.ref??"logged from artifact"},_triggered_by:e.ask})})}function G(e){return new s({tool:"leadbay_add_note",fields:[e.note],args:()=>({leadId:e.leadId,note:e.note.value})})}function Z(e){return new s({tool:"leadbay_like_lead",args:{lead_id:e}})}function Y(e){return new s({tool:"leadbay_dislike_lead",args:{lead_id:e}})}function B(e,r){return new u({autoLoad:!1,load:()=>l("leadbay_account_history",{leadId:e,_triggered_by:r})})}function K(e,r){return new u({autoLoad:!1,load:()=>l("leadbay_research_lead_by_id",{leadId:e,_triggered_by:r})})}function J(e){let r=null;return new u({...e.autoLoad!==void 0?{autoLoad:e.autoLoad}:{},pollEvery:e.pollEvery??4e3,until:t=>!!t?.all_done,load:async()=>{if(!r){let t=await l("leadbay_enrich_titles",{...e.leadIds?{leadIds:e.leadIds}:{},titles:e.titles,...e.email!==void 0?{email:e.email}:{},...e.phone!==void 0?{phone:e.phone}:{},...e.confirm!==void 0?{confirm:e.confirm}:{},_triggered_by:e.ask}),o=Array.isArray(t?.lead_ids)?t.lead_ids:[],n=t?.notification_id??null;if(r=n||o.length>0?{notification_id:n,lead_ids:o}:null,!r)return{...t,all_done:!0,no_job:!0}}return l("leadbay_bulk_enrich_status",{...r.notification_id?{notification_id:r.notification_id}:{},...r.lead_ids.length>0?{lead_ids:r.lead_ids}:{},...e.titles?{titles:e.titles}:{},...e.email!==void 0?{email:e.email}:{},...e.phone!==void 0?{phone:e.phone}:{},_triggered_by:e.ask})}})}function Q(e){let r=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:t,pageSize:o})=>{let n=await l("leadbay_pull_leads",{page:t,count:o,...e.lensId?{lensId:e.lensId}:{},...r()?{order:r()}:{},_triggered_by:e.ask}),i=n.leads??[];return{items:i,total:n.pagination?.total??i.length}}})}function X(e){let r=e.source??"followups",t=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:o,pageSize:n})=>{let d=r==="campaign"?await l("leadbay_campaign_call_sheet",{campaign_id:e.campaignId,page:o,count:n,_triggered_by:e.ask}):await l("leadbay_pull_followups",{page:o,count:n,...e.city?{city:e.city}:{},...t()?{order:t()}:{},_triggered_by:e.ask}),w=d.leads??d.items??[];return{items:w,total:d.total_leads??d.pagination?.total??w.length}}})}function ee(e){return new u({load:()=>l("leadbay_team_activity",{weeks:e.weeks??4,_triggered_by:e.ask})})}var re={VERSION:x,configure:O,styles:R,call:l,field:e=>new b(e),action:e=>new s(e),resource:e=>new u(e),list:e=>new h(e),bindSelect:D,bindValue:z,bindAction:F,campaigns:$,outreach:V,note:G,like:Z,dislike:Y,leadStatus:j,setStatus:W,sortOrder:U,leadHistory:B,leadProfile:K,enrichment:J,callList:X,leadList:Q,teamActivity:ee,EPILOGUE_STATUSES:q,LEAD_STATUSES:m,SORT_ORDERS:k};typeof globalThis<"u"&&(globalThis.LeadbayArtifacts=re);})();';
|
|
23155
23401
|
var ARTIFACT_USAGE_GUIDE = '# Leadbay Artifact Kit \u2014 headless domain components\n\nYou are building a single-file HTML **artifact** the user runs inside cowork. This\nkit gives you **headless view-models** that own a control\'s whole data lifecycle \u2014\nload/populate from a Leadbay call, hold value/state, poll, validate, and\nencapsulate the API call + business rules. **You own 100% of markup/layout/style.**\nThe library renders nothing. Inline the runtime once as a `<script>`; it exposes\none global `window.LeadbayArtifacts` (call it `lb`). Vanilla, no React, no build.\n\nPass every tool you use as the artifact\'s `mcp_tools` so the host permits it.\n\n## Two layers\n\n**Primitives** (generic):\n- `lb.field({ load, options, value, validate, dependsOn })` \u2014 a value + optionally\n API-populated options. `.value/.setValue/.options/.loading/.error/.valid/.subscribe`.\n- `lb.action({ tool, args, fields, confirm, onSuccess, onError })` \u2014 a write/submit.\n `.run()/.loading/.error/.lastResult/.subscribe`.\n- `lb.resource({ load, pollEvery?, until?, autoLoad? })` \u2014 one read that may change:\n load-on-click or poll-until-`until`. `.data/.loading/.refreshing/.error/.done/.load()/.refresh()/.stop()/.subscribe`.\n- `lb.list({ load, pageSize })` \u2014 paginated rows. `.items/.page/.total/.loading/.loadPage(n)/.next()/.prev()/.hasMore/.subscribe`.\n\n`.error` is `{ message, unavailable } | null`. `subscribe(cb)` fires immediately\nthen on every change \u2014 render your own DOM from it.\n\n**Domain components** (pre-wired \u2014 bake in the tool name, arg shape, and footguns):\n\n| Call | Returns | For |\n|---|---|---|\n| `lb.campaigns(ask)` | field | a campaign `<select>`, options from `leadbay_list_campaigns` |\n| `lb.outreach({leadId, ask, status?, note?})` | action | log a call \u2192 `report_outreach` (verification + `_triggered_by` baked in) |\n| `lb.note({leadId, note})` | action | add a note \u2192 `add_note` |\n| `lb.like(leadId)` / `lb.dislike(leadId)` | action | taste signal |\n| `lb.leadStatus(current?)` | field | a status `<select>` (Wanted/Won/Lost/Unwanted) |\n| `lb.setStatus({leadId or leadIds, status, date?, ask})` | action | write the org CRM status \u2192 `set_lead_status` |\n| `lb.leadHistory(leadId, ask)` | resource (lazy) | notes + activities + engagement \u2192 `account_history` |\n| `lb.leadProfile(leadId, ask)` | resource (lazy) | full lead profile \u2192 `research_lead_by_id` |\n| `lb.sortOrder(current?)` | field | a sort `<select>` mirroring the app\'s TableSort |\n| `lb.leadList({lensId?, order?, ask})` | list | a sortable Discover batch \u2192 `pull_leads` |\n| `lb.callList({source:\'followups\'\\|\'campaign\', campaignId?, city?, ask})` | list | a cold-call list (Monitor or a campaign) |\n| `lb.enrichment({leadIds, titles, ask, pollEvery?})` | resource (polling) | launch + watch contact enrichment |\n| `lb.teamActivity({weeks, ask})` | resource | manager leaderboard + activity trend \u2192 `leadbay_team_activity` |\n\n`lb.EPILOGUE_STATUSES` = the 4 disposition values\n(`STILL_CHASING`, `COULD_NOT_REACH_STILL_TRYING`, `INTEREST_VALIDATED_OR_MEETING_PLANED`, `NOT_INTERESTED_LOST`).\n`lb.LEAD_STATUSES` = the 4 org CRM statuses as `{value,label}` (`WANTED`, `WON`, `LOST`, `UNWANTED`).\n`lb.SORT_ORDERS` = the sort options as `{value,label}`; values are the backend `FIELD:ASC|DESC` enum.\n\n**Sorting is a SERVER concern.** `lb.leadList` and `lb.callList` take an `order`\n(a `lb.sortOrder()` field or a literal) and send it upstream; the backend sorts\nthe whole lens / Monitor and returns the requested page of that. Never re-sort\nrows in the browser \u2014 you would be reordering one page of a larger set, showing\nleads that do not belong at that position. The empty value means "no order\nparam", i.e. the tab\'s own ranking, which is the right default. Changing the\nsort should reset to page 0. Campaign call sheets cannot sort:\n`leadbay_campaign_call_sheet` has no `order` param, and `lb.callList` drops it\nfor that source rather than sending something the tool would reject.\n\n**Two different systems.** Epilogue = how one outreach attempt went (drives\nfollow-up ranking). Lead status = the commercial outcome, org-wide \u2014 the same\nfield the website\'s status selector writes. A won deal is a LEAD STATUS;\n"she didn\'t pick up" is an EPILOGUE. Setting one never sets the other, so when\nthe user reports both in one breath, fire both actions.\n\n**Binding sugar** (optional; binds a view-model to YOUR native element, no style):\n`lb.bindSelect(selectEl, field)` (populates options + value), `lb.bindValue(inputEl, field)`,\n`lb.bindAction(buttonEl, action)`. They set `data-lb-state`\n(`ready|loading|error|success|unavailable`) + `data-lb-error` on your element as\nstyling hooks. For lists/resources, use `.subscribe()` and render yourself.\n\n`ask` is the user\'s request this artifact serves \u2014 it becomes `_triggered_by`.\n\n## The skin (optional) \u2014 `lb.styles()`\n\nCall it once and you get a small `lb-*` stylesheet, so every artifact you build\nshares one visual language instead of re-inventing padding and colours. It is\n**opt-in**: skip it and you get exactly the unstyled HTML you wrote. It injects\nno markup and never touches your `class` attributes.\n\n```js\nlb.styles(); // idempotent \u2014 safe to call per row\n```\n\n| Class | For |\n|---|---|\n| `lb-card` / `lb-card-head` / `lb-title` / `lb-sub` | a lead card + its header |\n| `lb-row` / `lb-stack` / `lb-spacer` | control row / vertical spacing / flex filler that right-aligns what follows |\n| `lb-link-out` | quiet external link (icon inherits currentColor) \u2014 "Open in Leadbay" |\n| `lb-select` / `lb-input` / `lb-btn` | form controls (state-aware, see below) |\n| `lb-msg` (`data-tone="error\\|ok"`) | inline feedback |\n| `lb-chip` (`data-status="WON\\|LOST"`) | a status pill |\n| `lb-table` | leads table |\n| `lb-spinner` | inline busy indicator |\n\nControls react to the `data-lb-state` the bind helpers already set \u2014 a bound\n`lb-btn` dims while loading, goes green on success, red on error, all with no\nextra CSS from you.\n\nThe palette is the **product design system**, ported from\n`frontend/packages/style/color.css` \u2014 same `--color-gray-1\u20269` ramp, same\nsemantic `--color-{green,red,blue,gold}-{background,foreground}` pairs, same\n`1rem` / `0.625rem` radii and `corner-shape: squircle` as the app\'s components.\nAn artifact therefore looks like Leadbay, not like a generic page.\n\nUse the tokens rather than hardcoded colours \u2014 the same rule the style package\nenforces. Re-theme by overriding them; don\'t fight specificity:\n\n```css\n:root { --lb-surface: var(--color-gray-2); --lb-radius: 0.5rem; }\n```\n\nDark mode works two ways: `data-theme="dark"` on `<html>` (the frontend\'s own\nhook) **and** `prefers-color-scheme`, because an artifact renders inside a host\nwhose theme attribute it cannot set. Never hardcode a light background over the\nskin.\n\nThe product face is `Nikkei Maru`; the stack names it first and falls back to\nthe system UI font. Do **not** add an `@font-face` \u2014 artifacts are inline-only\nand a remote font URL will silently fail.\n\n## What every lead card MUST carry\n\nA card is the artifact form of the `pull_leads` table, and it inherits that\ntable\'s rules. A card with a name and a button is not enough: the rep cannot\ntell *why* this lead is on screen. Four lines, in this order.\n\n```html\n<div class="lb-card">\n <div class="lb-card-head">\n <span class="lb-title"></span> <!-- 1. company -->\n <span class="lb-chips"> <!-- 2. state -->\n <span class="lb-chip" data-taste hidden></span>\n <span class="lb-chip" data-status hidden></span>\n </span>\n </div>\n <div class="lb-sub"></div> <!-- 3. firmographics -->\n <div class="lb-sub" data-why></div> <!-- 4. why it fits -->\n <div class="lb-row"><!-- actions --></div>\n</div>\n```\n\n1. **Company** \u2014 `name`, linked to `website` (prefix `https://` on a bare host).\n Never render the numeric `score`; use the `\u25B0\u2756\u25B1` bar if you want the signal.\n\n Also give every card an **Open in Leadbay** link to the lead\'s panel in the\n product. Put it at the **right-hand end of the card\'s last action row** \u2014\n same row as the buttons, pushed right by an `lb-spacer`, not on a line of\n its own. Style it `lb-link-out`: quiet text plus a plain arrow-up-right,\n never a filled button. It is an escape hatch, not a call to action.\n\n ```html\n <div class="lb-row">\n <button class="lb-btn">Like</button>\n <button class="lb-btn">Set status</button>\n <span class="lb-spacer"></span> <!-- pushes the link right -->\n <a class="lb-link-out" data-k="open" target="_blank" rel="noopener">\n Open in Leadbay\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"\n stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">\n <line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/>\n </svg>\n </a>\n </div>\n ```\n\n Keep the arrow a bare diagonal stroke \u2014 the text already says where the link\n goes, so the glyph only has to mark "leaves this page". Mark the `<svg>`\n `aria-hidden="true"`: it is decorative, and the link text is the accessible\n name.\n **Pick the view the lead actually lives in** \u2014 the URL is\n `/app/<view>?lead=<uuid>`, and the three views are `discover`, `monitor`,\n `campaign`. Landing a Monitor lead on Discover drops the rep into a list\n that does not contain it:\n\n ```js\n function leadUrl(lead, campaignId) {\n const id = encodeURIComponent(lead.id);\n // A campaign card carries TWO params \u2014 the campaign selects the list, the\n // lead opens the panel inside it. Campaign wins even when in_monitor is\n // also true, because that is the list the rep is looking at.\n if (campaignId) {\n return `https://leadbay.app/app/campaign?campaign=${encodeURIComponent(campaignId)}&lead=${id}`;\n }\n const view = lead.in_monitor ? "monitor" : "discover";\n return `https://leadbay.app/app/${view}?lead=${id}`;\n }\n openEl.href = leadUrl(lead, campaignId);\n ```\n\n `in_monitor` / `in_discover` are booleans on the `pull_followups` payload \u2014\n every follow-up carries `in_monitor: true`, so a call sheet must link to\n `monitor`. `pull_leads` omits both flags entirely; its leads are the Discover\n batch by definition, so `discover` is the default. A campaign card\n (`lb.callList({source:"campaign", campaignId})`) needs `?campaign=<id>&lead=<id>`\n \u2014 the param names are `CAMPAIGN_QUERY_PARAM` and `LEAD_QUERY_PARAM`, and the\n app\'s own `useLeadPanel` preserves whatever params are already set, so the\n two coexist by design. Omitting `campaign=` opens an empty campaign view.\n\n Inline the glyph as SVG rather than an emoji or `\u2197` \u2014 it inherits\n `currentColor` and scales with the text, so it stays legible in both themes.\n `?lead=<uuid>` is the real deep-link (`LEAD_QUERY_PARAM` in the web app, read\n on load; the panel is an overlay, so the view choice only decides what sits\n behind it). This is the ONE place a card may use `lead.id`: as a link target,\n never as visible text.\n2. **State chips** \u2014 taste (`data-taste`) and CRM status (`data-status`) are\n INDEPENDENT axes; render both, hide the empty one. Never collapse to one chip.\n3. **Firmographics** \u2014 sector of activity first, then city, then size, then the\n contact. `sector_id` is a RAW ID (`"5136"`), not a label: resolve it via\n `leadbay_list_sectors` (1346 rows \u2014 fetch once, cache, never inline the lot)\n or omit it. Never print the raw id.\n\n **Always show whether the lead is reachable \u2014 and never merge the person\n with the company\'s switchboard.** These are two separate lines:\n\n ```js\n // WHO \u2014 recommended_contact. Name, and job_title ONLY when present; on list\n // payloads it is usually null, and inventing one is worse than omitting it.\n const rc = lead.recommended_contact;\n const who = rc ? [rc.first_name, rc.last_name].filter(Boolean).join(" ") : null;\n const whoLine = who ? who + (rc.job_title ? " \xB7 " + rc.job_title : "") : "No named contact";\n\n // HOW \u2014 company-level channels. `phone_numbers` and `email` belong to the\n // COMPANY, not to `recommended_contact`. Rendering "Jean \xB7 \u260E 0123\u2026" claims a\n // direct line that does not exist; it is the switchboard.\n const phone = (lead.phone_numbers || [])[0] || null;\n // The API returns the STRING "null" for a missing email \u2014 guard for it or\n // you will print the word "null" as an address.\n const email = lead.email && lead.email !== "null" ? lead.email : null;\n const howLine = [phone && "\u260E " + phone, email && "\u2709 " + email].filter(Boolean)\n .join(" \xB7 ") || "No direct channel \u2014 enrich to reveal";\n ```\n\n ```html\n <div class="lb-sub">Sector \xB7 City \xB7 Size</div>\n <div class="lb-sub">\u{1F464} Jean-Fran\xE7ois Froemer \xB7 G\xE9rant</div> <!-- WHO -->\n <div class="lb-sub">\u{1F3E2} \u260E 01 23 45 67 89</div> <!-- HOW: company -->\n ```\n\n Label the channel line as the **company\'s**, so a rep reading fast cannot\n mistake it for a direct line. A per-contact email or phone exists only after\n enrichment \u2014 `research_lead_by_id` exposes it as `contacts.reachable[]`, and\n `_meta.has_reachable_contact` is the authoritative flag. The list payloads\n carry neither, so a card built from `pull_leads` / `pull_followups` can only\n ever show company channels. Say "enrich to reveal" rather than implying the\n contact is callable.\n\n Two things that look like reachability and are not: a `linkedin_page` alone\n (the rep cannot message a URL without leaving the artifact \u2014 same rule\n `research_lead_by_id` applies), and `contacts_count > 0` (it counts known\n people, not people you can contact; a lead can show 2518 contacts and zero\n channels). `pull_followups` carries `has_phone` as a ready-made boolean;\n `pull_leads` omits it, so derive from `phone_numbers` there.\n4. **Why it fits** \u2014 one sentence, \u226420 words. Walk this chain and stop at the\n first hit:\n\n 1. `short_description`\n 2. `description` (longer; only on `research_lead_by_id` /\n `research_lead_by_name_fuzzy` \u2014 the trim payloads omit it)\n 3. top 2 `tags[].display_name`\n 4. `qualification_summary.best_response_excerpt`, trimmed to one sentence\n 5. `keywords`, first 3, joined with ` \xB7 `\n 6. the resolved sector label \u2014 better than nothing, and if step 3 already\n printed the sector on the firmographics line, skip to step 7\n 7. the literal *"No description yet \u2014 run qualification to generate one"*\n\n Never leave this line blank: a silent gap reads as a rendering bug, whereas\n the fallback tells the rep the data is missing and what fixes it.\n\n **The two list payloads are complementary, so the chain must span both.**\n `pull_leads` returns `short_description` on every lead but no `sector_id`;\n `pull_followups` returns `sector_id` but no `short_description` at all. A\n card fed by one will fall through to a different step than the same card fed\n by the other \u2014 that is expected, not a bug. Never call\n `research_lead_by_id` per row just to fill this line: it is one request per\n lead. Fetch it lazily when the rep expands a card.\n\n**Never show** on a card: `id`, `sector_id`, `location.pos`, `location.country`\n(unless city and state are both missing), `is_hq`, `*_in_progress`,\n`highlighted_fields`, `custom_fields`, `stale_at`, `deal_insights`,\n`need_attention*`, any count that is 0, any value that is the string `"null"`.\n\n**Minimum actions.** A card that only displays is a table row that costs more \u2014\nif you are not wiring an action, render the markdown table instead. Wire at\nleast one write, and prefer the set the rep actually needs:\n\n| Card is for | Wire |\n|---|---|\n| triage a discovery batch | `lb.like` / `lb.dislike` + `lb.setStatus` |\n| working a call list | `lb.outreach` (gated on a note) + `lb.leadHistory` |\n| pipeline review | `lb.setStatus` + `lb.note` |\n\nAlways render the `.error` branch of every view-model \u2014 a control that cannot\nreach the host must say so, not sit silent.\n\n## Recipe: cold-call sheet (one row per lead)\n\n```js\nconst lb = window.LeadbayArtifacts; lb.configure();\nconst ASK = "<the user\'s request>";\n\nconst list = lb.callList({ source: "campaign", campaignId: CID, ask: ASK });\nlist.subscribe((l) => renderRows(l.items, l.loading)); // your render\n\n// per lead row (call when you build a row):\nfunction wireRow(lead, els) {\n const status = lb.field({ value: "STILL_CHASING" }); // static-enum <select>\n const note = lb.field({ validate: (v) => (v && v.trim() ? null : "Add a note") });\n lb.bindValue(els.status, status);\n lb.bindValue(els.note, note);\n lb.bindAction(els.log, lb.outreach({ leadId: lead.id, ask: ASK, status, note }));\n lb.bindAction(els.like, lb.like(lead.id));\n\n const history = lb.leadHistory(lead.id, ASK); // lazy\n history.subscribe((h) => renderHistory(els.history, h));\n els.expand.onclick = () => history.load(); // load on click\n}\n```\n\n## Recipe: lead-status dropdown (Wanted / Won / Lost)\n\nThe org-wide CRM status, as a `<select>` + Apply button. You write the markup;\n`lb.leadStatus` fills the options and holds the value, `lb.setStatus` does the write.\n\n```html\n<div class="lb-card">\n <div class="lb-card-head">\n <span class="lb-title">Acme Corp</span>\n <span class="lb-chips">\n <span id="taste" class="lb-chip" data-taste="liked">Liked</span>\n <span id="crm" class="lb-chip" data-status="WANTED">Wanted</span>\n </span>\n </div>\n <div class="lb-row">\n <select id="st" class="lb-select"></select>\n <button id="go" class="lb-btn">Apply</button>\n <span id="msg" class="lb-msg"></span>\n </div>\n</div>\n```\n\n**Two badges, never one.** Taste (`liked`/`disliked`, from `lb.like`/`lb.dislike`)\nand CRM status (`WANTED`/`WON`/`LOST`/`UNWANTED`, from `lb.setStatus`) are\nindependent axes \u2014 a lead can be liked *and* lost. Collapsing them into a single\nchip destroys information: the rep can no longer see that a lead they liked went\nnowhere. Render `data-taste` and `data-status` as separate chips inside\n`lb-chips`, and hide the one that has no value rather than reusing it.\n\n```js\nlb.styles(); // once per artifact \u2014 see below\n\nconst status = lb.leadStatus(lead.org_lead_status); // seed with the CURRENT value\nconst save = lb.setStatus({ leadId: lead.id, status, ask: ASK });\n\nlb.bindSelect(document.getElementById("st"), status); // populates the 4 options\nlb.bindAction(document.getElementById("go"), save); // click \u2192 write\n\nsave.subscribe((a) => { // render your own feedback\n msg.textContent = a.loading ? "Saving\u2026"\n : a.error ? a.error.message // includes partial failures\n : a.lastResult ? `Set to ${a.lastResult.status}` : "";\n msg.dataset.tone = a.error ? "error" : a.lastResult ? "ok" : "";\n});\n```\n\nLoading / success / error styling comes free: `bindAction` and `bindSelect` set\n`data-lb-state` (`ready|loading|error|success|unavailable`) and the skin already\ntargets those attributes. No extra wiring.\n\nSave-on-change instead of an Apply button \u2014 drop `bindAction` and run it yourself:\n\n```js\ndocument.getElementById("st").addEventListener("change", () => save.run());\n```\n\n**Bulk apply** across checked rows \u2014 pass `leadIds` and a `confirm`, since one\nclick rewrites a field every rep in the org sees:\n\n```js\nconst bulk = lb.setStatus({\n leadIds: () => checkedIds, // \u2190 read at run() time, not at build time\n status, ask: ASK,\n confirm: "Set this status on every selected lead?",\n});\n```\n\n`leadIds` is read when the action runs, so a live selection works \u2014 but pass the\narray itself if your selection is fixed. A partial write (some leads rejected)\nsurfaces as `.error`, never as a green button: `setStatus` checks the `failed[]`\nthe tool returns.\n\nThe backend stamps the status date as "now" on every write, which is what a rep\nclicking a dropdown means. Don\'t add a date picker unless the user asks to\nbackdate \u2014 then pass an optional `date` field holding `YYYY-MM-DD`:\n`lb.setStatus({ leadId, status, date, ask })`.\n\n## Recipe: manager dashboard\n\n```js\nconst team = lb.teamActivity({ weeks: 4, ask: ASK });\nteam.subscribe((t) => {\n if (t.loading) showSpinner();\n if (t.data) {\n renderLeaderboard(t.data.reps); // sorted by total_activities; cols: name, notes, meetings_or_interest, lost\u2026\n renderTrendChart(t.data.trend); // [{date,count}] \u2192 Chart.js (allowed from CDN)\n }\n});\nrefreshBtn.onclick = () => team.refresh();\n```\n\n## Recipe: live enrichment\n\n```js\nconst job = lb.enrichment({ leadIds: [LEAD], titles: ["CEO", "VP Sales"], ask: ASK });\njob.subscribe((j) => {\n const p = j.data && j.data.overall_progress; // {done,total,done_ratio}\n renderBar(p);\n if (j.done) renderContacts(j.data.leads); // enriched contacts\n});\nrefreshBtn.onclick = () => job.refresh();\n```\n\n## Write-call rules\n\nThe domain factories handle these for you. If you hand-roll an action:\n`leadbay_report_outreach` args MUST include `verification:{source:"user_confirmed", ref}`\nAND `_triggered_by`; `leadbay_add_leads_to_campaign` needs `_triggered_by`;\n`add_note`/`like_lead`/`dislike_lead` take only their own args. `epilogue_status` is\none of `lb.EPILOGUE_STATUSES`. Snoozing (pushback) is advanced-gated \u2014 not\ncallable from a default artifact. Org lead status IS on the default surface:\nuse `lb.setStatus`, which owns the arg shape AND the partial-write check \u2014\n`leadbay_set_lead_status` writes each lead separately, so it can resolve 200\nwith a non-empty `failed[]`. Hand-rolling that action will report a green\nbutton over a write that never landed.\n\n## Degradation + live updates\n\nIf the host bridge is absent, a view-model\'s `.error` is set with `.error.unavailable\n=== true` (bind helpers set `data-lb-state="unavailable"`) \u2014 nothing throws. Every\ncall also has a **30s timeout** (configurable via `lb.configure({ timeoutMs })`): a\nhost call that never settles becomes `.error` with `code:"timeout"`, so a control is\nnever stuck loading forever \u2014 always render the `.error` branch so the user can retry.\nAuto-poll (`pollEvery`) depends on the cowork host serving FRESH reads; `.refresh()`\nis the guaranteed manual path \u2014 always wire a Refresh control for polling resources.';
|
|
23156
23402
|
|
|
23157
23403
|
// ../core/dist/tools/artifact-kit.js
|
|
@@ -24216,7 +24462,7 @@ function buildProtocolPrimitivesParagraph(has) {
|
|
|
24216
24462
|
}
|
|
24217
24463
|
if (longRunners.length > 0) {
|
|
24218
24464
|
parts.push(
|
|
24219
|
-
"(2) `notifications/cancelled` \u2014 when the user clicks Cancel in the host UI, the polling loop exits within \u22642 seconds
|
|
24465
|
+
"(2) `notifications/cancelled` \u2014 when the user clicks Cancel in the host UI, the polling loop exits within \u22642 seconds. The job itself keeps running on the backend; poll its notification_id / importIds later to pick it up."
|
|
24220
24466
|
);
|
|
24221
24467
|
} else {
|
|
24222
24468
|
parts.push(
|
|
@@ -24736,7 +24982,6 @@ ${url}
|
|
|
24736
24982
|
const shapeError = findShapeMismatch(tool, args);
|
|
24737
24983
|
const result = shapeError ?? await runWithRequestSignal(extra.signal, () => tool.execute(client, args, {
|
|
24738
24984
|
logger: opts.logger,
|
|
24739
|
-
bulkTracker: opts.bulkTracker,
|
|
24740
24985
|
notificationsInbox: opts.notificationsInbox,
|
|
24741
24986
|
signal: extra.signal,
|
|
24742
24987
|
progress,
|
|
@@ -25165,7 +25410,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
25165
25410
|
}
|
|
25166
25411
|
|
|
25167
25412
|
// src/http-server.ts
|
|
25168
|
-
var VERSION = true ? "0.
|
|
25413
|
+
var VERSION = true ? "0.35.1" : "0.0.0-dev";
|
|
25169
25414
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
25170
25415
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
25171
25416
|
var logger = {
|
|
@@ -25570,7 +25815,7 @@ var isEntrypoint = (() => {
|
|
|
25570
25815
|
}
|
|
25571
25816
|
})();
|
|
25572
25817
|
if (isEntrypoint) {
|
|
25573
|
-
const _boot =
|
|
25818
|
+
const _boot = randomUUID2();
|
|
25574
25819
|
serve({ fetch: app.fetch, port: PORT, hostname: HOST }, (info) => {
|
|
25575
25820
|
process.stderr.write(
|
|
25576
25821
|
`leadbay-mcp-http ${VERSION} listening on http://${info.address}:${info.port} (boot=${_boot})
|