@sellable/mcp 0.1.507 → 0.1.509
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/server.js +4 -1
- package/dist/tools/campaign-message-preparation.d.ts +26 -27
- package/dist/tools/campaign-message-preparation.js +27 -12
- package/dist/tools/campaigns.d.ts +54 -1
- package/dist/tools/campaigns.js +18 -3
- package/dist/tools/leads.d.ts +49 -0
- package/dist/tools/leads.js +138 -41
- package/dist/tools/prompts.js +1 -1
- package/dist/tools/refill-sends.d.ts +54 -79
- package/dist/tools/refill-sends.js +787 -171
- package/dist/tools/refill-target-plan.js +61 -8
- package/dist/tools/registry.d.ts +97 -84
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +22 -11
- package/skills/refill-sends-workflow/SKILL.md +11 -6
|
@@ -1,15 +1,19 @@
|
|
|
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";
|
|
1
6
|
import { getRefillTargetPlan } from "./refill-target-plan.js";
|
|
2
|
-
import { getPrepareCampaignMessagesStatus, startPrepareCampaignMessages, } from "./campaign-message-preparation.js";
|
|
3
7
|
import { refreshPaidInmailCredits } from "./senders.js";
|
|
4
|
-
import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
|
|
8
|
+
import { createWorkspaceContext, normalizeExplicitWorkspaceId, workspaceRequestOptions, } from "./workspace-context.js";
|
|
5
9
|
const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const PAID_INMAIL_REFRESH_MAX_ATTEMPTS = 3;
|
|
11
|
+
const PAID_INMAIL_REFRESH_RETRY_DELAY_MS = Number(process.env.SELLABLE_MCP_PAID_REFRESH_RETRY_DELAY_MS ?? "1000");
|
|
12
|
+
const SIGNAL_DISCOVERY_MIN_REFILL_POSTS = 3;
|
|
13
|
+
const SIGNAL_DISCOVERY_MAX_REFILL_POSTS = 10;
|
|
14
|
+
const SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST = 150;
|
|
15
|
+
const YOLO_MAX_ACTIONS = Math.max(1, Math.floor(Number(process.env.SELLABLE_MCP_REFILL_YOLO_MAX_ACTIONS ?? "24")));
|
|
16
|
+
const YOLO_SCHEDULER_POLL_DELAY_MS = Math.max(0, Math.floor(Number(process.env.SELLABLE_MCP_REFILL_YOLO_POLL_DELAY_MS ?? "0")));
|
|
13
17
|
function normalizeStrings(values) {
|
|
14
18
|
if (!Array.isArray(values))
|
|
15
19
|
return [];
|
|
@@ -19,6 +23,11 @@ function normalizeStrings(values) {
|
|
|
19
23
|
.filter(Boolean)),
|
|
20
24
|
];
|
|
21
25
|
}
|
|
26
|
+
function sleep(ms) {
|
|
27
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
22
31
|
function normalizeHorizonSendDays(value) {
|
|
23
32
|
if (value === undefined || value === null)
|
|
24
33
|
return null;
|
|
@@ -64,10 +73,24 @@ function normalizeTargetDate(value) {
|
|
|
64
73
|
}
|
|
65
74
|
return trimmed;
|
|
66
75
|
}
|
|
76
|
+
function userAddedRowsLimitPayloadFromError(error) {
|
|
77
|
+
if (!(error instanceof SellableApiError) || error.status !== 400) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(error.body);
|
|
82
|
+
if (parsed?.code !== "USER_ADDED_ROWS_LIMIT_EXCEEDED")
|
|
83
|
+
return null;
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
67
90
|
export const refillSendsToolDefinitions = [
|
|
68
91
|
{
|
|
69
92
|
name: "refill_sends",
|
|
70
|
-
description: "Typed command entrypoint for Sellable refill sends. Accepts --yolo semantics and optional sender selectors, then returns the bounded execution contract for the skill-led refill workflow. In --yolo, it may refresh stale paid InMail credit cache facts
|
|
93
|
+
description: "Typed command entrypoint for Sellable refill sends. Accepts --yolo semantics and optional sender selectors, then returns the bounded execution contract for the skill-led refill workflow. In --yolo, it may refresh stale paid InMail credit cache facts once, then run a bounded loop of safe primitives from target.globalActionQueue[0] (start exact selected paused campaign, bounded existing-row preparation, bounded same-source copy, bounded generated-message approval, safe Signal Discovery continuation, or read-only scheduler wait) with a fresh target-plan reread after each primitive. It does not run unbounded approval, lower paid InMail thresholds, switch source families outside the selected packet, create campaigns, schedule sends, archive, delete, or write scheduler rows.",
|
|
71
94
|
inputSchema: {
|
|
72
95
|
type: "object",
|
|
73
96
|
properties: {
|
|
@@ -210,9 +233,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
210
233
|
firstOperationalSteps: [
|
|
211
234
|
'Load get_subskill_prompt({ subskillName: "refill-sends-workflow" }) before any product operation.',
|
|
212
235
|
"A skill cannot create or invoke /goal by itself. If this refill is already running inside an active Codex goal, keep that goal open until every selected sender lane is horizon-filled by projected coverage (sent + scheduled), Christian explicitly stops/statuses the run, or a concrete non-scheduler blocker appears.",
|
|
213
|
-
`Call get_refill_target_plan({ intent: "${intent}"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""}${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""}${senderIds.length > 0
|
|
214
|
-
? `, senderIds: ${JSON.stringify(senderIds)}`
|
|
215
|
-
: ""}${senderNames.length > 0
|
|
236
|
+
`Call get_refill_target_plan({ intent: "${intent}"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""}${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""}${senderIds.length > 0 ? `, senderIds: ${JSON.stringify(senderIds)}` : ""}${senderNames.length > 0
|
|
216
237
|
? `, senderNames: ${JSON.stringify(senderNames)}`
|
|
217
238
|
: ""}${senders.length > 0 ? `, senders: ${JSON.stringify(senders)}` : ""}${targetDate
|
|
218
239
|
? `, targetDate: "${targetDate}"`
|
|
@@ -242,14 +263,15 @@ export function refillSendsCommand(input = {}) {
|
|
|
242
263
|
"Treat current dashboard-active PAUSED campaign-backed sequence campaigns as start-eligible candidates: read refill state before deciding whether to prep, approve, start, or skip.",
|
|
243
264
|
"Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
|
|
244
265
|
"Maintain a target-window saturation ledger per selected sender from get_refill_target_plan: selected days, gross capacity, actual sent counts, future scheduler-owned scheduled counts, projected counts, ready-to-schedule buffer, remaining projected gap, paid InMail threshold feasibility, targetShapeRevision, stateRevision, and next MCP primitive.",
|
|
245
|
-
"In --yolo, this tool automatically maintains a run-local refreshedPaidInmailSenderIds set: when the first target plan returns refresh_paid_inmail_credits candidates for selected paid-InMail lanes, it refreshes each exact sender at most once, reruns get_refill_target_plan
|
|
246
|
-
"Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh
|
|
266
|
+
"In --yolo, this tool automatically maintains a run-local refreshedPaidInmailSenderIds set: when the first target plan returns refresh_paid_inmail_credits candidates for selected paid-InMail lanes, it refreshes each exact sender at most once, reruns get_refill_target_plan, and returns the post-refresh targetPlan before any prep/source-copy/bounded-approval/read-only wait action is chosen.",
|
|
267
|
+
"Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh and the post-refresh targetPlan.",
|
|
247
268
|
"If paid InMail feasibility remains below threshold after that automatic refresh, report the exact sender/campaign/table/column threshold action or same-campaign connection fallback; --yolo must not lower paid InMail thresholds or create campaigns.",
|
|
248
269
|
"In --yolo or after one Accept, continue through every safe selected sender/campaign action covered by the rendered target packet; sent/scheduled-count progress changes stateRevision and should continue while targetShapeRevision is stable.",
|
|
270
|
+
"A start_paused_campaign action is safe in --yolo only when it is target.globalActionQueue[0] for the exact selected PAUSED campaign in the current refill target plan and the request has explicit workspaceId.",
|
|
249
271
|
"Do not complete a fill/schedule request until a final get_refill_target_plan or refill-state reread proves projected coverage (sent + scheduled) fills the scheduler-forward target window. Treat awaiting_scheduler_after_ready_buffer as loaded, awaiting scheduler when ready buffer covers the gap; do not import/prep more rows in that state, and keep polling unless Christian stops/statuses the run or a concrete non-scheduler blocker such as paid_inmail_below_threshold appears.",
|
|
250
272
|
],
|
|
251
273
|
approvalContract: yolo
|
|
252
|
-
? "Auto-accept only the rendered bounded refill target packet after get_refill_target_plan and fresh reread. Execute one globally ranked primitive from target.globalActionQueue at a time, then rerun get_refill_target_plan before taking another action. Continue through every safe
|
|
274
|
+
? "Auto-accept only the rendered bounded refill target packet after get_refill_target_plan and fresh reread. Execute one globally ranked primitive from target.globalActionQueue at a time, then rerun get_refill_target_plan before taking another action. Continue through every safe prep/source-copy/bounded-approval/start exact selected paused campaign/safe same-source continuation/read-only wait action inside that packet. Unbounded approval, source-family switches outside the selected packet, threshold changes, and campaign creation require their own explicit gates. After each safe action, rerun get_refill_target_plan; keep going while targetShapeRevision is stable and projected coverage (sent + scheduled) progresses toward the target, even though stateRevision changes. Ready-to-schedule rows are buffer, not completion. If ready buffer covers the projected gap, use only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Stop immediately if targetShapeRevision changes because sender set, route, ids, caps, dates, blockers, action class, paid InMail threshold feasibility, or side-effect class drifts. --yolo does not authorize lowering paid InMail thresholds or creating connection campaigns."
|
|
253
275
|
: hasSenderSelectors
|
|
254
276
|
? "Before mutation, post the full bounded refill packet in normal chat as Markdown, including workspace, sender scope, campaign table, exact ids, caps/dates, gross target, sent count, scheduled count, projected count, ready buffer, remaining projected gap, paid InMail threshold feasibility, targetShapeRevision/stateRevision, side effects, forbidden actions, and stop condition. Then ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet instead of duplicating it. Only ask when get_refill_target_plan reports a positive remaining projected/ready gap. If target is complete by projected coverage, no-op without approval. If ready buffer covers the projected gap, run only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Threshold changes and campaign creation need separate explicit approval."
|
|
255
277
|
: "Ask which eligible enrolled senders to refill first, then, before mutation, post the full bounded refill packet in normal chat as Markdown and ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet.",
|
|
@@ -337,46 +359,41 @@ function stringValue(value) {
|
|
|
337
359
|
function numberValue(value) {
|
|
338
360
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
339
361
|
}
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
345
|
-
return null;
|
|
346
|
-
const parsed = Math.floor(value);
|
|
347
|
-
return parsed > 0 ? parsed : null;
|
|
362
|
+
function stringArray(value) {
|
|
363
|
+
if (!Array.isArray(value))
|
|
364
|
+
return [];
|
|
365
|
+
return value.filter((item) => typeof item === "string");
|
|
348
366
|
}
|
|
349
|
-
function
|
|
367
|
+
function prepareRowSelectorValue(value) {
|
|
350
368
|
const selector = recordValue(value);
|
|
351
369
|
const type = stringValue(selector?.type);
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return rowIds.length > 0 ? { type, rowIds } : null;
|
|
370
|
+
if (type !== "needsEnrichment" &&
|
|
371
|
+
type !== "needsApproval" &&
|
|
372
|
+
type !== "needsGeneratedMessage" &&
|
|
373
|
+
type !== "reviewBatch" &&
|
|
374
|
+
type !== "staleGeneratedMessages") {
|
|
375
|
+
return undefined;
|
|
359
376
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
377
|
+
const limit = numberValue(selector?.limit);
|
|
378
|
+
return {
|
|
379
|
+
type,
|
|
380
|
+
...(limit && limit > 0 ? { limit: Math.floor(limit) } : {}),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function uniqueStrings(values) {
|
|
384
|
+
const seen = new Set();
|
|
385
|
+
const result = [];
|
|
386
|
+
for (const value of values) {
|
|
387
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
388
|
+
if (!normalized)
|
|
389
|
+
continue;
|
|
390
|
+
const key = normalized.toLowerCase();
|
|
391
|
+
if (seen.has(key))
|
|
392
|
+
continue;
|
|
393
|
+
seen.add(key);
|
|
394
|
+
result.push(normalized);
|
|
365
395
|
}
|
|
366
|
-
|
|
367
|
-
return withLimit({ type });
|
|
368
|
-
if (type === "needsApproval")
|
|
369
|
-
return withLimit({ type });
|
|
370
|
-
if (type === "passedRows")
|
|
371
|
-
return withLimit({ type });
|
|
372
|
-
if (type === "needsGeneratedMessage")
|
|
373
|
-
return withLimit({ type });
|
|
374
|
-
if (type === "staleGeneratedMessages")
|
|
375
|
-
return withLimit({ type });
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
function delay(ms) {
|
|
379
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
396
|
+
return result;
|
|
380
397
|
}
|
|
381
398
|
function paidRefreshActionFrom(value) {
|
|
382
399
|
const action = recordValue(value);
|
|
@@ -428,127 +445,726 @@ function collectPaidInmailRefreshActions(plan) {
|
|
|
428
445
|
function firstGlobalAction(plan) {
|
|
429
446
|
const root = recordValue(plan);
|
|
430
447
|
const target = recordValue(root?.target);
|
|
431
|
-
const
|
|
448
|
+
const globalActionQueue = Array.isArray(target?.globalActionQueue)
|
|
432
449
|
? target.globalActionQueue
|
|
433
450
|
: [];
|
|
434
|
-
|
|
451
|
+
const [first] = globalActionQueue;
|
|
452
|
+
return recordValue(first);
|
|
435
453
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
454
|
+
function planTarget(plan) {
|
|
455
|
+
return recordValue(recordValue(plan)?.target);
|
|
456
|
+
}
|
|
457
|
+
function planStatus(plan) {
|
|
458
|
+
return stringValue(recordValue(plan)?.status);
|
|
459
|
+
}
|
|
460
|
+
function planTargetShapeRevision(plan) {
|
|
461
|
+
const root = recordValue(plan);
|
|
462
|
+
return (stringValue(root?.targetShapeRevision) ?? stringValue(root?.targetRevision));
|
|
463
|
+
}
|
|
464
|
+
function planStateRevision(plan) {
|
|
465
|
+
return stringValue(recordValue(plan)?.stateRevision);
|
|
466
|
+
}
|
|
467
|
+
function planSummary(plan) {
|
|
468
|
+
const target = planTarget(plan);
|
|
469
|
+
const senderPlans = Array.isArray(target?.senderRefillPlans)
|
|
470
|
+
? target.senderRefillPlans
|
|
471
|
+
: [];
|
|
472
|
+
const firstSenderPlan = recordValue(senderPlans[0]);
|
|
473
|
+
return {
|
|
474
|
+
status: planStatus(plan),
|
|
475
|
+
targetShapeRevision: planTargetShapeRevision(plan),
|
|
476
|
+
stateRevision: planStateRevision(plan),
|
|
477
|
+
grossTarget: numberValue(target?.grossTarget),
|
|
478
|
+
sent: numberValue(target?.sent),
|
|
479
|
+
scheduled: numberValue(target?.scheduled),
|
|
480
|
+
projected: numberValue(target?.projected),
|
|
481
|
+
readyBuffer: numberValue(target?.readyBuffer),
|
|
482
|
+
remainingProjectedGap: numberValue(target?.remainingProjectedGap),
|
|
483
|
+
remainingReadyOrProjectedGap: numberValue(target?.remainingReadyOrProjectedGap),
|
|
484
|
+
actionType: stringValue(firstSenderPlan?.actionType),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function planIsComplete(plan) {
|
|
488
|
+
const summary = planSummary(plan);
|
|
489
|
+
return summary.status === "complete" || summary.remainingProjectedGap === 0;
|
|
490
|
+
}
|
|
491
|
+
function yoloLoopExecution(params) {
|
|
492
|
+
const lastAction = params.actions[params.actions.length - 1];
|
|
493
|
+
return {
|
|
494
|
+
enabled: true,
|
|
495
|
+
status: params.status,
|
|
496
|
+
stopReason: params.stopReason,
|
|
497
|
+
selectedAction: params.selectedAction ??
|
|
498
|
+
params.actions[0]?.selectedAction ??
|
|
499
|
+
firstGlobalAction(params.initialPlan),
|
|
500
|
+
actions: params.actions,
|
|
501
|
+
actionCount: params.actions.length,
|
|
502
|
+
result: params.result ?? lastAction?.primitive.result,
|
|
503
|
+
targetPlanReread: params.targetPlanRereads > 0,
|
|
504
|
+
targetPlanRereads: params.targetPlanRereads,
|
|
505
|
+
initialTargetShapeRevision: planTargetShapeRevision(params.initialPlan),
|
|
506
|
+
finalTargetShapeRevision: planTargetShapeRevision(params.finalPlan),
|
|
507
|
+
finalStateRevision: planStateRevision(params.finalPlan),
|
|
508
|
+
maxActions: YOLO_MAX_ACTIONS,
|
|
509
|
+
refusalReason: params.refusalReason ?? lastAction?.primitive.refusalReason,
|
|
510
|
+
postActionFirstAction: firstGlobalAction(params.finalPlan),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function actionIds(action) {
|
|
514
|
+
return recordValue(action.ids) ?? {};
|
|
515
|
+
}
|
|
516
|
+
function actionToolInput(action) {
|
|
517
|
+
return recordValue(action.toolInput) ?? {};
|
|
518
|
+
}
|
|
519
|
+
function actionCampaignId(action) {
|
|
520
|
+
const ids = actionIds(action);
|
|
521
|
+
const toolInput = actionToolInput(action);
|
|
522
|
+
return (stringValue(toolInput.campaignId) ??
|
|
523
|
+
stringValue(toolInput.campaignOfferId) ??
|
|
524
|
+
stringValue(ids.campaignId) ??
|
|
525
|
+
stringValue(action.campaignId));
|
|
526
|
+
}
|
|
527
|
+
function actionTableId(action) {
|
|
528
|
+
const ids = actionIds(action);
|
|
529
|
+
const toolInput = actionToolInput(action);
|
|
530
|
+
return (stringValue(toolInput.tableId) ??
|
|
531
|
+
stringValue(ids.tableId) ??
|
|
532
|
+
stringValue(action.tableId));
|
|
533
|
+
}
|
|
534
|
+
function actionSourceLeadListId(action) {
|
|
535
|
+
const ids = actionIds(action);
|
|
536
|
+
const toolInput = actionToolInput(action);
|
|
537
|
+
return (stringValue(toolInput.sourceLeadListId) ??
|
|
538
|
+
stringValue(ids.sourceLeadListId) ??
|
|
539
|
+
stringValue(action.sourceLeadListId));
|
|
540
|
+
}
|
|
541
|
+
function actionSenderId(action) {
|
|
542
|
+
const ids = actionIds(action);
|
|
543
|
+
const toolInput = actionToolInput(action);
|
|
544
|
+
return (stringValue(toolInput.senderId) ??
|
|
545
|
+
stringValue(ids.senderId) ??
|
|
546
|
+
stringValue(action.senderId));
|
|
547
|
+
}
|
|
548
|
+
function actionActionType(action) {
|
|
549
|
+
const ids = actionIds(action);
|
|
550
|
+
const toolInput = actionToolInput(action);
|
|
551
|
+
return (stringValue(toolInput.actionType) ??
|
|
552
|
+
stringValue(toolInput.selectedLane) ??
|
|
553
|
+
stringValue(ids.actionType) ??
|
|
554
|
+
stringValue(action.actionType) ??
|
|
555
|
+
stringValue(action.selectedLane));
|
|
556
|
+
}
|
|
557
|
+
function actionSourceFingerprint(action) {
|
|
558
|
+
const ids = actionIds(action);
|
|
559
|
+
const toolInput = actionToolInput(action);
|
|
560
|
+
return (stringValue(toolInput.sourceFingerprint) ??
|
|
561
|
+
stringValue(ids.sourceFingerprint) ??
|
|
562
|
+
stringValue(action.sourceFingerprint));
|
|
563
|
+
}
|
|
564
|
+
function actionLeadSourceProvider(action) {
|
|
565
|
+
const ids = actionIds(action);
|
|
566
|
+
const toolInput = actionToolInput(action);
|
|
567
|
+
return (stringValue(toolInput.leadSourceProvider) ??
|
|
568
|
+
stringValue(toolInput.provider) ??
|
|
569
|
+
stringValue(ids.leadSourceProvider) ??
|
|
570
|
+
stringValue(action.leadSourceProvider));
|
|
571
|
+
}
|
|
572
|
+
function normalizedSignalKeyword(value) {
|
|
573
|
+
if (typeof value !== "string")
|
|
574
|
+
return null;
|
|
575
|
+
const keyword = value.trim();
|
|
576
|
+
if (!keyword)
|
|
577
|
+
return null;
|
|
578
|
+
if (/^(https?:\/\/|www\.|linkedin\.com\/|\/?in\/)/i.test(keyword)) {
|
|
579
|
+
return null;
|
|
446
580
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
timedOut: false,
|
|
463
|
-
receipt: lastStatus,
|
|
464
|
-
};
|
|
581
|
+
return keyword;
|
|
582
|
+
}
|
|
583
|
+
function keywordsFromSignalTabs(tabs) {
|
|
584
|
+
const selectedKeywords = [];
|
|
585
|
+
const fallbackKeywords = [];
|
|
586
|
+
for (const tab of tabs) {
|
|
587
|
+
const keyword = normalizedSignalKeyword(tab.keyword);
|
|
588
|
+
if (!keyword)
|
|
589
|
+
continue;
|
|
590
|
+
const selected = (tab.posts ?? []).some((post) => post.isSelected === true);
|
|
591
|
+
if (selected) {
|
|
592
|
+
selectedKeywords.push(keyword);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
fallbackKeywords.push(keyword);
|
|
465
596
|
}
|
|
466
597
|
}
|
|
467
|
-
return
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
598
|
+
return uniqueStrings([...selectedKeywords, ...fallbackKeywords]).slice(0, 5);
|
|
599
|
+
}
|
|
600
|
+
function selectedPostIdsFromSignalTabs(tabs) {
|
|
601
|
+
return uniqueStrings(tabs.flatMap((tab) => (tab.posts ?? [])
|
|
602
|
+
.filter((post) => post.isSelected === true)
|
|
603
|
+
.map((post) => post.id)));
|
|
604
|
+
}
|
|
605
|
+
function postIdsFromSignalSearch(summary, maxPosts, options = {}) {
|
|
606
|
+
const excludedPostIds = new Set((options.excludePostIds ?? []).map((postId) => postId.toLowerCase()));
|
|
607
|
+
const recommendedPostIds = stringArray(summary?.recommendedPostIds);
|
|
608
|
+
const topPostIds = Array.isArray(summary?.topPosts)
|
|
609
|
+
? summary.topPosts
|
|
610
|
+
.map((post) => post && typeof post === "object"
|
|
611
|
+
? stringValue(post.id)
|
|
612
|
+
: null)
|
|
613
|
+
.filter((id) => Boolean(id))
|
|
614
|
+
: [];
|
|
615
|
+
return uniqueStrings([...recommendedPostIds, ...topPostIds])
|
|
616
|
+
.filter((postId) => !excludedPostIds.has(postId.toLowerCase()))
|
|
617
|
+
.slice(0, Math.max(1, maxPosts));
|
|
618
|
+
}
|
|
619
|
+
function maxSignalDiscoveryPostsForSourceLimit(sourceRowLimit) {
|
|
620
|
+
return Math.min(SIGNAL_DISCOVERY_MAX_REFILL_POSTS, Math.max(SIGNAL_DISCOVERY_MIN_REFILL_POSTS, Math.ceil(sourceRowLimit / SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST)));
|
|
621
|
+
}
|
|
622
|
+
function refillPrepareRequestHash(params) {
|
|
623
|
+
return [
|
|
624
|
+
"refill_sends",
|
|
625
|
+
"prepare_messages",
|
|
626
|
+
params.campaignId,
|
|
627
|
+
params.tableId ?? "no-table",
|
|
628
|
+
actionSenderId(params.action) ?? "all-senders",
|
|
629
|
+
actionSourceLeadListId(params.action) ?? "no-source-list",
|
|
630
|
+
actionActionType(params.action) ?? "no-action-type",
|
|
631
|
+
params.approvalMode,
|
|
632
|
+
params.rowSelector
|
|
633
|
+
? `${params.rowSelector.type}:${params.rowSelector.limit ?? "no-limit"}`
|
|
634
|
+
: "no-row-selector",
|
|
635
|
+
].join(":");
|
|
636
|
+
}
|
|
637
|
+
function boundedApprovalLimit(action) {
|
|
638
|
+
const toolInput = actionToolInput(action);
|
|
639
|
+
const rowSelector = recordValue(toolInput.rowSelector);
|
|
640
|
+
const selectorLimit = numberValue(rowSelector?.limit);
|
|
641
|
+
const inputLimit = numberValue(toolInput.limit);
|
|
642
|
+
const limit = selectorLimit ?? inputLimit;
|
|
643
|
+
if (!limit || limit <= 0)
|
|
644
|
+
return null;
|
|
645
|
+
return Math.floor(limit);
|
|
646
|
+
}
|
|
647
|
+
async function approveGeneratedMessagesBatch(action, workspaceId) {
|
|
648
|
+
const tableId = actionTableId(action);
|
|
649
|
+
const toolInput = actionToolInput(action);
|
|
650
|
+
const columnId = stringValue(toolInput.columnId) ?? stringValue(actionIds(action).columnId);
|
|
651
|
+
const limit = boundedApprovalLimit(action);
|
|
652
|
+
if (!tableId || !limit) {
|
|
653
|
+
return {
|
|
654
|
+
status: "refused",
|
|
655
|
+
refusalReason: "approve_messages action is missing tableId or a bounded rowSelector.limit",
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const api = getApi();
|
|
659
|
+
const requestOptions = workspaceRequestOptions(workspaceId);
|
|
660
|
+
const body = {
|
|
661
|
+
tableId,
|
|
662
|
+
...(columnId ? { columnId } : {}),
|
|
663
|
+
limit,
|
|
664
|
+
scope: "generated_unapproved",
|
|
665
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
471
666
|
};
|
|
667
|
+
const result = requestOptions
|
|
668
|
+
? await api.post("/api/v3/workflow-tables/cells/approve-batch", body, requestOptions)
|
|
669
|
+
: await api.post("/api/v3/workflow-tables/cells/approve-batch", body);
|
|
670
|
+
return { status: "executed_and_reread", result };
|
|
472
671
|
}
|
|
473
|
-
async function
|
|
474
|
-
const
|
|
475
|
-
|
|
672
|
+
async function continueSignalDiscoverySource(action, workspaceId) {
|
|
673
|
+
const campaignOfferId = actionCampaignId(action);
|
|
674
|
+
const sourceLeadListId = actionSourceLeadListId(action);
|
|
675
|
+
const sourceFingerprint = actionSourceFingerprint(action);
|
|
676
|
+
const toolInput = actionToolInput(action);
|
|
677
|
+
const requestOptions = workspaceRequestOptions(workspaceId);
|
|
678
|
+
const sourceRowLimit = Math.min(1500, Math.max(100, Math.floor(numberValue(toolInput.sourceRowLimit) ??
|
|
679
|
+
numberValue(toolInput.targetRows) ??
|
|
680
|
+
numberValue(action.targetRows) ??
|
|
681
|
+
100)));
|
|
682
|
+
const maxPostsToScrape = maxSignalDiscoveryPostsForSourceLimit(sourceRowLimit);
|
|
683
|
+
if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
|
|
476
684
|
return {
|
|
477
|
-
status: "
|
|
478
|
-
|
|
479
|
-
note: "No target.globalActionQueue[0] action was present after the fresh target-plan reread.",
|
|
685
|
+
status: "refused",
|
|
686
|
+
refusalReason: "continue_signal_discovery_source action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
|
|
480
687
|
};
|
|
481
688
|
}
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
689
|
+
const requestedProvider = actionLeadSourceProvider(action);
|
|
690
|
+
if (requestedProvider &&
|
|
691
|
+
requestedProvider !== "signal-discovery" &&
|
|
692
|
+
requestedProvider !== "campaign-tracked-post") {
|
|
486
693
|
return {
|
|
487
|
-
status: "
|
|
488
|
-
|
|
489
|
-
action,
|
|
490
|
-
note: "The first global action was not marked yoloEligible:true, so refill_sends did not mutate.",
|
|
694
|
+
status: "refused",
|
|
695
|
+
refusalReason: "continue_signal_discovery_source can only run for Signal Discovery source families",
|
|
491
696
|
};
|
|
492
697
|
}
|
|
493
|
-
|
|
494
|
-
|
|
698
|
+
const api = getApi();
|
|
699
|
+
const campaign = requestOptions
|
|
700
|
+
? await api.get(`/api/v2/campaign-offers/${campaignOfferId}`, requestOptions)
|
|
701
|
+
: await api.get(`/api/v2/campaign-offers/${campaignOfferId}`);
|
|
702
|
+
if (campaign.leadSourceProvider !== "signal-discovery" &&
|
|
703
|
+
campaign.leadSourceProvider !== "campaign-tracked-post") {
|
|
495
704
|
return {
|
|
496
|
-
status: "
|
|
497
|
-
|
|
498
|
-
action,
|
|
499
|
-
note: "refill_sends currently auto-applies yolo prepare_messages actions; this action needs the skill workflow/operator path.",
|
|
705
|
+
status: "refused",
|
|
706
|
+
refusalReason: "campaign leadSourceProvider is not Signal Discovery; refusing same-source continuation",
|
|
500
707
|
};
|
|
501
708
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const campaignId = stringValue(toolInput?.campaignId) ?? stringValue(ids?.campaignId);
|
|
505
|
-
const tableId = stringValue(toolInput?.tableId) ?? stringValue(ids?.tableId);
|
|
506
|
-
if (!campaignId) {
|
|
709
|
+
if (campaign.selectedLeadListId &&
|
|
710
|
+
campaign.selectedLeadListId !== sourceLeadListId) {
|
|
507
711
|
return {
|
|
508
|
-
status: "
|
|
509
|
-
|
|
510
|
-
action,
|
|
511
|
-
note: "The yolo prepare_messages action did not include a campaignId.",
|
|
712
|
+
status: "refused",
|
|
713
|
+
refusalReason: "campaign selectedLeadListId changed since the plan packet; rerun get_refill_target_plan before source continuation",
|
|
512
714
|
};
|
|
513
715
|
}
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
716
|
+
const sourceMeta = requestOptions
|
|
717
|
+
? await api.get(`/api/v3/workflow-tables/${sourceLeadListId}?mode=meta`, requestOptions)
|
|
718
|
+
: await api.get(`/api/v3/workflow-tables/${sourceLeadListId}?mode=meta`);
|
|
719
|
+
const sourceConfig = sourceMeta.table?.config ?? null;
|
|
720
|
+
const headlineICPCriteria = stringArray(sourceConfig?.headlineICPCriteria).length > 0
|
|
721
|
+
? stringArray(sourceConfig?.headlineICPCriteria)
|
|
722
|
+
: stringArray(sourceConfig?.rubricGuidelines);
|
|
723
|
+
const tabsResponse = requestOptions
|
|
724
|
+
? await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`, requestOptions)
|
|
725
|
+
: await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
|
|
726
|
+
const signalTabs = tabsResponse.tabs ?? [];
|
|
727
|
+
const keywords = keywordsFromSignalTabs(signalTabs);
|
|
728
|
+
const excludedPostIds = selectedPostIdsFromSignalTabs(signalTabs);
|
|
729
|
+
if (keywords.length === 0) {
|
|
519
730
|
return {
|
|
520
|
-
status: "
|
|
521
|
-
|
|
522
|
-
action,
|
|
523
|
-
note: "The yolo prepare_messages action included a rowSelector, but it was not valid for start_campaign_message_preparation.",
|
|
731
|
+
status: "refused",
|
|
732
|
+
refusalReason: "no reusable Signal Discovery keywords were found on the campaign tabs",
|
|
524
733
|
};
|
|
525
734
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
...(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
735
|
+
markProviderPromptLoaded({
|
|
736
|
+
provider: "signal-discovery",
|
|
737
|
+
campaignOfferId,
|
|
738
|
+
});
|
|
739
|
+
const searchSummary = await searchSignals({
|
|
740
|
+
type: "keywords",
|
|
741
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
742
|
+
keywords: keywords.map((keyword) => ({
|
|
743
|
+
keyword,
|
|
744
|
+
source: "refill-sends-source-continuation",
|
|
745
|
+
})),
|
|
746
|
+
campaignOfferId,
|
|
747
|
+
currentStep: null,
|
|
748
|
+
headlineICPCriteria,
|
|
749
|
+
rubricGuidelines: headlineICPCriteria,
|
|
750
|
+
confirmed: true,
|
|
751
|
+
limit: 50,
|
|
752
|
+
});
|
|
753
|
+
const selectedPostIds = postIdsFromSignalSearch(searchSummary, maxPostsToScrape, { excludePostIds: excludedPostIds });
|
|
754
|
+
if (selectedPostIds.length === 0) {
|
|
755
|
+
return {
|
|
756
|
+
status: "refused",
|
|
757
|
+
refusalReason: "Signal Discovery search returned no new recommended posts to continue the source",
|
|
758
|
+
result: { keywords, excludedPostIds, searchSummary },
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
const selectionResult = await selectPromisingPosts({
|
|
762
|
+
campaignOfferId,
|
|
763
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
764
|
+
selections: selectedPostIds.map((postId) => ({
|
|
765
|
+
postId,
|
|
766
|
+
reason: "refill_sends same-source continuation: recent post from the campaign's existing Signal Discovery keyword family",
|
|
767
|
+
})),
|
|
768
|
+
headlineICPCriteria,
|
|
769
|
+
currentStep: null,
|
|
770
|
+
selectionMode: "replace",
|
|
771
|
+
scrapePlanMode: "all-selected",
|
|
772
|
+
});
|
|
773
|
+
if (selectionResult.success === false) {
|
|
774
|
+
return {
|
|
775
|
+
status: "refused",
|
|
776
|
+
refusalReason: stringValue(selectionResult.message) ??
|
|
777
|
+
"select_promising_posts did not select any posts",
|
|
778
|
+
result: { keywords, selectedPostIds, selectionResult },
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const importResult = await importLeads({
|
|
782
|
+
campaignOfferId,
|
|
783
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
784
|
+
provider: "signal-discovery",
|
|
785
|
+
sourceLeadListId,
|
|
786
|
+
currentStep: null,
|
|
787
|
+
headlineICPCriteria,
|
|
788
|
+
rubricGuidelines: headlineICPCriteria,
|
|
789
|
+
confirmed: true,
|
|
790
|
+
maxPostsToScrape: selectedPostIds.length,
|
|
791
|
+
allowInvalidSignalPosts: true,
|
|
541
792
|
});
|
|
793
|
+
const importRecord = recordValue(importResult);
|
|
794
|
+
if (importRecord?.error) {
|
|
795
|
+
return {
|
|
796
|
+
status: "refused",
|
|
797
|
+
refusalReason: stringValue(importRecord.message) ??
|
|
798
|
+
`Signal Discovery import returned ${String(importRecord.error)}`,
|
|
799
|
+
result: {
|
|
800
|
+
keywords,
|
|
801
|
+
excludedPostIds,
|
|
802
|
+
selectedPostIds,
|
|
803
|
+
selectionResult,
|
|
804
|
+
importResult,
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
}
|
|
542
808
|
return {
|
|
543
|
-
status:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
809
|
+
status: "executed_and_reread",
|
|
810
|
+
result: {
|
|
811
|
+
provider: "signal-discovery",
|
|
812
|
+
campaignOfferId,
|
|
813
|
+
previousSourceLeadListId: sourceLeadListId,
|
|
814
|
+
sourceFingerprint,
|
|
815
|
+
keywords,
|
|
816
|
+
excludedPostIds,
|
|
817
|
+
selectedPostIds,
|
|
818
|
+
sourceRowLimit,
|
|
819
|
+
maxPostsToScrape: selectedPostIds.length,
|
|
820
|
+
searchSummary,
|
|
821
|
+
selectionResult,
|
|
822
|
+
importResult,
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
async function refreshPaidInmailCreditsWithRetry(senderId, workspaceId) {
|
|
827
|
+
const errors = [];
|
|
828
|
+
for (let attempt = 1; attempt <= PAID_INMAIL_REFRESH_MAX_ATTEMPTS; attempt += 1) {
|
|
829
|
+
try {
|
|
830
|
+
const receipt = await refreshPaidInmailCredits({
|
|
831
|
+
senderId,
|
|
832
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
833
|
+
});
|
|
834
|
+
return { receipt, attempts: attempt, errors };
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
838
|
+
if (attempt < PAID_INMAIL_REFRESH_MAX_ATTEMPTS) {
|
|
839
|
+
await sleep(PAID_INMAIL_REFRESH_RETRY_DELAY_MS);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
receipt: null,
|
|
845
|
+
attempts: PAID_INMAIL_REFRESH_MAX_ATTEMPTS,
|
|
846
|
+
errors,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
async function executeOneYoloPrimitive(action, context = {}) {
|
|
850
|
+
if (!action)
|
|
851
|
+
return { status: "no_action" };
|
|
852
|
+
if (action.yoloEligible === false) {
|
|
853
|
+
return {
|
|
854
|
+
status: "refused",
|
|
855
|
+
refusalReason: "first global action is not yolo eligible",
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
switch (action.type) {
|
|
859
|
+
case "wait_for_scheduler":
|
|
860
|
+
case "wait_for_active_work":
|
|
861
|
+
case "wait_for_source_import":
|
|
862
|
+
return { status: "read_only_reread", result: { waited: true } };
|
|
863
|
+
case "prepare_messages": {
|
|
864
|
+
const campaignId = actionCampaignId(action);
|
|
865
|
+
const tableId = actionTableId(action) ?? undefined;
|
|
866
|
+
const toolInput = actionToolInput(action);
|
|
867
|
+
const targetPreparedMessages = numberValue(toolInput.targetPreparedMessages);
|
|
868
|
+
const rowSelector = prepareRowSelectorValue(toolInput.rowSelector);
|
|
869
|
+
const approvalMode = toolInput.approvalMode === "approve" ? "approve" : "mark_ready";
|
|
870
|
+
if (!campaignId ||
|
|
871
|
+
!targetPreparedMessages ||
|
|
872
|
+
targetPreparedMessages <= 0) {
|
|
873
|
+
return {
|
|
874
|
+
status: "refused",
|
|
875
|
+
refusalReason: "prepare_messages action is missing campaignId or bounded targetPreparedMessages",
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const result = await startPrepareCampaignMessages({
|
|
879
|
+
campaignId,
|
|
880
|
+
tableId,
|
|
881
|
+
workspaceId: context.workspaceId,
|
|
882
|
+
targetPreparedMessages,
|
|
883
|
+
maxRowsToCheck: numberValue(toolInput.maxRowsToCheck) ?? 300,
|
|
884
|
+
approvalMode,
|
|
885
|
+
rowSelector,
|
|
886
|
+
autoContinue: true,
|
|
887
|
+
disableLowPassRateStop: true,
|
|
888
|
+
senderId: actionSenderId(action) ?? undefined,
|
|
889
|
+
actionType: actionActionType(action) ?? undefined,
|
|
890
|
+
requestHash: refillPrepareRequestHash({
|
|
891
|
+
action,
|
|
892
|
+
campaignId,
|
|
893
|
+
tableId,
|
|
894
|
+
approvalMode,
|
|
895
|
+
rowSelector,
|
|
896
|
+
}),
|
|
897
|
+
requestSource: "refill_sends",
|
|
898
|
+
});
|
|
899
|
+
return { status: "executed_and_reread", result };
|
|
900
|
+
}
|
|
901
|
+
case "copy_selected_source_rows": {
|
|
902
|
+
const campaignOfferId = actionCampaignId(action);
|
|
903
|
+
const sourceLeadListId = actionSourceLeadListId(action);
|
|
904
|
+
const toolInput = actionToolInput(action);
|
|
905
|
+
const sourceRowIds = stringArray(toolInput.sourceRowIds);
|
|
906
|
+
const sourceRowLimit = numberValue(toolInput.sourceRowLimit);
|
|
907
|
+
const sourceFingerprint = stringValue(toolInput.sourceFingerprint) ??
|
|
908
|
+
stringValue(actionIds(action).sourceFingerprint);
|
|
909
|
+
if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
|
|
910
|
+
return {
|
|
911
|
+
status: "refused",
|
|
912
|
+
refusalReason: "copy_selected_source_rows action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
if (sourceRowIds.length === 0 &&
|
|
916
|
+
(!sourceRowLimit || sourceRowLimit <= 0)) {
|
|
917
|
+
return {
|
|
918
|
+
status: "refused",
|
|
919
|
+
refusalReason: "copy_selected_source_rows action is missing exact sourceRowIds or a bounded sourceRowLimit",
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
const copyInput = {
|
|
923
|
+
campaignOfferId,
|
|
924
|
+
workspaceId: context.workspaceId,
|
|
925
|
+
sourceLeadListId,
|
|
926
|
+
currentStep: null,
|
|
927
|
+
confirmed: true,
|
|
928
|
+
sourceRowIds: sourceRowIds.length > 0 ? sourceRowIds : undefined,
|
|
929
|
+
sourceRowLimit: sourceRowIds.length > 0 ? undefined : (sourceRowLimit ?? undefined),
|
|
930
|
+
reviewBatchLimit: sourceRowIds.length > 0
|
|
931
|
+
? sourceRowIds.length
|
|
932
|
+
: (sourceRowLimit ?? undefined),
|
|
933
|
+
};
|
|
934
|
+
let result;
|
|
935
|
+
try {
|
|
936
|
+
result = await confirmLeadList(copyInput);
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
const rowLimitPayload = userAddedRowsLimitPayloadFromError(error);
|
|
940
|
+
if (!rowLimitPayload)
|
|
941
|
+
throw error;
|
|
942
|
+
const remainingRows = typeof rowLimitPayload.remainingRows === "number" &&
|
|
943
|
+
Number.isFinite(rowLimitPayload.remainingRows)
|
|
944
|
+
? Math.floor(rowLimitPayload.remainingRows)
|
|
945
|
+
: 0;
|
|
946
|
+
if (remainingRows <= 0) {
|
|
947
|
+
return {
|
|
948
|
+
status: "refused",
|
|
949
|
+
refusalReason: rowLimitPayload.error ??
|
|
950
|
+
"selected campaign table is at the workflow row limit",
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
const retrySourceRowIds = sourceRowIds.length > 0 ? sourceRowIds.slice(0, remainingRows) : [];
|
|
954
|
+
const retrySourceRowLimit = retrySourceRowIds.length > 0
|
|
955
|
+
? undefined
|
|
956
|
+
: Math.min(sourceRowLimit ?? remainingRows, remainingRows);
|
|
957
|
+
const retryReviewBatchLimit = retrySourceRowIds.length > 0
|
|
958
|
+
? retrySourceRowIds.length
|
|
959
|
+
: retrySourceRowLimit;
|
|
960
|
+
if (!retryReviewBatchLimit || retryReviewBatchLimit <= 0) {
|
|
961
|
+
return {
|
|
962
|
+
status: "refused",
|
|
963
|
+
refusalReason: rowLimitPayload.error ??
|
|
964
|
+
"selected campaign table cannot accept more workflow rows",
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
const retryResult = await confirmLeadList({
|
|
968
|
+
...copyInput,
|
|
969
|
+
sourceRowIds: retrySourceRowIds.length > 0 ? retrySourceRowIds : undefined,
|
|
970
|
+
sourceRowLimit: retrySourceRowIds.length > 0 ? undefined : retrySourceRowLimit,
|
|
971
|
+
reviewBatchLimit: retryReviewBatchLimit,
|
|
972
|
+
});
|
|
973
|
+
result =
|
|
974
|
+
retryResult && typeof retryResult === "object"
|
|
975
|
+
? {
|
|
976
|
+
...retryResult,
|
|
977
|
+
rowLimitRetry: {
|
|
978
|
+
code: rowLimitPayload.code,
|
|
979
|
+
maxRows: rowLimitPayload.maxRows,
|
|
980
|
+
currentRows: rowLimitPayload.currentRows,
|
|
981
|
+
requestedRows: rowLimitPayload.requestedRows,
|
|
982
|
+
remainingRows,
|
|
983
|
+
retriedRows: retryReviewBatchLimit,
|
|
984
|
+
},
|
|
985
|
+
}
|
|
986
|
+
: {
|
|
987
|
+
result: retryResult,
|
|
988
|
+
rowLimitRetry: {
|
|
989
|
+
code: rowLimitPayload.code,
|
|
990
|
+
remainingRows,
|
|
991
|
+
retriedRows: retryReviewBatchLimit,
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
return { status: "executed_and_reread", result };
|
|
996
|
+
}
|
|
997
|
+
case "continue_signal_discovery_source":
|
|
998
|
+
return continueSignalDiscoverySource(action, context.workspaceId);
|
|
999
|
+
case "approve_messages":
|
|
1000
|
+
return approveGeneratedMessagesBatch(action, context.workspaceId);
|
|
1001
|
+
case "start_paused_campaign": {
|
|
1002
|
+
const campaignId = actionCampaignId(action);
|
|
1003
|
+
if (!campaignId || !context.workspaceId) {
|
|
1004
|
+
return {
|
|
1005
|
+
status: "refused",
|
|
1006
|
+
refusalReason: "start_paused_campaign action is missing campaignId or explicit workspaceId",
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
const result = await startCampaign({
|
|
1010
|
+
campaignId,
|
|
1011
|
+
workspaceId: context.workspaceId,
|
|
1012
|
+
});
|
|
1013
|
+
return { status: "executed_and_reread", result };
|
|
1014
|
+
}
|
|
1015
|
+
default:
|
|
1016
|
+
return {
|
|
1017
|
+
status: "refused",
|
|
1018
|
+
refusalReason: `action ${String(action.type)} is not a safe yolo primitive`,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
async function executeYoloPrimitiveLoop(params) {
|
|
1023
|
+
let currentPlan = params.initialPlan;
|
|
1024
|
+
const actions = [];
|
|
1025
|
+
let targetPlanRereads = 0;
|
|
1026
|
+
let targetPlanBeforeYoloPrimitive = null;
|
|
1027
|
+
const initialTargetShapeRevision = planTargetShapeRevision(currentPlan);
|
|
1028
|
+
if (planIsComplete(currentPlan)) {
|
|
1029
|
+
return {
|
|
1030
|
+
finalTargetPlan: currentPlan,
|
|
1031
|
+
targetPlanBeforeYoloPrimitive: null,
|
|
1032
|
+
execution: yoloLoopExecution({
|
|
1033
|
+
status: "complete",
|
|
1034
|
+
stopReason: "target_complete",
|
|
1035
|
+
actions,
|
|
1036
|
+
targetPlanRereads,
|
|
1037
|
+
initialPlan: params.initialPlan,
|
|
1038
|
+
finalPlan: currentPlan,
|
|
1039
|
+
}),
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
for (let attempt = 1; attempt <= YOLO_MAX_ACTIONS; attempt += 1) {
|
|
1043
|
+
const selectedAction = firstGlobalAction(currentPlan);
|
|
1044
|
+
if (!selectedAction) {
|
|
1045
|
+
return {
|
|
1046
|
+
finalTargetPlan: currentPlan,
|
|
1047
|
+
targetPlanBeforeYoloPrimitive,
|
|
1048
|
+
execution: yoloLoopExecution({
|
|
1049
|
+
status: "no_action",
|
|
1050
|
+
stopReason: "no_global_action",
|
|
1051
|
+
actions,
|
|
1052
|
+
targetPlanRereads,
|
|
1053
|
+
initialPlan: params.initialPlan,
|
|
1054
|
+
finalPlan: currentPlan,
|
|
1055
|
+
selectedAction: null,
|
|
1056
|
+
}),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const before = planSummary(currentPlan);
|
|
1060
|
+
if (!targetPlanBeforeYoloPrimitive) {
|
|
1061
|
+
targetPlanBeforeYoloPrimitive = currentPlan;
|
|
1062
|
+
}
|
|
1063
|
+
if (selectedAction.type === "wait_for_scheduler" &&
|
|
1064
|
+
YOLO_SCHEDULER_POLL_DELAY_MS > 0) {
|
|
1065
|
+
await sleep(YOLO_SCHEDULER_POLL_DELAY_MS);
|
|
1066
|
+
}
|
|
1067
|
+
const primitive = await executeOneYoloPrimitive(selectedAction, {
|
|
1068
|
+
workspaceId: params.workspaceId,
|
|
1069
|
+
});
|
|
1070
|
+
const shouldReread = primitive.status === "executed_and_reread" ||
|
|
1071
|
+
primitive.status === "read_only_reread";
|
|
1072
|
+
const nextPlan = shouldReread
|
|
1073
|
+
? await getRefillTargetPlan(params.targetPlanInput)
|
|
1074
|
+
: null;
|
|
1075
|
+
if (shouldReread)
|
|
1076
|
+
targetPlanRereads += 1;
|
|
1077
|
+
const receipt = {
|
|
1078
|
+
attempt,
|
|
1079
|
+
selectedAction,
|
|
1080
|
+
before,
|
|
1081
|
+
primitive,
|
|
1082
|
+
after: nextPlan ? planSummary(nextPlan) : null,
|
|
1083
|
+
postActionFirstAction: nextPlan ? firstGlobalAction(nextPlan) : null,
|
|
1084
|
+
};
|
|
1085
|
+
actions.push(receipt);
|
|
1086
|
+
if (primitive.status === "refused") {
|
|
1087
|
+
return {
|
|
1088
|
+
finalTargetPlan: currentPlan,
|
|
1089
|
+
targetPlanBeforeYoloPrimitive,
|
|
1090
|
+
execution: yoloLoopExecution({
|
|
1091
|
+
status: "refused",
|
|
1092
|
+
stopReason: "primitive_refused",
|
|
1093
|
+
actions,
|
|
1094
|
+
targetPlanRereads,
|
|
1095
|
+
initialPlan: params.initialPlan,
|
|
1096
|
+
finalPlan: currentPlan,
|
|
1097
|
+
selectedAction,
|
|
1098
|
+
result: primitive.result,
|
|
1099
|
+
refusalReason: primitive.refusalReason,
|
|
1100
|
+
}),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
if (!nextPlan) {
|
|
1104
|
+
return {
|
|
1105
|
+
finalTargetPlan: currentPlan,
|
|
1106
|
+
targetPlanBeforeYoloPrimitive,
|
|
1107
|
+
execution: yoloLoopExecution({
|
|
1108
|
+
status: primitive.status,
|
|
1109
|
+
stopReason: "no_global_action",
|
|
1110
|
+
actions,
|
|
1111
|
+
targetPlanRereads,
|
|
1112
|
+
initialPlan: params.initialPlan,
|
|
1113
|
+
finalPlan: currentPlan,
|
|
1114
|
+
selectedAction,
|
|
1115
|
+
result: primitive.result,
|
|
1116
|
+
}),
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
currentPlan = nextPlan;
|
|
1120
|
+
const nextTargetShapeRevision = planTargetShapeRevision(currentPlan);
|
|
1121
|
+
if (initialTargetShapeRevision &&
|
|
1122
|
+
nextTargetShapeRevision &&
|
|
1123
|
+
nextTargetShapeRevision !== initialTargetShapeRevision &&
|
|
1124
|
+
!planIsComplete(currentPlan)) {
|
|
1125
|
+
return {
|
|
1126
|
+
finalTargetPlan: currentPlan,
|
|
1127
|
+
targetPlanBeforeYoloPrimitive,
|
|
1128
|
+
execution: yoloLoopExecution({
|
|
1129
|
+
status: "target_shape_drift",
|
|
1130
|
+
stopReason: "target_shape_drift",
|
|
1131
|
+
actions,
|
|
1132
|
+
targetPlanRereads,
|
|
1133
|
+
initialPlan: params.initialPlan,
|
|
1134
|
+
finalPlan: currentPlan,
|
|
1135
|
+
selectedAction,
|
|
1136
|
+
result: primitive.result,
|
|
1137
|
+
}),
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
if (planIsComplete(currentPlan)) {
|
|
1141
|
+
return {
|
|
1142
|
+
finalTargetPlan: currentPlan,
|
|
1143
|
+
targetPlanBeforeYoloPrimitive,
|
|
1144
|
+
execution: yoloLoopExecution({
|
|
1145
|
+
status: "complete",
|
|
1146
|
+
stopReason: "target_complete",
|
|
1147
|
+
actions,
|
|
1148
|
+
targetPlanRereads,
|
|
1149
|
+
initialPlan: params.initialPlan,
|
|
1150
|
+
finalPlan: currentPlan,
|
|
1151
|
+
selectedAction,
|
|
1152
|
+
result: primitive.result,
|
|
1153
|
+
}),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
finalTargetPlan: currentPlan,
|
|
1159
|
+
targetPlanBeforeYoloPrimitive,
|
|
1160
|
+
execution: yoloLoopExecution({
|
|
1161
|
+
status: "max_actions_reached",
|
|
1162
|
+
stopReason: "max_actions_reached",
|
|
1163
|
+
actions,
|
|
1164
|
+
targetPlanRereads,
|
|
1165
|
+
initialPlan: params.initialPlan,
|
|
1166
|
+
finalPlan: currentPlan,
|
|
1167
|
+
}),
|
|
552
1168
|
};
|
|
553
1169
|
}
|
|
554
1170
|
export async function executeRefillSendsCommand(input = {}) {
|
|
@@ -579,6 +1195,12 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
579
1195
|
failedPaidInmailRefreshes: [],
|
|
580
1196
|
note: "Automatic paid InMail credit refresh only runs for --yolo refill_sends calls.",
|
|
581
1197
|
},
|
|
1198
|
+
yoloExecution: {
|
|
1199
|
+
enabled: false,
|
|
1200
|
+
status: "not_run_without_yolo",
|
|
1201
|
+
selectedAction: null,
|
|
1202
|
+
targetPlanReread: false,
|
|
1203
|
+
},
|
|
582
1204
|
};
|
|
583
1205
|
}
|
|
584
1206
|
const workspaceId = workspaceContext && workspaceContext.ok
|
|
@@ -591,41 +1213,41 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
591
1213
|
const failedPaidInmailRefreshes = [];
|
|
592
1214
|
const refreshReceipts = [];
|
|
593
1215
|
for (const action of refreshActions) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
senderId: action.senderId,
|
|
597
|
-
...(workspaceId ? { workspaceId } : {}),
|
|
598
|
-
});
|
|
1216
|
+
const refreshResult = await refreshPaidInmailCreditsWithRetry(action.senderId, workspaceId ?? undefined);
|
|
1217
|
+
if (refreshResult.receipt) {
|
|
599
1218
|
refreshedPaidInmailSenderIds.push(action.senderId);
|
|
600
1219
|
refreshReceipts.push({
|
|
601
1220
|
senderId: action.senderId,
|
|
602
1221
|
actionKey: action.actionKey ?? null,
|
|
603
|
-
|
|
1222
|
+
attempts: refreshResult.attempts,
|
|
1223
|
+
retryErrors: refreshResult.errors,
|
|
1224
|
+
receipt: refreshResult.receipt,
|
|
604
1225
|
});
|
|
605
1226
|
}
|
|
606
|
-
|
|
1227
|
+
else {
|
|
607
1228
|
failedPaidInmailRefreshes.push({
|
|
608
1229
|
senderId: action.senderId,
|
|
609
|
-
|
|
1230
|
+
attempts: refreshResult.attempts,
|
|
1231
|
+
error: refreshResult.errors[refreshResult.errors.length - 1] ??
|
|
1232
|
+
"paid InMail credit refresh failed",
|
|
1233
|
+
errors: refreshResult.errors,
|
|
610
1234
|
});
|
|
611
1235
|
}
|
|
612
1236
|
}
|
|
613
1237
|
const targetPlan = refreshActions.length > 0
|
|
614
1238
|
? await getRefillTargetPlan(targetPlanInput)
|
|
615
1239
|
: targetPlanBeforePaidRefresh;
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
workspaceId,
|
|
1240
|
+
const yoloLoop = await executeYoloPrimitiveLoop({
|
|
1241
|
+
initialPlan: targetPlan,
|
|
1242
|
+
targetPlanInput,
|
|
1243
|
+
workspaceId: workspaceId ?? undefined,
|
|
620
1244
|
});
|
|
621
|
-
const finalTargetPlan =
|
|
622
|
-
? await getRefillTargetPlan(targetPlanInput)
|
|
623
|
-
: targetPlan;
|
|
1245
|
+
const finalTargetPlan = yoloLoop.finalTargetPlan;
|
|
624
1246
|
return {
|
|
625
1247
|
...command,
|
|
626
1248
|
targetPlan: finalTargetPlan,
|
|
627
|
-
targetPlanBeforeYoloAction,
|
|
628
1249
|
targetPlanBeforePaidRefresh: refreshActions.length > 0 ? targetPlanBeforePaidRefresh : null,
|
|
1250
|
+
targetPlanBeforeYoloPrimitive: yoloLoop.targetPlanBeforeYoloPrimitive,
|
|
629
1251
|
autoPaidInmailRefresh: {
|
|
630
1252
|
enabled: true,
|
|
631
1253
|
status: refreshActions.length === 0
|
|
@@ -641,15 +1263,9 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
641
1263
|
workspaceId,
|
|
642
1264
|
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
643
1265
|
note: refreshActions.length > 0
|
|
644
|
-
? "refill_sends refreshed stale paid InMail credit facts internally, once per sender, then
|
|
1266
|
+
? "refill_sends refreshed stale paid InMail credit facts internally with bounded retries, once per sender, reran the target plan, then continued the bounded yolo primitive loop from the post-refresh targetPlan."
|
|
645
1267
|
: "No stale or missing paid InMail credit refresh candidates were present in the initial target plan.",
|
|
646
1268
|
},
|
|
647
|
-
yoloExecution:
|
|
648
|
-
enabled: true,
|
|
649
|
-
workspaceId,
|
|
650
|
-
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
651
|
-
targetPlanRereadAfterAction: yoloExecution.applied,
|
|
652
|
-
...yoloExecution,
|
|
653
|
-
},
|
|
1269
|
+
yoloExecution: yoloLoop.execution,
|
|
654
1270
|
};
|
|
655
1271
|
}
|