@sellable/mcp 0.1.513 → 0.1.514

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.
@@ -0,0 +1,788 @@
1
+ import { getApi, SellableApiError } from "../api.js";
2
+ import { startPrepareCampaignMessages } from "./campaign-message-preparation.js";
3
+ import { startCampaign } from "./campaigns.js";
4
+ import { confirmLeadList, importLeads, searchSignals, selectPromisingPosts, } from "./leads.js";
5
+ import { markProviderPromptLoaded } from "./provider-preflight.js";
6
+ import { refreshPaidInmailCredits } from "./senders.js";
7
+ import { workspaceRequestOptions } from "./workspace-context.js";
8
+ const PAID_INMAIL_REFRESH_MAX_ATTEMPTS = 3;
9
+ const PAID_INMAIL_REFRESH_RETRY_DELAY_MS = Number(process.env.SELLABLE_MCP_PAID_REFRESH_RETRY_DELAY_MS ?? "1000");
10
+ const SIGNAL_DISCOVERY_MIN_REFILL_POSTS = 3;
11
+ const SIGNAL_DISCOVERY_MAX_REFILL_POSTS = 10;
12
+ const SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST = 150;
13
+ export function normalizeStrings(values) {
14
+ if (!Array.isArray(values))
15
+ return [];
16
+ return [
17
+ ...new Set(values
18
+ .map((value) => (typeof value === "string" ? value.trim() : ""))
19
+ .filter(Boolean)),
20
+ ];
21
+ }
22
+ export function sleep(ms) {
23
+ if (!Number.isFinite(ms) || ms <= 0)
24
+ return Promise.resolve();
25
+ return new Promise((resolve) => setTimeout(resolve, ms));
26
+ }
27
+ export function recordValue(value) {
28
+ return value && typeof value === "object" && !Array.isArray(value)
29
+ ? value
30
+ : null;
31
+ }
32
+ export function stringValue(value) {
33
+ return typeof value === "string" && value.trim() ? value.trim() : null;
34
+ }
35
+ export function numberValue(value) {
36
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
37
+ }
38
+ export function stringArray(value) {
39
+ if (!Array.isArray(value))
40
+ return [];
41
+ return value.filter((item) => typeof item === "string");
42
+ }
43
+ export function prepareRowSelectorValue(value) {
44
+ const selector = recordValue(value);
45
+ const type = stringValue(selector?.type);
46
+ if (type !== "needsEnrichment" &&
47
+ type !== "needsApproval" &&
48
+ type !== "needsGeneratedMessage" &&
49
+ type !== "reviewBatch" &&
50
+ type !== "staleGeneratedMessages") {
51
+ return undefined;
52
+ }
53
+ const limit = numberValue(selector?.limit);
54
+ return {
55
+ type,
56
+ ...(limit && limit > 0 ? { limit: Math.floor(limit) } : {}),
57
+ };
58
+ }
59
+ export function uniqueStrings(values) {
60
+ const seen = new Set();
61
+ const result = [];
62
+ for (const value of values) {
63
+ const normalized = typeof value === "string" ? value.trim() : "";
64
+ if (!normalized)
65
+ continue;
66
+ const key = normalized.toLowerCase();
67
+ if (seen.has(key))
68
+ continue;
69
+ seen.add(key);
70
+ result.push(normalized);
71
+ }
72
+ return result;
73
+ }
74
+ export function userAddedRowsLimitPayloadFromError(error) {
75
+ if (!(error instanceof SellableApiError) || error.status !== 400) {
76
+ return null;
77
+ }
78
+ try {
79
+ const parsed = JSON.parse(error.body);
80
+ if (parsed?.code !== "USER_ADDED_ROWS_LIMIT_EXCEEDED")
81
+ return null;
82
+ return parsed;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ export function paidRefreshActionFrom(value) {
89
+ const action = recordValue(value);
90
+ if (!action || action.type !== "refresh_paid_inmail_credits")
91
+ return null;
92
+ const ids = recordValue(action.ids);
93
+ const senderId = stringValue(ids?.senderId) ?? stringValue(action.senderId);
94
+ if (!senderId)
95
+ return null;
96
+ return {
97
+ senderId,
98
+ actionKey: stringValue(action.actionKey),
99
+ action,
100
+ };
101
+ }
102
+ export function collectPaidInmailRefreshActions(plan) {
103
+ const root = recordValue(plan);
104
+ const target = recordValue(root?.target);
105
+ const bySender = new Map();
106
+ const add = (candidate) => {
107
+ const action = paidRefreshActionFrom(candidate);
108
+ if (action && !bySender.has(action.senderId)) {
109
+ bySender.set(action.senderId, action);
110
+ }
111
+ };
112
+ const senderPlans = Array.isArray(target?.senderRefillPlans)
113
+ ? target.senderRefillPlans
114
+ : [];
115
+ for (const senderPlan of senderPlans) {
116
+ const planRecord = recordValue(senderPlan);
117
+ const nextActions = Array.isArray(planRecord?.nextActions)
118
+ ? planRecord.nextActions
119
+ : [];
120
+ for (const action of nextActions)
121
+ add(action);
122
+ const paidInmail = recordValue(planRecord?.paidInmail);
123
+ const paidStatus = stringValue(paidInmail?.status);
124
+ const senderId = stringValue(planRecord?.senderId) ?? stringValue(paidInmail?.senderId);
125
+ if (senderId &&
126
+ (paidStatus === "missing_credit_facts" ||
127
+ paidStatus === "stale_credit_facts")) {
128
+ add({
129
+ type: "refresh_paid_inmail_credits",
130
+ actionKey: [
131
+ "refresh_paid_inmail_credits",
132
+ senderId,
133
+ stringValue(paidInmail?.campaignId) ?? "unknown",
134
+ stringValue(paidInmail?.columnId) ?? "unknown",
135
+ ].join(":"),
136
+ actionType: "send_inmail_closed",
137
+ senderId,
138
+ campaignId: stringValue(paidInmail?.campaignId) ?? undefined,
139
+ tableId: stringValue(paidInmail?.tableId) ?? undefined,
140
+ columnId: stringValue(paidInmail?.columnId) ?? undefined,
141
+ oldThreshold: numberValue(paidInmail?.threshold) ?? undefined,
142
+ maxStalenessSeconds: numberValue(paidInmail?.maxStalenessSeconds) ?? undefined,
143
+ ids: {
144
+ senderId,
145
+ campaignId: stringValue(paidInmail?.campaignId) ?? undefined,
146
+ tableId: stringValue(paidInmail?.tableId) ?? undefined,
147
+ columnId: stringValue(paidInmail?.columnId) ?? undefined,
148
+ },
149
+ });
150
+ }
151
+ }
152
+ const globalActionQueue = Array.isArray(target?.globalActionQueue)
153
+ ? target.globalActionQueue
154
+ : [];
155
+ for (const action of globalActionQueue)
156
+ add(action);
157
+ const actionCandidates = Array.isArray(root?.actionCandidates)
158
+ ? root.actionCandidates
159
+ : [];
160
+ for (const action of actionCandidates)
161
+ add(action);
162
+ return [...bySender.values()];
163
+ }
164
+ export function firstGlobalAction(plan) {
165
+ const root = recordValue(plan);
166
+ const target = recordValue(root?.target);
167
+ const globalActionQueue = Array.isArray(target?.globalActionQueue)
168
+ ? target.globalActionQueue
169
+ : [];
170
+ const [first] = globalActionQueue;
171
+ return recordValue(first);
172
+ }
173
+ export function actionIds(action) {
174
+ return recordValue(action.ids) ?? {};
175
+ }
176
+ export function actionToolInput(action) {
177
+ return recordValue(action.toolInput) ?? {};
178
+ }
179
+ export function actionCampaignId(action) {
180
+ const ids = actionIds(action);
181
+ const toolInput = actionToolInput(action);
182
+ return (stringValue(toolInput.campaignId) ??
183
+ stringValue(toolInput.campaignOfferId) ??
184
+ stringValue(ids.campaignId) ??
185
+ stringValue(action.campaignId));
186
+ }
187
+ export function actionTableId(action) {
188
+ const ids = actionIds(action);
189
+ const toolInput = actionToolInput(action);
190
+ return (stringValue(toolInput.tableId) ??
191
+ stringValue(ids.tableId) ??
192
+ stringValue(action.tableId));
193
+ }
194
+ export function actionSourceLeadListId(action) {
195
+ const ids = actionIds(action);
196
+ const toolInput = actionToolInput(action);
197
+ return (stringValue(toolInput.sourceLeadListId) ??
198
+ stringValue(ids.sourceLeadListId) ??
199
+ stringValue(action.sourceLeadListId));
200
+ }
201
+ export function actionSenderId(action) {
202
+ const ids = actionIds(action);
203
+ const toolInput = actionToolInput(action);
204
+ return (stringValue(toolInput.senderId) ??
205
+ stringValue(ids.senderId) ??
206
+ stringValue(action.senderId));
207
+ }
208
+ export function actionActionType(action) {
209
+ const ids = actionIds(action);
210
+ const toolInput = actionToolInput(action);
211
+ return (stringValue(toolInput.actionType) ??
212
+ stringValue(toolInput.selectedLane) ??
213
+ stringValue(ids.actionType) ??
214
+ stringValue(action.actionType) ??
215
+ stringValue(action.selectedLane));
216
+ }
217
+ function rerunErroredCellsOperations(action) {
218
+ const toolInput = actionToolInput(action);
219
+ const operations = Array.isArray(toolInput.operations)
220
+ ? toolInput.operations
221
+ : [];
222
+ return operations.flatMap((raw) => {
223
+ const operation = recordValue(raw);
224
+ const columnRole = stringValue(operation?.columnRole);
225
+ if (columnRole !== "icpScore" && columnRole !== "generateMessage") {
226
+ return [];
227
+ }
228
+ const rowSelector = recordValue(operation?.rowSelector);
229
+ if (rowSelector?.type !== "rowIds")
230
+ return [];
231
+ const rowIds = stringArray(rowSelector.rowIds);
232
+ if (rowIds.length === 0)
233
+ return [];
234
+ return [
235
+ {
236
+ columnRole,
237
+ rowSelector: { type: "rowIds", rowIds },
238
+ cellIds: stringArray(operation?.cellIds),
239
+ },
240
+ ];
241
+ });
242
+ }
243
+ export function actionSourceFingerprint(action) {
244
+ const ids = actionIds(action);
245
+ const toolInput = actionToolInput(action);
246
+ return (stringValue(toolInput.sourceFingerprint) ??
247
+ stringValue(ids.sourceFingerprint) ??
248
+ stringValue(action.sourceFingerprint));
249
+ }
250
+ export function actionLeadSourceProvider(action) {
251
+ const ids = actionIds(action);
252
+ const toolInput = actionToolInput(action);
253
+ return (stringValue(toolInput.leadSourceProvider) ??
254
+ stringValue(toolInput.provider) ??
255
+ stringValue(ids.leadSourceProvider) ??
256
+ stringValue(action.leadSourceProvider));
257
+ }
258
+ export function normalizedSignalKeyword(value) {
259
+ if (typeof value !== "string")
260
+ return null;
261
+ const keyword = value.trim();
262
+ if (!keyword)
263
+ return null;
264
+ if (/^(https?:\/\/|www\.|linkedin\.com\/|\/?in\/)/i.test(keyword)) {
265
+ return null;
266
+ }
267
+ return keyword;
268
+ }
269
+ export function keywordsFromSignalTabs(tabs) {
270
+ const selectedKeywords = [];
271
+ const fallbackKeywords = [];
272
+ for (const tab of tabs) {
273
+ const keyword = normalizedSignalKeyword(tab.keyword);
274
+ if (!keyword)
275
+ continue;
276
+ const selected = (tab.posts ?? []).some((post) => post.isSelected === true);
277
+ if (selected) {
278
+ selectedKeywords.push(keyword);
279
+ }
280
+ else {
281
+ fallbackKeywords.push(keyword);
282
+ }
283
+ }
284
+ return uniqueStrings([...selectedKeywords, ...fallbackKeywords]).slice(0, 5);
285
+ }
286
+ export function selectedPostIdsFromSignalTabs(tabs) {
287
+ return uniqueStrings(tabs.flatMap((tab) => (tab.posts ?? [])
288
+ .filter((post) => post.isSelected === true)
289
+ .map((post) => post.id)));
290
+ }
291
+ export function postIdsFromSignalSearch(summary, maxPosts, options = {}) {
292
+ const excludedPostIds = new Set((options.excludePostIds ?? []).map((postId) => postId.toLowerCase()));
293
+ const recommendedPostIds = stringArray(summary?.recommendedPostIds);
294
+ const topPostIds = Array.isArray(summary?.topPosts)
295
+ ? summary.topPosts
296
+ .map((post) => post && typeof post === "object"
297
+ ? stringValue(post.id)
298
+ : null)
299
+ .filter((id) => Boolean(id))
300
+ : [];
301
+ return uniqueStrings([...recommendedPostIds, ...topPostIds])
302
+ .filter((postId) => !excludedPostIds.has(postId.toLowerCase()))
303
+ .slice(0, Math.max(1, maxPosts));
304
+ }
305
+ export function maxSignalDiscoveryPostsForSourceLimit(sourceRowLimit) {
306
+ return Math.min(SIGNAL_DISCOVERY_MAX_REFILL_POSTS, Math.max(SIGNAL_DISCOVERY_MIN_REFILL_POSTS, Math.ceil(sourceRowLimit / SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST)));
307
+ }
308
+ export function refillPrepareRequestHash(params) {
309
+ return [
310
+ "refill_sends",
311
+ "prepare_messages",
312
+ params.campaignId,
313
+ params.tableId ?? "no-table",
314
+ actionSenderId(params.action) ?? "all-senders",
315
+ actionSourceLeadListId(params.action) ?? "no-source-list",
316
+ actionActionType(params.action) ?? "no-action-type",
317
+ params.approvalMode,
318
+ params.rowSelector
319
+ ? `${params.rowSelector.type}:${params.rowSelector.limit ?? "no-limit"}`
320
+ : "no-row-selector",
321
+ ].join(":");
322
+ }
323
+ export function boundedApprovalLimit(action) {
324
+ const toolInput = actionToolInput(action);
325
+ const rowSelector = recordValue(toolInput.rowSelector);
326
+ const selectorLimit = numberValue(rowSelector?.limit);
327
+ const inputLimit = numberValue(toolInput.limit);
328
+ const limit = selectorLimit ?? inputLimit;
329
+ if (!limit || limit <= 0)
330
+ return null;
331
+ return Math.floor(limit);
332
+ }
333
+ export async function approveGeneratedMessagesBatch(action, workspaceId) {
334
+ const tableId = actionTableId(action);
335
+ const toolInput = actionToolInput(action);
336
+ const columnId = stringValue(toolInput.columnId) ?? stringValue(actionIds(action).columnId);
337
+ const limit = boundedApprovalLimit(action);
338
+ if (!tableId || !limit) {
339
+ return {
340
+ status: "refused",
341
+ refusalReason: "approve_messages action is missing tableId or a bounded rowSelector.limit",
342
+ };
343
+ }
344
+ const api = getApi();
345
+ const requestOptions = workspaceRequestOptions(workspaceId);
346
+ const result = await api.post("/api/v3/workflow-tables/cells/approve-batch", {
347
+ tableId,
348
+ ...(columnId ? { columnId } : {}),
349
+ limit,
350
+ scope: "generated_unapproved",
351
+ ...(workspaceId ? { workspaceId } : {}),
352
+ }, requestOptions);
353
+ return { status: "executed_and_reread", result };
354
+ }
355
+ export async function continueSignalDiscoverySource(action, workspaceId) {
356
+ const campaignOfferId = actionCampaignId(action);
357
+ const sourceLeadListId = actionSourceLeadListId(action);
358
+ const sourceFingerprint = actionSourceFingerprint(action);
359
+ const toolInput = actionToolInput(action);
360
+ const sourceRowLimit = Math.min(1500, Math.max(100, Math.floor(numberValue(toolInput.sourceRowLimit) ??
361
+ numberValue(toolInput.targetRows) ??
362
+ numberValue(action.targetRows) ??
363
+ 100)));
364
+ const maxPostsToScrape = maxSignalDiscoveryPostsForSourceLimit(sourceRowLimit);
365
+ if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
366
+ return {
367
+ status: "refused",
368
+ refusalReason: "continue_signal_discovery_source action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
369
+ };
370
+ }
371
+ const requestedProvider = actionLeadSourceProvider(action);
372
+ if (requestedProvider &&
373
+ requestedProvider !== "signal-discovery" &&
374
+ requestedProvider !== "campaign-tracked-post") {
375
+ return {
376
+ status: "refused",
377
+ refusalReason: "continue_signal_discovery_source can only run for Signal Discovery source families",
378
+ };
379
+ }
380
+ const api = getApi();
381
+ const requestOptions = workspaceRequestOptions(workspaceId);
382
+ const campaign = await api.get(`/api/v2/campaign-offers/${campaignOfferId}`, requestOptions);
383
+ if (campaign.leadSourceProvider !== "signal-discovery" &&
384
+ campaign.leadSourceProvider !== "campaign-tracked-post") {
385
+ return {
386
+ status: "refused",
387
+ refusalReason: "campaign leadSourceProvider is not Signal Discovery; refusing same-source continuation",
388
+ };
389
+ }
390
+ if (campaign.selectedLeadListId &&
391
+ campaign.selectedLeadListId !== sourceLeadListId) {
392
+ return {
393
+ status: "refused",
394
+ refusalReason: "campaign selectedLeadListId changed since the plan packet; rerun get_refill_target_plan before source continuation",
395
+ };
396
+ }
397
+ const sourceMeta = await api.get(`/api/v3/workflow-tables/${sourceLeadListId}?mode=meta`, requestOptions);
398
+ const sourceConfig = sourceMeta.table?.config ?? null;
399
+ const headlineICPCriteria = stringArray(sourceConfig?.headlineICPCriteria).length > 0
400
+ ? stringArray(sourceConfig?.headlineICPCriteria)
401
+ : stringArray(sourceConfig?.rubricGuidelines);
402
+ const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`, requestOptions);
403
+ const signalTabs = tabsResponse.tabs ?? [];
404
+ const keywords = keywordsFromSignalTabs(signalTabs);
405
+ const excludedPostIds = selectedPostIdsFromSignalTabs(signalTabs);
406
+ if (keywords.length === 0) {
407
+ return {
408
+ status: "refused",
409
+ refusalReason: "no reusable Signal Discovery keywords were found on the campaign tabs",
410
+ };
411
+ }
412
+ markProviderPromptLoaded({
413
+ provider: "signal-discovery",
414
+ campaignOfferId,
415
+ });
416
+ const searchSummary = await searchSignals({
417
+ type: "keywords",
418
+ keywords: keywords.map((keyword) => ({
419
+ keyword,
420
+ source: "refill-sends-source-continuation",
421
+ })),
422
+ campaignOfferId,
423
+ currentStep: null,
424
+ headlineICPCriteria,
425
+ rubricGuidelines: headlineICPCriteria,
426
+ confirmed: true,
427
+ limit: 50,
428
+ ...(workspaceId ? { workspaceId } : {}),
429
+ });
430
+ const selectedPostIds = postIdsFromSignalSearch(searchSummary, maxPostsToScrape, { excludePostIds: excludedPostIds });
431
+ if (selectedPostIds.length === 0) {
432
+ return {
433
+ status: "refused",
434
+ refusalReason: "Signal Discovery search returned no new recommended posts to continue the source",
435
+ result: { keywords, excludedPostIds, searchSummary },
436
+ };
437
+ }
438
+ const selectionResult = await selectPromisingPosts({
439
+ campaignOfferId,
440
+ selections: selectedPostIds.map((postId) => ({
441
+ postId,
442
+ reason: "refill_sends same-source continuation: recent post from the campaign's existing Signal Discovery keyword family",
443
+ })),
444
+ headlineICPCriteria,
445
+ currentStep: null,
446
+ selectionMode: "replace",
447
+ scrapePlanMode: "all-selected",
448
+ ...(workspaceId ? { workspaceId } : {}),
449
+ });
450
+ if (selectionResult.success === false) {
451
+ return {
452
+ status: "refused",
453
+ refusalReason: stringValue(selectionResult.message) ??
454
+ "select_promising_posts did not select any posts",
455
+ result: { keywords, selectedPostIds, selectionResult },
456
+ };
457
+ }
458
+ const importResult = await importLeads({
459
+ campaignOfferId,
460
+ provider: "signal-discovery",
461
+ sourceLeadListId,
462
+ currentStep: null,
463
+ headlineICPCriteria,
464
+ rubricGuidelines: headlineICPCriteria,
465
+ confirmed: true,
466
+ maxPostsToScrape: selectedPostIds.length,
467
+ allowInvalidSignalPosts: true,
468
+ ...(workspaceId ? { workspaceId } : {}),
469
+ });
470
+ const importRecord = recordValue(importResult);
471
+ if (importRecord?.error) {
472
+ return {
473
+ status: "refused",
474
+ refusalReason: stringValue(importRecord.message) ??
475
+ `Signal Discovery import returned ${String(importRecord.error)}`,
476
+ result: {
477
+ keywords,
478
+ excludedPostIds,
479
+ selectedPostIds,
480
+ selectionResult,
481
+ importResult,
482
+ },
483
+ };
484
+ }
485
+ return {
486
+ status: "executed_and_reread",
487
+ result: {
488
+ provider: "signal-discovery",
489
+ campaignOfferId,
490
+ previousSourceLeadListId: sourceLeadListId,
491
+ sourceFingerprint,
492
+ keywords,
493
+ excludedPostIds,
494
+ selectedPostIds,
495
+ sourceRowLimit,
496
+ maxPostsToScrape: selectedPostIds.length,
497
+ searchSummary,
498
+ selectionResult,
499
+ importResult,
500
+ },
501
+ };
502
+ }
503
+ export async function refreshPaidInmailCreditsWithRetry(senderId, workspaceId) {
504
+ const errors = [];
505
+ for (let attempt = 1; attempt <= PAID_INMAIL_REFRESH_MAX_ATTEMPTS; attempt += 1) {
506
+ try {
507
+ const receipt = await refreshPaidInmailCredits({
508
+ senderId,
509
+ workspaceId,
510
+ });
511
+ return { receipt, attempts: attempt, errors };
512
+ }
513
+ catch (error) {
514
+ errors.push(error instanceof Error ? error.message : String(error));
515
+ if (attempt < PAID_INMAIL_REFRESH_MAX_ATTEMPTS) {
516
+ await sleep(PAID_INMAIL_REFRESH_RETRY_DELAY_MS);
517
+ }
518
+ }
519
+ }
520
+ return {
521
+ receipt: null,
522
+ attempts: PAID_INMAIL_REFRESH_MAX_ATTEMPTS,
523
+ errors,
524
+ };
525
+ }
526
+ export async function executeStartCampaignPrimitive(params) {
527
+ void params.workspaceId;
528
+ const campaignId = actionCampaignId(params.action);
529
+ if (params.action.type !== "start_paused_campaign") {
530
+ return {
531
+ executed: false,
532
+ refused: true,
533
+ reason: "action_type_not_start_paused_campaign",
534
+ campaignId,
535
+ laneChainCampaignIds: params.authority.laneChainCampaignIds,
536
+ };
537
+ }
538
+ const authorizedByPinnedCampaign = campaignId !== null && campaignId === params.authority.pinnedCampaignId;
539
+ if (campaignId &&
540
+ (params.authority.laneChainCampaignIds.includes(campaignId) ||
541
+ authorizedByPinnedCampaign)) {
542
+ try {
543
+ const result = await startCampaign(campaignId);
544
+ return {
545
+ executed: true,
546
+ type: "start_paused_campaign",
547
+ campaignId,
548
+ authorizedBy: {
549
+ planRevision: params.authority.planRevision,
550
+ action: params.action,
551
+ },
552
+ result,
553
+ };
554
+ }
555
+ catch (error) {
556
+ if (error instanceof SellableApiError) {
557
+ return {
558
+ executed: false,
559
+ blocked: true,
560
+ reason: "start_route_rejected",
561
+ status: error.status,
562
+ body: error.body,
563
+ campaignId,
564
+ };
565
+ }
566
+ throw error;
567
+ }
568
+ }
569
+ return {
570
+ executed: false,
571
+ refused: true,
572
+ reason: "campaign_not_in_pinned_lane_chain",
573
+ campaignId,
574
+ laneChainCampaignIds: params.authority.laneChainCampaignIds,
575
+ };
576
+ }
577
+ export async function executeRerunErroredCellsPrimitive(params) {
578
+ if (params.action.type !== "rerun_errored_cells") {
579
+ return {
580
+ executed: false,
581
+ refused: true,
582
+ reason: "action_type_not_rerun_errored_cells",
583
+ };
584
+ }
585
+ const campaignId = actionCampaignId(params.action);
586
+ const tableId = actionTableId(params.action);
587
+ const operations = rerunErroredCellsOperations(params.action);
588
+ if (!campaignId || !tableId || operations.length === 0) {
589
+ return {
590
+ executed: false,
591
+ refused: true,
592
+ reason: "missing_bounded_operations",
593
+ };
594
+ }
595
+ const api = getApi();
596
+ const requestOptions = workspaceRequestOptions(params.workspaceId ?? null);
597
+ const results = [];
598
+ for (const operation of operations) {
599
+ const body = {
600
+ action: "queue",
601
+ campaignId,
602
+ tableId,
603
+ columnRole: operation.columnRole,
604
+ rowSelector: operation.rowSelector,
605
+ };
606
+ const result = requestOptions
607
+ ? await api.post("/api/v3/mcp/campaign-processing", body, requestOptions)
608
+ : await api.post("/api/v3/mcp/campaign-processing", body);
609
+ results.push({ ...operation, result });
610
+ }
611
+ return {
612
+ executed: true,
613
+ type: "rerun_errored_cells",
614
+ operations: results,
615
+ };
616
+ }
617
+ export async function executeOneYoloPrimitive(action, workspaceId) {
618
+ if (!action)
619
+ return { status: "no_action" };
620
+ if (action.yoloEligible === false) {
621
+ return {
622
+ status: "refused",
623
+ refusalReason: "first global action is not yolo eligible",
624
+ };
625
+ }
626
+ switch (action.type) {
627
+ case "wait_for_scheduler":
628
+ case "wait_for_active_work":
629
+ case "wait_for_source_import":
630
+ return { status: "read_only_reread", result: { waited: true } };
631
+ case "prepare_messages": {
632
+ const campaignId = actionCampaignId(action);
633
+ const tableId = actionTableId(action) ?? undefined;
634
+ const toolInput = actionToolInput(action);
635
+ const targetPreparedMessages = numberValue(toolInput.targetPreparedMessages);
636
+ const rowSelector = prepareRowSelectorValue(toolInput.rowSelector);
637
+ const approvalMode = toolInput.approvalMode === "approve" ? "approve" : "mark_ready";
638
+ if (!campaignId ||
639
+ !targetPreparedMessages ||
640
+ targetPreparedMessages <= 0) {
641
+ return {
642
+ status: "refused",
643
+ refusalReason: "prepare_messages action is missing campaignId or bounded targetPreparedMessages",
644
+ };
645
+ }
646
+ const result = await startPrepareCampaignMessages({
647
+ campaignId,
648
+ tableId,
649
+ ...(workspaceId ? { workspaceId } : {}),
650
+ targetPreparedMessages,
651
+ maxRowsToCheck: numberValue(toolInput.maxRowsToCheck) ?? 300,
652
+ approvalMode,
653
+ rowSelector,
654
+ autoContinue: true,
655
+ disableLowPassRateStop: true,
656
+ senderId: actionSenderId(action) ?? undefined,
657
+ actionType: actionActionType(action) ?? undefined,
658
+ requestHash: refillPrepareRequestHash({
659
+ action,
660
+ campaignId,
661
+ tableId,
662
+ approvalMode,
663
+ rowSelector,
664
+ }),
665
+ requestSource: "refill_sends",
666
+ });
667
+ return { status: "executed_and_reread", result };
668
+ }
669
+ case "copy_selected_source_rows": {
670
+ const campaignOfferId = actionCampaignId(action);
671
+ const sourceLeadListId = actionSourceLeadListId(action);
672
+ const toolInput = actionToolInput(action);
673
+ const sourceRowIds = stringArray(toolInput.sourceRowIds);
674
+ const sourceRowLimit = numberValue(toolInput.sourceRowLimit);
675
+ const sourceFingerprint = stringValue(toolInput.sourceFingerprint) ??
676
+ stringValue(actionIds(action).sourceFingerprint);
677
+ if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
678
+ return {
679
+ status: "refused",
680
+ refusalReason: "copy_selected_source_rows action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
681
+ };
682
+ }
683
+ if (sourceRowIds.length === 0 &&
684
+ (!sourceRowLimit || sourceRowLimit <= 0)) {
685
+ return {
686
+ status: "refused",
687
+ refusalReason: "copy_selected_source_rows action is missing exact sourceRowIds or a bounded sourceRowLimit",
688
+ };
689
+ }
690
+ const copyInput = {
691
+ campaignOfferId,
692
+ sourceLeadListId,
693
+ currentStep: null,
694
+ confirmed: true,
695
+ ...(workspaceId ? { workspaceId } : {}),
696
+ sourceRowIds: sourceRowIds.length > 0 ? sourceRowIds : undefined,
697
+ sourceRowLimit: sourceRowIds.length > 0 ? undefined : (sourceRowLimit ?? undefined),
698
+ reviewBatchLimit: sourceRowIds.length > 0
699
+ ? sourceRowIds.length
700
+ : (sourceRowLimit ?? undefined),
701
+ };
702
+ let result;
703
+ try {
704
+ result = await confirmLeadList(copyInput);
705
+ }
706
+ catch (error) {
707
+ const rowLimitPayload = userAddedRowsLimitPayloadFromError(error);
708
+ if (!rowLimitPayload)
709
+ throw error;
710
+ const remainingRows = typeof rowLimitPayload.remainingRows === "number" &&
711
+ Number.isFinite(rowLimitPayload.remainingRows)
712
+ ? Math.floor(rowLimitPayload.remainingRows)
713
+ : 0;
714
+ if (remainingRows <= 0) {
715
+ return {
716
+ status: "refused",
717
+ refusalReason: rowLimitPayload.error ??
718
+ "selected campaign table is at the workflow row limit",
719
+ };
720
+ }
721
+ const retrySourceRowIds = sourceRowIds.length > 0 ? sourceRowIds.slice(0, remainingRows) : [];
722
+ const retrySourceRowLimit = retrySourceRowIds.length > 0
723
+ ? undefined
724
+ : Math.min(sourceRowLimit ?? remainingRows, remainingRows);
725
+ const retryReviewBatchLimit = retrySourceRowIds.length > 0
726
+ ? retrySourceRowIds.length
727
+ : retrySourceRowLimit;
728
+ if (!retryReviewBatchLimit || retryReviewBatchLimit <= 0) {
729
+ return {
730
+ status: "refused",
731
+ refusalReason: rowLimitPayload.error ??
732
+ "selected campaign table cannot accept more workflow rows",
733
+ };
734
+ }
735
+ const retryResult = await confirmLeadList({
736
+ ...copyInput,
737
+ sourceRowIds: retrySourceRowIds.length > 0 ? retrySourceRowIds : undefined,
738
+ sourceRowLimit: retrySourceRowIds.length > 0 ? undefined : retrySourceRowLimit,
739
+ reviewBatchLimit: retryReviewBatchLimit,
740
+ });
741
+ result =
742
+ retryResult && typeof retryResult === "object"
743
+ ? {
744
+ ...retryResult,
745
+ rowLimitRetry: {
746
+ code: rowLimitPayload.code,
747
+ maxRows: rowLimitPayload.maxRows,
748
+ currentRows: rowLimitPayload.currentRows,
749
+ requestedRows: rowLimitPayload.requestedRows,
750
+ remainingRows,
751
+ retriedRows: retryReviewBatchLimit,
752
+ },
753
+ }
754
+ : {
755
+ result: retryResult,
756
+ rowLimitRetry: {
757
+ code: rowLimitPayload.code,
758
+ remainingRows,
759
+ retriedRows: retryReviewBatchLimit,
760
+ },
761
+ };
762
+ }
763
+ return { status: "executed_and_reread", result };
764
+ }
765
+ case "rerun_errored_cells": {
766
+ const result = await executeRerunErroredCellsPrimitive({
767
+ action,
768
+ workspaceId,
769
+ });
770
+ if (!result.executed) {
771
+ return {
772
+ status: "refused",
773
+ refusalReason: result.reason,
774
+ };
775
+ }
776
+ return { status: "executed_and_reread", result };
777
+ }
778
+ case "continue_signal_discovery_source":
779
+ return continueSignalDiscoverySource(action, workspaceId);
780
+ case "approve_messages":
781
+ return approveGeneratedMessagesBatch(action, workspaceId);
782
+ default:
783
+ return {
784
+ status: "refused",
785
+ refusalReason: `action ${String(action.type)} is not a safe yolo primitive`,
786
+ };
787
+ }
788
+ }