@sellable/mcp 0.1.504 → 0.1.506
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 +5 -0
- package/dist/tools/campaign-fill-routing.js +13 -3
- package/dist/tools/campaign-message-preparation.d.ts +33 -23
- package/dist/tools/campaign-message-preparation.js +31 -30
- package/dist/tools/campaign-processing.d.ts +19 -0
- package/dist/tools/campaign-processing.js +31 -8
- package/dist/tools/campaign-refill-state.d.ts +5 -0
- package/dist/tools/campaign-refill-state.js +13 -3
- package/dist/tools/leads.d.ts +6 -47
- package/dist/tools/leads.js +26 -30
- package/dist/tools/prompts.js +1 -1
- package/dist/tools/readiness.d.ts +6 -0
- package/dist/tools/readiness.js +12 -5
- package/dist/tools/refill-sends.d.ts +112 -29
- package/dist/tools/refill-sends.js +222 -569
- package/dist/tools/refill-target-plan.d.ts +5 -0
- package/dist/tools/refill-target-plan.js +32 -65
- package/dist/tools/registry.d.ts +232 -48
- package/dist/tools/scheduler-fill-capacity.d.ts +5 -0
- package/dist/tools/scheduler-fill-capacity.js +13 -3
- package/dist/tools/sender-routing.d.ts +8 -1
- package/dist/tools/sender-routing.js +12 -3
- package/dist/tools/senders.d.ts +14 -1
- package/dist/tools/senders.js +31 -5
- package/dist/tools/workspace-context.d.ts +36 -0
- package/dist/tools/workspace-context.js +39 -0
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +53 -29
- package/skills/refill-sends-workflow/SKILL.md +19 -12
|
@@ -1,15 +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";
|
|
4
|
+
import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
|
|
7
5
|
const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
]);
|
|
13
13
|
function normalizeStrings(values) {
|
|
14
14
|
if (!Array.isArray(values))
|
|
15
15
|
return [];
|
|
@@ -19,11 +19,6 @@ function normalizeStrings(values) {
|
|
|
19
19
|
.filter(Boolean)),
|
|
20
20
|
];
|
|
21
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
|
-
}
|
|
27
22
|
function normalizeHorizonSendDays(value) {
|
|
28
23
|
if (value === undefined || value === null)
|
|
29
24
|
return null;
|
|
@@ -69,24 +64,10 @@ function normalizeTargetDate(value) {
|
|
|
69
64
|
}
|
|
70
65
|
return trimmed;
|
|
71
66
|
}
|
|
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
|
-
}
|
|
86
67
|
export const refillSendsToolDefinitions = [
|
|
87
68
|
{
|
|
88
69
|
name: "refill_sends",
|
|
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
|
|
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.",
|
|
90
71
|
inputSchema: {
|
|
91
72
|
type: "object",
|
|
92
73
|
properties: {
|
|
@@ -94,6 +75,19 @@ export const refillSendsToolDefinitions = [
|
|
|
94
75
|
type: "boolean",
|
|
95
76
|
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.",
|
|
96
77
|
},
|
|
78
|
+
executionMode: {
|
|
79
|
+
type: "string",
|
|
80
|
+
enum: ["manual", "scheduled", "yolo"],
|
|
81
|
+
description: "Explicit run mode. scheduled and yolo require workspaceId and never fall back to the shared active workspace.",
|
|
82
|
+
},
|
|
83
|
+
requireWorkspace: {
|
|
84
|
+
type: "boolean",
|
|
85
|
+
description: "When true, require an explicit request-scoped workspaceId before any refill API call. Scheduled automation should set this.",
|
|
86
|
+
},
|
|
87
|
+
workspaceId: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Explicit request-scoped workspace id for scheduled/yolo refill automation. Pass this on every refill call instead of switching the shared active workspace.",
|
|
90
|
+
},
|
|
97
91
|
senders: {
|
|
98
92
|
type: "array",
|
|
99
93
|
items: { type: "string" },
|
|
@@ -150,11 +144,13 @@ export const refillSendsToolDefinitions = [
|
|
|
150
144
|
},
|
|
151
145
|
];
|
|
152
146
|
export function refillSendsCommand(input = {}) {
|
|
147
|
+
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
153
148
|
const senders = normalizeStrings(input.senders);
|
|
154
149
|
const senderIds = normalizeStrings(input.senderIds);
|
|
155
150
|
const senderNames = normalizeStrings(input.senderNames);
|
|
156
151
|
const hasSenderSelectors = senders.length > 0 || senderIds.length > 0 || senderNames.length > 0;
|
|
157
152
|
const yolo = input.yolo === true;
|
|
153
|
+
const executionMode = input.executionMode ?? (yolo ? "yolo" : "manual");
|
|
158
154
|
const targetDate = normalizeTargetDate(input.targetDate);
|
|
159
155
|
const untilDate = targetDate ? null : normalizeUntilDate(input.untilDate);
|
|
160
156
|
const horizonSendDays = targetDate || untilDate
|
|
@@ -173,6 +169,9 @@ export function refillSendsCommand(input = {}) {
|
|
|
173
169
|
promptName: "refill-sends",
|
|
174
170
|
workflowPromptName: "refill-sends-workflow",
|
|
175
171
|
yolo,
|
|
172
|
+
executionMode,
|
|
173
|
+
workspaceId,
|
|
174
|
+
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
176
175
|
intent,
|
|
177
176
|
targetDate,
|
|
178
177
|
untilDate,
|
|
@@ -211,7 +210,9 @@ export function refillSendsCommand(input = {}) {
|
|
|
211
210
|
firstOperationalSteps: [
|
|
212
211
|
'Load get_subskill_prompt({ subskillName: "refill-sends-workflow" }) before any product operation.',
|
|
213
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.",
|
|
214
|
-
`Call get_refill_target_plan({ intent: "${intent}"${
|
|
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
|
|
215
216
|
? `, senderNames: ${JSON.stringify(senderNames)}`
|
|
216
217
|
: ""}${senders.length > 0 ? `, senders: ${JSON.stringify(senders)}` : ""}${targetDate
|
|
217
218
|
? `, targetDate: "${targetDate}"`
|
|
@@ -225,9 +226,11 @@ export function refillSendsCommand(input = {}) {
|
|
|
225
226
|
"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.",
|
|
226
227
|
"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.",
|
|
227
228
|
"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.",
|
|
228
|
-
`Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""} }).`,
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
`Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""}${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""} }).`,
|
|
230
|
+
`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"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""} }) and inspect current dashboard-active ACTIVE/PAUSED campaign-backed sequence campaigns before declaring a sender blocked.`,
|
|
231
|
+
workspaceId
|
|
232
|
+
? `Call list_senders({ workspaceId: "${workspaceId}" }) and get_sender_routing({ workspaceId: "${workspaceId}" }), then resolve sender selectors against active enrolled campaign-backed sequence senders.`
|
|
233
|
+
: "Call list_senders and get_sender_routing, then resolve sender selectors against active enrolled campaign-backed sequence senders.",
|
|
231
234
|
targetDate
|
|
232
235
|
? `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.`
|
|
233
236
|
: untilDate
|
|
@@ -239,14 +242,14 @@ export function refillSendsCommand(input = {}) {
|
|
|
239
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.",
|
|
240
243
|
"Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
|
|
241
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.",
|
|
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
|
|
243
|
-
"Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh
|
|
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.",
|
|
244
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.",
|
|
245
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.",
|
|
246
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.",
|
|
247
250
|
],
|
|
248
251
|
approvalContract: yolo
|
|
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/
|
|
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."
|
|
250
253
|
: hasSenderSelectors
|
|
251
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."
|
|
252
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.",
|
|
@@ -288,12 +291,14 @@ export function refillSendsCommand(input = {}) {
|
|
|
288
291
|
tableId: input.tableId,
|
|
289
292
|
intent,
|
|
290
293
|
approvalMode,
|
|
294
|
+
workspaceId,
|
|
291
295
|
},
|
|
292
296
|
},
|
|
293
297
|
},
|
|
294
298
|
};
|
|
295
299
|
}
|
|
296
300
|
function targetPlanInputFor(input = {}) {
|
|
301
|
+
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
297
302
|
const senders = normalizeStrings(input.senders);
|
|
298
303
|
const senderIds = normalizeStrings(input.senderIds);
|
|
299
304
|
const senderNames = normalizeStrings(input.senderNames);
|
|
@@ -318,6 +323,7 @@ function targetPlanInputFor(input = {}) {
|
|
|
318
323
|
? {}
|
|
319
324
|
: { horizonSendDays }),
|
|
320
325
|
approvalMode,
|
|
326
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
321
327
|
};
|
|
322
328
|
}
|
|
323
329
|
function recordValue(value) {
|
|
@@ -331,41 +337,46 @@ function stringValue(value) {
|
|
|
331
337
|
function numberValue(value) {
|
|
332
338
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
333
339
|
}
|
|
334
|
-
function
|
|
335
|
-
|
|
336
|
-
return [];
|
|
337
|
-
return value.filter((item) => typeof item === "string");
|
|
340
|
+
function approvalModeValue(value) {
|
|
341
|
+
return value === "approve" || value === "mark_ready" ? value : null;
|
|
338
342
|
}
|
|
339
|
-
function
|
|
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;
|
|
348
|
+
}
|
|
349
|
+
function rowSelectorValue(value) {
|
|
340
350
|
const selector = recordValue(value);
|
|
341
351
|
const type = stringValue(selector?.type);
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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;
|
|
348
359
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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);
|
|
360
|
+
if (type === "reviewBatch") {
|
|
361
|
+
const basisHash = stringValue(selector.basisHash);
|
|
362
|
+
return basisHash
|
|
363
|
+
? withLimit({ type, basisHash })
|
|
364
|
+
: withLimit({ type });
|
|
367
365
|
}
|
|
368
|
-
|
|
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));
|
|
369
380
|
}
|
|
370
381
|
function paidRefreshActionFrom(value) {
|
|
371
382
|
const action = recordValue(value);
|
|
@@ -417,490 +428,148 @@ function collectPaidInmailRefreshActions(plan) {
|
|
|
417
428
|
function firstGlobalAction(plan) {
|
|
418
429
|
const root = recordValue(plan);
|
|
419
430
|
const target = recordValue(root?.target);
|
|
420
|
-
const
|
|
431
|
+
const queue = Array.isArray(target?.globalActionQueue)
|
|
421
432
|
? target.globalActionQueue
|
|
422
433
|
: [];
|
|
423
|
-
|
|
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));
|
|
434
|
+
return recordValue(queue[0]);
|
|
469
435
|
}
|
|
470
|
-
function
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
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) {
|
|
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))) {
|
|
566
441
|
return {
|
|
567
|
-
|
|
568
|
-
|
|
442
|
+
waited: false,
|
|
443
|
+
timedOut: false,
|
|
444
|
+
receipt: params.startReceipt,
|
|
569
445
|
};
|
|
570
446
|
}
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
};
|
|
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
|
+
}
|
|
613
466
|
}
|
|
614
|
-
|
|
615
|
-
|
|
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) {
|
|
616
476
|
return {
|
|
617
|
-
status: "
|
|
618
|
-
|
|
477
|
+
status: "no_global_action",
|
|
478
|
+
applied: false,
|
|
479
|
+
note: "No target.globalActionQueue[0] action was present after the fresh target-plan reread.",
|
|
619
480
|
};
|
|
620
481
|
}
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
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) {
|
|
482
|
+
const actionType = stringValue(action.type);
|
|
483
|
+
const toolName = stringValue(action.toolName);
|
|
484
|
+
const yoloEligible = action.yoloEligible === true;
|
|
485
|
+
if (!yoloEligible) {
|
|
631
486
|
return {
|
|
632
|
-
status: "
|
|
633
|
-
|
|
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.",
|
|
634
491
|
};
|
|
635
492
|
}
|
|
636
|
-
|
|
637
|
-
|
|
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) {
|
|
493
|
+
if (actionType !== "prepare_messages" &&
|
|
494
|
+
toolName !== "start_campaign_message_preparation") {
|
|
655
495
|
return {
|
|
656
|
-
status: "
|
|
657
|
-
|
|
658
|
-
|
|
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.",
|
|
659
500
|
};
|
|
660
501
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
})),
|
|
667
|
-
headlineICPCriteria,
|
|
668
|
-
currentStep: null,
|
|
669
|
-
selectionMode: "replace",
|
|
670
|
-
scrapePlanMode: "all-selected",
|
|
671
|
-
});
|
|
672
|
-
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) {
|
|
673
507
|
return {
|
|
674
|
-
status: "
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
508
|
+
status: "missing_campaign_id",
|
|
509
|
+
applied: false,
|
|
510
|
+
action,
|
|
511
|
+
note: "The yolo prepare_messages action did not include a campaignId.",
|
|
678
512
|
};
|
|
679
513
|
}
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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) {
|
|
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) {
|
|
693
519
|
return {
|
|
694
|
-
status: "
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
keywords,
|
|
699
|
-
excludedPostIds,
|
|
700
|
-
selectedPostIds,
|
|
701
|
-
selectionResult,
|
|
702
|
-
importResult,
|
|
703
|
-
},
|
|
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.",
|
|
704
524
|
};
|
|
705
525
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
keywords,
|
|
714
|
-
excludedPostIds,
|
|
715
|
-
selectedPostIds,
|
|
716
|
-
sourceRowLimit,
|
|
717
|
-
maxPostsToScrape: selectedPostIds.length,
|
|
718
|
-
searchSummary,
|
|
719
|
-
selectionResult,
|
|
720
|
-
importResult,
|
|
721
|
-
},
|
|
526
|
+
const startInput = {
|
|
527
|
+
campaignId,
|
|
528
|
+
...(tableId ? { tableId } : {}),
|
|
529
|
+
...(targetPreparedMessages ? { targetPreparedMessages } : {}),
|
|
530
|
+
...(approvalMode ? { approvalMode } : {}),
|
|
531
|
+
...(rowSelector ? { rowSelector } : {}),
|
|
532
|
+
...(params.workspaceId ? { workspaceId: params.workspaceId } : {}),
|
|
722
533
|
};
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
}
|
|
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
|
+
});
|
|
738
542
|
return {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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,
|
|
742
552
|
};
|
|
743
553
|
}
|
|
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
|
-
};
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
554
|
export async function executeRefillSendsCommand(input = {}) {
|
|
902
|
-
const
|
|
903
|
-
|
|
555
|
+
const yolo = input.yolo === true;
|
|
556
|
+
const executionMode = input.executionMode ?? (yolo ? "yolo" : "manual");
|
|
557
|
+
const mustHaveWorkspace = yolo || input.requireWorkspace === true || executionMode === "scheduled";
|
|
558
|
+
const workspaceContext = mustHaveWorkspace
|
|
559
|
+
? createWorkspaceContext({
|
|
560
|
+
workspaceId: input.workspaceId,
|
|
561
|
+
executionMode,
|
|
562
|
+
toolName: "refill_sends",
|
|
563
|
+
})
|
|
564
|
+
: null;
|
|
565
|
+
if (workspaceContext && !workspaceContext.ok) {
|
|
566
|
+
return workspaceContext;
|
|
567
|
+
}
|
|
568
|
+
const scopedInput = workspaceContext && workspaceContext.ok
|
|
569
|
+
? { ...input, workspaceId: workspaceContext.context.workspaceId }
|
|
570
|
+
: input;
|
|
571
|
+
const command = refillSendsCommand(scopedInput);
|
|
572
|
+
if (!yolo) {
|
|
904
573
|
return {
|
|
905
574
|
...command,
|
|
906
575
|
autoPaidInmailRefresh: {
|
|
@@ -910,60 +579,53 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
910
579
|
failedPaidInmailRefreshes: [],
|
|
911
580
|
note: "Automatic paid InMail credit refresh only runs for --yolo refill_sends calls.",
|
|
912
581
|
},
|
|
913
|
-
yoloExecution: {
|
|
914
|
-
enabled: false,
|
|
915
|
-
status: "not_run_without_yolo",
|
|
916
|
-
selectedAction: null,
|
|
917
|
-
targetPlanReread: false,
|
|
918
|
-
},
|
|
919
582
|
};
|
|
920
583
|
}
|
|
921
|
-
const
|
|
584
|
+
const workspaceId = workspaceContext && workspaceContext.ok
|
|
585
|
+
? workspaceContext.context.workspaceId
|
|
586
|
+
: normalizeExplicitWorkspaceId(scopedInput.workspaceId);
|
|
587
|
+
const targetPlanInput = targetPlanInputFor(scopedInput);
|
|
922
588
|
const targetPlanBeforePaidRefresh = await getRefillTargetPlan(targetPlanInput);
|
|
923
589
|
const refreshActions = collectPaidInmailRefreshActions(targetPlanBeforePaidRefresh);
|
|
924
590
|
const refreshedPaidInmailSenderIds = [];
|
|
925
591
|
const failedPaidInmailRefreshes = [];
|
|
926
592
|
const refreshReceipts = [];
|
|
927
593
|
for (const action of refreshActions) {
|
|
928
|
-
|
|
929
|
-
|
|
594
|
+
try {
|
|
595
|
+
const receipt = await refreshPaidInmailCredits({
|
|
596
|
+
senderId: action.senderId,
|
|
597
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
598
|
+
});
|
|
930
599
|
refreshedPaidInmailSenderIds.push(action.senderId);
|
|
931
600
|
refreshReceipts.push({
|
|
932
601
|
senderId: action.senderId,
|
|
933
602
|
actionKey: action.actionKey ?? null,
|
|
934
|
-
|
|
935
|
-
retryErrors: refreshResult.errors,
|
|
936
|
-
receipt: refreshResult.receipt,
|
|
603
|
+
receipt,
|
|
937
604
|
});
|
|
938
605
|
}
|
|
939
|
-
|
|
606
|
+
catch (error) {
|
|
940
607
|
failedPaidInmailRefreshes.push({
|
|
941
608
|
senderId: action.senderId,
|
|
942
|
-
|
|
943
|
-
error: refreshResult.errors[refreshResult.errors.length - 1] ??
|
|
944
|
-
"paid InMail credit refresh failed",
|
|
945
|
-
errors: refreshResult.errors,
|
|
609
|
+
error: error instanceof Error ? error.message : String(error),
|
|
946
610
|
});
|
|
947
611
|
}
|
|
948
612
|
}
|
|
949
613
|
const targetPlan = refreshActions.length > 0
|
|
950
614
|
? await getRefillTargetPlan(targetPlanInput)
|
|
951
615
|
: targetPlanBeforePaidRefresh;
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const postActionTargetPlan = shouldRereadAfterPrimitive
|
|
616
|
+
const targetPlanBeforeYoloAction = targetPlan;
|
|
617
|
+
const yoloExecution = await executeYoloGlobalAction({
|
|
618
|
+
action: firstGlobalAction(targetPlan),
|
|
619
|
+
workspaceId,
|
|
620
|
+
});
|
|
621
|
+
const finalTargetPlan = yoloExecution.applied
|
|
959
622
|
? await getRefillTargetPlan(targetPlanInput)
|
|
960
|
-
:
|
|
961
|
-
const finalTargetPlan = postActionTargetPlan ?? targetPlan;
|
|
623
|
+
: targetPlan;
|
|
962
624
|
return {
|
|
963
625
|
...command,
|
|
964
626
|
targetPlan: finalTargetPlan,
|
|
627
|
+
targetPlanBeforeYoloAction,
|
|
965
628
|
targetPlanBeforePaidRefresh: refreshActions.length > 0 ? targetPlanBeforePaidRefresh : null,
|
|
966
|
-
targetPlanBeforeYoloPrimitive: refreshActions.length === 0 && postActionTargetPlan ? targetPlan : null,
|
|
967
629
|
autoPaidInmailRefresh: {
|
|
968
630
|
enabled: true,
|
|
969
631
|
status: refreshActions.length === 0
|
|
@@ -976,27 +638,18 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
976
638
|
refreshReceipts,
|
|
977
639
|
attemptedSenderIds: refreshActions.map((action) => action.senderId),
|
|
978
640
|
targetPlanReread: refreshActions.length > 0,
|
|
641
|
+
workspaceId,
|
|
642
|
+
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
979
643
|
note: refreshActions.length > 0
|
|
980
|
-
? "refill_sends refreshed stale paid InMail credit facts internally
|
|
644
|
+
? "refill_sends refreshed stale paid InMail credit facts internally, once per sender, then reread the post-refresh targetPlan before yolo action execution."
|
|
981
645
|
: "No stale or missing paid InMail credit refresh candidates were present in the initial target plan.",
|
|
982
646
|
},
|
|
983
|
-
yoloExecution:
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
},
|
|
647
|
+
yoloExecution: {
|
|
648
|
+
enabled: true,
|
|
649
|
+
workspaceId,
|
|
650
|
+
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
651
|
+
targetPlanRereadAfterAction: yoloExecution.applied,
|
|
652
|
+
...yoloExecution,
|
|
653
|
+
},
|
|
1001
654
|
};
|
|
1002
655
|
}
|