@sellable/mcp 0.1.503 → 0.1.504
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/api.d.ts +5 -5
- package/dist/api.js +10 -10
- package/dist/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/server.js +2 -2
- package/dist/tools/campaign-fill-routing.d.ts +0 -5
- package/dist/tools/campaign-fill-routing.js +3 -13
- package/dist/tools/campaign-message-preparation.d.ts +48 -7
- package/dist/tools/campaign-message-preparation.js +44 -21
- package/dist/tools/campaign-processing.d.ts +0 -19
- package/dist/tools/campaign-processing.js +8 -31
- package/dist/tools/campaign-refill-state.d.ts +0 -5
- package/dist/tools/campaign-refill-state.js +3 -13
- package/dist/tools/leads.d.ts +47 -6
- package/dist/tools/leads.js +30 -26
- package/dist/tools/prompts.js +1 -1
- package/dist/tools/readiness.d.ts +0 -6
- package/dist/tools/readiness.js +5 -12
- package/dist/tools/refill-sends.d.ts +32 -34
- package/dist/tools/refill-sends.js +608 -67
- package/dist/tools/refill-target-plan.d.ts +0 -5
- package/dist/tools/refill-target-plan.js +65 -32
- package/dist/tools/registry.d.ts +48 -205
- package/dist/tools/scheduler-fill-capacity.d.ts +0 -5
- package/dist/tools/scheduler-fill-capacity.js +3 -13
- package/dist/tools/sender-routing.d.ts +1 -8
- package/dist/tools/sender-routing.js +3 -12
- package/dist/tools/senders.d.ts +1 -14
- package/dist/tools/senders.js +5 -31
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +28 -49
- package/skills/refill-sends-workflow/SKILL.md +10 -15
- package/dist/tools/workspace-context.d.ts +0 -36
- package/dist/tools/workspace-context.js +0 -39
|
@@ -1,7 +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";
|
|
1
5
|
import { getRefillTargetPlan } from "./refill-target-plan.js";
|
|
2
6
|
import { refreshPaidInmailCredits } from "./senders.js";
|
|
3
|
-
import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
|
|
4
7
|
const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
|
|
8
|
+
const PAID_INMAIL_REFRESH_MAX_ATTEMPTS = 3;
|
|
9
|
+
const PAID_INMAIL_REFRESH_RETRY_DELAY_MS = Number(process.env.SELLABLE_MCP_PAID_REFRESH_RETRY_DELAY_MS ?? "1000");
|
|
10
|
+
const SIGNAL_DISCOVERY_MIN_REFILL_POSTS = 3;
|
|
11
|
+
const SIGNAL_DISCOVERY_MAX_REFILL_POSTS = 10;
|
|
12
|
+
const SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST = 150;
|
|
5
13
|
function normalizeStrings(values) {
|
|
6
14
|
if (!Array.isArray(values))
|
|
7
15
|
return [];
|
|
@@ -11,6 +19,11 @@ function normalizeStrings(values) {
|
|
|
11
19
|
.filter(Boolean)),
|
|
12
20
|
];
|
|
13
21
|
}
|
|
22
|
+
function sleep(ms) {
|
|
23
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
14
27
|
function normalizeHorizonSendDays(value) {
|
|
15
28
|
if (value === undefined || value === null)
|
|
16
29
|
return null;
|
|
@@ -56,10 +69,24 @@ function normalizeTargetDate(value) {
|
|
|
56
69
|
}
|
|
57
70
|
return trimmed;
|
|
58
71
|
}
|
|
72
|
+
function userAddedRowsLimitPayloadFromError(error) {
|
|
73
|
+
if (!(error instanceof SellableApiError) || error.status !== 400) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(error.body);
|
|
78
|
+
if (parsed?.code !== "USER_ADDED_ROWS_LIMIT_EXCEEDED")
|
|
79
|
+
return null;
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
59
86
|
export const refillSendsToolDefinitions = [
|
|
60
87
|
{
|
|
61
88
|
name: "refill_sends",
|
|
62
|
-
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
|
|
89
|
+
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.",
|
|
63
90
|
inputSchema: {
|
|
64
91
|
type: "object",
|
|
65
92
|
properties: {
|
|
@@ -67,19 +94,6 @@ export const refillSendsToolDefinitions = [
|
|
|
67
94
|
type: "boolean",
|
|
68
95
|
description: "When true, auto-accept the rendered bounded refill packet after the required fresh state reread. Without sender selectors, yolo means all eligible healthy senders enrolled in active campaign-backed sequence campaigns.",
|
|
69
96
|
},
|
|
70
|
-
executionMode: {
|
|
71
|
-
type: "string",
|
|
72
|
-
enum: ["manual", "scheduled", "yolo"],
|
|
73
|
-
description: "Explicit run mode. scheduled and yolo require workspaceId and never fall back to the shared active workspace.",
|
|
74
|
-
},
|
|
75
|
-
requireWorkspace: {
|
|
76
|
-
type: "boolean",
|
|
77
|
-
description: "When true, require an explicit request-scoped workspaceId before any refill API call. Scheduled automation should set this.",
|
|
78
|
-
},
|
|
79
|
-
workspaceId: {
|
|
80
|
-
type: "string",
|
|
81
|
-
description: "Explicit request-scoped workspace id for scheduled/yolo refill automation. Pass this on every refill call instead of switching the shared active workspace.",
|
|
82
|
-
},
|
|
83
97
|
senders: {
|
|
84
98
|
type: "array",
|
|
85
99
|
items: { type: "string" },
|
|
@@ -136,13 +150,11 @@ export const refillSendsToolDefinitions = [
|
|
|
136
150
|
},
|
|
137
151
|
];
|
|
138
152
|
export function refillSendsCommand(input = {}) {
|
|
139
|
-
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
140
153
|
const senders = normalizeStrings(input.senders);
|
|
141
154
|
const senderIds = normalizeStrings(input.senderIds);
|
|
142
155
|
const senderNames = normalizeStrings(input.senderNames);
|
|
143
156
|
const hasSenderSelectors = senders.length > 0 || senderIds.length > 0 || senderNames.length > 0;
|
|
144
157
|
const yolo = input.yolo === true;
|
|
145
|
-
const executionMode = input.executionMode ?? (yolo ? "yolo" : "manual");
|
|
146
158
|
const targetDate = normalizeTargetDate(input.targetDate);
|
|
147
159
|
const untilDate = targetDate ? null : normalizeUntilDate(input.untilDate);
|
|
148
160
|
const horizonSendDays = targetDate || untilDate
|
|
@@ -161,9 +173,6 @@ export function refillSendsCommand(input = {}) {
|
|
|
161
173
|
promptName: "refill-sends",
|
|
162
174
|
workflowPromptName: "refill-sends-workflow",
|
|
163
175
|
yolo,
|
|
164
|
-
executionMode,
|
|
165
|
-
workspaceId,
|
|
166
|
-
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
167
176
|
intent,
|
|
168
177
|
targetDate,
|
|
169
178
|
untilDate,
|
|
@@ -202,9 +211,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
202
211
|
firstOperationalSteps: [
|
|
203
212
|
'Load get_subskill_prompt({ subskillName: "refill-sends-workflow" }) before any product operation.',
|
|
204
213
|
"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.",
|
|
205
|
-
`Call get_refill_target_plan({ intent: "${intent}"${
|
|
206
|
-
? `, senderIds: ${JSON.stringify(senderIds)}`
|
|
207
|
-
: ""}${senderNames.length > 0
|
|
214
|
+
`Call get_refill_target_plan({ intent: "${intent}"${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""}${senderIds.length > 0 ? `, senderIds: ${JSON.stringify(senderIds)}` : ""}${senderNames.length > 0
|
|
208
215
|
? `, senderNames: ${JSON.stringify(senderNames)}`
|
|
209
216
|
: ""}${senders.length > 0 ? `, senders: ${JSON.stringify(senders)}` : ""}${targetDate
|
|
210
217
|
? `, targetDate: "${targetDate}"`
|
|
@@ -218,11 +225,9 @@ export function refillSendsCommand(input = {}) {
|
|
|
218
225
|
"If get_refill_target_plan returns status complete, report eligible sender ledger, target.senderRefillPlans, gross target, selected days, sent count, scheduled count, projected count, campaign ids, targetShapeRevision, and no-op proof without asking for approval.",
|
|
219
226
|
"Refill target lanes are connection invites (send_invite), standalone paid InMails (send_inmail_closed), or unified Sales Nav cascades represented publicly as send_inmail_closed with campaign classification sales_nav_cascade. For a Sales Nav cascade, refill the selected campaign first; its sequence can route prospects to Open InMail, paid InMail while fresh credits are >= 5, or same-campaign connection fallback without asking for separate open/paid/connection campaigns. Do not count send_dm as horizon target capacity.",
|
|
220
227
|
"If remainingReadyOrProjectedGap is 0 but remainingProjectedGap is positive, run only a persistent read-only scheduler wait/reread loop; do not ask for prep/import/approval and do not close out while scheduler pickup is the only remaining state.",
|
|
221
|
-
`Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
? `Call list_senders({ workspaceId: "${workspaceId}" }) and get_sender_routing({ workspaceId: "${workspaceId}" }), then resolve sender selectors against active enrolled campaign-backed sequence senders.`
|
|
225
|
-
: "Call list_senders and get_sender_routing, then resolve sender selectors against active enrolled campaign-backed sequence senders.",
|
|
228
|
+
`Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""} }).`,
|
|
229
|
+
'If the plain route shows stale managed waterfall evidence, archived/completed shared slots, or targets that do not cover the selected sender set, immediately call resolve_campaign_fill_route({ intent: "active" }) and inspect current dashboard-active ACTIVE/PAUSED campaign-backed sequence campaigns before declaring a sender blocked.',
|
|
230
|
+
"Call list_senders and get_sender_routing, then resolve sender selectors against active enrolled campaign-backed sequence senders.",
|
|
226
231
|
targetDate
|
|
227
232
|
? `Use fill window targetDate="${targetDate}" as one exact sender-local date; prepare rows only for scheduler-fillable slots on that date. Do not treat it as an inclusive through-date.`
|
|
228
233
|
: untilDate
|
|
@@ -234,14 +239,14 @@ export function refillSendsCommand(input = {}) {
|
|
|
234
239
|
"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.",
|
|
235
240
|
"Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
|
|
236
241
|
"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.",
|
|
237
|
-
"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/
|
|
242
|
+
"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.",
|
|
238
243
|
"Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh and the post-refresh targetPlan.",
|
|
239
244
|
"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.",
|
|
240
245
|
"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.",
|
|
241
246
|
"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.",
|
|
242
247
|
],
|
|
243
248
|
approvalContract: yolo
|
|
244
|
-
? "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
|
|
249
|
+
? "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."
|
|
245
250
|
: hasSenderSelectors
|
|
246
251
|
? "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."
|
|
247
252
|
: "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.",
|
|
@@ -283,14 +288,12 @@ export function refillSendsCommand(input = {}) {
|
|
|
283
288
|
tableId: input.tableId,
|
|
284
289
|
intent,
|
|
285
290
|
approvalMode,
|
|
286
|
-
workspaceId,
|
|
287
291
|
},
|
|
288
292
|
},
|
|
289
293
|
},
|
|
290
294
|
};
|
|
291
295
|
}
|
|
292
296
|
function targetPlanInputFor(input = {}) {
|
|
293
|
-
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
294
297
|
const senders = normalizeStrings(input.senders);
|
|
295
298
|
const senderIds = normalizeStrings(input.senderIds);
|
|
296
299
|
const senderNames = normalizeStrings(input.senderNames);
|
|
@@ -315,7 +318,6 @@ function targetPlanInputFor(input = {}) {
|
|
|
315
318
|
? {}
|
|
316
319
|
: { horizonSendDays }),
|
|
317
320
|
approvalMode,
|
|
318
|
-
...(workspaceId ? { workspaceId } : {}),
|
|
319
321
|
};
|
|
320
322
|
}
|
|
321
323
|
function recordValue(value) {
|
|
@@ -326,6 +328,45 @@ function recordValue(value) {
|
|
|
326
328
|
function stringValue(value) {
|
|
327
329
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
328
330
|
}
|
|
331
|
+
function numberValue(value) {
|
|
332
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
333
|
+
}
|
|
334
|
+
function stringArray(value) {
|
|
335
|
+
if (!Array.isArray(value))
|
|
336
|
+
return [];
|
|
337
|
+
return value.filter((item) => typeof item === "string");
|
|
338
|
+
}
|
|
339
|
+
function prepareRowSelectorValue(value) {
|
|
340
|
+
const selector = recordValue(value);
|
|
341
|
+
const type = stringValue(selector?.type);
|
|
342
|
+
if (type !== "needsEnrichment" &&
|
|
343
|
+
type !== "needsApproval" &&
|
|
344
|
+
type !== "needsGeneratedMessage" &&
|
|
345
|
+
type !== "reviewBatch" &&
|
|
346
|
+
type !== "staleGeneratedMessages") {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
const limit = numberValue(selector?.limit);
|
|
350
|
+
return {
|
|
351
|
+
type,
|
|
352
|
+
...(limit && limit > 0 ? { limit: Math.floor(limit) } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function uniqueStrings(values) {
|
|
356
|
+
const seen = new Set();
|
|
357
|
+
const result = [];
|
|
358
|
+
for (const value of values) {
|
|
359
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
360
|
+
if (!normalized)
|
|
361
|
+
continue;
|
|
362
|
+
const key = normalized.toLowerCase();
|
|
363
|
+
if (seen.has(key))
|
|
364
|
+
continue;
|
|
365
|
+
seen.add(key);
|
|
366
|
+
result.push(normalized);
|
|
367
|
+
}
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
329
370
|
function paidRefreshActionFrom(value) {
|
|
330
371
|
const action = recordValue(value);
|
|
331
372
|
if (!action || action.type !== "refresh_paid_inmail_credits")
|
|
@@ -373,25 +414,493 @@ function collectPaidInmailRefreshActions(plan) {
|
|
|
373
414
|
add(action);
|
|
374
415
|
return [...bySender.values()];
|
|
375
416
|
}
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
417
|
+
function firstGlobalAction(plan) {
|
|
418
|
+
const root = recordValue(plan);
|
|
419
|
+
const target = recordValue(root?.target);
|
|
420
|
+
const globalActionQueue = Array.isArray(target?.globalActionQueue)
|
|
421
|
+
? target.globalActionQueue
|
|
422
|
+
: [];
|
|
423
|
+
const [first] = globalActionQueue;
|
|
424
|
+
return recordValue(first);
|
|
425
|
+
}
|
|
426
|
+
function actionIds(action) {
|
|
427
|
+
return recordValue(action.ids) ?? {};
|
|
428
|
+
}
|
|
429
|
+
function actionToolInput(action) {
|
|
430
|
+
return recordValue(action.toolInput) ?? {};
|
|
431
|
+
}
|
|
432
|
+
function actionCampaignId(action) {
|
|
433
|
+
const ids = actionIds(action);
|
|
434
|
+
const toolInput = actionToolInput(action);
|
|
435
|
+
return (stringValue(toolInput.campaignId) ??
|
|
436
|
+
stringValue(toolInput.campaignOfferId) ??
|
|
437
|
+
stringValue(ids.campaignId) ??
|
|
438
|
+
stringValue(action.campaignId));
|
|
439
|
+
}
|
|
440
|
+
function actionTableId(action) {
|
|
441
|
+
const ids = actionIds(action);
|
|
442
|
+
const toolInput = actionToolInput(action);
|
|
443
|
+
return (stringValue(toolInput.tableId) ??
|
|
444
|
+
stringValue(ids.tableId) ??
|
|
445
|
+
stringValue(action.tableId));
|
|
446
|
+
}
|
|
447
|
+
function actionSourceLeadListId(action) {
|
|
448
|
+
const ids = actionIds(action);
|
|
449
|
+
const toolInput = actionToolInput(action);
|
|
450
|
+
return (stringValue(toolInput.sourceLeadListId) ??
|
|
451
|
+
stringValue(ids.sourceLeadListId) ??
|
|
452
|
+
stringValue(action.sourceLeadListId));
|
|
453
|
+
}
|
|
454
|
+
function actionSenderId(action) {
|
|
455
|
+
const ids = actionIds(action);
|
|
456
|
+
const toolInput = actionToolInput(action);
|
|
457
|
+
return (stringValue(toolInput.senderId) ??
|
|
458
|
+
stringValue(ids.senderId) ??
|
|
459
|
+
stringValue(action.senderId));
|
|
460
|
+
}
|
|
461
|
+
function actionActionType(action) {
|
|
462
|
+
const ids = actionIds(action);
|
|
463
|
+
const toolInput = actionToolInput(action);
|
|
464
|
+
return (stringValue(toolInput.actionType) ??
|
|
465
|
+
stringValue(toolInput.selectedLane) ??
|
|
466
|
+
stringValue(ids.actionType) ??
|
|
467
|
+
stringValue(action.actionType) ??
|
|
468
|
+
stringValue(action.selectedLane));
|
|
469
|
+
}
|
|
470
|
+
function actionSourceFingerprint(action) {
|
|
471
|
+
const ids = actionIds(action);
|
|
472
|
+
const toolInput = actionToolInput(action);
|
|
473
|
+
return (stringValue(toolInput.sourceFingerprint) ??
|
|
474
|
+
stringValue(ids.sourceFingerprint) ??
|
|
475
|
+
stringValue(action.sourceFingerprint));
|
|
476
|
+
}
|
|
477
|
+
function actionLeadSourceProvider(action) {
|
|
478
|
+
const ids = actionIds(action);
|
|
479
|
+
const toolInput = actionToolInput(action);
|
|
480
|
+
return (stringValue(toolInput.leadSourceProvider) ??
|
|
481
|
+
stringValue(toolInput.provider) ??
|
|
482
|
+
stringValue(ids.leadSourceProvider) ??
|
|
483
|
+
stringValue(action.leadSourceProvider));
|
|
484
|
+
}
|
|
485
|
+
function normalizedSignalKeyword(value) {
|
|
486
|
+
if (typeof value !== "string")
|
|
487
|
+
return null;
|
|
488
|
+
const keyword = value.trim();
|
|
489
|
+
if (!keyword)
|
|
490
|
+
return null;
|
|
491
|
+
if (/^(https?:\/\/|www\.|linkedin\.com\/|\/?in\/)/i.test(keyword)) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return keyword;
|
|
495
|
+
}
|
|
496
|
+
function keywordsFromSignalTabs(tabs) {
|
|
497
|
+
const selectedKeywords = [];
|
|
498
|
+
const fallbackKeywords = [];
|
|
499
|
+
for (const tab of tabs) {
|
|
500
|
+
const keyword = normalizedSignalKeyword(tab.keyword);
|
|
501
|
+
if (!keyword)
|
|
502
|
+
continue;
|
|
503
|
+
const selected = (tab.posts ?? []).some((post) => post.isSelected === true);
|
|
504
|
+
if (selected) {
|
|
505
|
+
selectedKeywords.push(keyword);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
fallbackKeywords.push(keyword);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return uniqueStrings([...selectedKeywords, ...fallbackKeywords]).slice(0, 5);
|
|
512
|
+
}
|
|
513
|
+
function selectedPostIdsFromSignalTabs(tabs) {
|
|
514
|
+
return uniqueStrings(tabs.flatMap((tab) => (tab.posts ?? [])
|
|
515
|
+
.filter((post) => post.isSelected === true)
|
|
516
|
+
.map((post) => post.id)));
|
|
517
|
+
}
|
|
518
|
+
function postIdsFromSignalSearch(summary, maxPosts, options = {}) {
|
|
519
|
+
const excludedPostIds = new Set((options.excludePostIds ?? []).map((postId) => postId.toLowerCase()));
|
|
520
|
+
const recommendedPostIds = stringArray(summary?.recommendedPostIds);
|
|
521
|
+
const topPostIds = Array.isArray(summary?.topPosts)
|
|
522
|
+
? summary.topPosts
|
|
523
|
+
.map((post) => post && typeof post === "object"
|
|
524
|
+
? stringValue(post.id)
|
|
525
|
+
: null)
|
|
526
|
+
.filter((id) => Boolean(id))
|
|
527
|
+
: [];
|
|
528
|
+
return uniqueStrings([...recommendedPostIds, ...topPostIds])
|
|
529
|
+
.filter((postId) => !excludedPostIds.has(postId.toLowerCase()))
|
|
530
|
+
.slice(0, Math.max(1, maxPosts));
|
|
531
|
+
}
|
|
532
|
+
function maxSignalDiscoveryPostsForSourceLimit(sourceRowLimit) {
|
|
533
|
+
return Math.min(SIGNAL_DISCOVERY_MAX_REFILL_POSTS, Math.max(SIGNAL_DISCOVERY_MIN_REFILL_POSTS, Math.ceil(sourceRowLimit / SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST)));
|
|
534
|
+
}
|
|
535
|
+
function refillPrepareRequestHash(params) {
|
|
536
|
+
return [
|
|
537
|
+
"refill_sends",
|
|
538
|
+
"prepare_messages",
|
|
539
|
+
params.campaignId,
|
|
540
|
+
params.tableId ?? "no-table",
|
|
541
|
+
actionSenderId(params.action) ?? "all-senders",
|
|
542
|
+
actionSourceLeadListId(params.action) ?? "no-source-list",
|
|
543
|
+
actionActionType(params.action) ?? "no-action-type",
|
|
544
|
+
params.approvalMode,
|
|
545
|
+
params.rowSelector
|
|
546
|
+
? `${params.rowSelector.type}:${params.rowSelector.limit ?? "no-limit"}`
|
|
547
|
+
: "no-row-selector",
|
|
548
|
+
].join(":");
|
|
549
|
+
}
|
|
550
|
+
function boundedApprovalLimit(action) {
|
|
551
|
+
const toolInput = actionToolInput(action);
|
|
552
|
+
const rowSelector = recordValue(toolInput.rowSelector);
|
|
553
|
+
const selectorLimit = numberValue(rowSelector?.limit);
|
|
554
|
+
const inputLimit = numberValue(toolInput.limit);
|
|
555
|
+
const limit = selectorLimit ?? inputLimit;
|
|
556
|
+
if (!limit || limit <= 0)
|
|
557
|
+
return null;
|
|
558
|
+
return Math.floor(limit);
|
|
559
|
+
}
|
|
560
|
+
async function approveGeneratedMessagesBatch(action) {
|
|
561
|
+
const tableId = actionTableId(action);
|
|
562
|
+
const toolInput = actionToolInput(action);
|
|
563
|
+
const columnId = stringValue(toolInput.columnId) ?? stringValue(actionIds(action).columnId);
|
|
564
|
+
const limit = boundedApprovalLimit(action);
|
|
565
|
+
if (!tableId || !limit) {
|
|
566
|
+
return {
|
|
567
|
+
status: "refused",
|
|
568
|
+
refusalReason: "approve_messages action is missing tableId or a bounded rowSelector.limit",
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const api = getApi();
|
|
572
|
+
const result = await api.post("/api/v3/workflow-tables/cells/approve-batch", {
|
|
573
|
+
tableId,
|
|
574
|
+
...(columnId ? { columnId } : {}),
|
|
575
|
+
limit,
|
|
576
|
+
scope: "generated_unapproved",
|
|
577
|
+
});
|
|
578
|
+
return { status: "executed_and_reread", result };
|
|
579
|
+
}
|
|
580
|
+
async function continueSignalDiscoverySource(action) {
|
|
581
|
+
const campaignOfferId = actionCampaignId(action);
|
|
582
|
+
const sourceLeadListId = actionSourceLeadListId(action);
|
|
583
|
+
const sourceFingerprint = actionSourceFingerprint(action);
|
|
584
|
+
const toolInput = actionToolInput(action);
|
|
585
|
+
const sourceRowLimit = Math.min(1500, Math.max(100, Math.floor(numberValue(toolInput.sourceRowLimit) ??
|
|
586
|
+
numberValue(toolInput.targetRows) ??
|
|
587
|
+
numberValue(action.targetRows) ??
|
|
588
|
+
100)));
|
|
589
|
+
const maxPostsToScrape = maxSignalDiscoveryPostsForSourceLimit(sourceRowLimit);
|
|
590
|
+
if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
|
|
591
|
+
return {
|
|
592
|
+
status: "refused",
|
|
593
|
+
refusalReason: "continue_signal_discovery_source action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const requestedProvider = actionLeadSourceProvider(action);
|
|
597
|
+
if (requestedProvider &&
|
|
598
|
+
requestedProvider !== "signal-discovery" &&
|
|
599
|
+
requestedProvider !== "campaign-tracked-post") {
|
|
600
|
+
return {
|
|
601
|
+
status: "refused",
|
|
602
|
+
refusalReason: "continue_signal_discovery_source can only run for Signal Discovery source families",
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const api = getApi();
|
|
606
|
+
const campaign = await api.get(`/api/v2/campaign-offers/${campaignOfferId}`);
|
|
607
|
+
if (campaign.leadSourceProvider !== "signal-discovery" &&
|
|
608
|
+
campaign.leadSourceProvider !== "campaign-tracked-post") {
|
|
609
|
+
return {
|
|
610
|
+
status: "refused",
|
|
611
|
+
refusalReason: "campaign leadSourceProvider is not Signal Discovery; refusing same-source continuation",
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
if (campaign.selectedLeadListId &&
|
|
615
|
+
campaign.selectedLeadListId !== sourceLeadListId) {
|
|
616
|
+
return {
|
|
617
|
+
status: "refused",
|
|
618
|
+
refusalReason: "campaign selectedLeadListId changed since the plan packet; rerun get_refill_target_plan before source continuation",
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const sourceMeta = await api.get(`/api/v3/workflow-tables/${sourceLeadListId}?mode=meta`);
|
|
622
|
+
const sourceConfig = sourceMeta.table?.config ?? null;
|
|
623
|
+
const headlineICPCriteria = stringArray(sourceConfig?.headlineICPCriteria).length > 0
|
|
624
|
+
? stringArray(sourceConfig?.headlineICPCriteria)
|
|
625
|
+
: stringArray(sourceConfig?.rubricGuidelines);
|
|
626
|
+
const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
|
|
627
|
+
const signalTabs = tabsResponse.tabs ?? [];
|
|
628
|
+
const keywords = keywordsFromSignalTabs(signalTabs);
|
|
629
|
+
const excludedPostIds = selectedPostIdsFromSignalTabs(signalTabs);
|
|
630
|
+
if (keywords.length === 0) {
|
|
631
|
+
return {
|
|
632
|
+
status: "refused",
|
|
633
|
+
refusalReason: "no reusable Signal Discovery keywords were found on the campaign tabs",
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
markProviderPromptLoaded({
|
|
637
|
+
provider: "signal-discovery",
|
|
638
|
+
campaignOfferId,
|
|
639
|
+
});
|
|
640
|
+
const searchSummary = await searchSignals({
|
|
641
|
+
type: "keywords",
|
|
642
|
+
keywords: keywords.map((keyword) => ({
|
|
643
|
+
keyword,
|
|
644
|
+
source: "refill-sends-source-continuation",
|
|
645
|
+
})),
|
|
646
|
+
campaignOfferId,
|
|
647
|
+
currentStep: null,
|
|
648
|
+
headlineICPCriteria,
|
|
649
|
+
rubricGuidelines: headlineICPCriteria,
|
|
650
|
+
confirmed: true,
|
|
651
|
+
limit: 50,
|
|
652
|
+
});
|
|
653
|
+
const selectedPostIds = postIdsFromSignalSearch(searchSummary, maxPostsToScrape, { excludePostIds: excludedPostIds });
|
|
654
|
+
if (selectedPostIds.length === 0) {
|
|
655
|
+
return {
|
|
656
|
+
status: "refused",
|
|
657
|
+
refusalReason: "Signal Discovery search returned no new recommended posts to continue the source",
|
|
658
|
+
result: { keywords, excludedPostIds, searchSummary },
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const selectionResult = await selectPromisingPosts({
|
|
662
|
+
campaignOfferId,
|
|
663
|
+
selections: selectedPostIds.map((postId) => ({
|
|
664
|
+
postId,
|
|
665
|
+
reason: "refill_sends same-source continuation: recent post from the campaign's existing Signal Discovery keyword family",
|
|
666
|
+
})),
|
|
667
|
+
headlineICPCriteria,
|
|
668
|
+
currentStep: null,
|
|
669
|
+
selectionMode: "replace",
|
|
670
|
+
scrapePlanMode: "all-selected",
|
|
671
|
+
});
|
|
672
|
+
if (selectionResult.success === false) {
|
|
673
|
+
return {
|
|
674
|
+
status: "refused",
|
|
675
|
+
refusalReason: stringValue(selectionResult.message) ??
|
|
676
|
+
"select_promising_posts did not select any posts",
|
|
677
|
+
result: { keywords, selectedPostIds, selectionResult },
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const importResult = await importLeads({
|
|
681
|
+
campaignOfferId,
|
|
682
|
+
provider: "signal-discovery",
|
|
683
|
+
sourceLeadListId,
|
|
684
|
+
currentStep: null,
|
|
685
|
+
headlineICPCriteria,
|
|
686
|
+
rubricGuidelines: headlineICPCriteria,
|
|
687
|
+
confirmed: true,
|
|
688
|
+
maxPostsToScrape: selectedPostIds.length,
|
|
689
|
+
allowInvalidSignalPosts: true,
|
|
690
|
+
});
|
|
691
|
+
const importRecord = recordValue(importResult);
|
|
692
|
+
if (importRecord?.error) {
|
|
693
|
+
return {
|
|
694
|
+
status: "refused",
|
|
695
|
+
refusalReason: stringValue(importRecord.message) ??
|
|
696
|
+
`Signal Discovery import returned ${String(importRecord.error)}`,
|
|
697
|
+
result: {
|
|
698
|
+
keywords,
|
|
699
|
+
excludedPostIds,
|
|
700
|
+
selectedPostIds,
|
|
701
|
+
selectionResult,
|
|
702
|
+
importResult,
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
status: "executed_and_reread",
|
|
708
|
+
result: {
|
|
709
|
+
provider: "signal-discovery",
|
|
710
|
+
campaignOfferId,
|
|
711
|
+
previousSourceLeadListId: sourceLeadListId,
|
|
712
|
+
sourceFingerprint,
|
|
713
|
+
keywords,
|
|
714
|
+
excludedPostIds,
|
|
715
|
+
selectedPostIds,
|
|
716
|
+
sourceRowLimit,
|
|
717
|
+
maxPostsToScrape: selectedPostIds.length,
|
|
718
|
+
searchSummary,
|
|
719
|
+
selectionResult,
|
|
720
|
+
importResult,
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
async function refreshPaidInmailCreditsWithRetry(senderId) {
|
|
725
|
+
const errors = [];
|
|
726
|
+
for (let attempt = 1; attempt <= PAID_INMAIL_REFRESH_MAX_ATTEMPTS; attempt += 1) {
|
|
727
|
+
try {
|
|
728
|
+
const receipt = await refreshPaidInmailCredits({ senderId });
|
|
729
|
+
return { receipt, attempts: attempt, errors };
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
733
|
+
if (attempt < PAID_INMAIL_REFRESH_MAX_ATTEMPTS) {
|
|
734
|
+
await sleep(PAID_INMAIL_REFRESH_RETRY_DELAY_MS);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
receipt: null,
|
|
740
|
+
attempts: PAID_INMAIL_REFRESH_MAX_ATTEMPTS,
|
|
741
|
+
errors,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
async function executeOneYoloPrimitive(action) {
|
|
745
|
+
if (!action)
|
|
746
|
+
return { status: "no_action" };
|
|
747
|
+
if (action.yoloEligible === false) {
|
|
748
|
+
return {
|
|
749
|
+
status: "refused",
|
|
750
|
+
refusalReason: "first global action is not yolo eligible",
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
switch (action.type) {
|
|
754
|
+
case "wait_for_scheduler":
|
|
755
|
+
case "wait_for_active_work":
|
|
756
|
+
case "wait_for_source_import":
|
|
757
|
+
return { status: "read_only_reread", result: { waited: true } };
|
|
758
|
+
case "prepare_messages": {
|
|
759
|
+
const campaignId = actionCampaignId(action);
|
|
760
|
+
const tableId = actionTableId(action) ?? undefined;
|
|
761
|
+
const toolInput = actionToolInput(action);
|
|
762
|
+
const targetPreparedMessages = numberValue(toolInput.targetPreparedMessages);
|
|
763
|
+
const rowSelector = prepareRowSelectorValue(toolInput.rowSelector);
|
|
764
|
+
const approvalMode = toolInput.approvalMode === "approve" ? "approve" : "mark_ready";
|
|
765
|
+
if (!campaignId ||
|
|
766
|
+
!targetPreparedMessages ||
|
|
767
|
+
targetPreparedMessages <= 0) {
|
|
768
|
+
return {
|
|
769
|
+
status: "refused",
|
|
770
|
+
refusalReason: "prepare_messages action is missing campaignId or bounded targetPreparedMessages",
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const result = await startPrepareCampaignMessages({
|
|
774
|
+
campaignId,
|
|
775
|
+
tableId,
|
|
776
|
+
targetPreparedMessages,
|
|
777
|
+
maxRowsToCheck: numberValue(toolInput.maxRowsToCheck) ?? 300,
|
|
778
|
+
approvalMode,
|
|
779
|
+
rowSelector,
|
|
780
|
+
autoContinue: true,
|
|
781
|
+
disableLowPassRateStop: true,
|
|
782
|
+
senderId: actionSenderId(action) ?? undefined,
|
|
783
|
+
actionType: actionActionType(action) ?? undefined,
|
|
784
|
+
requestHash: refillPrepareRequestHash({
|
|
785
|
+
action,
|
|
786
|
+
campaignId,
|
|
787
|
+
tableId,
|
|
788
|
+
approvalMode,
|
|
789
|
+
rowSelector,
|
|
790
|
+
}),
|
|
791
|
+
requestSource: "refill_sends",
|
|
792
|
+
});
|
|
793
|
+
return { status: "executed_and_reread", result };
|
|
794
|
+
}
|
|
795
|
+
case "copy_selected_source_rows": {
|
|
796
|
+
const campaignOfferId = actionCampaignId(action);
|
|
797
|
+
const sourceLeadListId = actionSourceLeadListId(action);
|
|
798
|
+
const toolInput = actionToolInput(action);
|
|
799
|
+
const sourceRowIds = stringArray(toolInput.sourceRowIds);
|
|
800
|
+
const sourceRowLimit = numberValue(toolInput.sourceRowLimit);
|
|
801
|
+
const sourceFingerprint = stringValue(toolInput.sourceFingerprint) ??
|
|
802
|
+
stringValue(actionIds(action).sourceFingerprint);
|
|
803
|
+
if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
|
|
804
|
+
return {
|
|
805
|
+
status: "refused",
|
|
806
|
+
refusalReason: "copy_selected_source_rows action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
if (sourceRowIds.length === 0 &&
|
|
810
|
+
(!sourceRowLimit || sourceRowLimit <= 0)) {
|
|
811
|
+
return {
|
|
812
|
+
status: "refused",
|
|
813
|
+
refusalReason: "copy_selected_source_rows action is missing exact sourceRowIds or a bounded sourceRowLimit",
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const copyInput = {
|
|
817
|
+
campaignOfferId,
|
|
818
|
+
sourceLeadListId,
|
|
819
|
+
currentStep: null,
|
|
820
|
+
confirmed: true,
|
|
821
|
+
sourceRowIds: sourceRowIds.length > 0 ? sourceRowIds : undefined,
|
|
822
|
+
sourceRowLimit: sourceRowIds.length > 0 ? undefined : (sourceRowLimit ?? undefined),
|
|
823
|
+
reviewBatchLimit: sourceRowIds.length > 0
|
|
824
|
+
? sourceRowIds.length
|
|
825
|
+
: (sourceRowLimit ?? undefined),
|
|
826
|
+
};
|
|
827
|
+
let result;
|
|
828
|
+
try {
|
|
829
|
+
result = await confirmLeadList(copyInput);
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
const rowLimitPayload = userAddedRowsLimitPayloadFromError(error);
|
|
833
|
+
if (!rowLimitPayload)
|
|
834
|
+
throw error;
|
|
835
|
+
const remainingRows = typeof rowLimitPayload.remainingRows === "number" &&
|
|
836
|
+
Number.isFinite(rowLimitPayload.remainingRows)
|
|
837
|
+
? Math.floor(rowLimitPayload.remainingRows)
|
|
838
|
+
: 0;
|
|
839
|
+
if (remainingRows <= 0) {
|
|
840
|
+
return {
|
|
841
|
+
status: "refused",
|
|
842
|
+
refusalReason: rowLimitPayload.error ??
|
|
843
|
+
"selected campaign table is at the workflow row limit",
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
const retrySourceRowIds = sourceRowIds.length > 0 ? sourceRowIds.slice(0, remainingRows) : [];
|
|
847
|
+
const retrySourceRowLimit = retrySourceRowIds.length > 0
|
|
848
|
+
? undefined
|
|
849
|
+
: Math.min(sourceRowLimit ?? remainingRows, remainingRows);
|
|
850
|
+
const retryReviewBatchLimit = retrySourceRowIds.length > 0
|
|
851
|
+
? retrySourceRowIds.length
|
|
852
|
+
: retrySourceRowLimit;
|
|
853
|
+
if (!retryReviewBatchLimit || retryReviewBatchLimit <= 0) {
|
|
854
|
+
return {
|
|
855
|
+
status: "refused",
|
|
856
|
+
refusalReason: rowLimitPayload.error ??
|
|
857
|
+
"selected campaign table cannot accept more workflow rows",
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const retryResult = await confirmLeadList({
|
|
861
|
+
...copyInput,
|
|
862
|
+
sourceRowIds: retrySourceRowIds.length > 0 ? retrySourceRowIds : undefined,
|
|
863
|
+
sourceRowLimit: retrySourceRowIds.length > 0 ? undefined : retrySourceRowLimit,
|
|
864
|
+
reviewBatchLimit: retryReviewBatchLimit,
|
|
865
|
+
});
|
|
866
|
+
result =
|
|
867
|
+
retryResult && typeof retryResult === "object"
|
|
868
|
+
? {
|
|
869
|
+
...retryResult,
|
|
870
|
+
rowLimitRetry: {
|
|
871
|
+
code: rowLimitPayload.code,
|
|
872
|
+
maxRows: rowLimitPayload.maxRows,
|
|
873
|
+
currentRows: rowLimitPayload.currentRows,
|
|
874
|
+
requestedRows: rowLimitPayload.requestedRows,
|
|
875
|
+
remainingRows,
|
|
876
|
+
retriedRows: retryReviewBatchLimit,
|
|
877
|
+
},
|
|
878
|
+
}
|
|
879
|
+
: {
|
|
880
|
+
result: retryResult,
|
|
881
|
+
rowLimitRetry: {
|
|
882
|
+
code: rowLimitPayload.code,
|
|
883
|
+
remainingRows,
|
|
884
|
+
retriedRows: retryReviewBatchLimit,
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
return { status: "executed_and_reread", result };
|
|
889
|
+
}
|
|
890
|
+
case "continue_signal_discovery_source":
|
|
891
|
+
return continueSignalDiscoverySource(action);
|
|
892
|
+
case "approve_messages":
|
|
893
|
+
return approveGeneratedMessagesBatch(action);
|
|
894
|
+
default:
|
|
895
|
+
return {
|
|
896
|
+
status: "refused",
|
|
897
|
+
refusalReason: `action ${String(action.type)} is not a safe yolo primitive`,
|
|
898
|
+
};
|
|
389
899
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (!yolo) {
|
|
900
|
+
}
|
|
901
|
+
export async function executeRefillSendsCommand(input = {}) {
|
|
902
|
+
const command = refillSendsCommand(input);
|
|
903
|
+
if (input.yolo !== true) {
|
|
395
904
|
return {
|
|
396
905
|
...command,
|
|
397
906
|
autoPaidInmailRefresh: {
|
|
@@ -401,44 +910,60 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
401
910
|
failedPaidInmailRefreshes: [],
|
|
402
911
|
note: "Automatic paid InMail credit refresh only runs for --yolo refill_sends calls.",
|
|
403
912
|
},
|
|
913
|
+
yoloExecution: {
|
|
914
|
+
enabled: false,
|
|
915
|
+
status: "not_run_without_yolo",
|
|
916
|
+
selectedAction: null,
|
|
917
|
+
targetPlanReread: false,
|
|
918
|
+
},
|
|
404
919
|
};
|
|
405
920
|
}
|
|
406
|
-
const
|
|
407
|
-
? workspaceContext.context.workspaceId
|
|
408
|
-
: normalizeExplicitWorkspaceId(scopedInput.workspaceId);
|
|
409
|
-
const targetPlanInput = targetPlanInputFor(scopedInput);
|
|
921
|
+
const targetPlanInput = targetPlanInputFor(input);
|
|
410
922
|
const targetPlanBeforePaidRefresh = await getRefillTargetPlan(targetPlanInput);
|
|
411
923
|
const refreshActions = collectPaidInmailRefreshActions(targetPlanBeforePaidRefresh);
|
|
412
924
|
const refreshedPaidInmailSenderIds = [];
|
|
413
925
|
const failedPaidInmailRefreshes = [];
|
|
414
926
|
const refreshReceipts = [];
|
|
415
927
|
for (const action of refreshActions) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
senderId: action.senderId,
|
|
419
|
-
...(workspaceId ? { workspaceId } : {}),
|
|
420
|
-
});
|
|
928
|
+
const refreshResult = await refreshPaidInmailCreditsWithRetry(action.senderId);
|
|
929
|
+
if (refreshResult.receipt) {
|
|
421
930
|
refreshedPaidInmailSenderIds.push(action.senderId);
|
|
422
931
|
refreshReceipts.push({
|
|
423
932
|
senderId: action.senderId,
|
|
424
933
|
actionKey: action.actionKey ?? null,
|
|
425
|
-
|
|
934
|
+
attempts: refreshResult.attempts,
|
|
935
|
+
retryErrors: refreshResult.errors,
|
|
936
|
+
receipt: refreshResult.receipt,
|
|
426
937
|
});
|
|
427
938
|
}
|
|
428
|
-
|
|
939
|
+
else {
|
|
429
940
|
failedPaidInmailRefreshes.push({
|
|
430
941
|
senderId: action.senderId,
|
|
431
|
-
|
|
942
|
+
attempts: refreshResult.attempts,
|
|
943
|
+
error: refreshResult.errors[refreshResult.errors.length - 1] ??
|
|
944
|
+
"paid InMail credit refresh failed",
|
|
945
|
+
errors: refreshResult.errors,
|
|
432
946
|
});
|
|
433
947
|
}
|
|
434
948
|
}
|
|
435
949
|
const targetPlan = refreshActions.length > 0
|
|
436
950
|
? await getRefillTargetPlan(targetPlanInput)
|
|
437
951
|
: targetPlanBeforePaidRefresh;
|
|
952
|
+
const selectedAction = refreshActions.length > 0 ? null : firstGlobalAction(targetPlan);
|
|
953
|
+
const primitiveAttempt = refreshActions.length > 0
|
|
954
|
+
? null
|
|
955
|
+
: await executeOneYoloPrimitive(selectedAction);
|
|
956
|
+
const shouldRereadAfterPrimitive = primitiveAttempt?.status === "executed_and_reread" ||
|
|
957
|
+
primitiveAttempt?.status === "read_only_reread";
|
|
958
|
+
const postActionTargetPlan = shouldRereadAfterPrimitive
|
|
959
|
+
? await getRefillTargetPlan(targetPlanInput)
|
|
960
|
+
: null;
|
|
961
|
+
const finalTargetPlan = postActionTargetPlan ?? targetPlan;
|
|
438
962
|
return {
|
|
439
963
|
...command,
|
|
440
|
-
targetPlan,
|
|
964
|
+
targetPlan: finalTargetPlan,
|
|
441
965
|
targetPlanBeforePaidRefresh: refreshActions.length > 0 ? targetPlanBeforePaidRefresh : null,
|
|
966
|
+
targetPlanBeforeYoloPrimitive: refreshActions.length === 0 && postActionTargetPlan ? targetPlan : null,
|
|
442
967
|
autoPaidInmailRefresh: {
|
|
443
968
|
enabled: true,
|
|
444
969
|
status: refreshActions.length === 0
|
|
@@ -451,11 +976,27 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
451
976
|
refreshReceipts,
|
|
452
977
|
attemptedSenderIds: refreshActions.map((action) => action.senderId),
|
|
453
978
|
targetPlanReread: refreshActions.length > 0,
|
|
454
|
-
workspaceId,
|
|
455
|
-
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
456
979
|
note: refreshActions.length > 0
|
|
457
|
-
? "refill_sends refreshed stale paid InMail credit facts internally, once per sender, then returned the post-refresh targetPlan."
|
|
980
|
+
? "refill_sends refreshed stale paid InMail credit facts internally with bounded retries, once per sender, then returned the post-refresh targetPlan."
|
|
458
981
|
: "No stale or missing paid InMail credit refresh candidates were present in the initial target plan.",
|
|
459
982
|
},
|
|
983
|
+
yoloExecution: refreshActions.length > 0
|
|
984
|
+
? {
|
|
985
|
+
enabled: true,
|
|
986
|
+
status: "skipped_after_paid_refresh",
|
|
987
|
+
selectedAction: null,
|
|
988
|
+
targetPlanReread: true,
|
|
989
|
+
}
|
|
990
|
+
: {
|
|
991
|
+
enabled: true,
|
|
992
|
+
status: primitiveAttempt?.status ?? "no_action",
|
|
993
|
+
selectedAction,
|
|
994
|
+
result: primitiveAttempt?.result,
|
|
995
|
+
targetPlanReread: shouldRereadAfterPrimitive,
|
|
996
|
+
refusalReason: primitiveAttempt?.refusalReason,
|
|
997
|
+
postActionFirstAction: postActionTargetPlan
|
|
998
|
+
? firstGlobalAction(postActionTargetPlan)
|
|
999
|
+
: null,
|
|
1000
|
+
},
|
|
460
1001
|
};
|
|
461
1002
|
}
|