@sellable/mcp 0.1.505 → 0.1.507

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.
@@ -1,16 +1,15 @@
1
- import { getApi, SellableApiError } from "../api.js";
2
- import { startPrepareCampaignMessages } from "./campaign-message-preparation.js";
3
- import { confirmLeadList, importLeads, searchSignals, selectPromisingPosts, } from "./leads.js";
4
- import { markProviderPromptLoaded } from "./provider-preflight.js";
5
1
  import { getRefillTargetPlan } from "./refill-target-plan.js";
2
+ import { getPrepareCampaignMessagesStatus, startPrepareCampaignMessages, } from "./campaign-message-preparation.js";
6
3
  import { refreshPaidInmailCredits } from "./senders.js";
7
4
  import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
8
5
  const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
9
- const PAID_INMAIL_REFRESH_MAX_ATTEMPTS = 3;
10
- const PAID_INMAIL_REFRESH_RETRY_DELAY_MS = Number(process.env.SELLABLE_MCP_PAID_REFRESH_RETRY_DELAY_MS ?? "1000");
11
- const SIGNAL_DISCOVERY_MIN_REFILL_POSTS = 3;
12
- const SIGNAL_DISCOVERY_MAX_REFILL_POSTS = 10;
13
- const SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST = 150;
6
+ const PREPARE_POLL_INTERVAL_MS = 5_000;
7
+ const PREPARE_POLL_TIMEOUT_MS = 120_000;
8
+ const TERMINAL_PREPARE_STATUSES = new Set([
9
+ "succeeded",
10
+ "failed",
11
+ "cancelled",
12
+ ]);
14
13
  function normalizeStrings(values) {
15
14
  if (!Array.isArray(values))
16
15
  return [];
@@ -20,11 +19,6 @@ function normalizeStrings(values) {
20
19
  .filter(Boolean)),
21
20
  ];
22
21
  }
23
- function sleep(ms) {
24
- if (!Number.isFinite(ms) || ms <= 0)
25
- return Promise.resolve();
26
- return new Promise((resolve) => setTimeout(resolve, ms));
27
- }
28
22
  function normalizeHorizonSendDays(value) {
29
23
  if (value === undefined || value === null)
30
24
  return null;
@@ -70,24 +64,10 @@ function normalizeTargetDate(value) {
70
64
  }
71
65
  return trimmed;
72
66
  }
73
- function userAddedRowsLimitPayloadFromError(error) {
74
- if (!(error instanceof SellableApiError) || error.status !== 400) {
75
- return null;
76
- }
77
- try {
78
- const parsed = JSON.parse(error.body);
79
- if (parsed?.code !== "USER_ADDED_ROWS_LIMIT_EXCEEDED")
80
- return null;
81
- return parsed;
82
- }
83
- catch {
84
- return null;
85
- }
86
- }
87
67
  export const refillSendsToolDefinitions = [
88
68
  {
89
69
  name: "refill_sends",
90
- 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, or execute exactly one safe primitive from target.globalActionQueue[0] (bounded existing-row preparation, bounded same-source copy, bounded generated-message approval, or read-only wait) and reread. It does not run unbounded approval, lower paid InMail thresholds, switch source families, create campaigns, schedule sends, launch, archive, delete, or write scheduler rows.",
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 for exact selected senders once, rerun the target plan, execute the first supported yolo-eligible global primitive, and reread the target plan. It never directly writes scheduler rows, launches unrelated campaigns, archives, deletes, or sends messages outside the product scheduler path.",
91
71
  inputSchema: {
92
72
  type: "object",
93
73
  properties: {
@@ -230,7 +210,9 @@ export function refillSendsCommand(input = {}) {
230
210
  firstOperationalSteps: [
231
211
  'Load get_subskill_prompt({ subskillName: "refill-sends-workflow" }) before any product operation.',
232
212
  "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.",
233
- `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
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
234
216
  ? `, senderNames: ${JSON.stringify(senderNames)}`
235
217
  : ""}${senders.length > 0 ? `, senders: ${JSON.stringify(senders)}` : ""}${targetDate
236
218
  ? `, targetDate: "${targetDate}"`
@@ -260,14 +242,14 @@ export function refillSendsCommand(input = {}) {
260
242
  "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.",
261
243
  "Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
262
244
  "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.",
263
- "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.",
264
- "Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh and the post-refresh targetPlan.",
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 to obtain the post-refresh targetPlan, then may apply the first supported yolo-eligible target.globalActionQueue[0] primitive and return yoloExecution plus a final targetPlan reread.",
246
+ "Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh. Inspect yoloExecution: if it applied an action, continue from the final targetPlan; otherwise continue from the post-refresh targetPlan/targetPlanBeforeYoloAction.",
265
247
  "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.",
266
248
  "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.",
267
249
  "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.",
268
250
  ],
269
251
  approvalContract: yolo
270
- ? "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/read-only wait action inside that packet. Unbounded approval, start_campaign, source-family switches, 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."
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 apply/prep/start action inside that packet, including start_campaign only for exact selected PAUSED campaign-backed sequence refill targets named in the packet. After each 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."
271
253
  : hasSenderSelectors
272
254
  ? "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."
273
255
  : "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.",
@@ -355,41 +337,46 @@ function stringValue(value) {
355
337
  function numberValue(value) {
356
338
  return typeof value === "number" && Number.isFinite(value) ? value : null;
357
339
  }
358
- function stringArray(value) {
359
- if (!Array.isArray(value))
360
- return [];
361
- return value.filter((item) => typeof item === "string");
340
+ function approvalModeValue(value) {
341
+ return value === "approve" || value === "mark_ready" ? value : null;
342
+ }
343
+ function positiveIntegerValue(value) {
344
+ if (typeof value !== "number" || !Number.isFinite(value))
345
+ return null;
346
+ const parsed = Math.floor(value);
347
+ return parsed > 0 ? parsed : null;
362
348
  }
363
- function prepareRowSelectorValue(value) {
349
+ function rowSelectorValue(value) {
364
350
  const selector = recordValue(value);
365
351
  const type = stringValue(selector?.type);
366
- if (type !== "needsEnrichment" &&
367
- type !== "needsApproval" &&
368
- type !== "needsGeneratedMessage" &&
369
- type !== "reviewBatch" &&
370
- type !== "staleGeneratedMessages") {
371
- return undefined;
352
+ if (!selector || !type)
353
+ return null;
354
+ const limit = positiveIntegerValue(selector.limit);
355
+ const withLimit = (base) => limit ? { ...base, limit: Math.min(limit, 500) } : base;
356
+ if (type === "rowIds") {
357
+ const rowIds = normalizeStrings(selector.rowIds);
358
+ return rowIds.length > 0 ? { type, rowIds } : null;
372
359
  }
373
- const limit = numberValue(selector?.limit);
374
- return {
375
- type,
376
- ...(limit && limit > 0 ? { limit: Math.floor(limit) } : {}),
377
- };
378
- }
379
- function uniqueStrings(values) {
380
- const seen = new Set();
381
- const result = [];
382
- for (const value of values) {
383
- const normalized = typeof value === "string" ? value.trim() : "";
384
- if (!normalized)
385
- continue;
386
- const key = normalized.toLowerCase();
387
- if (seen.has(key))
388
- continue;
389
- seen.add(key);
390
- result.push(normalized);
360
+ if (type === "reviewBatch") {
361
+ const basisHash = stringValue(selector.basisHash);
362
+ return basisHash
363
+ ? withLimit({ type, basisHash })
364
+ : withLimit({ type });
391
365
  }
392
- return result;
366
+ if (type === "needsEnrichment")
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));
393
380
  }
394
381
  function paidRefreshActionFrom(value) {
395
382
  const action = recordValue(value);
@@ -441,490 +428,129 @@ function collectPaidInmailRefreshActions(plan) {
441
428
  function firstGlobalAction(plan) {
442
429
  const root = recordValue(plan);
443
430
  const target = recordValue(root?.target);
444
- const globalActionQueue = Array.isArray(target?.globalActionQueue)
431
+ const queue = Array.isArray(target?.globalActionQueue)
445
432
  ? target.globalActionQueue
446
433
  : [];
447
- const [first] = globalActionQueue;
448
- return recordValue(first);
449
- }
450
- function actionIds(action) {
451
- return recordValue(action.ids) ?? {};
452
- }
453
- function actionToolInput(action) {
454
- return recordValue(action.toolInput) ?? {};
455
- }
456
- function actionCampaignId(action) {
457
- const ids = actionIds(action);
458
- const toolInput = actionToolInput(action);
459
- return (stringValue(toolInput.campaignId) ??
460
- stringValue(toolInput.campaignOfferId) ??
461
- stringValue(ids.campaignId) ??
462
- stringValue(action.campaignId));
463
- }
464
- function actionTableId(action) {
465
- const ids = actionIds(action);
466
- const toolInput = actionToolInput(action);
467
- return (stringValue(toolInput.tableId) ??
468
- stringValue(ids.tableId) ??
469
- stringValue(action.tableId));
470
- }
471
- function actionSourceLeadListId(action) {
472
- const ids = actionIds(action);
473
- const toolInput = actionToolInput(action);
474
- return (stringValue(toolInput.sourceLeadListId) ??
475
- stringValue(ids.sourceLeadListId) ??
476
- stringValue(action.sourceLeadListId));
477
- }
478
- function actionSenderId(action) {
479
- const ids = actionIds(action);
480
- const toolInput = actionToolInput(action);
481
- return (stringValue(toolInput.senderId) ??
482
- stringValue(ids.senderId) ??
483
- stringValue(action.senderId));
434
+ return recordValue(queue[0]);
484
435
  }
485
- function actionActionType(action) {
486
- const ids = actionIds(action);
487
- const toolInput = actionToolInput(action);
488
- return (stringValue(toolInput.actionType) ??
489
- stringValue(toolInput.selectedLane) ??
490
- stringValue(ids.actionType) ??
491
- stringValue(action.actionType) ??
492
- stringValue(action.selectedLane));
493
- }
494
- function actionSourceFingerprint(action) {
495
- const ids = actionIds(action);
496
- const toolInput = actionToolInput(action);
497
- return (stringValue(toolInput.sourceFingerprint) ??
498
- stringValue(ids.sourceFingerprint) ??
499
- stringValue(action.sourceFingerprint));
500
- }
501
- function actionLeadSourceProvider(action) {
502
- const ids = actionIds(action);
503
- const toolInput = actionToolInput(action);
504
- return (stringValue(toolInput.leadSourceProvider) ??
505
- stringValue(toolInput.provider) ??
506
- stringValue(ids.leadSourceProvider) ??
507
- stringValue(action.leadSourceProvider));
508
- }
509
- function normalizedSignalKeyword(value) {
510
- if (typeof value !== "string")
511
- return null;
512
- const keyword = value.trim();
513
- if (!keyword)
514
- return null;
515
- if (/^(https?:\/\/|www\.|linkedin\.com\/|\/?in\/)/i.test(keyword)) {
516
- return null;
517
- }
518
- return keyword;
519
- }
520
- function keywordsFromSignalTabs(tabs) {
521
- const selectedKeywords = [];
522
- const fallbackKeywords = [];
523
- for (const tab of tabs) {
524
- const keyword = normalizedSignalKeyword(tab.keyword);
525
- if (!keyword)
526
- continue;
527
- const selected = (tab.posts ?? []).some((post) => post.isSelected === true);
528
- if (selected) {
529
- selectedKeywords.push(keyword);
530
- }
531
- else {
532
- fallbackKeywords.push(keyword);
533
- }
534
- }
535
- return uniqueStrings([...selectedKeywords, ...fallbackKeywords]).slice(0, 5);
536
- }
537
- function selectedPostIdsFromSignalTabs(tabs) {
538
- return uniqueStrings(tabs.flatMap((tab) => (tab.posts ?? [])
539
- .filter((post) => post.isSelected === true)
540
- .map((post) => post.id)));
541
- }
542
- function postIdsFromSignalSearch(summary, maxPosts, options = {}) {
543
- const excludedPostIds = new Set((options.excludePostIds ?? []).map((postId) => postId.toLowerCase()));
544
- const recommendedPostIds = stringArray(summary?.recommendedPostIds);
545
- const topPostIds = Array.isArray(summary?.topPosts)
546
- ? summary.topPosts
547
- .map((post) => post && typeof post === "object"
548
- ? stringValue(post.id)
549
- : null)
550
- .filter((id) => Boolean(id))
551
- : [];
552
- return uniqueStrings([...recommendedPostIds, ...topPostIds])
553
- .filter((postId) => !excludedPostIds.has(postId.toLowerCase()))
554
- .slice(0, Math.max(1, maxPosts));
555
- }
556
- function maxSignalDiscoveryPostsForSourceLimit(sourceRowLimit) {
557
- return Math.min(SIGNAL_DISCOVERY_MAX_REFILL_POSTS, Math.max(SIGNAL_DISCOVERY_MIN_REFILL_POSTS, Math.ceil(sourceRowLimit / SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST)));
558
- }
559
- function refillPrepareRequestHash(params) {
560
- return [
561
- "refill_sends",
562
- "prepare_messages",
563
- params.campaignId,
564
- params.tableId ?? "no-table",
565
- actionSenderId(params.action) ?? "all-senders",
566
- actionSourceLeadListId(params.action) ?? "no-source-list",
567
- actionActionType(params.action) ?? "no-action-type",
568
- params.approvalMode,
569
- params.rowSelector
570
- ? `${params.rowSelector.type}:${params.rowSelector.limit ?? "no-limit"}`
571
- : "no-row-selector",
572
- ].join(":");
573
- }
574
- function boundedApprovalLimit(action) {
575
- const toolInput = actionToolInput(action);
576
- const rowSelector = recordValue(toolInput.rowSelector);
577
- const selectorLimit = numberValue(rowSelector?.limit);
578
- const inputLimit = numberValue(toolInput.limit);
579
- const limit = selectorLimit ?? inputLimit;
580
- if (!limit || limit <= 0)
581
- return null;
582
- return Math.floor(limit);
583
- }
584
- async function approveGeneratedMessagesBatch(action) {
585
- const tableId = actionTableId(action);
586
- const toolInput = actionToolInput(action);
587
- const columnId = stringValue(toolInput.columnId) ?? stringValue(actionIds(action).columnId);
588
- const limit = boundedApprovalLimit(action);
589
- if (!tableId || !limit) {
590
- return {
591
- status: "refused",
592
- refusalReason: "approve_messages action is missing tableId or a bounded rowSelector.limit",
593
- };
594
- }
595
- const api = getApi();
596
- const result = await api.post("/api/v3/workflow-tables/cells/approve-batch", {
597
- tableId,
598
- ...(columnId ? { columnId } : {}),
599
- limit,
600
- scope: "generated_unapproved",
601
- });
602
- return { status: "executed_and_reread", result };
603
- }
604
- async function continueSignalDiscoverySource(action) {
605
- const campaignOfferId = actionCampaignId(action);
606
- const sourceLeadListId = actionSourceLeadListId(action);
607
- const sourceFingerprint = actionSourceFingerprint(action);
608
- const toolInput = actionToolInput(action);
609
- const sourceRowLimit = Math.min(1500, Math.max(100, Math.floor(numberValue(toolInput.sourceRowLimit) ??
610
- numberValue(toolInput.targetRows) ??
611
- numberValue(action.targetRows) ??
612
- 100)));
613
- const maxPostsToScrape = maxSignalDiscoveryPostsForSourceLimit(sourceRowLimit);
614
- if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
436
+ async function waitForPrepareAction(params) {
437
+ const receipt = recordValue(params.startReceipt);
438
+ const jobId = stringValue(receipt?.jobId);
439
+ const initialStatus = stringValue(receipt?.status) ?? stringValue(receipt?.jobStatus);
440
+ if (!jobId || (initialStatus && TERMINAL_PREPARE_STATUSES.has(initialStatus))) {
615
441
  return {
616
- status: "refused",
617
- refusalReason: "continue_signal_discovery_source action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
442
+ waited: false,
443
+ timedOut: false,
444
+ receipt: params.startReceipt,
618
445
  };
619
446
  }
620
- const requestedProvider = actionLeadSourceProvider(action);
621
- if (requestedProvider &&
622
- requestedProvider !== "signal-discovery" &&
623
- requestedProvider !== "campaign-tracked-post") {
624
- return {
625
- status: "refused",
626
- refusalReason: "continue_signal_discovery_source can only run for Signal Discovery source families",
627
- };
447
+ const startedAt = Date.now();
448
+ let lastStatus = params.startReceipt;
449
+ while (Date.now() - startedAt < PREPARE_POLL_TIMEOUT_MS) {
450
+ await delay(PREPARE_POLL_INTERVAL_MS);
451
+ lastStatus = await getPrepareCampaignMessagesStatus({
452
+ jobId,
453
+ campaignId: params.campaignId,
454
+ ...(params.tableId ? { tableId: params.tableId } : {}),
455
+ ...(params.workspaceId ? { workspaceId: params.workspaceId } : {}),
456
+ });
457
+ const statusRecord = recordValue(lastStatus);
458
+ const status = stringValue(statusRecord?.status) ?? stringValue(statusRecord?.jobStatus);
459
+ if (status && TERMINAL_PREPARE_STATUSES.has(status)) {
460
+ return {
461
+ waited: true,
462
+ timedOut: false,
463
+ receipt: lastStatus,
464
+ };
465
+ }
628
466
  }
629
- const api = getApi();
630
- const campaign = await api.get(`/api/v2/campaign-offers/${campaignOfferId}`);
631
- if (campaign.leadSourceProvider !== "signal-discovery" &&
632
- campaign.leadSourceProvider !== "campaign-tracked-post") {
467
+ return {
468
+ waited: true,
469
+ timedOut: true,
470
+ receipt: lastStatus,
471
+ };
472
+ }
473
+ async function executeYoloGlobalAction(params) {
474
+ const action = params.action;
475
+ if (!action) {
633
476
  return {
634
- status: "refused",
635
- refusalReason: "campaign leadSourceProvider is not Signal Discovery; refusing same-source continuation",
477
+ status: "no_global_action",
478
+ applied: false,
479
+ note: "No target.globalActionQueue[0] action was present after the fresh target-plan reread.",
636
480
  };
637
481
  }
638
- if (campaign.selectedLeadListId &&
639
- campaign.selectedLeadListId !== sourceLeadListId) {
482
+ const actionType = stringValue(action.type);
483
+ const toolName = stringValue(action.toolName);
484
+ const yoloEligible = action.yoloEligible === true;
485
+ if (!yoloEligible) {
640
486
  return {
641
- status: "refused",
642
- refusalReason: "campaign selectedLeadListId changed since the plan packet; rerun get_refill_target_plan before source continuation",
487
+ status: "not_yolo_eligible",
488
+ applied: false,
489
+ action,
490
+ note: "The first global action was not marked yoloEligible:true, so refill_sends did not mutate.",
643
491
  };
644
492
  }
645
- const sourceMeta = await api.get(`/api/v3/workflow-tables/${sourceLeadListId}?mode=meta`);
646
- const sourceConfig = sourceMeta.table?.config ?? null;
647
- const headlineICPCriteria = stringArray(sourceConfig?.headlineICPCriteria).length > 0
648
- ? stringArray(sourceConfig?.headlineICPCriteria)
649
- : stringArray(sourceConfig?.rubricGuidelines);
650
- const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
651
- const signalTabs = tabsResponse.tabs ?? [];
652
- const keywords = keywordsFromSignalTabs(signalTabs);
653
- const excludedPostIds = selectedPostIdsFromSignalTabs(signalTabs);
654
- if (keywords.length === 0) {
493
+ if (actionType !== "prepare_messages" &&
494
+ toolName !== "start_campaign_message_preparation") {
655
495
  return {
656
- status: "refused",
657
- refusalReason: "no reusable Signal Discovery keywords were found on the campaign tabs",
496
+ status: "unsupported_action",
497
+ applied: false,
498
+ action,
499
+ note: "refill_sends currently auto-applies yolo prepare_messages actions; this action needs the skill workflow/operator path.",
658
500
  };
659
501
  }
660
- markProviderPromptLoaded({
661
- provider: "signal-discovery",
662
- campaignOfferId,
663
- });
664
- const searchSummary = await searchSignals({
665
- type: "keywords",
666
- keywords: keywords.map((keyword) => ({
667
- keyword,
668
- source: "refill-sends-source-continuation",
669
- })),
670
- campaignOfferId,
671
- currentStep: null,
672
- headlineICPCriteria,
673
- rubricGuidelines: headlineICPCriteria,
674
- confirmed: true,
675
- limit: 50,
676
- });
677
- const selectedPostIds = postIdsFromSignalSearch(searchSummary, maxPostsToScrape, { excludePostIds: excludedPostIds });
678
- if (selectedPostIds.length === 0) {
679
- return {
680
- status: "refused",
681
- refusalReason: "Signal Discovery search returned no new recommended posts to continue the source",
682
- result: { keywords, excludedPostIds, searchSummary },
683
- };
684
- }
685
- const selectionResult = await selectPromisingPosts({
686
- campaignOfferId,
687
- selections: selectedPostIds.map((postId) => ({
688
- postId,
689
- reason: "refill_sends same-source continuation: recent post from the campaign's existing Signal Discovery keyword family",
690
- })),
691
- headlineICPCriteria,
692
- currentStep: null,
693
- selectionMode: "replace",
694
- scrapePlanMode: "all-selected",
695
- });
696
- if (selectionResult.success === false) {
502
+ const ids = recordValue(action.ids);
503
+ const toolInput = recordValue(action.toolInput);
504
+ const campaignId = stringValue(toolInput?.campaignId) ?? stringValue(ids?.campaignId);
505
+ const tableId = stringValue(toolInput?.tableId) ?? stringValue(ids?.tableId);
506
+ if (!campaignId) {
697
507
  return {
698
- status: "refused",
699
- refusalReason: stringValue(selectionResult.message) ??
700
- "select_promising_posts did not select any posts",
701
- result: { keywords, selectedPostIds, selectionResult },
508
+ status: "missing_campaign_id",
509
+ applied: false,
510
+ action,
511
+ note: "The yolo prepare_messages action did not include a campaignId.",
702
512
  };
703
513
  }
704
- const importResult = await importLeads({
705
- campaignOfferId,
706
- provider: "signal-discovery",
707
- sourceLeadListId,
708
- currentStep: null,
709
- headlineICPCriteria,
710
- rubricGuidelines: headlineICPCriteria,
711
- confirmed: true,
712
- maxPostsToScrape: selectedPostIds.length,
713
- allowInvalidSignalPosts: true,
714
- });
715
- const importRecord = recordValue(importResult);
716
- if (importRecord?.error) {
514
+ const targetPreparedMessages = numberValue(toolInput?.targetPreparedMessages);
515
+ const approvalMode = approvalModeValue(toolInput?.approvalMode);
516
+ const rawRowSelector = toolInput?.rowSelector;
517
+ const rowSelector = rawRowSelector === undefined ? null : rowSelectorValue(rawRowSelector);
518
+ if (rawRowSelector !== undefined && !rowSelector) {
717
519
  return {
718
- status: "refused",
719
- refusalReason: stringValue(importRecord.message) ??
720
- `Signal Discovery import returned ${String(importRecord.error)}`,
721
- result: {
722
- keywords,
723
- excludedPostIds,
724
- selectedPostIds,
725
- selectionResult,
726
- importResult,
727
- },
520
+ status: "invalid_row_selector",
521
+ applied: false,
522
+ action,
523
+ note: "The yolo prepare_messages action included a rowSelector, but it was not valid for start_campaign_message_preparation.",
728
524
  };
729
525
  }
730
- return {
731
- status: "executed_and_reread",
732
- result: {
733
- provider: "signal-discovery",
734
- campaignOfferId,
735
- previousSourceLeadListId: sourceLeadListId,
736
- sourceFingerprint,
737
- keywords,
738
- excludedPostIds,
739
- selectedPostIds,
740
- sourceRowLimit,
741
- maxPostsToScrape: selectedPostIds.length,
742
- searchSummary,
743
- selectionResult,
744
- importResult,
745
- },
526
+ const startInput = {
527
+ campaignId,
528
+ ...(tableId ? { tableId } : {}),
529
+ ...(targetPreparedMessages ? { targetPreparedMessages } : {}),
530
+ ...(approvalMode ? { approvalMode } : {}),
531
+ ...(rowSelector ? { rowSelector } : {}),
532
+ ...(params.workspaceId ? { workspaceId: params.workspaceId } : {}),
746
533
  };
747
- }
748
- async function refreshPaidInmailCreditsWithRetry(senderId, workspaceId) {
749
- const errors = [];
750
- for (let attempt = 1; attempt <= PAID_INMAIL_REFRESH_MAX_ATTEMPTS; attempt += 1) {
751
- try {
752
- const receipt = await refreshPaidInmailCredits({
753
- senderId,
754
- ...(workspaceId ? { workspaceId } : {}),
755
- });
756
- return { receipt, attempts: attempt, errors };
757
- }
758
- catch (error) {
759
- errors.push(error instanceof Error ? error.message : String(error));
760
- if (attempt < PAID_INMAIL_REFRESH_MAX_ATTEMPTS) {
761
- await sleep(PAID_INMAIL_REFRESH_RETRY_DELAY_MS);
762
- }
763
- }
764
- }
534
+ const startedAt = new Date().toISOString();
535
+ const startReceipt = await startPrepareCampaignMessages(startInput);
536
+ const waitReceipt = await waitForPrepareAction({
537
+ workspaceId: params.workspaceId,
538
+ campaignId,
539
+ tableId,
540
+ startReceipt,
541
+ });
765
542
  return {
766
- receipt: null,
767
- attempts: PAID_INMAIL_REFRESH_MAX_ATTEMPTS,
768
- errors,
543
+ status: waitReceipt.timedOut ? "applied_wait_timeout" : "applied",
544
+ applied: true,
545
+ action,
546
+ toolName: "start_campaign_message_preparation",
547
+ input: startInput,
548
+ startedAt,
549
+ endedAt: new Date().toISOString(),
550
+ startReceipt,
551
+ waitReceipt,
769
552
  };
770
553
  }
771
- async function executeOneYoloPrimitive(action) {
772
- if (!action)
773
- return { status: "no_action" };
774
- if (action.yoloEligible === false) {
775
- return {
776
- status: "refused",
777
- refusalReason: "first global action is not yolo eligible",
778
- };
779
- }
780
- switch (action.type) {
781
- case "wait_for_scheduler":
782
- case "wait_for_active_work":
783
- case "wait_for_source_import":
784
- return { status: "read_only_reread", result: { waited: true } };
785
- case "prepare_messages": {
786
- const campaignId = actionCampaignId(action);
787
- const tableId = actionTableId(action) ?? undefined;
788
- const toolInput = actionToolInput(action);
789
- const targetPreparedMessages = numberValue(toolInput.targetPreparedMessages);
790
- const rowSelector = prepareRowSelectorValue(toolInput.rowSelector);
791
- const approvalMode = toolInput.approvalMode === "approve" ? "approve" : "mark_ready";
792
- if (!campaignId ||
793
- !targetPreparedMessages ||
794
- targetPreparedMessages <= 0) {
795
- return {
796
- status: "refused",
797
- refusalReason: "prepare_messages action is missing campaignId or bounded targetPreparedMessages",
798
- };
799
- }
800
- const result = await startPrepareCampaignMessages({
801
- campaignId,
802
- tableId,
803
- targetPreparedMessages,
804
- maxRowsToCheck: numberValue(toolInput.maxRowsToCheck) ?? 300,
805
- approvalMode,
806
- rowSelector,
807
- autoContinue: true,
808
- disableLowPassRateStop: true,
809
- senderId: actionSenderId(action) ?? undefined,
810
- actionType: actionActionType(action) ?? undefined,
811
- requestHash: refillPrepareRequestHash({
812
- action,
813
- campaignId,
814
- tableId,
815
- approvalMode,
816
- rowSelector,
817
- }),
818
- requestSource: "refill_sends",
819
- });
820
- return { status: "executed_and_reread", result };
821
- }
822
- case "copy_selected_source_rows": {
823
- const campaignOfferId = actionCampaignId(action);
824
- const sourceLeadListId = actionSourceLeadListId(action);
825
- const toolInput = actionToolInput(action);
826
- const sourceRowIds = stringArray(toolInput.sourceRowIds);
827
- const sourceRowLimit = numberValue(toolInput.sourceRowLimit);
828
- const sourceFingerprint = stringValue(toolInput.sourceFingerprint) ??
829
- stringValue(actionIds(action).sourceFingerprint);
830
- if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
831
- return {
832
- status: "refused",
833
- refusalReason: "copy_selected_source_rows action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
834
- };
835
- }
836
- if (sourceRowIds.length === 0 &&
837
- (!sourceRowLimit || sourceRowLimit <= 0)) {
838
- return {
839
- status: "refused",
840
- refusalReason: "copy_selected_source_rows action is missing exact sourceRowIds or a bounded sourceRowLimit",
841
- };
842
- }
843
- const copyInput = {
844
- campaignOfferId,
845
- sourceLeadListId,
846
- currentStep: null,
847
- confirmed: true,
848
- sourceRowIds: sourceRowIds.length > 0 ? sourceRowIds : undefined,
849
- sourceRowLimit: sourceRowIds.length > 0 ? undefined : (sourceRowLimit ?? undefined),
850
- reviewBatchLimit: sourceRowIds.length > 0
851
- ? sourceRowIds.length
852
- : (sourceRowLimit ?? undefined),
853
- };
854
- let result;
855
- try {
856
- result = await confirmLeadList(copyInput);
857
- }
858
- catch (error) {
859
- const rowLimitPayload = userAddedRowsLimitPayloadFromError(error);
860
- if (!rowLimitPayload)
861
- throw error;
862
- const remainingRows = typeof rowLimitPayload.remainingRows === "number" &&
863
- Number.isFinite(rowLimitPayload.remainingRows)
864
- ? Math.floor(rowLimitPayload.remainingRows)
865
- : 0;
866
- if (remainingRows <= 0) {
867
- return {
868
- status: "refused",
869
- refusalReason: rowLimitPayload.error ??
870
- "selected campaign table is at the workflow row limit",
871
- };
872
- }
873
- const retrySourceRowIds = sourceRowIds.length > 0 ? sourceRowIds.slice(0, remainingRows) : [];
874
- const retrySourceRowLimit = retrySourceRowIds.length > 0
875
- ? undefined
876
- : Math.min(sourceRowLimit ?? remainingRows, remainingRows);
877
- const retryReviewBatchLimit = retrySourceRowIds.length > 0
878
- ? retrySourceRowIds.length
879
- : retrySourceRowLimit;
880
- if (!retryReviewBatchLimit || retryReviewBatchLimit <= 0) {
881
- return {
882
- status: "refused",
883
- refusalReason: rowLimitPayload.error ??
884
- "selected campaign table cannot accept more workflow rows",
885
- };
886
- }
887
- const retryResult = await confirmLeadList({
888
- ...copyInput,
889
- sourceRowIds: retrySourceRowIds.length > 0 ? retrySourceRowIds : undefined,
890
- sourceRowLimit: retrySourceRowIds.length > 0 ? undefined : retrySourceRowLimit,
891
- reviewBatchLimit: retryReviewBatchLimit,
892
- });
893
- result =
894
- retryResult && typeof retryResult === "object"
895
- ? {
896
- ...retryResult,
897
- rowLimitRetry: {
898
- code: rowLimitPayload.code,
899
- maxRows: rowLimitPayload.maxRows,
900
- currentRows: rowLimitPayload.currentRows,
901
- requestedRows: rowLimitPayload.requestedRows,
902
- remainingRows,
903
- retriedRows: retryReviewBatchLimit,
904
- },
905
- }
906
- : {
907
- result: retryResult,
908
- rowLimitRetry: {
909
- code: rowLimitPayload.code,
910
- remainingRows,
911
- retriedRows: retryReviewBatchLimit,
912
- },
913
- };
914
- }
915
- return { status: "executed_and_reread", result };
916
- }
917
- case "continue_signal_discovery_source":
918
- return continueSignalDiscoverySource(action);
919
- case "approve_messages":
920
- return approveGeneratedMessagesBatch(action);
921
- default:
922
- return {
923
- status: "refused",
924
- refusalReason: `action ${String(action.type)} is not a safe yolo primitive`,
925
- };
926
- }
927
- }
928
554
  export async function executeRefillSendsCommand(input = {}) {
929
555
  const yolo = input.yolo === true;
930
556
  const executionMode = input.executionMode ?? (yolo ? "yolo" : "manual");
@@ -953,12 +579,6 @@ export async function executeRefillSendsCommand(input = {}) {
953
579
  failedPaidInmailRefreshes: [],
954
580
  note: "Automatic paid InMail credit refresh only runs for --yolo refill_sends calls.",
955
581
  },
956
- yoloExecution: {
957
- enabled: false,
958
- status: "not_run_without_yolo",
959
- selectedAction: null,
960
- targetPlanReread: false,
961
- },
962
582
  };
963
583
  }
964
584
  const workspaceId = workspaceContext && workspaceContext.ok
@@ -971,45 +591,41 @@ export async function executeRefillSendsCommand(input = {}) {
971
591
  const failedPaidInmailRefreshes = [];
972
592
  const refreshReceipts = [];
973
593
  for (const action of refreshActions) {
974
- const refreshResult = await refreshPaidInmailCreditsWithRetry(action.senderId, workspaceId ?? undefined);
975
- if (refreshResult.receipt) {
594
+ try {
595
+ const receipt = await refreshPaidInmailCredits({
596
+ senderId: action.senderId,
597
+ ...(workspaceId ? { workspaceId } : {}),
598
+ });
976
599
  refreshedPaidInmailSenderIds.push(action.senderId);
977
600
  refreshReceipts.push({
978
601
  senderId: action.senderId,
979
602
  actionKey: action.actionKey ?? null,
980
- attempts: refreshResult.attempts,
981
- retryErrors: refreshResult.errors,
982
- receipt: refreshResult.receipt,
603
+ receipt,
983
604
  });
984
605
  }
985
- else {
606
+ catch (error) {
986
607
  failedPaidInmailRefreshes.push({
987
608
  senderId: action.senderId,
988
- attempts: refreshResult.attempts,
989
- error: refreshResult.errors[refreshResult.errors.length - 1] ??
990
- "paid InMail credit refresh failed",
991
- errors: refreshResult.errors,
609
+ error: error instanceof Error ? error.message : String(error),
992
610
  });
993
611
  }
994
612
  }
995
613
  const targetPlan = refreshActions.length > 0
996
614
  ? await getRefillTargetPlan(targetPlanInput)
997
615
  : targetPlanBeforePaidRefresh;
998
- const selectedAction = refreshActions.length > 0 ? null : firstGlobalAction(targetPlan);
999
- const primitiveAttempt = refreshActions.length > 0
1000
- ? null
1001
- : await executeOneYoloPrimitive(selectedAction);
1002
- const shouldRereadAfterPrimitive = primitiveAttempt?.status === "executed_and_reread" ||
1003
- primitiveAttempt?.status === "read_only_reread";
1004
- const postActionTargetPlan = shouldRereadAfterPrimitive
616
+ const targetPlanBeforeYoloAction = targetPlan;
617
+ const yoloExecution = await executeYoloGlobalAction({
618
+ action: firstGlobalAction(targetPlan),
619
+ workspaceId,
620
+ });
621
+ const finalTargetPlan = yoloExecution.applied
1005
622
  ? await getRefillTargetPlan(targetPlanInput)
1006
- : null;
1007
- const finalTargetPlan = postActionTargetPlan ?? targetPlan;
623
+ : targetPlan;
1008
624
  return {
1009
625
  ...command,
1010
626
  targetPlan: finalTargetPlan,
627
+ targetPlanBeforeYoloAction,
1011
628
  targetPlanBeforePaidRefresh: refreshActions.length > 0 ? targetPlanBeforePaidRefresh : null,
1012
- targetPlanBeforeYoloPrimitive: refreshActions.length === 0 && postActionTargetPlan ? targetPlan : null,
1013
629
  autoPaidInmailRefresh: {
1014
630
  enabled: true,
1015
631
  status: refreshActions.length === 0
@@ -1025,26 +641,15 @@ export async function executeRefillSendsCommand(input = {}) {
1025
641
  workspaceId,
1026
642
  workspaceResolution: workspaceId ? "explicit" : "active_config",
1027
643
  note: refreshActions.length > 0
1028
- ? "refill_sends refreshed stale paid InMail credit facts internally with bounded retries, once per sender, then returned the post-refresh targetPlan."
644
+ ? "refill_sends refreshed stale paid InMail credit facts internally, once per sender, then reread the post-refresh targetPlan before yolo action execution."
1029
645
  : "No stale or missing paid InMail credit refresh candidates were present in the initial target plan.",
1030
646
  },
1031
- yoloExecution: refreshActions.length > 0
1032
- ? {
1033
- enabled: true,
1034
- status: "skipped_after_paid_refresh",
1035
- selectedAction: null,
1036
- targetPlanReread: true,
1037
- }
1038
- : {
1039
- enabled: true,
1040
- status: primitiveAttempt?.status ?? "no_action",
1041
- selectedAction,
1042
- result: primitiveAttempt?.result,
1043
- targetPlanReread: shouldRereadAfterPrimitive,
1044
- refusalReason: primitiveAttempt?.refusalReason,
1045
- postActionFirstAction: postActionTargetPlan
1046
- ? firstGlobalAction(postActionTargetPlan)
1047
- : null,
1048
- },
647
+ yoloExecution: {
648
+ enabled: true,
649
+ workspaceId,
650
+ workspaceResolution: workspaceId ? "explicit" : "active_config",
651
+ targetPlanRereadAfterAction: yoloExecution.applied,
652
+ ...yoloExecution,
653
+ },
1049
654
  };
1050
655
  }