@sellable/mcp 0.1.552 → 0.1.553
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/tools/refill-executors.d.ts +30 -0
- package/dist/tools/refill-executors.js +126 -0
- package/dist/tools/refill-sends.d.ts +181 -39
- package/dist/tools/refill-sends.js +141 -13
- package/dist/tools/refill-target-plan.js +179 -1
- package/dist/tools/registry.d.ts +8 -0
- package/dist/tools/scheduler-run.d.ts +10 -0
- package/dist/tools/scheduler-run.js +20 -0
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +38 -7
- package/skills/refill-sends-workflow/SKILL.md +44 -6
- package/skills/refill-sends-workflow/core/flow.v1.json +63 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { actionIds, classifyPaidInmailRefreshReceipt, collectPaidInmailRefreshActions, executeOneYoloPrimitive, firstGlobalAction, normalizeStrings, numberValue, recordValue, refreshPaidInmailCreditsWithRetry, stringValue, } from "./refill-executors.js";
|
|
2
2
|
import { getRefillTargetPlan } from "./refill-target-plan.js";
|
|
3
3
|
import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
|
|
4
|
+
const REFILL_SEND_ACTION_TYPES = ["send_invite", "send_inmail_closed"];
|
|
5
|
+
const REFILL_SEND_ACTION_TYPE_SET = new Set(REFILL_SEND_ACTION_TYPES);
|
|
4
6
|
const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
|
|
5
7
|
const MCP_PAID_INMAIL_CREDITS_MAX_STALENESS_SECONDS = 4 * 60 * 60;
|
|
6
8
|
function normalizeHorizonSendDays(value) {
|
|
@@ -48,10 +50,51 @@ function normalizeTargetDate(value) {
|
|
|
48
50
|
}
|
|
49
51
|
return trimmed;
|
|
50
52
|
}
|
|
53
|
+
function normalizeRefillActionTypes(value) {
|
|
54
|
+
if (value === undefined || value === null)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (!Array.isArray(value)) {
|
|
57
|
+
throw new Error("actionTypes must be an array of supported refill lanes.");
|
|
58
|
+
}
|
|
59
|
+
if (value.some((actionType) => typeof actionType !== "string")) {
|
|
60
|
+
throw new Error(`Unsupported refill actionTypes. Supported values are ${REFILL_SEND_ACTION_TYPES.join(", ")}.`);
|
|
61
|
+
}
|
|
62
|
+
const normalized = normalizeStrings(value);
|
|
63
|
+
const unsupported = normalized.filter((actionType) => !REFILL_SEND_ACTION_TYPE_SET.has(actionType));
|
|
64
|
+
if (unsupported.length > 0) {
|
|
65
|
+
throw new Error(`Unsupported refill actionTypes: ${unsupported.join(", ")}. Supported values are ${REFILL_SEND_ACTION_TYPES.join(", ")}.`);
|
|
66
|
+
}
|
|
67
|
+
return normalized.length > 0
|
|
68
|
+
? normalized
|
|
69
|
+
: undefined;
|
|
70
|
+
}
|
|
71
|
+
function normalizeExpectedPacketValue(value, field) {
|
|
72
|
+
if (value === undefined || value === null)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
75
|
+
throw new Error(`${field} must be a non-empty string when provided.`);
|
|
76
|
+
}
|
|
77
|
+
return value.trim();
|
|
78
|
+
}
|
|
79
|
+
function normalizeRefillSendsInput(input) {
|
|
80
|
+
const actionTypes = normalizeRefillActionTypes(input.actionTypes);
|
|
81
|
+
const expectedTargetShapeRevision = normalizeExpectedPacketValue(input.expectedTargetShapeRevision, "expectedTargetShapeRevision");
|
|
82
|
+
const expectedActionKey = normalizeExpectedPacketValue(input.expectedActionKey, "expectedActionKey");
|
|
83
|
+
const normalized = { ...input };
|
|
84
|
+
delete normalized.actionTypes;
|
|
85
|
+
delete normalized.expectedTargetShapeRevision;
|
|
86
|
+
delete normalized.expectedActionKey;
|
|
87
|
+
return {
|
|
88
|
+
...normalized,
|
|
89
|
+
...(actionTypes ? { actionTypes } : {}),
|
|
90
|
+
...(expectedTargetShapeRevision ? { expectedTargetShapeRevision } : {}),
|
|
91
|
+
...(expectedActionKey ? { expectedActionKey } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
51
94
|
export const refillSendsToolDefinitions = [
|
|
52
95
|
{
|
|
53
96
|
name: "refill_sends",
|
|
54
|
-
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 generated-message approval, receipt-proven same-source copy, or read-only wait) and reread. Same-source copy/source fallback is only safe after receipt-proven exhaustion: hasMoreFrontierRows:false, zero approvalCandidates, no stuckActiveCells, and no non-terminal approvedNotDispatched work. Wait primitives are gates, not competing goals, and carry absolute wait.deadlineAt when present. It does not run unbounded approval, lower paid InMail thresholds, switch source families, create campaigns,
|
|
97
|
+
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 generated-message approval, receipt-proven same-source copy, a request-scoped run_scheduler_sweep for an exact date, or a read-only wait) and reread. Same-source copy/source fallback is only safe after receipt-proven exhaustion: hasMoreFrontierRows:false, zero approvalCandidates, no stuckActiveCells, and no non-terminal approvedNotDispatched work. The exact-date sweep may schedule eligible workspace cells through existing product gates; it never sends directly or raw-writes scheduler fields. Wait primitives are gates, not competing goals, and carry absolute wait.deadlineAt when present. It does not run unbounded approval, lower paid InMail thresholds, switch source families, create campaigns, launch, archive, or delete.",
|
|
55
98
|
inputSchema: {
|
|
56
99
|
type: "object",
|
|
57
100
|
properties: {
|
|
@@ -87,6 +130,14 @@ export const refillSendsToolDefinitions = [
|
|
|
87
130
|
items: { type: "string" },
|
|
88
131
|
description: "Optional sender display names to resolve through list_senders before campaign selection.",
|
|
89
132
|
},
|
|
133
|
+
actionTypes: {
|
|
134
|
+
type: "array",
|
|
135
|
+
items: {
|
|
136
|
+
type: "string",
|
|
137
|
+
enum: ["send_invite", "send_inmail_closed"],
|
|
138
|
+
},
|
|
139
|
+
description: "Optional explicit refill lane. Omit to preserve planner inference; supported values are connection invites and paid InMail / unified Sales Nav cascades.",
|
|
140
|
+
},
|
|
90
141
|
horizonSendDays: {
|
|
91
142
|
type: "number",
|
|
92
143
|
minimum: 1,
|
|
@@ -121,6 +172,14 @@ export const refillSendsToolDefinitions = [
|
|
|
121
172
|
enum: ["approve", "mark_ready"],
|
|
122
173
|
description: 'Preparation mode for the final packet. Use "approve" only when the user asked to fill/schedule sender sends; otherwise default to "mark_ready".',
|
|
123
174
|
},
|
|
175
|
+
expectedTargetShapeRevision: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "Optional targetShapeRevision from an already rendered and approved packet. A mismatch returns approval_scope_changed before any refresh or mutation.",
|
|
178
|
+
},
|
|
179
|
+
expectedActionKey: {
|
|
180
|
+
type: "string",
|
|
181
|
+
description: "Optional actionKey from the approved packet's first global action. A mismatch returns approval_scope_changed before any refresh or mutation.",
|
|
182
|
+
},
|
|
124
183
|
},
|
|
125
184
|
required: [],
|
|
126
185
|
additionalProperties: false,
|
|
@@ -128,6 +187,9 @@ export const refillSendsToolDefinitions = [
|
|
|
128
187
|
},
|
|
129
188
|
];
|
|
130
189
|
export function refillSendsCommand(input = {}) {
|
|
190
|
+
return buildRefillSendsCommand(normalizeRefillSendsInput(input));
|
|
191
|
+
}
|
|
192
|
+
function buildRefillSendsCommand(input) {
|
|
131
193
|
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
132
194
|
const senders = normalizeStrings(input.senders);
|
|
133
195
|
const senderIds = normalizeStrings(input.senderIds);
|
|
@@ -142,6 +204,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
142
204
|
: normalizeHorizonSendDays(input.horizonSendDays);
|
|
143
205
|
const intent = input.intent ?? "plain";
|
|
144
206
|
const approvalMode = input.approvalMode ?? (yolo ? "approve" : "mark_ready");
|
|
207
|
+
const actionTypes = input.actionTypes;
|
|
145
208
|
const senderScope = hasSenderSelectors
|
|
146
209
|
? "selected_senders"
|
|
147
210
|
: yolo
|
|
@@ -161,6 +224,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
161
224
|
workspaceId,
|
|
162
225
|
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
163
226
|
intent,
|
|
227
|
+
...(actionTypes ? { actionTypes } : {}),
|
|
164
228
|
targetDate,
|
|
165
229
|
untilDate,
|
|
166
230
|
horizonSendDays,
|
|
@@ -201,7 +265,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
201
265
|
"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.",
|
|
202
266
|
`Call get_refill_target_plan({ intent: "${intent}"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""}${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""}${senderIds.length > 0 ? `, senderIds: ${JSON.stringify(senderIds)}` : ""}${senderNames.length > 0
|
|
203
267
|
? `, senderNames: ${JSON.stringify(senderNames)}`
|
|
204
|
-
: ""}${senders.length > 0 ? `, senders: ${JSON.stringify(senders)}` : ""}${targetDate
|
|
268
|
+
: ""}${senders.length > 0 ? `, senders: ${JSON.stringify(senders)}` : ""}${actionTypes ? `, actionTypes: ${JSON.stringify(actionTypes)}` : ""}${targetDate
|
|
205
269
|
? `, targetDate: "${targetDate}"`
|
|
206
270
|
: untilDate
|
|
207
271
|
? `, untilDate: "${untilDate}"`
|
|
@@ -210,10 +274,11 @@ export function refillSendsCommand(input = {}) {
|
|
|
210
274
|
: `, horizonSendDays: ${horizonSendDays}`}, approvalMode: "${approvalMode}" }) before any import, prep, approval, start, or schedule-affecting action.`,
|
|
211
275
|
"Render target.eligibleSenderLedger and target.senderRefillPlans before mutation. Preserve the coverage labels Need to prepare, Goal, Already sent, Scheduled, Ready and waiting to be scheduled, and Still need.",
|
|
212
276
|
"Use target.globalActionQueue as the only cross-sender yolo queue: execute only target.globalActionQueue[0], one globally ranked primitive, then rerun get_refill_target_plan before choosing another action.",
|
|
277
|
+
"When expectedTargetShapeRevision or expectedActionKey is supplied from an already rendered packet, compare both against the first fresh target plan before paid-credit refresh or any primitive. Return approval_scope_changed with the fresh packet on mismatch.",
|
|
213
278
|
"Read target.senderRefillPlans[].refillReceipt as the public ladder receipt: it carries the selected campaign/sender/lane summary, skippedRungs, existingRowFrontier, and any absolute wait.deadlineAt. Do not choose source/copy/fallback work until that receipt proves the earlier ready/prep/approval rungs are exhausted.",
|
|
214
279
|
"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.",
|
|
215
280
|
"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.",
|
|
216
|
-
"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.",
|
|
281
|
+
"If the exact-date global action is run_scheduler_sweep, execute it once and fully reread when remainingReadyOrProjectedGap is 0 but remainingProjectedGap is positive. Otherwise, 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.",
|
|
217
282
|
`Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""}${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""} }).`,
|
|
218
283
|
`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.`,
|
|
219
284
|
workspaceId
|
|
@@ -231,7 +296,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
231
296
|
"Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
|
|
232
297
|
"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.",
|
|
233
298
|
"Source/fallback primitives require receipt-proven exhaustion: existingRowFrontier.hasMoreFrontierRows:false, approvalCandidates:0, no fresh active prep, no stuckActiveCells, and no non-terminal approvedNotDispatched rows. Treat anomalies, stuckActiveCells, and non-terminal approvedNotDispatched as diagnose-and-report gates, not exhaustion. Terminal approvedNotDispatched blockers may be reported and then the ladder can proceed.",
|
|
234
|
-
"In manual/non-yolo, scheduled, and --yolo modes, this tool may automatically refresh stale or missing paid-InMail credit facts once per selected sender when the first target plan returns refresh_paid_inmail_credits candidates. This is the only newly allowed automatic write in manual/non-yolo mode; it reruns get_refill_target_plan and returns autoPaidInmailRefresh with attemptedSenderIds, refreshedPaidInmailSenderIds, failedPaidInmailRefreshes, and the post-refresh targetPlan before any prep/source-copy/bounded-approval/read-only
|
|
299
|
+
"In manual/non-yolo, scheduled, and --yolo modes, this tool may automatically refresh stale or missing paid-InMail credit facts once per selected sender when the first target plan returns refresh_paid_inmail_credits candidates. This is the only newly allowed automatic write in manual/non-yolo mode; it reruns get_refill_target_plan and returns autoPaidInmailRefresh with attemptedSenderIds, refreshedPaidInmailSenderIds, failedPaidInmailRefreshes, and the post-refresh targetPlan before any prep/source-copy/bounded-approval/exact-date-sweep/read-only-wait action is chosen.",
|
|
235
300
|
"Freshness gate precedes scheduler wait: if any selected target.senderRefillPlans[].paidInmail.status is missing_credit_facts or stale_credit_facts, or the target plan contains refresh_paid_inmail_credits, do not enter wait_for_scheduler even when remainingReadyOrProjectedGap is 0; refresh exact sender credit facts once, reread get_refill_target_plan, then choose scheduler wait only if freshness is clean.",
|
|
236
301
|
"Scheduler-run receipt interpretation: cellsConsidered is allocation-attempt count, readyCellsFound is ready inventory before prefilters, and campaignScopeSummary must include the selected campaign/table before inferring it was ready-but-blocked. Read prefiltered, skipped, and deferred separately. For ready closed-InMail cells with stale paid-credit facts, route to refresh_paid_inmail_credits_then_rerun once, then rerun/status. wait_for_capacity_or_window means report loaded/capped/waiting and do not source or prep more rows. no_ready_cells_continue_refill_prep means return to the refill/prep ladder.",
|
|
237
302
|
"For wait_for_active_work and wait_for_scheduler, honor the receipt's absolute wait.deadlineAt when present. If the deadline is expired on this call, escalate to diagnostics with the receipt evidence instead of issuing another blind wait.",
|
|
@@ -241,9 +306,9 @@ export function refillSendsCommand(input = {}) {
|
|
|
241
306
|
"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
307
|
],
|
|
243
308
|
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 prep/source-copy/bounded-approval/read-only
|
|
309
|
+
? "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/exact-date-sweep/read-only-wait action inside that packet, but source-copy/fallback requires receipt-proven exhaustion of earlier ready/prep/approval rungs. 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 and the exact-date run_scheduler_sweep is target.globalActionQueue[0], execute it once and fully reread. Only non-exact or no-sweep scheduler waits are read-only; use a 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 workspace, sender set, campaign/table/stable source identity, configured caps, exact date/window, lane, or the approved action/side-effect envelope drifts. Treat coverage, blockers, receipts, timestamps, and current action progress as stateRevision changes inside that envelope. --yolo does not authorize lowering paid InMail thresholds or creating connection campaigns."
|
|
245
310
|
: hasSenderSelectors
|
|
246
|
-
? "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,
|
|
311
|
+
? "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 and the approved exact-date run_scheduler_sweep is target.globalActionQueue[0], execute it once and fully reread. Only non-exact or no-sweep scheduler waits are read-only; keep the run open until projected coverage fills or Christian stops/statuses it. Threshold changes and campaign creation need separate explicit approval."
|
|
247
312
|
: "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.",
|
|
248
313
|
forbiddenActions: [
|
|
249
314
|
"start_campaign outside an exact selected PAUSED campaign-backed refill packet",
|
|
@@ -276,6 +341,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
276
341
|
senders,
|
|
277
342
|
senderIds,
|
|
278
343
|
senderNames,
|
|
344
|
+
...(actionTypes ? { actionTypes } : {}),
|
|
279
345
|
targetDate,
|
|
280
346
|
untilDate,
|
|
281
347
|
horizonSendDays,
|
|
@@ -284,6 +350,14 @@ export function refillSendsCommand(input = {}) {
|
|
|
284
350
|
intent,
|
|
285
351
|
approvalMode,
|
|
286
352
|
workspaceId,
|
|
353
|
+
...(input.expectedTargetShapeRevision
|
|
354
|
+
? {
|
|
355
|
+
expectedTargetShapeRevision: input.expectedTargetShapeRevision,
|
|
356
|
+
}
|
|
357
|
+
: {}),
|
|
358
|
+
...(input.expectedActionKey
|
|
359
|
+
? { expectedActionKey: input.expectedActionKey }
|
|
360
|
+
: {}),
|
|
287
361
|
},
|
|
288
362
|
},
|
|
289
363
|
},
|
|
@@ -307,6 +381,7 @@ function targetPlanInputFor(input = {}) {
|
|
|
307
381
|
...(senderIds.length > 0 ? { senderIds } : {}),
|
|
308
382
|
...(senderNames.length > 0 ? { senderNames } : {}),
|
|
309
383
|
...(senders.length > 0 ? { senders } : {}),
|
|
384
|
+
...(input.actionTypes ? { actionTypes: input.actionTypes } : {}),
|
|
310
385
|
...(targetDate
|
|
311
386
|
? { targetDate }
|
|
312
387
|
: untilDate
|
|
@@ -327,6 +402,24 @@ function targetPlanFingerprint(plan) {
|
|
|
327
402
|
generatedAt: stringValue(root?.generatedAt) ?? null,
|
|
328
403
|
};
|
|
329
404
|
}
|
|
405
|
+
function approvedPacketComparison(input, targetPlan) {
|
|
406
|
+
const root = recordValue(targetPlan);
|
|
407
|
+
const action = recordValue(firstGlobalAction(targetPlan));
|
|
408
|
+
const actualTargetShapeRevision = stringValue(root?.targetShapeRevision) ?? null;
|
|
409
|
+
const actualActionKey = stringValue(action?.actionKey) ?? null;
|
|
410
|
+
const shapeChanged = Boolean(input.expectedTargetShapeRevision &&
|
|
411
|
+
input.expectedTargetShapeRevision !== actualTargetShapeRevision);
|
|
412
|
+
const actionChanged = Boolean(input.expectedActionKey && input.expectedActionKey !== actualActionKey);
|
|
413
|
+
return {
|
|
414
|
+
changed: shapeChanged || actionChanged,
|
|
415
|
+
expectedTargetShapeRevision: input.expectedTargetShapeRevision ?? null,
|
|
416
|
+
actualTargetShapeRevision,
|
|
417
|
+
expectedActionKey: input.expectedActionKey ?? null,
|
|
418
|
+
actualActionKey,
|
|
419
|
+
shapeChanged,
|
|
420
|
+
actionChanged,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
330
423
|
function paidRefreshApprovalPacket(params) {
|
|
331
424
|
const action = params.action.action ?? {};
|
|
332
425
|
const ids = actionIds(action);
|
|
@@ -358,12 +451,15 @@ function paidRefreshOptionsFromApprovalPacket(packet) {
|
|
|
358
451
|
};
|
|
359
452
|
}
|
|
360
453
|
export async function executeRefillSendsCommand(input = {}) {
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
const
|
|
454
|
+
const normalizedInput = normalizeRefillSendsInput(input);
|
|
455
|
+
const yolo = normalizedInput.yolo === true;
|
|
456
|
+
const executionMode = normalizedInput.executionMode ?? (yolo ? "yolo" : "manual");
|
|
457
|
+
const mustHaveWorkspace = yolo ||
|
|
458
|
+
normalizedInput.requireWorkspace === true ||
|
|
459
|
+
executionMode === "scheduled";
|
|
364
460
|
const workspaceContext = mustHaveWorkspace
|
|
365
461
|
? createWorkspaceContext({
|
|
366
|
-
workspaceId:
|
|
462
|
+
workspaceId: normalizedInput.workspaceId,
|
|
367
463
|
executionMode,
|
|
368
464
|
toolName: "refill_sends",
|
|
369
465
|
})
|
|
@@ -372,9 +468,12 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
372
468
|
return workspaceContext;
|
|
373
469
|
}
|
|
374
470
|
const scopedInput = workspaceContext && workspaceContext.ok
|
|
375
|
-
? {
|
|
376
|
-
|
|
377
|
-
|
|
471
|
+
? {
|
|
472
|
+
...normalizedInput,
|
|
473
|
+
workspaceId: workspaceContext.context.workspaceId,
|
|
474
|
+
}
|
|
475
|
+
: normalizedInput;
|
|
476
|
+
const command = buildRefillSendsCommand(scopedInput);
|
|
378
477
|
const workspaceId = workspaceContext && workspaceContext.ok
|
|
379
478
|
? workspaceContext.context.workspaceId
|
|
380
479
|
: normalizeExplicitWorkspaceId(scopedInput.workspaceId);
|
|
@@ -399,6 +498,35 @@ export async function executeRefillSendsCommand(input = {}) {
|
|
|
399
498
|
}
|
|
400
499
|
const targetPlanInput = targetPlanInputFor(scopedInput);
|
|
401
500
|
const targetPlanBeforePaidRefresh = await getRefillTargetPlan(targetPlanInput);
|
|
501
|
+
const approvedPacket = approvedPacketComparison(scopedInput, targetPlanBeforePaidRefresh);
|
|
502
|
+
if (approvedPacket.changed) {
|
|
503
|
+
return {
|
|
504
|
+
...command,
|
|
505
|
+
ok: false,
|
|
506
|
+
code: "approval_scope_changed",
|
|
507
|
+
targetPlan: targetPlanBeforePaidRefresh,
|
|
508
|
+
targetPlanBeforePaidRefresh: null,
|
|
509
|
+
targetPlanBeforeYoloPrimitive: null,
|
|
510
|
+
...approvedPacket,
|
|
511
|
+
autoPaidInmailRefresh: {
|
|
512
|
+
enabled: false,
|
|
513
|
+
status: "skipped_approval_scope_changed",
|
|
514
|
+
refreshedPaidInmailSenderIds: [],
|
|
515
|
+
failedPaidInmailRefreshes: [],
|
|
516
|
+
attemptedSenderIds: [],
|
|
517
|
+
targetPlanReread: false,
|
|
518
|
+
workspaceId,
|
|
519
|
+
workspaceResolution: workspaceId ? "explicit" : "active_config",
|
|
520
|
+
note: "Approved refill scope changed before paid-credit refresh or primitive execution.",
|
|
521
|
+
},
|
|
522
|
+
yoloExecution: {
|
|
523
|
+
enabled: yolo,
|
|
524
|
+
status: "approval_scope_changed",
|
|
525
|
+
selectedAction: firstGlobalAction(targetPlanBeforePaidRefresh),
|
|
526
|
+
targetPlanReread: false,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
|
402
530
|
const refreshActions = collectPaidInmailRefreshActions(targetPlanBeforePaidRefresh);
|
|
403
531
|
const refreshedPaidInmailSenderIds = [];
|
|
404
532
|
const failedPaidInmailRefreshes = [];
|
|
@@ -10,6 +10,7 @@ const SAFE_PACKET_ACTION_TYPES = new Set([
|
|
|
10
10
|
"approve_messages",
|
|
11
11
|
"start_paused_campaign",
|
|
12
12
|
"rerun_errored_cells",
|
|
13
|
+
"run_scheduler_sweep",
|
|
13
14
|
"wait_for_scheduler",
|
|
14
15
|
"wait_for_active_work",
|
|
15
16
|
"wait_for_source_import",
|
|
@@ -73,6 +74,21 @@ const PREP_FAILURE_RECOMMENDED_ACTIONS = new Set([
|
|
|
73
74
|
"approve_or_repair_approval_cells",
|
|
74
75
|
"inspect_rows",
|
|
75
76
|
]);
|
|
77
|
+
const SCHEDULER_SWEEP_ALLOWED_SIDE_EFFECTS = [
|
|
78
|
+
"schedule any eligible workspace cells on the approved date within scheduler gates",
|
|
79
|
+
];
|
|
80
|
+
const SCHEDULER_SWEEP_FORBIDDEN_SIDE_EFFECTS = [
|
|
81
|
+
"send directly",
|
|
82
|
+
"write scheduler fields directly",
|
|
83
|
+
"change campaign state",
|
|
84
|
+
"change sender limits",
|
|
85
|
+
"change source selection",
|
|
86
|
+
];
|
|
87
|
+
const SCHEDULER_SWEEP_RECEIPT_GROUPS = [
|
|
88
|
+
"campaignId",
|
|
89
|
+
"tableId",
|
|
90
|
+
"actionType",
|
|
91
|
+
];
|
|
76
92
|
async function postRefillTargetPlan(body, workspaceId) {
|
|
77
93
|
const api = getApi();
|
|
78
94
|
const requestOptions = workspaceRequestOptions(workspaceId);
|
|
@@ -515,6 +531,7 @@ function sanitizeStructuredAction(action, selectedBySender, mode, rank) {
|
|
|
515
531
|
: null,
|
|
516
532
|
stopCondition: stringValue(action.stopCondition) ?? "",
|
|
517
533
|
yoloEligible,
|
|
534
|
+
actionKey: stringValue(action.actionKey),
|
|
518
535
|
capSaturated: booleanValue(action.capSaturated) === true ? true : undefined,
|
|
519
536
|
prepFailureDiagnosis: sanitizePrepFailureDiagnosis(action.prepFailureDiagnosis),
|
|
520
537
|
};
|
|
@@ -814,6 +831,154 @@ function buildSanitizedGlobalActionQueue(senderRefillPlans) {
|
|
|
814
831
|
rank: index + 1,
|
|
815
832
|
}));
|
|
816
833
|
}
|
|
834
|
+
function sortedUniqueStrings(value) {
|
|
835
|
+
return [...new Set(stringArray(value))].sort();
|
|
836
|
+
}
|
|
837
|
+
function sameStrings(left, right) {
|
|
838
|
+
return (left.length === right.length &&
|
|
839
|
+
left.every((value, index) => value === right[index]));
|
|
840
|
+
}
|
|
841
|
+
function sanitizeSchedulerSweepSourceScopes(value) {
|
|
842
|
+
if (!Array.isArray(value))
|
|
843
|
+
return [];
|
|
844
|
+
const scopes = value.flatMap((entry) => {
|
|
845
|
+
if (!isRecord(entry))
|
|
846
|
+
return [];
|
|
847
|
+
const campaignId = stringValue(entry.campaignId)?.trim();
|
|
848
|
+
const tableId = stringValue(entry.tableId)?.trim();
|
|
849
|
+
const sourceLeadListId = stringValue(entry.sourceLeadListId)?.trim();
|
|
850
|
+
const leadSourceProvider = stringValue(entry.leadSourceProvider)?.trim();
|
|
851
|
+
if (!campaignId ||
|
|
852
|
+
!tableId ||
|
|
853
|
+
!sourceLeadListId ||
|
|
854
|
+
!leadSourceProvider) {
|
|
855
|
+
return [];
|
|
856
|
+
}
|
|
857
|
+
return [
|
|
858
|
+
{ campaignId, tableId, sourceLeadListId, leadSourceProvider },
|
|
859
|
+
];
|
|
860
|
+
});
|
|
861
|
+
const unique = new Map(scopes.map((scope) => [
|
|
862
|
+
[
|
|
863
|
+
scope.campaignId,
|
|
864
|
+
scope.tableId,
|
|
865
|
+
scope.sourceLeadListId,
|
|
866
|
+
scope.leadSourceProvider,
|
|
867
|
+
].join(":"),
|
|
868
|
+
scope,
|
|
869
|
+
]));
|
|
870
|
+
return [...unique.values()].sort((a, b) => {
|
|
871
|
+
if (a.campaignId !== b.campaignId) {
|
|
872
|
+
return a.campaignId.localeCompare(b.campaignId);
|
|
873
|
+
}
|
|
874
|
+
return a.tableId.localeCompare(b.tableId);
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
function sanitizeTargetSchedulerSweep(params) {
|
|
878
|
+
if (!Array.isArray(params.value) ||
|
|
879
|
+
!params.expectedWorkspaceId ||
|
|
880
|
+
!params.expectedTargetDate ||
|
|
881
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(params.expectedTargetDate) ||
|
|
882
|
+
!params.expectedTargetShapeRevision ||
|
|
883
|
+
!params.expectedStateRevision ||
|
|
884
|
+
params.remainingProjectedGap <= 0 ||
|
|
885
|
+
params.remainingReadyOrProjectedGap !== 0) {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
const expectedSenderIds = [...params.selectedBySender.keys()].sort();
|
|
889
|
+
const expectedActionTypes = [
|
|
890
|
+
...new Set(params.selectedBySender.values()),
|
|
891
|
+
].sort();
|
|
892
|
+
if (expectedSenderIds.length === 0 || expectedActionTypes.length === 0) {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
for (const rawAction of params.value) {
|
|
896
|
+
if (!isRecord(rawAction) || rawAction.type !== "run_scheduler_sweep") {
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const input = isRecord(rawAction.toolInput) ? rawAction.toolInput : {};
|
|
900
|
+
const actionKey = stringValue(rawAction.actionKey)?.trim();
|
|
901
|
+
const requestKey = stringValue(input.requestKey)?.trim();
|
|
902
|
+
const senderIds = sortedUniqueStrings(input.senderIds);
|
|
903
|
+
const actionTypes = sortedUniqueStrings(input.actionTypes).filter((actionType) => allowedActionType(actionType) === actionType);
|
|
904
|
+
const campaignIds = sortedUniqueStrings(input.campaignIds);
|
|
905
|
+
const tableIds = sortedUniqueStrings(input.tableIds);
|
|
906
|
+
const sourceScopes = sanitizeSchedulerSweepSourceScopes(input.sourceScopes);
|
|
907
|
+
const receiptRequirements = isRecord(rawAction.receiptRequirements)
|
|
908
|
+
? rawAction.receiptRequirements
|
|
909
|
+
: {};
|
|
910
|
+
const groupBy = stringArray(receiptRequirements.groupBy);
|
|
911
|
+
const allowedSideEffects = stringArray(rawAction.allowedSideEffects);
|
|
912
|
+
const forbiddenSideEffects = stringArray(rawAction.forbiddenSideEffects);
|
|
913
|
+
const rawActionType = allowedActionType(rawAction.actionType);
|
|
914
|
+
if (input.action !== "run" ||
|
|
915
|
+
input.workspaceId !== params.expectedWorkspaceId ||
|
|
916
|
+
input.targetDate !== params.expectedTargetDate ||
|
|
917
|
+
input.targetDateKey !== params.expectedTargetDate ||
|
|
918
|
+
input.targetShapeRevision !== params.expectedTargetShapeRevision ||
|
|
919
|
+
input.stateRevision !== params.expectedStateRevision ||
|
|
920
|
+
!actionKey ||
|
|
921
|
+
requestKey !== actionKey ||
|
|
922
|
+
!sameStrings(senderIds, expectedSenderIds) ||
|
|
923
|
+
!sameStrings(actionTypes, expectedActionTypes) ||
|
|
924
|
+
(expectedActionTypes.length === 1 &&
|
|
925
|
+
rawActionType !== expectedActionTypes[0]) ||
|
|
926
|
+
campaignIds.length === 0 ||
|
|
927
|
+
tableIds.length === 0 ||
|
|
928
|
+
sourceScopes.length === 0 ||
|
|
929
|
+
sourceScopes.some((scope) => !campaignIds.includes(scope.campaignId) ||
|
|
930
|
+
!tableIds.includes(scope.tableId)) ||
|
|
931
|
+
rawAction.toolName !== "run_scheduler_sweep" ||
|
|
932
|
+
rawAction.sideEffectClass !== "scheduler_placement" ||
|
|
933
|
+
rawAction.workspaceWide !== true ||
|
|
934
|
+
rawAction.yoloEligible !== true ||
|
|
935
|
+
receiptRequirements.nonTruncated !== true ||
|
|
936
|
+
!sameStrings(groupBy, SCHEDULER_SWEEP_RECEIPT_GROUPS) ||
|
|
937
|
+
!sameStrings(allowedSideEffects, SCHEDULER_SWEEP_ALLOWED_SIDE_EFFECTS) ||
|
|
938
|
+
!sameStrings(forbiddenSideEffects, SCHEDULER_SWEEP_FORBIDDEN_SIDE_EFFECTS)) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
rank: 1,
|
|
943
|
+
type: "run_scheduler_sweep",
|
|
944
|
+
actionType: rawActionType,
|
|
945
|
+
toolName: "run_scheduler_sweep",
|
|
946
|
+
sideEffectClass: "scheduler_placement",
|
|
947
|
+
ids: {},
|
|
948
|
+
toolInput: {
|
|
949
|
+
action: "run",
|
|
950
|
+
workspaceId: params.expectedWorkspaceId,
|
|
951
|
+
targetDate: params.expectedTargetDate,
|
|
952
|
+
targetDateKey: params.expectedTargetDate,
|
|
953
|
+
senderIds,
|
|
954
|
+
actionTypes,
|
|
955
|
+
campaignIds,
|
|
956
|
+
tableIds,
|
|
957
|
+
sourceScopes,
|
|
958
|
+
targetShapeRevision: params.expectedTargetShapeRevision,
|
|
959
|
+
stateRevision: params.expectedStateRevision,
|
|
960
|
+
requestKey,
|
|
961
|
+
},
|
|
962
|
+
inputSummary: stringValue(rawAction.inputSummary) ?? "",
|
|
963
|
+
reason: stringValue(rawAction.reason) ?? "",
|
|
964
|
+
prerequisites: stringArray(rawAction.prerequisites),
|
|
965
|
+
rereadAfter: rawAction.rereadAfter === "get_refill_target_plan"
|
|
966
|
+
? "get_refill_target_plan"
|
|
967
|
+
: null,
|
|
968
|
+
stopCondition: stringValue(rawAction.stopCondition) ?? "",
|
|
969
|
+
yoloEligible: true,
|
|
970
|
+
workspaceWide: true,
|
|
971
|
+
allowedSideEffects: [...SCHEDULER_SWEEP_ALLOWED_SIDE_EFFECTS],
|
|
972
|
+
forbiddenSideEffects: [...SCHEDULER_SWEEP_FORBIDDEN_SIDE_EFFECTS],
|
|
973
|
+
receiptRequirements: {
|
|
974
|
+
nonTruncated: true,
|
|
975
|
+
groupBy: [...SCHEDULER_SWEEP_RECEIPT_GROUPS],
|
|
976
|
+
},
|
|
977
|
+
actionKey,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
817
982
|
function paidRefreshNeededSenderIds(senderRefillPlans) {
|
|
818
983
|
const senderIds = new Set();
|
|
819
984
|
for (const plan of senderRefillPlans) {
|
|
@@ -975,7 +1140,20 @@ function sanitizeRefillTargetPlanResult(result) {
|
|
|
975
1140
|
const senderRefillPlans = sanitizeStructuredSenderPlans(target.senderRefillPlans, selectedBySender);
|
|
976
1141
|
const paidRefreshNeededSenderIdSet = paidRefreshNeededSenderIds(senderRefillPlans);
|
|
977
1142
|
const blockers = sanitizeBlockers(result.blockers, selectedKeys, paidRefreshNeededSenderIdSet);
|
|
978
|
-
const
|
|
1143
|
+
const request = isRecord(result.request) ? result.request : {};
|
|
1144
|
+
const schedulerSweepAction = sanitizeTargetSchedulerSweep({
|
|
1145
|
+
value: target.globalActionQueue,
|
|
1146
|
+
selectedBySender,
|
|
1147
|
+
expectedWorkspaceId: stringValue(target.workspaceId),
|
|
1148
|
+
expectedTargetDate: stringValue(request.targetDate),
|
|
1149
|
+
expectedTargetShapeRevision: stringValue(result.targetShapeRevision),
|
|
1150
|
+
expectedStateRevision: stringValue(result.stateRevision),
|
|
1151
|
+
remainingProjectedGap,
|
|
1152
|
+
remainingReadyOrProjectedGap,
|
|
1153
|
+
});
|
|
1154
|
+
const globalActionQueue = schedulerSweepAction
|
|
1155
|
+
? [schedulerSweepAction]
|
|
1156
|
+
: buildSanitizedGlobalActionQueue(senderRefillPlans);
|
|
979
1157
|
const hasTerminalNoActionBlocker = globalActionQueue.length === 0 &&
|
|
980
1158
|
blockers.some((blocker) => TERMINAL_NO_ACTION_BLOCKER_CODES.has(String(blocker.code)));
|
|
981
1159
|
const status = remainingProjectedGap === 0
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -7546,6 +7546,14 @@ export declare const allTools: ({
|
|
|
7546
7546
|
type: string;
|
|
7547
7547
|
description: string;
|
|
7548
7548
|
};
|
|
7549
|
+
requestKey: {
|
|
7550
|
+
type: string;
|
|
7551
|
+
description: string;
|
|
7552
|
+
};
|
|
7553
|
+
targetShapeRevision: {
|
|
7554
|
+
type: string;
|
|
7555
|
+
description: string;
|
|
7556
|
+
};
|
|
7549
7557
|
};
|
|
7550
7558
|
required: string[];
|
|
7551
7559
|
additionalProperties: boolean;
|
|
@@ -3,6 +3,8 @@ type RunSchedulerSweepInput = {
|
|
|
3
3
|
workspaceId: string;
|
|
4
4
|
action?: SchedulerRunAction;
|
|
5
5
|
targetDate?: string;
|
|
6
|
+
requestKey?: string;
|
|
7
|
+
targetShapeRevision?: string;
|
|
6
8
|
};
|
|
7
9
|
export declare const schedulerRunToolDefinitions: {
|
|
8
10
|
name: string;
|
|
@@ -23,6 +25,14 @@ export declare const schedulerRunToolDefinitions: {
|
|
|
23
25
|
type: string;
|
|
24
26
|
description: string;
|
|
25
27
|
};
|
|
28
|
+
requestKey: {
|
|
29
|
+
type: string;
|
|
30
|
+
description: string;
|
|
31
|
+
};
|
|
32
|
+
targetShapeRevision: {
|
|
33
|
+
type: string;
|
|
34
|
+
description: string;
|
|
35
|
+
};
|
|
26
36
|
};
|
|
27
37
|
required: string[];
|
|
28
38
|
additionalProperties: boolean;
|
|
@@ -21,6 +21,14 @@ function normalizeTargetDate(value) {
|
|
|
21
21
|
}
|
|
22
22
|
return targetDate;
|
|
23
23
|
}
|
|
24
|
+
function normalizeOptionalScopeValue(value, field) {
|
|
25
|
+
if (value === undefined || value === null)
|
|
26
|
+
return null;
|
|
27
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
28
|
+
throw new Error(`${field} must be a non-empty string when provided.`);
|
|
29
|
+
}
|
|
30
|
+
return value.trim();
|
|
31
|
+
}
|
|
24
32
|
export const schedulerRunToolDefinitions = [
|
|
25
33
|
{
|
|
26
34
|
name: "run_scheduler_sweep",
|
|
@@ -41,6 +49,14 @@ export const schedulerRunToolDefinitions = [
|
|
|
41
49
|
type: "string",
|
|
42
50
|
description: "Optional one exact sender-local date (YYYY-MM-DD) to scope scheduler placement/status. The run remains workspace-wide inside that date.",
|
|
43
51
|
},
|
|
52
|
+
requestKey: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "Optional deterministic planner action key used to attach to or replay the same exact scheduler attempt.",
|
|
55
|
+
},
|
|
56
|
+
targetShapeRevision: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Optional stable approved refill target-shape revision associated with requestKey.",
|
|
59
|
+
},
|
|
44
60
|
},
|
|
45
61
|
required: ["workspaceId"],
|
|
46
62
|
additionalProperties: false,
|
|
@@ -54,6 +70,8 @@ export async function runSchedulerSweep(input) {
|
|
|
54
70
|
}
|
|
55
71
|
const action = input.action ?? "run";
|
|
56
72
|
const targetDate = normalizeTargetDate(input.targetDate);
|
|
73
|
+
const requestKey = normalizeOptionalScopeValue(input.requestKey, "requestKey");
|
|
74
|
+
const targetShapeRevision = normalizeOptionalScopeValue(input.targetShapeRevision, "targetShapeRevision");
|
|
57
75
|
if (action !== "run" && action !== "status") {
|
|
58
76
|
throw new Error('action must be "run" or "status" for run_scheduler_sweep.');
|
|
59
77
|
}
|
|
@@ -61,5 +79,7 @@ export async function runSchedulerSweep(input) {
|
|
|
61
79
|
workspaceId,
|
|
62
80
|
action,
|
|
63
81
|
...(targetDate ? { targetDate } : {}),
|
|
82
|
+
...(requestKey ? { requestKey } : {}),
|
|
83
|
+
...(targetShapeRevision ? { targetShapeRevision } : {}),
|
|
64
84
|
}, workspaceId);
|
|
65
85
|
}
|