@sellable/mcp 0.1.466 → 0.1.468
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.
|
@@ -150,6 +150,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
150
150
|
? `, untilDate: "${untilDate}"`
|
|
151
151
|
: `, horizonSendDays: ${horizonSendDays}`}, approvalMode: "${approvalMode}" }) before any import, prep, approval, start, or schedule-affecting action.`,
|
|
152
152
|
"If get_refill_target_plan returns status complete, report eligible sender ledger, gross target, selected days, sent count, scheduled count, projected count, campaign ids, targetShapeRevision, and no-op proof without asking for approval.",
|
|
153
|
+
"Refill target lanes are only connection invites (send_invite) or paid InMails (send_inmail_closed). Do not count send_dm or send_inmail_open as horizon target capacity; they may exist in sequences, but the planner must pick the refill lane from the most recent active campaign-backed send evidence for each sender.",
|
|
153
154
|
"If remainingReadyOrProjectedGap is 0 but remainingProjectedGap is positive, run only a bounded read-only scheduler settle/reread loop; do not ask for prep/import/approval.",
|
|
154
155
|
`Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""} }).`,
|
|
155
156
|
'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.',
|
|
@@ -1,12 +1,238 @@
|
|
|
1
1
|
import { getApi } from "../api.js";
|
|
2
|
+
const REFILL_TARGET_ACTION_TYPES = [
|
|
3
|
+
"send_invite",
|
|
4
|
+
"send_inmail_closed",
|
|
5
|
+
];
|
|
6
|
+
const REFILL_TARGET_ACTION_SET = new Set(REFILL_TARGET_ACTION_TYPES);
|
|
2
7
|
async function postRefillTargetPlan(body) {
|
|
3
8
|
const api = getApi();
|
|
4
9
|
return api.post("/api/v3/mcp/refill-target-plan", body);
|
|
5
10
|
}
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
13
|
+
}
|
|
14
|
+
function allowedActionType(value) {
|
|
15
|
+
return typeof value === "string" && REFILL_TARGET_ACTION_SET.has(value)
|
|
16
|
+
? value
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
function actionTypesFrom(value) {
|
|
20
|
+
if (!Array.isArray(value))
|
|
21
|
+
return [];
|
|
22
|
+
return [...new Set(value.map(allowedActionType).filter(Boolean))];
|
|
23
|
+
}
|
|
24
|
+
function numberValue(value) {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
26
|
+
}
|
|
27
|
+
function evidenceLatestMs(evidence) {
|
|
28
|
+
const latestAt = evidence.latestAt;
|
|
29
|
+
if (typeof latestAt !== "string")
|
|
30
|
+
return 0;
|
|
31
|
+
const timestamp = new Date(latestAt).getTime();
|
|
32
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
33
|
+
}
|
|
34
|
+
function chooseEvidenceAction(evidence, fallback) {
|
|
35
|
+
const byAction = new Map();
|
|
36
|
+
for (const item of evidence) {
|
|
37
|
+
if (!isRecord(item))
|
|
38
|
+
continue;
|
|
39
|
+
const actionType = allowedActionType(item.actionType);
|
|
40
|
+
if (!actionType)
|
|
41
|
+
continue;
|
|
42
|
+
const current = byAction.get(actionType) ?? { count: 0, latestMs: 0 };
|
|
43
|
+
current.count += numberValue(item.count);
|
|
44
|
+
current.latestMs = Math.max(current.latestMs, evidenceLatestMs(item));
|
|
45
|
+
byAction.set(actionType, current);
|
|
46
|
+
}
|
|
47
|
+
const [best] = [...byAction.entries()].sort((a, b) => {
|
|
48
|
+
if (a[1].latestMs !== b[1].latestMs)
|
|
49
|
+
return b[1].latestMs - a[1].latestMs;
|
|
50
|
+
if (a[1].count !== b[1].count)
|
|
51
|
+
return b[1].count - a[1].count;
|
|
52
|
+
return (REFILL_TARGET_ACTION_TYPES.indexOf(a[0]) -
|
|
53
|
+
REFILL_TARGET_ACTION_TYPES.indexOf(b[0]));
|
|
54
|
+
});
|
|
55
|
+
return best?.[0] ?? fallback[0] ?? null;
|
|
56
|
+
}
|
|
57
|
+
function senderActionKey(senderId, actionType) {
|
|
58
|
+
return typeof senderId === "string" && typeof actionType === "string"
|
|
59
|
+
? `${senderId}:${actionType}`
|
|
60
|
+
: null;
|
|
61
|
+
}
|
|
62
|
+
function sanitizeCounts(counts, selectedKeys) {
|
|
63
|
+
if (!Array.isArray(counts))
|
|
64
|
+
return [];
|
|
65
|
+
return counts.filter((item) => {
|
|
66
|
+
if (!isRecord(item))
|
|
67
|
+
return false;
|
|
68
|
+
const actionType = allowedActionType(item.actionType);
|
|
69
|
+
const key = senderActionKey(item.senderId, actionType);
|
|
70
|
+
return Boolean(key && selectedKeys.has(key));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function sanitizeActionCandidates(candidates, selectedKeys, status, remainingReadyOrProjectedGap, remainingProjectedGap) {
|
|
74
|
+
const filtered = Array.isArray(candidates)
|
|
75
|
+
? candidates.filter((item) => {
|
|
76
|
+
if (!isRecord(item))
|
|
77
|
+
return false;
|
|
78
|
+
const actionType = item.actionType;
|
|
79
|
+
if (actionType === undefined)
|
|
80
|
+
return item.type === "wait_for_scheduler";
|
|
81
|
+
const allowed = allowedActionType(actionType);
|
|
82
|
+
const key = senderActionKey(item.senderId, allowed);
|
|
83
|
+
return Boolean(key && selectedKeys.has(key));
|
|
84
|
+
})
|
|
85
|
+
: [];
|
|
86
|
+
if (status === "awaiting_scheduler_after_ready_buffer" &&
|
|
87
|
+
remainingProjectedGap > 0 &&
|
|
88
|
+
remainingReadyOrProjectedGap === 0 &&
|
|
89
|
+
!filtered.some((item) => item.type === "wait_for_scheduler")) {
|
|
90
|
+
filtered.push({
|
|
91
|
+
type: "wait_for_scheduler",
|
|
92
|
+
reason: "ready buffer covers the projected target gap but scheduledFor coverage is still short",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return filtered;
|
|
96
|
+
}
|
|
97
|
+
function sanitizeBlockers(blockers, selectedKeys) {
|
|
98
|
+
if (!Array.isArray(blockers))
|
|
99
|
+
return [];
|
|
100
|
+
return blockers.filter((item) => {
|
|
101
|
+
if (!isRecord(item))
|
|
102
|
+
return false;
|
|
103
|
+
const detail = item.detail;
|
|
104
|
+
const code = String(item.code ?? "");
|
|
105
|
+
if (typeof detail !== "string")
|
|
106
|
+
return true;
|
|
107
|
+
const detailSenderId = detail.split(":")[0];
|
|
108
|
+
if (code.startsWith("paid_inmail_")) {
|
|
109
|
+
return selectedKeys.has(`${detailSenderId}:send_inmail_closed`);
|
|
110
|
+
}
|
|
111
|
+
if (detail.includes(":send_dm") || detail.includes(":send_inmail_open")) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const parts = detail.split(":");
|
|
115
|
+
if (parts.length < 2)
|
|
116
|
+
return true;
|
|
117
|
+
const actionType = allowedActionType(parts[parts.length - 1]);
|
|
118
|
+
if (!actionType)
|
|
119
|
+
return true;
|
|
120
|
+
const senderId = parts[0];
|
|
121
|
+
return selectedKeys.size === 0 || selectedKeys.has(`${senderId}:${actionType}`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function sanitizeRefillTargetPlanResult(result) {
|
|
125
|
+
if (!isRecord(result) || !isRecord(result.target))
|
|
126
|
+
return result;
|
|
127
|
+
const target = result.target;
|
|
128
|
+
const rawSelections = Array.isArray(target.actionSelections)
|
|
129
|
+
? target.actionSelections
|
|
130
|
+
: [];
|
|
131
|
+
const rawSenderPlans = Array.isArray(target.senderPlans)
|
|
132
|
+
? target.senderPlans
|
|
133
|
+
: [];
|
|
134
|
+
const selectedBySender = new Map();
|
|
135
|
+
for (const rawSelection of rawSelections) {
|
|
136
|
+
if (!isRecord(rawSelection) || typeof rawSelection.senderId !== "string") {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const fallback = actionTypesFrom(rawSelection.actionTypes);
|
|
140
|
+
const evidence = Array.isArray(rawSelection.evidence)
|
|
141
|
+
? rawSelection.evidence
|
|
142
|
+
: [];
|
|
143
|
+
const chosen = chooseEvidenceAction(evidence, fallback);
|
|
144
|
+
if (chosen)
|
|
145
|
+
selectedBySender.set(rawSelection.senderId, chosen);
|
|
146
|
+
}
|
|
147
|
+
for (const rawPlan of rawSenderPlans) {
|
|
148
|
+
if (!isRecord(rawPlan) ||
|
|
149
|
+
typeof rawPlan.senderId !== "string" ||
|
|
150
|
+
selectedBySender.has(rawPlan.senderId)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const actionType = allowedActionType(rawPlan.actionType);
|
|
154
|
+
if (actionType)
|
|
155
|
+
selectedBySender.set(rawPlan.senderId, actionType);
|
|
156
|
+
}
|
|
157
|
+
const selectedKeys = new Set([...selectedBySender.entries()].map(([senderId, actionType]) => `${senderId}:${actionType}`));
|
|
158
|
+
const actionTypes = [...new Set(selectedBySender.values())];
|
|
159
|
+
if (selectedKeys.size === 0)
|
|
160
|
+
return result;
|
|
161
|
+
const senderPlans = rawSenderPlans.filter((plan) => isRecord(plan) &&
|
|
162
|
+
selectedKeys.has(`${String(plan.senderId)}:${String(plan.actionType)}`));
|
|
163
|
+
const selectedDays = Array.isArray(target.selectedDays)
|
|
164
|
+
? target.selectedDays.filter((day) => isRecord(day) &&
|
|
165
|
+
selectedKeys.has(`${String(day.senderId)}:${String(day.actionType)}`))
|
|
166
|
+
: [];
|
|
167
|
+
const actionSelections = rawSelections
|
|
168
|
+
.filter((selection) => isRecord(selection) &&
|
|
169
|
+
typeof selection.senderId === "string" &&
|
|
170
|
+
selectedBySender.has(selection.senderId))
|
|
171
|
+
.map((selection) => {
|
|
172
|
+
const actionType = selectedBySender.get(selection.senderId);
|
|
173
|
+
const evidence = Array.isArray(selection.evidence)
|
|
174
|
+
? selection.evidence.filter((item) => isRecord(item) && item.actionType === actionType)
|
|
175
|
+
: [];
|
|
176
|
+
return {
|
|
177
|
+
...selection,
|
|
178
|
+
actionTypes: actionType ? [actionType] : [],
|
|
179
|
+
evidence,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
const grossTarget = senderPlans.reduce((sum, plan) => sum + numberValue(plan.grossTarget), 0);
|
|
183
|
+
const sent = senderPlans.reduce((sum, plan) => sum + numberValue(plan.sent), 0);
|
|
184
|
+
const scheduled = senderPlans.reduce((sum, plan) => sum + numberValue(plan.scheduled), 0);
|
|
185
|
+
const projected = sent + scheduled;
|
|
186
|
+
const readyBuffer = senderPlans.reduce((sum, plan) => sum + numberValue(plan.readyBuffer), 0);
|
|
187
|
+
const remainingScheduledGap = senderPlans.reduce((sum, plan) => sum + numberValue(plan.remainingScheduledGap), 0);
|
|
188
|
+
const remainingProjectedGap = senderPlans.reduce((sum, plan) => sum + numberValue(plan.remainingProjectedGap), 0);
|
|
189
|
+
const remainingReadyOrProjectedGap = senderPlans.reduce((sum, plan) => sum + numberValue(plan.remainingReadyOrProjectedGap), 0);
|
|
190
|
+
const blockers = sanitizeBlockers(result.blockers, selectedKeys);
|
|
191
|
+
const status = blockers.some((blocker) => ["sender_disconnected", "sender_sales_nav_disconnected"].includes(String(blocker.code))) || grossTarget <= 0
|
|
192
|
+
? "blocked"
|
|
193
|
+
: remainingProjectedGap === 0
|
|
194
|
+
? "complete"
|
|
195
|
+
: remainingReadyOrProjectedGap === 0
|
|
196
|
+
? "awaiting_scheduler_after_ready_buffer"
|
|
197
|
+
: "needs_refill";
|
|
198
|
+
return {
|
|
199
|
+
...result,
|
|
200
|
+
status,
|
|
201
|
+
request: isRecord(result.request)
|
|
202
|
+
? { ...result.request, actionTypes }
|
|
203
|
+
: result.request,
|
|
204
|
+
target: {
|
|
205
|
+
...target,
|
|
206
|
+
actionSelections,
|
|
207
|
+
selectedDays,
|
|
208
|
+
senderPlans,
|
|
209
|
+
grossTarget,
|
|
210
|
+
sent,
|
|
211
|
+
scheduled,
|
|
212
|
+
projected,
|
|
213
|
+
readyBuffer,
|
|
214
|
+
remainingScheduledGap,
|
|
215
|
+
remainingProjectedGap,
|
|
216
|
+
remainingReadyOrProjectedGap,
|
|
217
|
+
remainingReadyOrScheduledGap: remainingReadyOrProjectedGap,
|
|
218
|
+
},
|
|
219
|
+
coverage: isRecord(result.coverage)
|
|
220
|
+
? {
|
|
221
|
+
...result.coverage,
|
|
222
|
+
sentCounts: sanitizeCounts(result.coverage.sentCounts, selectedKeys),
|
|
223
|
+
scheduledCounts: sanitizeCounts(result.coverage.scheduledCounts, selectedKeys),
|
|
224
|
+
readyCounts: sanitizeCounts(result.coverage.readyCounts, selectedKeys),
|
|
225
|
+
}
|
|
226
|
+
: result.coverage,
|
|
227
|
+
actionCandidates: sanitizeActionCandidates(result.actionCandidates, selectedKeys, status, remainingReadyOrProjectedGap, remainingProjectedGap),
|
|
228
|
+
blockers,
|
|
229
|
+
mcpSanitizedRefillLanes: true,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
6
232
|
export const refillTargetPlanToolDefinitions = [
|
|
7
233
|
{
|
|
8
234
|
name: "get_refill_target_plan",
|
|
9
|
-
description: "read-only refill target planner to call before any refill mutation. It identifies eligible senders
|
|
235
|
+
description: "read-only refill target planner to call before any refill mutation. It identifies eligible senders and infers one implicit refill lane per sender: connection invites or paid InMails, chosen from future scheduled, recent scheduled, and ready-to-schedule evidence in active campaign-backed sequence campaigns before falling back to campaign sequences. DMs and open-profile InMails are follow-up sequence actions, not refill target lanes. The planner computes selected sender-local days, gross target, actual sent coverage, scheduler-owned scheduled coverage, projected coverage (sent + scheduled), ready buffer, remaining projected gap, paid InMail credit/threshold feasibility, bounded action candidates, and targetRevision drift proof. This tool does not create rows, import leads, prepare messages, approve messages, does not schedule sends, start campaigns, lower paid InMail thresholds, create campaigns, launch, spend InMail credits, or write scheduler fields. Complete projected targets no-op without approval; incomplete targets must be followed by a bounded approval packet.",
|
|
10
236
|
inputSchema: {
|
|
11
237
|
type: "object",
|
|
12
238
|
properties: {
|
|
@@ -53,15 +279,9 @@ export const refillTargetPlanToolDefinitions = [
|
|
|
53
279
|
type: "array",
|
|
54
280
|
items: {
|
|
55
281
|
type: "string",
|
|
56
|
-
enum: [
|
|
57
|
-
"send_invite",
|
|
58
|
-
"send_dm",
|
|
59
|
-
"send_inmail_open",
|
|
60
|
-
"send_inmail_closed",
|
|
61
|
-
"react_and_comment",
|
|
62
|
-
],
|
|
282
|
+
enum: ["send_invite", "send_inmail_closed"],
|
|
63
283
|
},
|
|
64
|
-
description: "Optional
|
|
284
|
+
description: "Optional refill target lane override. Supported refill lanes are connection invites (send_invite) and paid InMails (send_inmail_closed) only. If omitted, the planner infers one per-sender lane from active campaign future scheduled, recent scheduled, and ready-to-schedule evidence before falling back to selected active campaign sequences.",
|
|
65
285
|
},
|
|
66
286
|
approvalMode: {
|
|
67
287
|
type: "string",
|
|
@@ -74,8 +294,8 @@ export const refillTargetPlanToolDefinitions = [
|
|
|
74
294
|
},
|
|
75
295
|
},
|
|
76
296
|
];
|
|
77
|
-
export function getRefillTargetPlan(input = {}) {
|
|
78
|
-
|
|
297
|
+
export async function getRefillTargetPlan(input = {}) {
|
|
298
|
+
const result = await postRefillTargetPlan({
|
|
79
299
|
intent: input.intent,
|
|
80
300
|
horizonSendDays: input.horizonSendDays,
|
|
81
301
|
untilDate: input.untilDate,
|
|
@@ -87,4 +307,5 @@ export function getRefillTargetPlan(input = {}) {
|
|
|
87
307
|
actionTypes: input.actionTypes,
|
|
88
308
|
approvalMode: input.approvalMode,
|
|
89
309
|
});
|
|
310
|
+
return sanitizeRefillTargetPlanResult(result);
|
|
90
311
|
}
|
package/package.json
CHANGED
|
@@ -117,21 +117,30 @@ eligible senders, selected sender-local days, gross target, actual sent
|
|
|
117
117
|
coverage, scheduler-owned scheduled coverage across active enrolled campaigns,
|
|
118
118
|
projected coverage (`sent + scheduled`), inferred per-sender send lane/action
|
|
119
119
|
selections, ready buffer, remaining projected gap, paid-InMail credit/threshold
|
|
120
|
-
feasibility, `targetShapeRevision`, and `stateRevision`.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
feasibility, `targetShapeRevision`, and `stateRevision`. Refill target lanes
|
|
121
|
+
are only connection invites (`send_invite`) or paid InMails
|
|
122
|
+
(`send_inmail_closed`), chosen per sender from the most recent current
|
|
123
|
+
dashboard-active campaign-backed send evidence. DMs (`send_dm`) and
|
|
124
|
+
open-profile InMails (`send_inmail_open`) may exist in sequences, but they are
|
|
125
|
+
not refill horizon target capacity and must not be counted as refill sent,
|
|
126
|
+
scheduled, or ready coverage. When `actionTypes` are omitted, trust the target
|
|
127
|
+
plan's inferred connection-or-paid-InMail lane rather than asking which of those
|
|
128
|
+
two lanes to fill. If a stale target plan selects `send_dm` or
|
|
129
|
+
`send_inmail_open`, stop and re-plan with the current planner before mutation.
|
|
130
|
+
Short form: trust the target plan's inferred lane only when it is a
|
|
131
|
+
connection-invite or paid-InMail refill lane.
|
|
132
|
+
If the target plan is complete by projected coverage, report that the selected
|
|
133
|
+
target is already filled and no-op without asking for approval. If the ready
|
|
134
|
+
buffer covers the projected gap but scheduled coverage is still short, run only
|
|
135
|
+
a bounded read-only scheduler settle/reread loop and report
|
|
136
|
+
`awaiting_scheduler_after_ready_buffer` if it does not settle.
|
|
127
137
|
If paid InMail credit facts are stale or missing and the target plan returns a
|
|
128
138
|
`refresh_paid_inmail_credits` action candidate, include the exact sender id,
|
|
129
139
|
campaign/table/column proof, expected sender-credit-cache writes, and rerun plan
|
|
130
140
|
step in the bounded packet. After `refresh_paid_inmail_credits`, rerun
|
|
131
141
|
`get_refill_target_plan` before any prep/import/approval/start action. If paid
|
|
132
142
|
InMail is below threshold after a fresh credit read, report the exact
|
|
133
|
-
campaign/table/column threshold action or connection fallback; `--yolo` does not
|
|
134
|
-
lower paid-InMail thresholds or create campaigns.
|
|
143
|
+
campaign/table/column threshold action or connection fallback; `--yolo` does not lower paid-InMail thresholds or create campaigns.
|
|
135
144
|
|
|
136
145
|
If the plain route's managed waterfall targets are stale, for example skipped
|
|
137
146
|
targets show archived/completed shared slots or the returned targets do not cover
|
|
@@ -67,10 +67,20 @@ campaign.
|
|
|
67
67
|
campaigns, projected coverage (`sent + scheduled`), ready buffer, remaining
|
|
68
68
|
projected gap, paid-InMail credit/threshold feasibility, bounded action
|
|
69
69
|
candidates, `targetShapeRevision`, and `stateRevision`.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
Refill target lanes are binary per sender: connection invites
|
|
71
|
+
(`send_invite`) or paid InMails (`send_inmail_closed`). DMs
|
|
72
|
+
(`send_dm`) and open-profile InMails (`send_inmail_open`) can remain in the
|
|
73
|
+
sequence, but they are follow-up actions, not refill horizon capacity. Do not
|
|
74
|
+
count their sent, scheduled, or ready cells when deciding whether a sender
|
|
75
|
+
needs refill. When `actionTypes` are omitted, trust the target plan's
|
|
76
|
+
inferred connection-or-paid-InMail lane; do not ask the operator which of
|
|
77
|
+
those two lanes to fill after the plan has inferred that from active
|
|
78
|
+
campaign future scheduled, recent scheduled, and ready evidence. If a stale
|
|
79
|
+
planner/tool response selects `send_dm` or `send_inmail_open`, treat that as
|
|
80
|
+
unsupported stale refill state and re-plan with a current planner before any
|
|
81
|
+
mutation.
|
|
82
|
+
Short form: trust the target plan's inferred lane only when it is a
|
|
83
|
+
connection-invite or paid-InMail refill lane.
|
|
74
84
|
If `status:"complete"`, report the target, selected dates, sent count,
|
|
75
85
|
scheduled count, projected count, campaign ids, and no-op proof without
|
|
76
86
|
asking for approval or mutating.
|
|
@@ -84,8 +94,7 @@ campaign.
|
|
|
84
94
|
step in the bounded packet. After `refresh_paid_inmail_credits`, rerun
|
|
85
95
|
`get_refill_target_plan` before any prep/import/approval/start action. If paid
|
|
86
96
|
InMail is below threshold after a fresh credit read, report the exact
|
|
87
|
-
campaign/table/column threshold action or connection fallback; `--yolo` does not
|
|
88
|
-
lower paid-InMail thresholds or create campaigns.
|
|
97
|
+
campaign/table/column threshold action or connection fallback; `--yolo` does not lower paid-InMail thresholds or create campaigns.
|
|
89
98
|
2. Call `resolve_campaign_fill_route`. Use `intent:"plain"` for generic
|
|
90
99
|
fill/load language, `intent:"active"` only when the user explicitly narrowed
|
|
91
100
|
to active regular campaigns or when the plain route has stale managed
|
|
@@ -161,7 +170,9 @@ Use the refill-state response as the current facts receipt:
|
|
|
161
170
|
approved, ready to schedule, scheduled;
|
|
162
171
|
- active message prep job;
|
|
163
172
|
- scheduler-owned scheduled counts by date/action from cells with non-null
|
|
164
|
-
`scheduledFor`; treat
|
|
173
|
+
`scheduledFor`; treat connection-invite and paid-InMail cells as the refill
|
|
174
|
+
lane continuation signal and the last sends/continuation signal, and ignore
|
|
175
|
+
DM/open-InMail cells for refill target capacity;
|
|
165
176
|
- latest scheduled send date, latest scheduled-cell update when present,
|
|
166
177
|
sender overlap with the requested or inferred sender set, and whether future
|
|
167
178
|
scheduled sends already exist;
|
|
@@ -210,6 +221,19 @@ in the best same-sender campaign and keep all source-copy/prep caps tied to that
|
|
|
210
221
|
two-day packet.
|
|
211
222
|
Short form: default `--yolo refill senders` target is each eligible sender's two-send-day gap.
|
|
212
223
|
|
|
224
|
+
Lane selection must choose one refill lane per sender from current
|
|
225
|
+
dashboard-active campaign-backed sequence evidence:
|
|
226
|
+
|
|
227
|
+
1. Prefer the sender's most recent future scheduled refill action, then the most
|
|
228
|
+
recent recent scheduled refill action, then ready refill rows.
|
|
229
|
+
2. Refill actions are only `send_invite` and `send_inmail_closed`.
|
|
230
|
+
3. If the most recent active campaign evidence is DM or open InMail, ignore it
|
|
231
|
+
for refill lane selection and use the nearest connection-invite or paid-InMail
|
|
232
|
+
evidence instead.
|
|
233
|
+
4. If no connection/paid-InMail evidence exists, fall back to the selected
|
|
234
|
+
active campaign sequence, still choosing only one of `send_invite` or
|
|
235
|
+
`send_inmail_closed` for that sender.
|
|
236
|
+
|
|
213
237
|
For `--yolo` fill/schedule requests, do not treat "ready" as completion. Build
|
|
214
238
|
and maintain a horizon saturation ledger for every selected sender from
|
|
215
239
|
`get_refill_target_plan`. A sender is complete only when a final target-plan or
|