@leadbay/mcp 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -553,6 +553,23 @@ var init_client = __esm({
553
553
  await this.resolveOrgId();
554
554
  await this.resolveTasteProfile();
555
555
  }
556
+ // ─── Notifications helpers ────────────────────────────────────────────
557
+ // Backend exposes `GET /notifications`, `POST /notifications/{id}/seen`,
558
+ // `POST /notifications/{id}/archive`, plus `GET /ws/ticket?v=1.0` to mint
559
+ // a one-shot WS URL. See backend/docs/adr/notifications.md for shape.
560
+ async listNotifications(args = {}) {
561
+ const params = new URLSearchParams();
562
+ params.set("archived", String(args.archived ?? false));
563
+ params.set("page", String(args.page ?? 0));
564
+ params.set("count", String(args.count ?? 50));
565
+ return this.request("GET", `/notifications?${params.toString()}`);
566
+ }
567
+ async acknowledgeNotification(notificationId, action = "seen") {
568
+ await this.requestVoid("POST", `/notifications/${notificationId}/${action}`);
569
+ }
570
+ async getWsTicket() {
571
+ return this.request("GET", "/auth/ws?v=1.0");
572
+ }
556
573
  makeError(code, message, hint, endpoint, retry_after, http_status) {
557
574
  const out = { error: true, code, message, hint };
558
575
  if (endpoint || this._region) {
@@ -5122,8 +5139,306 @@ var init_composite_file_names = __esm({
5122
5139
  }
5123
5140
  });
5124
5141
 
5142
+ // ../core/dist/notifications/revise-hint.js
5143
+ function reviseHintFor(kind) {
5144
+ switch (kind) {
5145
+ case "bulk_enrich":
5146
+ return HINT_BULK_ENRICH;
5147
+ case "bulk_qualify":
5148
+ return HINT_BULK_QUALIFY;
5149
+ case "import":
5150
+ return HINT_IMPORT;
5151
+ default:
5152
+ return HINT_OTHER;
5153
+ }
5154
+ }
5155
+ function inferKind(n) {
5156
+ if (n.links.some((l) => l.type === "bulk_enrichment"))
5157
+ return "bulk_enrich";
5158
+ if (n.file_import_id)
5159
+ return "import";
5160
+ if (n.bulk_progress)
5161
+ return "bulk_qualify";
5162
+ return "other";
5163
+ }
5164
+ function anchorIdFor(n, kind) {
5165
+ if (kind === "bulk_enrich") {
5166
+ const link = n.links.find((l) => l.type === "bulk_enrichment");
5167
+ return link ? String(link.id) : null;
5168
+ }
5169
+ if (kind === "import")
5170
+ return n.file_import_id;
5171
+ return null;
5172
+ }
5173
+ function toInboxEntry(n) {
5174
+ const kind = inferKind(n);
5175
+ return {
5176
+ notification_id: n.id,
5177
+ kind,
5178
+ anchor_id: anchorIdFor(n, kind),
5179
+ title: n.title,
5180
+ bulk_progress: n.bulk_progress,
5181
+ completed_at: n.updated_at,
5182
+ revise_hint: reviseHintFor(kind)
5183
+ };
5184
+ }
5185
+ var HINT_BULK_ENRICH, HINT_BULK_QUALIFY, HINT_IMPORT, HINT_OTHER;
5186
+ var init_revise_hint = __esm({
5187
+ "../core/dist/notifications/revise-hint.js"() {
5188
+ "use strict";
5189
+ HINT_BULK_ENRICH = "Contact enrichment just finished. Revise any prior output that named these leads' contacts (outreach drafts, contact lists, recommended-lead lists with contact_count, NEXT STEPS asking the user to wait for emails / phones). Re-fetch contacts via leadbay_get_contacts for the affected leads.";
5190
+ HINT_BULK_QUALIFY = "Lead qualification just finished. Revise any prior lead list / ranking / outreach shortlist that depended on ai_agent_lead_score for these leads \u2014 today's leads, top-of-inbox, followups maps, prepare-outreach shortlists. Re-pull qualification answers via leadbay_research_lead_by_id or re-rank via leadbay_pull_leads.";
5191
+ HINT_IMPORT = "CSV / CRM import just finished. Revise any prior output that referenced 'leads available' before the import landed \u2014 lead lists pulled from the affected lens, 'what's new today', followup planning. Re-pull the affected lens via leadbay_pull_leads / leadbay_pull_followups.";
5192
+ HINT_OTHER = "Background work just completed. If you referenced its subject in prior output, re-fetch the affected data and revise.";
5193
+ }
5194
+ });
5195
+
5196
+ // ../core/dist/notifications/inbox.js
5197
+ var DEFAULT_TTL_MS, NotificationsInbox;
5198
+ var init_inbox = __esm({
5199
+ "../core/dist/notifications/inbox.js"() {
5200
+ "use strict";
5201
+ init_revise_hint();
5202
+ DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
5203
+ NotificationsInbox = class {
5204
+ entries = /* @__PURE__ */ new Map();
5205
+ ttl_ms;
5206
+ now;
5207
+ constructor(opts = {}) {
5208
+ this.ttl_ms = opts.ttl_ms ?? DEFAULT_TTL_MS;
5209
+ this.now = opts.now ?? Date.now;
5210
+ }
5211
+ // Upsert by notification id. Latest write wins so duplicate arrivals
5212
+ // (WS event + REST catch-up landing the same row) collapse cleanly.
5213
+ record(n) {
5214
+ if (!n.bulk_progress)
5215
+ return;
5216
+ if (n.in_progress)
5217
+ return;
5218
+ const entry = toInboxEntry(n);
5219
+ this.entries.set(entry.notification_id, {
5220
+ entry,
5221
+ recordedAt: this.now()
5222
+ });
5223
+ }
5224
+ list() {
5225
+ this.expireStale();
5226
+ return [...this.entries.values()].map((e) => e.entry);
5227
+ }
5228
+ markSeen(notification_id) {
5229
+ this.entries.delete(notification_id);
5230
+ }
5231
+ size() {
5232
+ this.expireStale();
5233
+ return this.entries.size;
5234
+ }
5235
+ expireStale() {
5236
+ const cutoff = this.now() - this.ttl_ms;
5237
+ for (const [id, e] of this.entries) {
5238
+ if (e.recordedAt < cutoff)
5239
+ this.entries.delete(id);
5240
+ }
5241
+ }
5242
+ };
5243
+ }
5244
+ });
5245
+
5246
+ // ../core/dist/notifications/catch-up.js
5247
+ async function catchUpNotifications(client, inbox, opts = {}) {
5248
+ const count = opts.count ?? DEFAULT_COUNT;
5249
+ let added = 0;
5250
+ try {
5251
+ const page = await client.listNotifications({
5252
+ archived: false,
5253
+ page: 0,
5254
+ count
5255
+ });
5256
+ for (const n of page.items) {
5257
+ if (!n.bulk_progress)
5258
+ continue;
5259
+ if (n.in_progress)
5260
+ continue;
5261
+ if (n.first_seen_at)
5262
+ continue;
5263
+ const sizeBefore = inbox.size();
5264
+ inbox.record(n);
5265
+ if (inbox.size() > sizeBefore)
5266
+ added += 1;
5267
+ }
5268
+ opts.logger?.info?.(`notifications.catch_up scanned=${page.items.length} seeded=${added}`);
5269
+ } catch (err) {
5270
+ opts.logger?.warn?.(`notifications.catch_up failed: ${err?.message ?? err?.code ?? err}`);
5271
+ }
5272
+ return added;
5273
+ }
5274
+ var DEFAULT_COUNT;
5275
+ var init_catch_up = __esm({
5276
+ "../core/dist/notifications/catch-up.js"() {
5277
+ "use strict";
5278
+ DEFAULT_COUNT = 50;
5279
+ }
5280
+ });
5281
+
5282
+ // ../core/dist/notifications/ws-client.js
5283
+ var PING_INTERVAL_MS, RECONNECT_INITIAL_MS, RECONNECT_MAX_MS, NotificationsWsClient;
5284
+ var init_ws_client = __esm({
5285
+ "../core/dist/notifications/ws-client.js"() {
5286
+ "use strict";
5287
+ init_catch_up();
5288
+ PING_INTERVAL_MS = 3e4;
5289
+ RECONNECT_INITIAL_MS = 1e3;
5290
+ RECONNECT_MAX_MS = 3e4;
5291
+ NotificationsWsClient = class {
5292
+ client;
5293
+ inbox;
5294
+ logger;
5295
+ ws = null;
5296
+ pingTimer = null;
5297
+ reconnectTimer = null;
5298
+ reconnectDelay = RECONNECT_INITIAL_MS;
5299
+ stopped = false;
5300
+ constructor(opts) {
5301
+ this.client = opts.client;
5302
+ this.inbox = opts.inbox;
5303
+ this.logger = opts.logger;
5304
+ }
5305
+ async start() {
5306
+ this.stopped = false;
5307
+ await catchUpNotifications(this.client, this.inbox, { logger: this.logger });
5308
+ void this.connect();
5309
+ }
5310
+ stop() {
5311
+ this.stopped = true;
5312
+ if (this.pingTimer)
5313
+ clearInterval(this.pingTimer);
5314
+ if (this.reconnectTimer)
5315
+ clearTimeout(this.reconnectTimer);
5316
+ this.pingTimer = null;
5317
+ this.reconnectTimer = null;
5318
+ if (this.ws) {
5319
+ try {
5320
+ this.ws.close(1e3, "shutdown");
5321
+ } catch {
5322
+ }
5323
+ this.ws = null;
5324
+ }
5325
+ }
5326
+ async connect() {
5327
+ if (this.stopped)
5328
+ return;
5329
+ let url;
5330
+ try {
5331
+ const ticket = await this.client.getWsTicket();
5332
+ url = ticket.url;
5333
+ } catch (err) {
5334
+ this.logger?.warn?.(`notifications.ws ticket_fetch_failed: ${err?.message ?? err?.code ?? err}`);
5335
+ this.scheduleReconnect();
5336
+ return;
5337
+ }
5338
+ let ws;
5339
+ try {
5340
+ ws = new WebSocket(url);
5341
+ } catch (err) {
5342
+ this.logger?.warn?.(`notifications.ws construct_failed: ${err?.message ?? err}`);
5343
+ this.scheduleReconnect();
5344
+ return;
5345
+ }
5346
+ this.ws = ws;
5347
+ ws.addEventListener("open", () => {
5348
+ this.logger?.info?.("notifications.ws connected");
5349
+ this.reconnectDelay = RECONNECT_INITIAL_MS;
5350
+ void catchUpNotifications(this.client, this.inbox, {
5351
+ logger: this.logger
5352
+ });
5353
+ this.pingTimer = setInterval(() => this.sendPing(), PING_INTERVAL_MS);
5354
+ });
5355
+ ws.addEventListener("message", (ev) => {
5356
+ const text = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
5357
+ if (!text)
5358
+ return;
5359
+ let msg;
5360
+ try {
5361
+ msg = JSON.parse(text);
5362
+ } catch {
5363
+ this.logger?.warn?.("notifications.ws non_json_frame");
5364
+ return;
5365
+ }
5366
+ this.handleMessage(msg);
5367
+ });
5368
+ ws.addEventListener("error", (ev) => {
5369
+ this.logger?.warn?.(`notifications.ws error: ${ev?.message ?? "(no detail)"}`);
5370
+ });
5371
+ ws.addEventListener("close", (ev) => {
5372
+ this.logger?.info?.(`notifications.ws closed code=${ev.code} reason=${ev.reason || "(none)"}`);
5373
+ if (this.pingTimer)
5374
+ clearInterval(this.pingTimer);
5375
+ this.pingTimer = null;
5376
+ this.ws = null;
5377
+ this.scheduleReconnect();
5378
+ });
5379
+ }
5380
+ handleMessage(msg) {
5381
+ if (msg.type === "pong")
5382
+ return;
5383
+ if (msg.type === "ping") {
5384
+ this.sendRaw({ type: "pong" });
5385
+ return;
5386
+ }
5387
+ if (msg.type !== "notification")
5388
+ return;
5389
+ const { type: _t, ...rest } = msg;
5390
+ void _t;
5391
+ const n = rest;
5392
+ if (n.bulk_progress == null || n.in_progress) {
5393
+ return;
5394
+ }
5395
+ this.inbox.record(n);
5396
+ this.logger?.info?.(`notifications.ws terminal id=${n.id} kind=${n.file_import_id ? "import" : n.links.some((l) => l.type === "bulk_enrichment") ? "bulk_enrich" : "bulk_qualify"}`);
5397
+ }
5398
+ sendPing() {
5399
+ this.sendRaw({ type: "ping" });
5400
+ }
5401
+ sendRaw(obj) {
5402
+ if (!this.ws)
5403
+ return;
5404
+ if (this.ws.readyState !== WebSocket.OPEN)
5405
+ return;
5406
+ try {
5407
+ this.ws.send(JSON.stringify(obj));
5408
+ } catch (err) {
5409
+ this.logger?.warn?.(`notifications.ws send_failed: ${err?.message ?? err}`);
5410
+ }
5411
+ }
5412
+ scheduleReconnect() {
5413
+ if (this.stopped)
5414
+ return;
5415
+ if (this.reconnectTimer)
5416
+ return;
5417
+ const delay = this.reconnectDelay;
5418
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
5419
+ this.reconnectTimer = setTimeout(() => {
5420
+ this.reconnectTimer = null;
5421
+ void this.connect();
5422
+ }, delay);
5423
+ this.logger?.info?.(`notifications.ws reconnect_in_${delay}ms`);
5424
+ }
5425
+ };
5426
+ }
5427
+ });
5428
+
5429
+ // ../core/dist/notifications/index.js
5430
+ var init_notifications = __esm({
5431
+ "../core/dist/notifications/index.js"() {
5432
+ "use strict";
5433
+ init_inbox();
5434
+ init_ws_client();
5435
+ init_catch_up();
5436
+ init_revise_hint();
5437
+ }
5438
+ });
5439
+
5125
5440
  // ../core/dist/tool-descriptions.generated.js
5126
- var leadbay_account_status, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_seed_candidates, leadbay_select_leads, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_update_lens, leadbay_update_lens_filter;
5441
+ var leadbay_account_status, leadbay_acknowledge_notification, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_seed_candidates, leadbay_select_leads, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_update_lens, leadbay_update_lens_filter;
5127
5442
  var init_tool_descriptions_generated = __esm({
5128
5443
  "../core/dist/tool-descriptions.generated.js"() {
5129
5444
  "use strict";
@@ -5162,9 +5477,52 @@ Show the user's account state \u2014 admin rights, language, last-active lens, c
5162
5477
 
5163
5478
  **After a user tops up, do NOT keep refusing \u2014 RETRY.** If the user signals they topped up / bought credits / added credits, the previous QUOTA_EXCEEDED is invalidated the moment the Stripe webhook lands. RE-CALL \`leadbay_account_status\` to pick up the new state AND retry the originally failed call. The retry itself does not require a successful account_status check first \u2014 a topped-up user has cleared the throttle whether or not your cached snapshot reflects it yet. If the retry hits the wall again, only then re-offer top-up / wait. **A stale quota snapshot is never a reason to gate-keep a topped-up user.**
5164
5479
 
5480
+ **\`notifications\` block.** The response now includes a top-level \`notifications\` array listing background work the user (or agent) initiated that has since completed (\`bulk_enrich\`, \`bulk_qualify\`, \`import\`). These are signals to revise prior agent outputs the just-finished work might have made stale \u2014 they're NOT a pending-task list for the user. After revising (or confirming nothing is affected), call \`leadbay_acknowledge_notification(notification_id)\`. Full handling protocol below.
5481
+
5482
+ ## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
5483
+
5484
+ Some Leadbay tool responses include a \`_meta.notifications\` array listing **background work the user (or you) initiated that has since completed**. The three relevant kinds:
5485
+
5486
+ | Kind | What just finished |
5487
+ |---|---|
5488
+ | \`bulk_enrich\` | A bulk contact-enrichment job (emails / phone numbers attached to leads' contacts). |
5489
+ | \`bulk_qualify\` | A bulk lead-qualification job (web-fetch + AI rescore, producing \`ai_agent_lead_score\` and qualification answers). |
5490
+ | \`import\` | A CSV / CRM file import (rows resolved to leads in the user's pipeline). |
5491
+
5492
+ **Your job when you see an entry: revise prior outputs the just-finished work might have made stale.** This is NOT a pending-task list. It's a "your earlier answer used data that has now changed" signal.
5493
+
5494
+ | Kind | Outputs you've produced that may now be stale \u2014 refresh them |
5495
+ |---|---|
5496
+ | \`bulk_enrich\` | Outreach drafts mentioning these leads' contacts; contact lists; recommended-lead lists citing \`contact_count\`; NEXT STEPS that asked the user to wait for emails / phones. Re-fetch via \`leadbay_get_contacts(leadId)\` for the affected leads. |
5497
+ | \`bulk_qualify\` | Lead rankings / shortlists you produced without \`ai_agent_lead_score\`; "today's leads"; followup maps; prepare-outreach picks. Re-pull via \`leadbay_pull_leads\` / \`leadbay_research_lead_by_id\`. |
5498
+ | \`import\` | "Available leads" claims; pulls from the affected lens that ran before the import landed; followup planning that needed the imported set. Re-pull via \`leadbay_pull_leads\` / \`leadbay_pull_followups\`. |
5499
+
5500
+ **After revising (or after confirming no prior output is affected):** call \`leadbay_acknowledge_notification(notification_id)\` so the entry stops resurfacing on every tool response. Ack-and-move-on is correct even when nothing was stale \u2014 that's how the inbox stays focused on what's actually pending.
5501
+
5502
+ **Do NOT** interpret these entries as "things waiting for the user." The user expects you to handle them silently. They are signals to YOU \u2014 agent \u2014 that prior outputs need a refresh.
5503
+
5504
+ **Don't poll.** When you launch a long operation (enrichment / qualification / import), simply continue the conversation \u2014 the next time you call any tool, the completed-work entry will appear in \`_meta.notifications\`. Status tools (\`leadbay_bulk_enrich_status\`, \`leadbay_qualify_status\`, \`leadbay_import_status\`) exist for the case where you want the answer NOW (mid-conversation), not for ambient polling.
5505
+
5506
+ Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
5507
+
5508
+
5165
5509
  WHEN TO USE: at the start of a session to know what the agent can/can't do, after a 429 to explain to the user which resource window was exhausted and when it resets (and to offer the top-up alternative), and after the user signals a top-up so the agent can resume the interrupted workflow.
5166
5510
 
5167
5511
  WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
5512
+ `;
5513
+ leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.5/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
5514
+
5515
+ **When to call.** After you read an entry from \`_meta.notifications\` or \`account_status.notifications\` and have revised whatever prior output the just-finished background work might have made stale (outreach drafts, lead lists, "available leads" claims, followup plans). Mark-seen tells the human team's pipeline you handled this and prevents the notification from re-surfacing on every subsequent tool response.
5516
+
5517
+ If nothing you produced for the user is affected, ack anyway with \`archive:false\` \u2014 the entry should still clear so the inbox stays focused on what's actually pending.
5518
+
5519
+ Use \`archive:true\` only when you want the row gone from the FE notification dropdown too (e.g. a non-actionable system notification that's already handled). Default behaviour is \`seen\` \u2014 same as the FE dropdown's "click to read" semantics.
5520
+
5521
+ WHEN TO USE: immediately after you finish reviewing / revising in response to a \`_meta.notifications[]\` entry. Idempotent \u2014 calling twice with the same id is safe.
5522
+
5523
+ WHEN NOT TO USE: before doing the revision work; for general "mark all read" sweeps (call once per notification id you've actually consumed).
5524
+
5525
+ 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\`.
5168
5526
  `;
5169
5527
  leadbay_add_leads_to_campaign = `## WHEN TO USE
5170
5528
 
@@ -10199,9 +10557,23 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
10199
10557
  await pollPreprocess(client, importId, phaseBudget, ctx, signal);
10200
10558
  ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
10201
10559
  if (dryRun) {
10202
- return { importId, records: [] };
10560
+ return { importId, records: [], notification_id: null };
10561
+ }
10562
+ let updateMappingsResp = null;
10563
+ try {
10564
+ updateMappingsResp = await client.request("POST", `/imports/${importId}/update_mappings`, mappings);
10565
+ } catch (err) {
10566
+ if (err?.code === "API_ERROR" || err?.code === "NOT_FOUND") {
10567
+ ctx?.logger?.warn?.(`import-leads: update_mappings raw error (${err?.code}); retrying void`);
10568
+ await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
10569
+ } else {
10570
+ throw err;
10571
+ }
10572
+ }
10573
+ const importNotificationId = updateMappingsResp?.notification_id ?? null;
10574
+ if (importNotificationId) {
10575
+ ctx?.logger?.info?.(`import-leads: notification_id=${importNotificationId} importId=${importId}`);
10203
10576
  }
10204
- await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
10205
10577
  ctx?.logger?.info?.(`import-leads: mappings committed for importId=${importId}`);
10206
10578
  const phaseBudget2 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
10207
10579
  await pollProcess(client, importId, phaseBudget2, ctx, signal);
@@ -10209,7 +10581,7 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
10209
10581
  const phaseBudget3 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
10210
10582
  const records = await pollRecordsToTerminal(client, importId, phaseBudget3, chunk.length, ctx, signal);
10211
10583
  ctx?.logger?.info?.(`import-leads: ${records.length} records terminal for importId=${importId}`);
10212
- return { importId, records };
10584
+ return { importId, records, notification_id: importNotificationId };
10213
10585
  }
10214
10586
  function reconcileOneChunk(prep, chunk, matched, notImported) {
10215
10587
  const seenInputIndex = /* @__PURE__ */ new Set();
@@ -10265,7 +10637,7 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
10265
10637
  }
10266
10638
  }
10267
10639
  }
10268
- function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled) {
10640
+ function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds) {
10269
10641
  const leads = [];
10270
10642
  const not_imported = [];
10271
10643
  if (dryRun) {
@@ -10330,6 +10702,7 @@ function buildImportLeadsResult(client, prep, importIds, matched, notImported, d
10330
10702
  leads,
10331
10703
  not_imported,
10332
10704
  importIds,
10705
+ notification_ids: notificationIds,
10333
10706
  region: client.region,
10334
10707
  cancelled: cancelled || void 0,
10335
10708
  dry_run: dryRun || void 0,
@@ -10355,17 +10728,21 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
10355
10728
  void (async () => {
10356
10729
  const bgCtx = { logger: ctx.logger, bulkTracker: tracker };
10357
10730
  const importIds = uploadedChunks.map((chunk) => chunk.importId);
10731
+ const notificationIds = [];
10358
10732
  const matched = /* @__PURE__ */ new Map();
10359
10733
  const notImported = /* @__PURE__ */ new Map();
10360
10734
  try {
10361
10735
  const totalDeadline = Date.now() + opts.totalBudget;
10362
10736
  for (const upload of uploadedChunks) {
10363
10737
  const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0);
10738
+ if (out.notification_id && !notificationIds.includes(out.notification_id)) {
10739
+ notificationIds.push(out.notification_id);
10740
+ }
10364
10741
  if (!opts.dryRun) {
10365
10742
  reconcileOneChunk(prep, out, matched, notImported);
10366
10743
  }
10367
10744
  }
10368
- const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false);
10745
+ const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false, notificationIds);
10369
10746
  await tracker.markImportComplete(handleId, {
10370
10747
  leads: result.leads,
10371
10748
  not_imported: result.not_imported,
@@ -10598,6 +10975,7 @@ var init_import_leads = __esm({
10598
10975
  leads: [],
10599
10976
  not_imported,
10600
10977
  importIds: [],
10978
+ notification_ids: [],
10601
10979
  region: client.region,
10602
10980
  dry_run: dryRun || void 0,
10603
10981
  _meta: client.lastMeta ?? {
@@ -10652,6 +11030,10 @@ var init_import_leads = __esm({
10652
11030
  status: "running",
10653
11031
  handle_id: reservation.record.bulk_id,
10654
11032
  importIds: importIds2,
11033
+ // Notifications fire from update_mappings, which the background
11034
+ // task hasn't called yet at this point. They surface via the WS
11035
+ // listener / catch-up REST on subsequent agent turns.
11036
+ notification_ids: [],
10655
11037
  progress: {
10656
11038
  phase: reservation.record.status === "complete" ? "complete" : importIds2.length > 0 ? "preprocess" : "queued",
10657
11039
  records_processed: reservation.record.status === "complete" ? reservation.record.records_total : 0,
@@ -10672,6 +11054,7 @@ var init_import_leads = __esm({
10672
11054
  }
10673
11055
  ctx?.logger?.info?.(`import-leads(${prep.mode}): ${prep.validInputs.length} rows \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
10674
11056
  const importIds = [];
11057
+ const notificationIds = [];
10675
11058
  const matched = /* @__PURE__ */ new Map();
10676
11059
  const notImported = /* @__PURE__ */ new Map();
10677
11060
  let cancelled = false;
@@ -10683,6 +11066,9 @@ var init_import_leads = __esm({
10683
11066
  for (let i = 0; i < chunks.length; i++) {
10684
11067
  const chunk = chunks[i];
10685
11068
  const out = await runOneChunk(client, chunk, i, chunks.length, prep.header, prep.mappings, dryRun, perPhaseBudget, totalDeadline, ctx, signal, recordImportId);
11069
+ if (out.notification_id && !notificationIds.includes(out.notification_id)) {
11070
+ notificationIds.push(out.notification_id);
11071
+ }
10686
11072
  if (!dryRun) {
10687
11073
  reconcileOneChunk(prep, out, matched, notImported);
10688
11074
  }
@@ -10703,7 +11089,7 @@ var init_import_leads = __esm({
10703
11089
  throw err;
10704
11090
  }
10705
11091
  }
10706
- return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled);
11092
+ return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds);
10707
11093
  }
10708
11094
  };
10709
11095
  }
@@ -11411,6 +11797,70 @@ var init_agent_memory_review = __esm({
11411
11797
  }
11412
11798
  });
11413
11799
 
11800
+ // ../core/dist/tools/acknowledge-notification.js
11801
+ var UUID_RE, acknowledgeNotification;
11802
+ var init_acknowledge_notification = __esm({
11803
+ "../core/dist/tools/acknowledge-notification.js"() {
11804
+ "use strict";
11805
+ init_tool_descriptions_generated();
11806
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11807
+ acknowledgeNotification = {
11808
+ name: "leadbay_acknowledge_notification",
11809
+ annotations: {
11810
+ title: "Acknowledge a Leadbay notification",
11811
+ readOnlyHint: false,
11812
+ destructiveHint: false,
11813
+ idempotentHint: true,
11814
+ openWorldHint: true
11815
+ },
11816
+ description: leadbay_acknowledge_notification,
11817
+ write: true,
11818
+ inputSchema: {
11819
+ type: "object",
11820
+ properties: {
11821
+ notification_id: {
11822
+ type: "string",
11823
+ description: "UUID of the notification to acknowledge. Use the notification_id from `_meta.notifications[]` or `account_status.notifications[]`."
11824
+ },
11825
+ archive: {
11826
+ type: "boolean",
11827
+ description: "If true, archive the notification (won't appear in `archived=false` listings). If false / omitted, mark seen (resets firstSeenAt)."
11828
+ }
11829
+ },
11830
+ required: ["notification_id"],
11831
+ additionalProperties: false
11832
+ },
11833
+ outputSchema: {
11834
+ type: "object",
11835
+ properties: {
11836
+ acknowledged: { type: "boolean" },
11837
+ notification_id: { type: "string" },
11838
+ action: { type: "string", enum: ["seen", "archive"] }
11839
+ },
11840
+ required: ["acknowledged", "notification_id", "action"]
11841
+ },
11842
+ execute: async (client, params, ctx) => {
11843
+ if (!UUID_RE.test(params.notification_id)) {
11844
+ return {
11845
+ error: true,
11846
+ code: "BAD_INPUT",
11847
+ message: "notification_id must be a UUID",
11848
+ hint: "Pass the notification_id verbatim from _meta.notifications[].notification_id or account_status.notifications[].notification_id."
11849
+ };
11850
+ }
11851
+ const action = params.archive ? "archive" : "seen";
11852
+ await client.acknowledgeNotification(params.notification_id, action);
11853
+ ctx?.notificationsInbox?.markSeen(params.notification_id);
11854
+ return {
11855
+ acknowledged: true,
11856
+ notification_id: params.notification_id,
11857
+ action
11858
+ };
11859
+ }
11860
+ };
11861
+ }
11862
+ });
11863
+
11414
11864
  // ../core/dist/tools/select-leads.js
11415
11865
  var selectLeads;
11416
11866
  var init_select_leads = __esm({
@@ -14760,6 +15210,11 @@ var init_account_status = __esm({
14760
15210
  type: ["object", "null"],
14761
15211
  description: "Per-resource quota state (llm_completion, ai_rescore, web_fetch, LENS_EXTRA_REFILL) across daily/weekly/monthly windows. Null if /quota_status failed (logged in stderr). Pre-check the LENS_EXTRA_REFILL entry before calling leadbay_extend_lens."
14762
15212
  },
15213
+ notifications: {
15214
+ type: "array",
15215
+ description: "Terminal bulk-progress notifications the MCP knows about (background work the user or agent started that has since completed). Each entry carries notification_id, kind (bulk_enrich | bulk_qualify | import | other), bulk_progress counters, and a revise_hint pointing at prior agent outputs the just-finished work might have made stale. After revising affected outputs, call leadbay_acknowledge_notification(notification_id) to clear the entry. Empty array when nothing has completed.",
15216
+ items: { type: "object" }
15217
+ },
14763
15218
  _meta: {
14764
15219
  type: "object",
14765
15220
  properties: {
@@ -14819,6 +15274,13 @@ var init_account_status = __esm({
14819
15274
  // on /me are intentionally NOT surfaced — they're defunct (see
14820
15275
  // SHAPE-DRIFT.md probe round 4).
14821
15276
  quota,
15277
+ // Inbox of terminal bulk-progress notifications. Same shape the MCP
15278
+ // server attaches to `_meta.notifications` on every tool response —
15279
+ // duplicated here as a top-level field so the agent's daily-rhythm
15280
+ // check-in (this composite) sees them without having to read _meta.
15281
+ // Empty array when the WS listener isn't wired (OpenClaw, tests) OR
15282
+ // when nothing has completed since the last ack.
15283
+ notifications: ctx?.notificationsInbox?.list() ?? [],
14822
15284
  _meta: {
14823
15285
  region: client.region
14824
15286
  }
@@ -14829,13 +15291,40 @@ var init_account_status = __esm({
14829
15291
  });
14830
15292
 
14831
15293
  // ../core/dist/composite/bulk-qualify-leads.js
14832
- var PAGE_SIZE, DEFAULT_COUNT, MAX_COUNT, DEFAULT_PER_LEAD_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS2, bulkQualifyLeads;
15294
+ async function launchBulkQualify(client, leadIds, ctx) {
15295
+ await client.acquireSelectionLock();
15296
+ try {
15297
+ try {
15298
+ const qs = leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
15299
+ await client.requestVoid("POST", `/leads/selection/select?${qs}`);
15300
+ try {
15301
+ const resp = await client.request("POST", "/leads/selection/web_fetch?force_fetch=false", {});
15302
+ return { resp, quotaExceeded: false };
15303
+ } catch (err) {
15304
+ if (err?.code === "QUOTA_EXCEEDED") {
15305
+ ctx?.logger?.warn?.("bulk_qualify_leads: 429 on bulk /leads/selection/web_fetch \u2014 no leads queued");
15306
+ return { resp: null, quotaExceeded: true };
15307
+ }
15308
+ throw err;
15309
+ }
15310
+ } finally {
15311
+ try {
15312
+ await client.requestVoid("POST", "/leads/selection/clear");
15313
+ } catch (e) {
15314
+ ctx?.logger?.warn?.(`bulk_qualify_leads: selection.clear failed: ${e?.message ?? e?.code}`);
15315
+ }
15316
+ }
15317
+ } finally {
15318
+ client.releaseSelectionLock();
15319
+ }
15320
+ }
15321
+ var PAGE_SIZE, DEFAULT_COUNT2, MAX_COUNT, DEFAULT_PER_LEAD_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS2, bulkQualifyLeads;
14833
15322
  var init_bulk_qualify_leads = __esm({
14834
15323
  "../core/dist/composite/bulk-qualify-leads.js"() {
14835
15324
  "use strict";
14836
15325
  init_tool_descriptions_generated();
14837
15326
  PAGE_SIZE = 50;
14838
- DEFAULT_COUNT = 10;
15327
+ DEFAULT_COUNT2 = 10;
14839
15328
  MAX_COUNT = 25;
14840
15329
  DEFAULT_PER_LEAD_BUDGET_MS = 9e4;
14841
15330
  DEFAULT_TOTAL_BUDGET_MS2 = 5 * 6e4;
@@ -14856,7 +15345,7 @@ var init_bulk_qualify_leads = __esm({
14856
15345
  properties: {
14857
15346
  count: {
14858
15347
  type: "number",
14859
- description: `How many fresh leads to qualify (default ${DEFAULT_COUNT}, max ${MAX_COUNT})`
15348
+ description: `How many fresh leads to qualify (default ${DEFAULT_COUNT2}, max ${MAX_COUNT})`
14860
15349
  },
14861
15350
  leadIds: {
14862
15351
  type: "array",
@@ -14947,7 +15436,7 @@ var init_bulk_qualify_leads = __esm({
14947
15436
  ]
14948
15437
  },
14949
15438
  execute: async (client, params, ctx) => {
14950
- const wantCount = Math.min(params.count ?? DEFAULT_COUNT, MAX_COUNT);
15439
+ const wantCount = Math.min(params.count ?? DEFAULT_COUNT2, MAX_COUNT);
14951
15440
  const perLeadBudget = params.per_lead_budget_ms ?? DEFAULT_PER_LEAD_BUDGET_MS;
14952
15441
  const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS2;
14953
15442
  const totalDeadline = Date.now() + totalBudget;
@@ -15006,69 +15495,50 @@ var init_bulk_qualify_leads = __esm({
15006
15495
  per_lead_budget_ms: perLeadBudget,
15007
15496
  total_budget_ms: totalBudget
15008
15497
  });
15009
- const launched2 = [];
15010
- const failed2 = [];
15498
+ let launchedCount = 0;
15499
+ let notificationId = null;
15011
15500
  let quotaExceeded2 = false;
15501
+ let failed2 = [];
15012
15502
  if (!reservation.reused) {
15013
- for (const leadId of candidates) {
15014
- if (quotaExceeded2)
15015
- break;
15016
- try {
15017
- await client.requestVoid("POST", `/leads/${leadId}/web_fetch?force_fetch=false`);
15018
- launched2.push(leadId);
15019
- } catch (err) {
15020
- if (err?.code === "QUOTA_EXCEEDED") {
15021
- quotaExceeded2 = true;
15022
- } else if (err?.code === "NOT_FOUND") {
15023
- failed2.push({ lead_id: leadId, error: "lead not found" });
15024
- } else {
15025
- failed2.push({
15026
- lead_id: leadId,
15027
- error: err?.message ?? err?.code ?? "unknown"
15028
- });
15029
- }
15030
- }
15031
- }
15032
- if (failed2.length === candidates.length || launched2.length > 0 || quotaExceeded2) {
15033
- await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
15503
+ const launch = await launchBulkQualify(client, candidates, ctx);
15504
+ quotaExceeded2 = launch.quotaExceeded;
15505
+ notificationId = launch.resp?.notification_id ?? null;
15506
+ const queuedIds = launch.resp?.queued_ids ?? [];
15507
+ const skippedIds = launch.resp?.skipped_ids ?? [];
15508
+ launchedCount = queuedIds.length;
15509
+ const seen = /* @__PURE__ */ new Set([...queuedIds, ...skippedIds]);
15510
+ failed2 = candidates.filter((id) => !seen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
15511
+ if (queuedIds.length > 0 || quotaExceeded2 || skippedIds.length > 0 || failed2.length === candidates.length) {
15512
+ await ctx.bulkTracker.markLaunched(reservation.record.bulk_id, notificationId);
15034
15513
  }
15514
+ } else {
15515
+ notificationId = reservation.record.notification_id ?? null;
15516
+ launchedCount = reservation.record.lead_ids.length;
15035
15517
  }
15036
15518
  const out = {
15037
15519
  status: "running",
15038
15520
  handle_id: reservation.record.bulk_id,
15039
15521
  qualify_id: reservation.record.bulk_id,
15040
15522
  lead_ids: candidates,
15041
- launched_count: reservation.reused ? reservation.record.lead_ids.length : launched2.length,
15523
+ launched_count: launchedCount,
15042
15524
  failed: failed2,
15043
15525
  quota_exceeded: quotaExceeded2,
15044
15526
  lens_id: lensId,
15527
+ notification_id: notificationId,
15045
15528
  _meta: { region: client.region }
15046
15529
  };
15047
15530
  return out;
15048
15531
  }
15049
- const launched = [];
15050
- const failed = [];
15051
- let quotaExceeded = false;
15052
- for (const leadId of candidates) {
15053
- if (quotaExceeded)
15054
- break;
15055
- try {
15056
- await client.requestVoid("POST", `/leads/${leadId}/web_fetch?force_fetch=false`);
15057
- launched.push(leadId);
15058
- } catch (err) {
15059
- if (err?.code === "QUOTA_EXCEEDED") {
15060
- quotaExceeded = true;
15061
- ctx?.logger?.warn?.(`bulk_qualify_leads: 429 mid-fanout after launching ${launched.length}/${candidates.length} \u2014 stopping further launches but polling those already in flight`);
15062
- } else if (err?.code === "NOT_FOUND") {
15063
- failed.push({ lead_id: leadId, error: "lead not found" });
15064
- } else {
15065
- failed.push({
15066
- lead_id: leadId,
15067
- error: err?.message ?? err?.code ?? "unknown"
15068
- });
15069
- }
15070
- }
15532
+ const inlineLaunch = await launchBulkQualify(client, candidates, ctx);
15533
+ const quotaExceeded = inlineLaunch.quotaExceeded;
15534
+ const launched = inlineLaunch.resp?.queued_ids ?? [];
15535
+ const inlineSkipped = inlineLaunch.resp?.skipped_ids ?? [];
15536
+ const inlineNotificationId = inlineLaunch.resp?.notification_id ?? null;
15537
+ if (inlineNotificationId) {
15538
+ ctx?.logger?.info?.(`bulk_qualify_leads: launched bulk progress_notification_id=${inlineNotificationId} queued=${launched.length} skipped=${inlineSkipped.length}`);
15071
15539
  }
15540
+ const inlineFailedSeen = /* @__PURE__ */ new Set([...launched, ...inlineSkipped]);
15541
+ const failed = candidates.filter((id) => !inlineFailedSeen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
15072
15542
  let progressDone = 0;
15073
15543
  const progressTotal = launched.length;
15074
15544
  if (progressTotal > 0) {
@@ -15904,6 +16374,7 @@ var init_import_and_qualify = __esm({
15904
16374
  ...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
15905
16375
  qualify_id: null,
15906
16376
  import_ids: queued.importIds,
16377
+ notification_ids: queued.notification_ids ?? [],
15907
16378
  imported: queued.leads.map((l) => ({
15908
16379
  leadId: l.leadId,
15909
16380
  ...l.domain ? { domain: l.domain } : {},
@@ -15928,6 +16399,7 @@ var init_import_and_qualify = __esm({
15928
16399
  ...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
15929
16400
  qualify_id: null,
15930
16401
  import_ids: queued.importIds,
16402
+ notification_ids: queued.notification_ids ?? [],
15931
16403
  imported: [],
15932
16404
  not_imported: [],
15933
16405
  qualified: [],
@@ -15965,6 +16437,7 @@ var init_import_and_qualify = __esm({
15965
16437
  ...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
15966
16438
  qualify_id: null,
15967
16439
  import_ids: importResult.importIds,
16440
+ notification_ids: importResult.notification_ids ?? [],
15968
16441
  imported: [],
15969
16442
  not_imported: importResult.not_imported.map(toNotImportedEntry),
15970
16443
  qualified: [],
@@ -16002,6 +16475,7 @@ var init_import_and_qualify = __esm({
16002
16475
  ...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
16003
16476
  qualify_id: null,
16004
16477
  import_ids: importResult.importIds,
16478
+ notification_ids: importResult.notification_ids ?? [],
16005
16479
  imported,
16006
16480
  not_imported,
16007
16481
  qualified: [],
@@ -16112,6 +16586,7 @@ var init_import_and_qualify = __esm({
16112
16586
  ...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
16113
16587
  qualify_id: reservation.record.bulk_id,
16114
16588
  import_ids: importResult.importIds,
16589
+ notification_ids: importResult.notification_ids ?? [],
16115
16590
  imported,
16116
16591
  not_imported,
16117
16592
  qualified,
@@ -16707,16 +17182,20 @@ var init_bulk_store = __esm({
16707
17182
  this.logger?.info?.(`bulk.import_failed bulk_id=${bulk_id}`);
16708
17183
  });
16709
17184
  }
16710
- async markLaunched(bulk_id) {
17185
+ async markLaunched(bulk_id, notification_id) {
16711
17186
  return this.mutex.run(async () => {
16712
17187
  const all = this.prune(await this.readAll());
16713
17188
  const idx = all.findIndex((r) => r.bulk_id === bulk_id);
16714
17189
  if (idx < 0) {
16715
17190
  throw new Error(`bulk_id not found: ${bulk_id}`);
16716
17191
  }
16717
- all[idx] = { ...all[idx], status: "launched" };
17192
+ all[idx] = {
17193
+ ...all[idx],
17194
+ status: "launched",
17195
+ ...notification_id ? { notification_id } : {}
17196
+ };
16718
17197
  await this.writeAll(all);
16719
- this.logger?.info?.(`bulk.launched bulk_id=${bulk_id}`);
17198
+ this.logger?.info?.(`bulk.launched bulk_id=${bulk_id}${notification_id ? ` notification_id=${notification_id}` : ""}`);
16720
17199
  return all[idx];
16721
17200
  });
16722
17201
  }
@@ -16968,6 +17447,14 @@ var init_import_status = __esm({
16968
17447
  });
16969
17448
 
16970
17449
  // ../core/dist/composite/qualify-status.js
17450
+ async function readNotification(client, notificationId) {
17451
+ try {
17452
+ const page = await client.listNotifications({ archived: false, count: 50 });
17453
+ return page.items.find((n) => n.id === notificationId) ?? null;
17454
+ } catch {
17455
+ return null;
17456
+ }
17457
+ }
16971
17458
  var qualifyStatus;
16972
17459
  var init_qualify_status = __esm({
16973
17460
  "../core/dist/composite/qualify-status.js"() {
@@ -17129,6 +17616,16 @@ var init_qualify_status = __esm({
17129
17616
  const { _stillRunning, _failedCode, ...rest } = r;
17130
17617
  qualified.push(rest);
17131
17618
  }
17619
+ let bulkProgress = null;
17620
+ let inProgressFlag = null;
17621
+ const notifId = record.notification_id ?? null;
17622
+ if (notifId) {
17623
+ const n = await readNotification(client, notifId);
17624
+ if (n) {
17625
+ bulkProgress = n.bulk_progress;
17626
+ inProgressFlag = n.in_progress;
17627
+ }
17628
+ }
17132
17629
  const out = {
17133
17630
  qualify_id: record.bulk_id,
17134
17631
  launched_at: record.launched_at,
@@ -17140,6 +17637,9 @@ var init_qualify_status = __esm({
17140
17637
  still_running,
17141
17638
  failed,
17142
17639
  not_in_lens: [...notInLensSet],
17640
+ notification_id: notifId,
17641
+ bulk_progress: bulkProgress,
17642
+ in_progress: inProgressFlag,
17143
17643
  region: client.region,
17144
17644
  _meta: client.lastMeta ?? {
17145
17645
  region: client.region,
@@ -17152,6 +17652,9 @@ var init_qualify_status = __esm({
17152
17652
  out.per_lead_budget_ms = record.per_lead_budget_ms;
17153
17653
  if (record.total_budget_ms !== void 0)
17154
17654
  out.total_budget_ms = record.total_budget_ms;
17655
+ if (bulkProgress && bulkProgress.quota_hit_count > 0) {
17656
+ 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.";
17657
+ }
17155
17658
  return out;
17156
17659
  }
17157
17660
  };
@@ -17429,6 +17932,7 @@ var init_enrich_titles = __esm({
17429
17932
  bulk_id: res.record.bulk_id,
17430
17933
  launched_at: res.record.launched_at,
17431
17934
  durability: res.record.durability,
17935
+ notification_id: res.record.notification_id ?? null,
17432
17936
  seconds_since_original_launch: bulkSecondsSinceOriginal ?? 0,
17433
17937
  titles: params.titles,
17434
17938
  email,
@@ -17444,8 +17948,9 @@ var init_enrich_titles = __esm({
17444
17948
  total: 3,
17445
17949
  message: `Launching enrichment for ${params.titles.length} title${params.titles.length === 1 ? "" : "s"}\u2026`
17446
17950
  });
17951
+ let launchResp = null;
17447
17952
  try {
17448
- await client.requestVoid("POST", "/leads/selection/enrichment/launch", { titles: params.titles, email, phone });
17953
+ launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles: params.titles, email, phone });
17449
17954
  } catch (err) {
17450
17955
  const aborted = err?.name === "AbortError" || ctx?.signal?.aborted === true;
17451
17956
  if (bulkRecord && tracker) {
@@ -17469,9 +17974,10 @@ var init_enrich_titles = __esm({
17469
17974
  }
17470
17975
  throw err;
17471
17976
  }
17977
+ const notificationId = launchResp?.notification_id ?? null;
17472
17978
  if (bulkRecord && tracker) {
17473
17979
  try {
17474
- await tracker.markLaunched(bulkRecord.bulk_id);
17980
+ await tracker.markLaunched(bulkRecord.bulk_id, notificationId);
17475
17981
  } catch (e) {
17476
17982
  ctx?.logger?.warn?.(`enrich_titles: tracker.markLaunched failed: ${e?.message ?? e}`);
17477
17983
  return {
@@ -17499,8 +18005,9 @@ var init_enrich_titles = __esm({
17499
18005
  bulk_id: bulkRecord?.bulk_id,
17500
18006
  launched_at: bulkRecord?.launched_at,
17501
18007
  durability: bulkRecord?.durability,
17502
- message: bulkRecord ? "Enrichment job launched. Backend has no server-side bulk_id yet; MCP minted a client-side bulk_id (persisted to disk by default) so you can poll via leadbay_bulk_enrich_status." : "Enrichment job launched. No bulk_id tracker configured \u2014 poll leadbay_get_contacts per lead after ~60s; contact.enrichment.done flips to true.",
17503
- next_action: bulkRecord ? "Call leadbay_bulk_enrich_status({bulk_id}) after ~60s; pass include_contacts=true for the final read." : "Wait ~60s, then call leadbay_research_lead_by_id or leadbay_get_contacts on the leads you care about."
18008
+ notification_id: notificationId,
18009
+ message: notificationId ? "Enrichment job launched. The MCP is now listening for the backend notification \u2014 when enrichment finishes, a `_meta.notifications` entry will surface on your next tool response (also visible in `leadbay_account_status.notifications`)." : bulkRecord ? "Enrichment job launched. Backend did not return a notification id this time; poll via leadbay_bulk_enrich_status with the bulk_id." : "Enrichment job launched. No bulk_id tracker configured \u2014 poll leadbay_get_contacts per lead after ~60s; contact.enrichment.done flips to true.",
18010
+ next_action: notificationId ? "Wait for the next `_meta.notifications` entry (typically <2 min for a small batch). If you want progress sooner, call leadbay_bulk_enrich_status({bulk_id})." : bulkRecord ? "Call leadbay_bulk_enrich_status({bulk_id}) after ~60s; pass include_contacts=true for the final read." : "Wait ~60s, then call leadbay_research_lead_by_id or leadbay_get_contacts on the leads you care about."
17504
18011
  };
17505
18012
  } finally {
17506
18013
  try {
@@ -17518,6 +18025,14 @@ var init_enrich_titles = __esm({
17518
18025
  });
17519
18026
 
17520
18027
  // ../core/dist/composite/bulk-enrich-status.js
18028
+ async function readNotification2(client, notificationId) {
18029
+ try {
18030
+ const page = await client.listNotifications({ archived: false, count: 50 });
18031
+ return page.items.find((n) => n.id === notificationId) ?? null;
18032
+ } catch {
18033
+ return null;
18034
+ }
18035
+ }
17521
18036
  async function pMap(items, fn, concurrency) {
17522
18037
  const out = new Array(items.length);
17523
18038
  let next = 0;
@@ -17689,6 +18204,53 @@ var init_bulk_enrich_status = __esm({
17689
18204
  launched_at: record.launched_at
17690
18205
  };
17691
18206
  }
18207
+ const notifId = record.notification_id ?? null;
18208
+ if (notifId) {
18209
+ const n = await readNotification2(client, notifId);
18210
+ if (n && n.bulk_progress) {
18211
+ const bp = n.bulk_progress;
18212
+ const inProgress = n.in_progress;
18213
+ let leads2 = [];
18214
+ if (!inProgress && includeContacts) {
18215
+ leads2 = await pMap(record.lead_ids, async (leadId) => {
18216
+ try {
18217
+ const out = await getContacts.execute(client, { leadId });
18218
+ const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
18219
+ return { lead_id: leadId, contacts };
18220
+ } catch {
18221
+ return { lead_id: leadId };
18222
+ }
18223
+ }, STATUS_FETCH_CONCURRENCY);
18224
+ } else {
18225
+ leads2 = record.lead_ids.map((id) => ({ lead_id: id }));
18226
+ }
18227
+ 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}`);
18228
+ return {
18229
+ bulk_id: record.bulk_id,
18230
+ notification_id: notifId,
18231
+ launched_at: record.launched_at,
18232
+ status: record.status,
18233
+ durability: record.durability,
18234
+ titles: record.titles,
18235
+ email: record.email,
18236
+ phone: record.phone,
18237
+ lens_id: record.lens_id,
18238
+ leads: leads2,
18239
+ overall_progress: {
18240
+ done: bp.success_count + bp.failure_count + bp.quota_hit_count,
18241
+ total: bp.total_count,
18242
+ done_ratio: bp.total_count === 0 ? 0 : (bp.success_count + bp.failure_count + bp.quota_hit_count) / bp.total_count
18243
+ },
18244
+ bulk_progress: bp,
18245
+ in_progress: inProgress,
18246
+ all_done: !inProgress,
18247
+ ...bp.quota_hit_count > 0 ? {
18248
+ 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."
18249
+ } : {}
18250
+ };
18251
+ }
18252
+ ctx?.logger?.info?.(`bulk_enrich_status: notification ${notifId} not yet visible; falling back to per-lead fan-out`);
18253
+ }
17692
18254
  let doneSoFar = 0;
17693
18255
  const totalLeads = record.lead_ids.length;
17694
18256
  const results = await pMap(record.lead_ids, async (leadId) => {
@@ -19457,8 +20019,11 @@ __export(dist_exports, {
19457
20019
  InMemoryBulkStore: () => InMemoryBulkStore,
19458
20020
  LeadbayClient: () => LeadbayClient,
19459
20021
  LocalBulkStore: () => LocalBulkStore,
20022
+ NotificationsInbox: () => NotificationsInbox,
20023
+ NotificationsWsClient: () => NotificationsWsClient,
19460
20024
  REGIONS: () => REGIONS,
19461
20025
  accountStatus: () => accountStatus,
20026
+ acknowledgeNotification: () => acknowledgeNotification,
19462
20027
  addLeadsToCampaign: () => addLeadsToCampaign,
19463
20028
  addNote: () => addNote,
19464
20029
  adjustAudience: () => adjustAudience,
@@ -19466,6 +20031,7 @@ __export(dist_exports, {
19466
20031
  agentMemoryRecall: () => agentMemoryRecall,
19467
20032
  agentMemoryReview: () => agentMemoryReview,
19468
20033
  agentMemoryTools: () => agentMemoryTools,
20034
+ anchorIdFor: () => anchorIdFor,
19469
20035
  answerClarification: () => answerClarification,
19470
20036
  appendEntry: () => appendEntry,
19471
20037
  appendTombstone: () => appendTombstone,
@@ -19474,6 +20040,7 @@ __export(dist_exports, {
19474
20040
  bulkQualifyLeads: () => bulkQualifyLeads,
19475
20041
  campaignCallSheet: () => campaignCallSheet,
19476
20042
  campaignProgression: () => campaignProgression,
20043
+ catchUpNotifications: () => catchUpNotifications,
19477
20044
  clearAgentMemoryCache: () => clearAgentMemoryCache,
19478
20045
  clearMockJournal: () => clearMockJournal,
19479
20046
  clearSelection: () => clearSelection,
@@ -19523,6 +20090,7 @@ __export(dist_exports, {
19523
20090
  importAndQualify: () => importAndQualify,
19524
20091
  importLeads: () => importLeads,
19525
20092
  importStatus: () => importStatus,
20093
+ inferKind: () => inferKind,
19526
20094
  invalidateAgentMemoryCache: () => invalidateAgentMemoryCache,
19527
20095
  isAgentMemoryEnabled: () => isAgentMemoryEnabled,
19528
20096
  isValidBulkId: () => isValidBulkId,
@@ -19563,12 +20131,14 @@ __export(dist_exports, {
19563
20131
  resolveAgentMemorySummary: () => resolveAgentMemorySummary,
19564
20132
  resolveImportRows: () => resolveImportRows,
19565
20133
  resolveRegion: () => resolveRegion,
20134
+ reviseHintFor: () => reviseHintFor,
19566
20135
  seedCandidates: () => seedCandidates,
19567
20136
  selectLeads: () => selectLeads,
19568
20137
  setActiveLens: () => setActiveLens,
19569
20138
  setEpilogueStatus: () => setEpilogueStatus,
19570
20139
  setPushback: () => setPushback,
19571
20140
  setUserPrompt: () => setUserPrompt,
20141
+ toInboxEntry: () => toInboxEntry,
19572
20142
  tools: () => tools,
19573
20143
  tourPlan: () => tourPlan,
19574
20144
  updateLens: () => updateLens,
@@ -19583,6 +20153,7 @@ var init_dist = __esm({
19583
20153
  init_types();
19584
20154
  init_agent_memory();
19585
20155
  init_composite_file_names();
20156
+ init_notifications();
19586
20157
  init_login();
19587
20158
  init_list_lenses();
19588
20159
  init_discover_leads();
@@ -19612,6 +20183,7 @@ var init_dist = __esm({
19612
20183
  init_agent_memory_recall();
19613
20184
  init_agent_memory_capture();
19614
20185
  init_agent_memory_review();
20186
+ init_acknowledge_notification();
19615
20187
  init_select_leads();
19616
20188
  init_deselect_leads();
19617
20189
  init_clear_selection();
@@ -19771,7 +20343,13 @@ var init_dist = __esm({
19771
20343
  // didn't deliver"). Does not mutate Leadbay state; emits a PostHog
19772
20344
  // event only. Companion to leadbay_report_outreach (which DOES write
19773
20345
  // to the backend and stays gated behind LEADBAY_MCP_WRITE).
19774
- reportFriction
20346
+ reportFriction,
20347
+ // Notification ack — ALWAYS exposed even though it POSTs to /seen.
20348
+ // _meta.notifications surfaces terminal bulk-progress notifications on
20349
+ // every tool response regardless of write gating; without ack the agent
20350
+ // sees the same entries on every call forever. Pairing the surfacing
20351
+ // channel with the clearing tool is non-optional.
20352
+ acknowledgeNotification
19775
20353
  ];
19776
20354
  compositeWriteTools = [
19777
20355
  bulkQualifyLeads,
@@ -22119,6 +22697,22 @@ function buildServer(client, opts = {}) {
22119
22697
  });
22120
22698
  }
22121
22699
  };
22700
+ const maybeAttachNotifications = (result) => {
22701
+ const inbox = opts.notificationsInbox;
22702
+ if (!inbox) return;
22703
+ if (result === null || typeof result !== "object" || Array.isArray(result)) {
22704
+ return;
22705
+ }
22706
+ const entries = inbox.list();
22707
+ if (entries.length === 0) return;
22708
+ const envelope = result;
22709
+ const target = envelope.__markdown_envelope === true && envelope.structured !== null && typeof envelope.structured === "object" && !Array.isArray(envelope.structured) ? envelope.structured : envelope;
22710
+ const existingMeta = target._meta && typeof target._meta === "object" && !Array.isArray(target._meta) ? target._meta : {};
22711
+ target._meta = {
22712
+ ...existingMeta,
22713
+ notifications: entries
22714
+ };
22715
+ };
22122
22716
  const isLeadbayBusinessError = (err) => err != null && typeof err === "object" && err.error === true && typeof err.code === "string";
22123
22717
  const buildBusinessCtx = (toolName, envelope, triggered_by) => {
22124
22718
  const meta = envelope._meta ?? {};
@@ -22238,11 +22832,13 @@ function buildServer(client, opts = {}) {
22238
22832
  const result = await tool.execute(client, args, {
22239
22833
  logger: opts.logger,
22240
22834
  bulkTracker: opts.bulkTracker,
22835
+ notificationsInbox: opts.notificationsInbox,
22241
22836
  signal: extra.signal,
22242
22837
  progress,
22243
22838
  elicit
22244
22839
  });
22245
22840
  maybeAttachUpdate(name, result);
22841
+ maybeAttachNotifications(result);
22246
22842
  if (result && typeof result === "object" && result.error === true) {
22247
22843
  const envText = formatErrorForLLM(result);
22248
22844
  const envDur = Date.now() - callStart;
@@ -23680,7 +24276,7 @@ var OAUTH_BASE_URLS = {
23680
24276
  fr: "https://staging.api.leadbay.app"
23681
24277
  }
23682
24278
  };
23683
- var VERSION = "0.17.3";
24279
+ var VERSION = "0.18.0";
23684
24280
  var HELP = `
23685
24281
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
23686
24282
 
@@ -24777,21 +25373,41 @@ async function main() {
24777
25373
  logger.warn?.(`update_check.unexpected ${err?.message ?? err}`);
24778
25374
  });
24779
25375
  }
25376
+ const notificationsInbox = new NotificationsInbox();
25377
+ let notificationsWs = null;
25378
+ const WS_DISABLED = process.env.LEADBAY_NOTIFICATIONS_WS_DISABLED === "1" || authState !== "ok";
25379
+ if (!WS_DISABLED) {
25380
+ notificationsWs = new NotificationsWsClient({
25381
+ client,
25382
+ inbox: notificationsInbox,
25383
+ logger
25384
+ });
25385
+ void notificationsWs.start().catch((err) => {
25386
+ logger.warn?.(
25387
+ `notifications.ws start_failed: ${err?.message ?? err}`
25388
+ );
25389
+ });
25390
+ }
24780
25391
  const server = buildServer(client, {
24781
25392
  includeAdvanced,
24782
25393
  includeWrite,
24783
25394
  logger,
24784
25395
  bulkTracker,
25396
+ notificationsInbox,
24785
25397
  version: VERSION,
24786
25398
  telemetry,
24787
25399
  updateStateStore
24788
25400
  });
24789
25401
  const transport = new StdioServerTransport();
24790
25402
  logger.info?.(
24791
- `Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability}, auth_state=${authState})`
25403
+ `Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability}, notifications_ws=${WS_DISABLED ? "disabled" : "enabled"}, auth_state=${authState})`
24792
25404
  );
24793
25405
  await server.connect(transport);
24794
25406
  const shutdown = async (code) => {
25407
+ try {
25408
+ notificationsWs?.stop();
25409
+ } catch {
25410
+ }
24795
25411
  try {
24796
25412
  await telemetry.shutdown();
24797
25413
  } finally {