@sellable/mcp 0.1.554 → 0.1.555

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.
Files changed (40) hide show
  1. package/dist/index-dev.js +0 -0
  2. package/dist/index.js +0 -0
  3. package/dist/refill-contract.d.ts +157 -0
  4. package/dist/refill-contract.js +487 -0
  5. package/dist/refill-run-client.d.ts +5 -0
  6. package/dist/refill-run-client.js +15 -0
  7. package/dist/refill-run-loop.d.ts +12 -1
  8. package/dist/refill-run-loop.js +158 -13
  9. package/dist/tools/campaign-message-preparation.d.ts +62 -0
  10. package/dist/tools/campaign-message-preparation.js +41 -0
  11. package/dist/tools/evergreen-refill-plan.d.ts +3 -0
  12. package/dist/tools/evergreen-refill-plan.js +29 -7
  13. package/dist/tools/prompts.js +9 -0
  14. package/dist/tools/refill-executors.d.ts +38 -0
  15. package/dist/tools/refill-executors.js +222 -3
  16. package/dist/tools/refill-sends-v2.d.ts +112 -1
  17. package/dist/tools/refill-sends-v2.js +302 -2
  18. package/dist/tools/refill-sends.d.ts +678 -32
  19. package/dist/tools/refill-sends.js +274 -13
  20. package/dist/tools/refill-target-plan.js +486 -14
  21. package/dist/tools/registry.d.ts +96 -27
  22. package/dist/tools/registry.js +1 -3
  23. package/dist/tools/scheduler-fill-capacity.js +1 -1
  24. package/dist/tools/scheduler-run.d.ts +71 -0
  25. package/dist/tools/scheduler-run.js +203 -1
  26. package/dist/tools/workspaces.d.ts +4 -6
  27. package/dist/tools/workspaces.js +11 -13
  28. package/package.json +1 -1
  29. package/skills/refill-sends/SKILL.md +91 -353
  30. package/skills/refill-sends-v2/SKILL.md +6 -6
  31. package/skills/refill-sends-v2-workflow/SKILL.md +5 -5
  32. package/skills/refill-sends-v2-workflow/core/flow.v1.json +8 -8
  33. package/skills/refill-sends-workflow/SKILL.md +100 -743
  34. package/skills/refill-sends-workflow/core/contract.v2.json +543 -0
  35. package/skills/refill-sends-workflow/core/flow.v1.json +185 -1
  36. package/dist/refill-date-window.d.ts +0 -34
  37. package/dist/refill-date-window.js +0 -210
  38. package/dist/tools/refill-sends-evergreen.d.ts +0 -28
  39. package/dist/tools/refill-sends-evergreen.js +0 -47
  40. package/skills/research/config.json +0 -9
@@ -10,6 +10,8 @@ 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",
14
+ "scheduler_first_settle",
13
15
  "wait_for_scheduler",
14
16
  "wait_for_active_work",
15
17
  "wait_for_source_import",
@@ -73,6 +75,17 @@ const PREP_FAILURE_RECOMMENDED_ACTIONS = new Set([
73
75
  "approve_or_repair_approval_cells",
74
76
  "inspect_rows",
75
77
  ]);
78
+ const SCHEDULER_SWEEP_ALLOWED_SIDE_EFFECTS = [
79
+ "schedule any eligible workspace cells on the approved date within scheduler gates",
80
+ ];
81
+ const SCHEDULER_SWEEP_FORBIDDEN_SIDE_EFFECTS = [
82
+ "send directly",
83
+ "write scheduler fields directly",
84
+ "change campaign state",
85
+ "change sender limits",
86
+ "change source selection",
87
+ ];
88
+ const SCHEDULER_SWEEP_RECEIPT_GROUPS = ["campaignId", "tableId", "actionType"];
76
89
  async function postRefillTargetPlan(body, workspaceId) {
77
90
  const api = getApi();
78
91
  const requestOptions = workspaceRequestOptions(workspaceId);
@@ -199,14 +212,289 @@ function senderActionKey(senderId, actionType) {
199
212
  function sanitizeCounts(counts, selectedKeys) {
200
213
  if (!Array.isArray(counts))
201
214
  return [];
202
- return counts.filter((item) => {
215
+ return counts
216
+ .filter((item) => {
203
217
  if (!isRecord(item))
204
218
  return false;
205
219
  const actionType = allowedActionType(item.actionType);
206
220
  const key = senderActionKey(item.senderId, actionType);
207
221
  return Boolean(key && selectedKeys.has(key));
222
+ })
223
+ .map((item) => ({
224
+ senderId: stringValue(item.senderId),
225
+ actionType: allowedActionType(item.actionType),
226
+ refillLaneKey: refillLaneKeyValue(item.refillLaneKey),
227
+ branchActionType: branchActionTypeValue(item.branchActionType),
228
+ date: stringValue(item.date),
229
+ count: nonNegativeNumber(item.count),
230
+ campaignId: stringValue(item.campaignId),
231
+ tableId: stringValue(item.tableId),
232
+ }));
233
+ }
234
+ const REFILL_BRANCH_ACTION_TYPES = new Set([
235
+ "send_invite",
236
+ "send_dm",
237
+ "send_inmail_open",
238
+ "send_inmail_closed",
239
+ "react_and_comment",
240
+ ]);
241
+ function nonNegativeNumber(value) {
242
+ return typeof value === "number" && Number.isFinite(value)
243
+ ? Math.max(0, value)
244
+ : 0;
245
+ }
246
+ function refillLaneKeyValue(value) {
247
+ if (value === "sales_nav_cascade")
248
+ return value;
249
+ return allowedActionType(value) ?? undefined;
250
+ }
251
+ function branchActionTypeValue(value) {
252
+ return typeof value === "string" && REFILL_BRANCH_ACTION_TYPES.has(value)
253
+ ? value
254
+ : undefined;
255
+ }
256
+ function sanitizeBranchMetrics(value) {
257
+ if (!Array.isArray(value))
258
+ return [];
259
+ return value
260
+ .filter((metric) => isRecord(metric))
261
+ .map((metric) => ({
262
+ actionType: branchActionTypeValue(metric.actionType),
263
+ count: nonNegativeNumber(metric.count),
264
+ }))
265
+ .filter((metric) => Boolean(metric.actionType));
266
+ }
267
+ function sanitizeBranchMetricGroups(value) {
268
+ if (!isRecord(value)) {
269
+ return { sentScheduled: [], assignedReady: [], sharedReady: [] };
270
+ }
271
+ return {
272
+ sentScheduled: sanitizeBranchMetrics(value.sentScheduled),
273
+ assignedReady: sanitizeBranchMetrics(value.assignedReady),
274
+ sharedReady: sanitizeBranchMetrics(value.sharedReady),
275
+ };
276
+ }
277
+ function sanitizeLedgerViews(value) {
278
+ const views = isRecord(value) ? value : {};
279
+ const raw = isRecord(views.raw) ? views.raw : {};
280
+ const goalCapped = isRecord(views.goalCapped) ? views.goalCapped : {};
281
+ const allocatable = isRecord(views.allocatable) ? views.allocatable : {};
282
+ const unallocatable = isRecord(views.unallocatable)
283
+ ? views.unallocatable
284
+ : {};
285
+ return {
286
+ raw: {
287
+ sentScheduled: nonNegativeNumber(raw.sentScheduled),
288
+ assignedReady: nonNegativeNumber(raw.assignedReady),
289
+ sharedReady: nonNegativeNumber(raw.sharedReady),
290
+ },
291
+ goalCapped: {
292
+ projected: nonNegativeNumber(goalCapped.projected),
293
+ ready: nonNegativeNumber(goalCapped.ready),
294
+ remaining: nonNegativeNumber(goalCapped.remaining),
295
+ },
296
+ allocatable: {
297
+ demand: nonNegativeNumber(allocatable.demand),
298
+ ready: nonNegativeNumber(allocatable.ready),
299
+ prepDeficit: nonNegativeNumber(allocatable.prepDeficit),
300
+ },
301
+ unallocatable: {
302
+ assignedReady: nonNegativeNumber(unallocatable.assignedReady),
303
+ sharedReady: nonNegativeNumber(unallocatable.sharedReady),
304
+ goalDeficit: nonNegativeNumber(unallocatable.goalDeficit),
305
+ },
306
+ };
307
+ }
308
+ function sanitizeIdentityChecks(value) {
309
+ const checks = isRecord(value) ? value : {};
310
+ return {
311
+ assignedConserved: checks.assignedConserved === true,
312
+ sharedConserved: checks.sharedConserved === true,
313
+ readyCapped: checks.readyCapped === true,
314
+ deficitConserved: checks.deficitConserved === true,
315
+ nonNegative: checks.nonNegative === true,
316
+ };
317
+ }
318
+ const LEDGER_NUMBER_FIELDS = [
319
+ "goal",
320
+ "sentScheduledRawUnique",
321
+ "goalAfterSentScheduled",
322
+ "schedulerFillableSlotsFromCandidateSimulation",
323
+ "preReadyAllocatableDemand",
324
+ "assignedRawReady",
325
+ "assignedCreditedReady",
326
+ "assignedUnallocatableReady",
327
+ "sharedRawReady",
328
+ "sharedAllocatedReady",
329
+ "sharedUnallocatableReady",
330
+ "readyGoalCredited",
331
+ "goalCappedProjected",
332
+ "remainingGoalGap",
333
+ "initialAllocatablePrepDeficit",
334
+ "unallocatableGoalDeficit",
335
+ "matchingCardinality",
336
+ ];
337
+ function sanitizeLaneLedger(value, requireScope) {
338
+ if (!isRecord(value))
339
+ return null;
340
+ const senderId = stringValue(value.senderId);
341
+ const refillLaneKey = refillLaneKeyValue(value.refillLaneKey);
342
+ const targetDate = stringValue(value.targetDate);
343
+ if (requireScope && (!senderId || !refillLaneKey || !targetDate))
344
+ return null;
345
+ const numbers = Object.fromEntries(LEDGER_NUMBER_FIELDS.map((field) => [
346
+ field,
347
+ nonNegativeNumber(value[field]),
348
+ ]));
349
+ return {
350
+ ...(senderId ? { senderId } : {}),
351
+ ...(refillLaneKey ? { refillLaneKey } : {}),
352
+ ...(targetDate ? { targetDate } : {}),
353
+ ...numbers,
354
+ unmatchedReadyReasons: numericRecord(value.unmatchedReadyReasons),
355
+ branchMetrics: sanitizeBranchMetricGroups(value.branchMetrics),
356
+ views: sanitizeLedgerViews(value.views),
357
+ identityChecks: sanitizeIdentityChecks(value.identityChecks),
358
+ };
359
+ }
360
+ function sanitizeLaneLedgers(value) {
361
+ if (!Array.isArray(value))
362
+ return [];
363
+ return value.flatMap((ledger) => {
364
+ const sanitized = sanitizeLaneLedger(ledger, true);
365
+ return sanitized ? [sanitized] : [];
208
366
  });
209
367
  }
368
+ function sanitizePhase98RefillContract(value) {
369
+ if (!isRecord(value))
370
+ return {};
371
+ const refillLaneKey = refillLaneKeyValue(value.refillLaneKey);
372
+ const laneLedgers = sanitizeLaneLedgers(value.laneLedgers);
373
+ const laneLedgerTotals = sanitizeLaneLedger(value.laneLedgerTotals, false);
374
+ const settleKey = stringValue(value.settleKey);
375
+ const settleDeadline = stringValue(value.settleDeadline);
376
+ const settleBasisRevision = stringValue(value.settleBasisRevision);
377
+ return {
378
+ ...(refillLaneKey ? { refillLaneKey } : {}),
379
+ ...(isRecord(value.branchMetrics)
380
+ ? { branchMetrics: sanitizeBranchMetricGroups(value.branchMetrics) }
381
+ : {}),
382
+ ...(Array.isArray(value.laneLedgers) ? { laneLedgers } : {}),
383
+ ...(laneLedgerTotals ? { laneLedgerTotals } : {}),
384
+ ...(settleKey?.startsWith("scheduler_first_settle:") ? { settleKey } : {}),
385
+ ...(settleDeadline ? { settleDeadline } : {}),
386
+ ...(settleBasisRevision ? { settleBasisRevision } : {}),
387
+ };
388
+ }
389
+ function sanitizeSelectedDay(value) {
390
+ if (!isRecord(value))
391
+ return null;
392
+ const senderId = stringValue(value.senderId);
393
+ const actionType = allowedActionType(value.actionType);
394
+ if (!senderId || !actionType)
395
+ return null;
396
+ return {
397
+ senderId,
398
+ senderName: stringValue(value.senderName) ?? senderId,
399
+ actionType,
400
+ refillLaneKey: refillLaneKeyValue(value.refillLaneKey),
401
+ date: stringValue(value.date),
402
+ timeZone: stringValue(value.timeZone),
403
+ startUtc: stringValue(value.startUtc),
404
+ endUtc: stringValue(value.endUtc),
405
+ limit: nonNegativeNumber(value.limit),
406
+ };
407
+ }
408
+ function sanitizeSenderPlan(value) {
409
+ if (!isRecord(value))
410
+ return null;
411
+ const senderId = stringValue(value.senderId);
412
+ const actionType = allowedActionType(value.actionType);
413
+ if (!senderId || !actionType)
414
+ return null;
415
+ const numericFields = [
416
+ "requestedTarget",
417
+ "grossTarget",
418
+ "effectiveTarget",
419
+ "sent",
420
+ "scheduled",
421
+ "projected",
422
+ "readyBuffer",
423
+ "remainingScheduledGap",
424
+ "remainingProjectedGap",
425
+ "remainingReadyOrProjectedGap",
426
+ "schedulerFillableSlots",
427
+ "schedulerCapacityTarget",
428
+ "remainingSchedulerFillableGap",
429
+ ];
430
+ const numbers = Object.fromEntries(numericFields.map((field) => [field, nonNegativeNumber(value[field])]));
431
+ const campaigns = Array.isArray(value.campaigns)
432
+ ? value.campaigns
433
+ .filter((campaign) => isRecord(campaign))
434
+ .map((campaign) => ({
435
+ campaignId: stringValue(campaign.campaignId),
436
+ campaignName: stringValue(campaign.campaignName) ?? "",
437
+ tableId: stringValue(campaign.tableId),
438
+ tableName: stringValue(campaign.tableName) ?? null,
439
+ campaignStatus: stringValue(campaign.campaignStatus) ?? null,
440
+ classification: campaignClassificationValue(campaign.classification),
441
+ selectedLeadListId: stringValue(campaign.selectedLeadListId) ?? null,
442
+ leadSourceProvider: stringValue(campaign.leadSourceProvider) ?? null,
443
+ sourceFingerprint: stringValue(campaign.sourceFingerprint) ?? null,
444
+ }))
445
+ : [];
446
+ return {
447
+ senderId,
448
+ senderName: stringValue(value.senderName) ?? senderId,
449
+ actionType,
450
+ ...sanitizePhase98RefillContract(value),
451
+ selectedDays: Array.isArray(value.selectedDays)
452
+ ? value.selectedDays
453
+ .map(sanitizeSelectedDay)
454
+ .filter((day) => Boolean(day))
455
+ : [],
456
+ campaigns,
457
+ ...numbers,
458
+ schedulerFillableSlotsKnown: booleanValue(value.schedulerFillableSlotsKnown) ?? undefined,
459
+ schedulerCapacityMode: stringValue(value.schedulerCapacityMode),
460
+ allocationScope: stringValue(value.allocationScope),
461
+ schedulerWarnings: stringArray(value.schedulerWarnings),
462
+ paidInmail: sanitizePaidInmail(value.paidInmail),
463
+ refillReceipt: sanitizeRefillReceipt(value.refillReceipt),
464
+ };
465
+ }
466
+ function sanitizeActionSelection(value) {
467
+ if (!isRecord(value))
468
+ return null;
469
+ const senderId = stringValue(value.senderId);
470
+ if (!senderId)
471
+ return null;
472
+ const evidence = Array.isArray(value.evidence)
473
+ ? value.evidence
474
+ .filter((item) => isRecord(item))
475
+ .map((item) => ({
476
+ senderId: stringValue(item.senderId),
477
+ actionType: allowedActionType(item.actionType),
478
+ refillLaneKey: refillLaneKeyValue(item.refillLaneKey),
479
+ source: stringValue(item.source),
480
+ count: nonNegativeNumber(item.count),
481
+ latestAt: stringValue(item.latestAt),
482
+ campaignId: stringValue(item.campaignId),
483
+ tableId: stringValue(item.tableId),
484
+ }))
485
+ : [];
486
+ const refillLaneKeys = Array.isArray(value.refillLaneKeys)
487
+ ? value.refillLaneKeys.map(refillLaneKeyValue).filter(Boolean)
488
+ : [];
489
+ return {
490
+ senderId,
491
+ senderName: stringValue(value.senderName) ?? senderId,
492
+ actionTypes: actionTypesFrom(value.actionTypes),
493
+ ...(refillLaneKeys.length > 0 ? { refillLaneKeys } : {}),
494
+ source: stringValue(value.source),
495
+ evidence,
496
+ };
497
+ }
210
498
  function sanitizeActionCandidates(candidates, selectedKeys, paidRefreshNeededSenderIds, status, remainingReadyOrProjectedGap, remainingProjectedGap) {
211
499
  const filtered = Array.isArray(candidates)
212
500
  ? candidates
@@ -277,6 +565,7 @@ function sanitizeActionCandidate(candidate) {
277
565
  ? "get_refill_target_plan"
278
566
  : undefined,
279
567
  reason: stringValue(candidate.reason),
568
+ ...sanitizePhase98RefillContract(candidate),
280
569
  };
281
570
  }
282
571
  function sanitizeRowSelector(value) {
@@ -465,6 +754,16 @@ function sanitizeToolInput(value) {
465
754
  wait: sanitizeWait(value.wait),
466
755
  refillReceipt: sanitizeRefillReceipt(value.refillReceipt),
467
756
  capSaturated: booleanValue(value.capSaturated) === true ? true : undefined,
757
+ settleKey: stringValue(value.settleKey),
758
+ settleDeadline: stringValue(value.settleDeadline),
759
+ settleBasisRevision: stringValue(value.settleBasisRevision),
760
+ calibration: isRecord(value.calibration)
761
+ ? {
762
+ trusted: value.calibration.trusted === true,
763
+ passRate: optionalNumberValue(value.calibration.passRate),
764
+ minimumRows: nonNegativeNumber(value.calibration.minimumRows),
765
+ }
766
+ : undefined,
468
767
  };
469
768
  }
470
769
  function sanitizePacketIds(value) {
@@ -515,8 +814,10 @@ function sanitizeStructuredAction(action, selectedBySender, mode, rank) {
515
814
  : null,
516
815
  stopCondition: stringValue(action.stopCondition) ?? "",
517
816
  yoloEligible,
817
+ actionKey: stringValue(action.actionKey),
518
818
  capSaturated: booleanValue(action.capSaturated) === true ? true : undefined,
519
819
  prepFailureDiagnosis: sanitizePrepFailureDiagnosis(action.prepFailureDiagnosis),
820
+ ...sanitizePhase98RefillContract(action),
520
821
  };
521
822
  }
522
823
  function sanitizeLedgerBlockers(value) {
@@ -709,7 +1010,12 @@ function paidInmailRefreshActionForSenderPlan(plan, paidInmail) {
709
1010
  availableCredits: numberValue(paidInmail?.availableCredits),
710
1011
  oldThreshold: numberValue(paidInmail?.threshold),
711
1012
  maxStalenessSeconds: MCP_PAID_INMAIL_CREDITS_MAX_STALENESS_SECONDS,
712
- actionKey: ["refresh_paid_inmail_credits", senderId, campaignId, columnId].join(":"),
1013
+ actionKey: [
1014
+ "refresh_paid_inmail_credits",
1015
+ senderId,
1016
+ campaignId,
1017
+ columnId,
1018
+ ].join(":"),
713
1019
  rereadAfter: "get_refill_target_plan",
714
1020
  reason: stringValue(paidInmail?.reason) ??
715
1021
  "MCP paid InMail sender-credit facts require refresh.",
@@ -755,6 +1061,7 @@ function sanitizeStructuredSenderPlans(value, selectedBySender) {
755
1061
  senderName: stringValue(plan.senderName) ?? "",
756
1062
  status: stringValue(plan.status) ?? "needs_refill",
757
1063
  selectedLane: allowedActionType(plan.selectedLane),
1064
+ ...sanitizePhase98RefillContract(plan),
758
1065
  horizon: isRecord(plan.horizon)
759
1066
  ? {
760
1067
  selectedDays: Array.isArray(plan.horizon.selectedDays)
@@ -814,6 +1121,149 @@ function buildSanitizedGlobalActionQueue(senderRefillPlans) {
814
1121
  rank: index + 1,
815
1122
  }));
816
1123
  }
1124
+ function sortedUniqueStrings(value) {
1125
+ return [...new Set(stringArray(value))].sort();
1126
+ }
1127
+ function sameStrings(left, right) {
1128
+ return (left.length === right.length &&
1129
+ left.every((value, index) => value === right[index]));
1130
+ }
1131
+ function sanitizeSchedulerSweepSourceScopes(value) {
1132
+ if (!Array.isArray(value))
1133
+ return [];
1134
+ const scopes = value.flatMap((entry) => {
1135
+ if (!isRecord(entry))
1136
+ return [];
1137
+ const campaignId = stringValue(entry.campaignId)?.trim();
1138
+ const tableId = stringValue(entry.tableId)?.trim();
1139
+ const sourceLeadListId = stringValue(entry.sourceLeadListId)?.trim();
1140
+ const leadSourceProvider = stringValue(entry.leadSourceProvider)?.trim();
1141
+ if (!campaignId || !tableId || !sourceLeadListId || !leadSourceProvider) {
1142
+ return [];
1143
+ }
1144
+ return [{ campaignId, tableId, sourceLeadListId, leadSourceProvider }];
1145
+ });
1146
+ const unique = new Map(scopes.map((scope) => [
1147
+ [
1148
+ scope.campaignId,
1149
+ scope.tableId,
1150
+ scope.sourceLeadListId,
1151
+ scope.leadSourceProvider,
1152
+ ].join(":"),
1153
+ scope,
1154
+ ]));
1155
+ return [...unique.values()].sort((a, b) => {
1156
+ if (a.campaignId !== b.campaignId) {
1157
+ return a.campaignId.localeCompare(b.campaignId);
1158
+ }
1159
+ return a.tableId.localeCompare(b.tableId);
1160
+ });
1161
+ }
1162
+ function sanitizeTargetSchedulerSweep(params) {
1163
+ if (!Array.isArray(params.value) ||
1164
+ !params.expectedWorkspaceId ||
1165
+ !params.expectedTargetDate ||
1166
+ !/^\d{4}-\d{2}-\d{2}$/.test(params.expectedTargetDate) ||
1167
+ !params.expectedTargetShapeRevision ||
1168
+ !params.expectedStateRevision ||
1169
+ params.remainingProjectedGap <= 0 ||
1170
+ params.remainingReadyOrProjectedGap !== 0) {
1171
+ return null;
1172
+ }
1173
+ const expectedSenderIds = [...params.selectedBySender.keys()].sort();
1174
+ const expectedActionTypes = [
1175
+ ...new Set(params.selectedBySender.values()),
1176
+ ].sort();
1177
+ if (expectedSenderIds.length === 0 || expectedActionTypes.length === 0) {
1178
+ return null;
1179
+ }
1180
+ for (const rawAction of params.value) {
1181
+ if (!isRecord(rawAction) || rawAction.type !== "run_scheduler_sweep") {
1182
+ continue;
1183
+ }
1184
+ const input = isRecord(rawAction.toolInput) ? rawAction.toolInput : {};
1185
+ const actionKey = stringValue(rawAction.actionKey)?.trim();
1186
+ const requestKey = stringValue(input.requestKey)?.trim();
1187
+ const senderIds = sortedUniqueStrings(input.senderIds);
1188
+ const actionTypes = sortedUniqueStrings(input.actionTypes).filter((actionType) => allowedActionType(actionType) === actionType);
1189
+ const campaignIds = sortedUniqueStrings(input.campaignIds);
1190
+ const tableIds = sortedUniqueStrings(input.tableIds);
1191
+ const sourceScopes = sanitizeSchedulerSweepSourceScopes(input.sourceScopes);
1192
+ const receiptRequirements = isRecord(rawAction.receiptRequirements)
1193
+ ? rawAction.receiptRequirements
1194
+ : {};
1195
+ const groupBy = stringArray(receiptRequirements.groupBy);
1196
+ const allowedSideEffects = stringArray(rawAction.allowedSideEffects);
1197
+ const forbiddenSideEffects = stringArray(rawAction.forbiddenSideEffects);
1198
+ const rawActionType = allowedActionType(rawAction.actionType);
1199
+ if (input.action !== "run" ||
1200
+ input.workspaceId !== params.expectedWorkspaceId ||
1201
+ input.targetDate !== params.expectedTargetDate ||
1202
+ input.targetDateKey !== params.expectedTargetDate ||
1203
+ input.targetShapeRevision !== params.expectedTargetShapeRevision ||
1204
+ input.stateRevision !== params.expectedStateRevision ||
1205
+ !actionKey ||
1206
+ requestKey !== actionKey ||
1207
+ !sameStrings(senderIds, expectedSenderIds) ||
1208
+ !sameStrings(actionTypes, expectedActionTypes) ||
1209
+ (expectedActionTypes.length === 1 &&
1210
+ rawActionType !== expectedActionTypes[0]) ||
1211
+ campaignIds.length === 0 ||
1212
+ tableIds.length === 0 ||
1213
+ sourceScopes.length === 0 ||
1214
+ sourceScopes.some((scope) => !campaignIds.includes(scope.campaignId) ||
1215
+ !tableIds.includes(scope.tableId)) ||
1216
+ rawAction.toolName !== "run_scheduler_sweep" ||
1217
+ rawAction.sideEffectClass !== "scheduler_placement" ||
1218
+ rawAction.workspaceWide !== true ||
1219
+ rawAction.yoloEligible !== true ||
1220
+ receiptRequirements.nonTruncated !== true ||
1221
+ !sameStrings(groupBy, SCHEDULER_SWEEP_RECEIPT_GROUPS) ||
1222
+ !sameStrings(allowedSideEffects, SCHEDULER_SWEEP_ALLOWED_SIDE_EFFECTS) ||
1223
+ !sameStrings(forbiddenSideEffects, SCHEDULER_SWEEP_FORBIDDEN_SIDE_EFFECTS)) {
1224
+ continue;
1225
+ }
1226
+ return {
1227
+ rank: 1,
1228
+ type: "run_scheduler_sweep",
1229
+ actionType: rawActionType,
1230
+ toolName: "run_scheduler_sweep",
1231
+ sideEffectClass: "scheduler_placement",
1232
+ ids: {},
1233
+ toolInput: {
1234
+ action: "run",
1235
+ workspaceId: params.expectedWorkspaceId,
1236
+ targetDate: params.expectedTargetDate,
1237
+ targetDateKey: params.expectedTargetDate,
1238
+ senderIds,
1239
+ actionTypes,
1240
+ campaignIds,
1241
+ tableIds,
1242
+ sourceScopes,
1243
+ targetShapeRevision: params.expectedTargetShapeRevision,
1244
+ stateRevision: params.expectedStateRevision,
1245
+ requestKey,
1246
+ },
1247
+ inputSummary: stringValue(rawAction.inputSummary) ?? "",
1248
+ reason: stringValue(rawAction.reason) ?? "",
1249
+ prerequisites: stringArray(rawAction.prerequisites),
1250
+ rereadAfter: rawAction.rereadAfter === "get_refill_target_plan"
1251
+ ? "get_refill_target_plan"
1252
+ : null,
1253
+ stopCondition: stringValue(rawAction.stopCondition) ?? "",
1254
+ yoloEligible: true,
1255
+ workspaceWide: true,
1256
+ allowedSideEffects: [...SCHEDULER_SWEEP_ALLOWED_SIDE_EFFECTS],
1257
+ forbiddenSideEffects: [...SCHEDULER_SWEEP_FORBIDDEN_SIDE_EFFECTS],
1258
+ receiptRequirements: {
1259
+ nonTruncated: true,
1260
+ groupBy: [...SCHEDULER_SWEEP_RECEIPT_GROUPS],
1261
+ },
1262
+ actionKey,
1263
+ };
1264
+ }
1265
+ return null;
1266
+ }
817
1267
  function paidRefreshNeededSenderIds(senderRefillPlans) {
818
1268
  const senderIds = new Set();
819
1269
  for (const plan of senderRefillPlans) {
@@ -935,11 +1385,17 @@ function sanitizeRefillTargetPlanResult(result) {
935
1385
  const actionTypes = [...new Set(selectedBySender.values())];
936
1386
  if (selectedKeys.size === 0)
937
1387
  return emptyUnsupportedResult(result);
938
- const senderPlans = rawSenderPlans.filter((plan) => isRecord(plan) &&
939
- selectedKeys.has(`${String(plan.senderId)}:${String(plan.actionType)}`));
1388
+ const senderPlans = rawSenderPlans
1389
+ .filter((plan) => isRecord(plan) &&
1390
+ selectedKeys.has(`${String(plan.senderId)}:${String(plan.actionType)}`))
1391
+ .map(sanitizeSenderPlan)
1392
+ .filter((plan) => Boolean(plan));
940
1393
  const selectedDays = Array.isArray(target.selectedDays)
941
- ? target.selectedDays.filter((day) => isRecord(day) &&
1394
+ ? target.selectedDays
1395
+ .filter((day) => isRecord(day) &&
942
1396
  selectedKeys.has(`${String(day.senderId)}:${String(day.actionType)}`))
1397
+ .map(sanitizeSelectedDay)
1398
+ .filter((day) => Boolean(day))
943
1399
  : [];
944
1400
  const actionSelections = rawSelections
945
1401
  .filter((selection) => isRecord(selection) &&
@@ -947,20 +1403,22 @@ function sanitizeRefillTargetPlanResult(result) {
947
1403
  selectedBySender.has(selection.senderId))
948
1404
  .map((selection) => {
949
1405
  const actionType = selectedBySender.get(selection.senderId);
950
- const evidence = Array.isArray(selection.evidence)
951
- ? selection.evidence.filter((item) => isRecord(item) && item.actionType === actionType)
952
- : [];
1406
+ const sanitized = sanitizeActionSelection(selection);
1407
+ if (!sanitized)
1408
+ return null;
1409
+ const evidence = sanitized.evidence.filter((item) => item.actionType === actionType);
953
1410
  return {
954
- ...selection,
1411
+ ...sanitized,
955
1412
  actionTypes: actionType ? [actionType] : [],
956
1413
  evidence,
957
1414
  };
958
- });
1415
+ })
1416
+ .filter((selection) => Boolean(selection));
959
1417
  const grossTarget = senderPlans.reduce((sum, plan) => sum + numberValue(plan.grossTarget), 0);
960
1418
  const requestedTarget = senderPlans.reduce((sum, plan) => sum + numberValue(plan.requestedTarget), 0);
961
1419
  const effectiveTarget = senderPlans.reduce((sum, plan) => {
962
- const target = typeof plan.schedulerCapacityTarget === "number"
963
- ? plan.schedulerCapacityTarget
1420
+ const target = typeof plan.effectiveTarget === "number"
1421
+ ? plan.effectiveTarget
964
1422
  : numberValue(plan.grossTarget);
965
1423
  return sum + target;
966
1424
  }, 0);
@@ -975,7 +1433,20 @@ function sanitizeRefillTargetPlanResult(result) {
975
1433
  const senderRefillPlans = sanitizeStructuredSenderPlans(target.senderRefillPlans, selectedBySender);
976
1434
  const paidRefreshNeededSenderIdSet = paidRefreshNeededSenderIds(senderRefillPlans);
977
1435
  const blockers = sanitizeBlockers(result.blockers, selectedKeys, paidRefreshNeededSenderIdSet);
978
- const globalActionQueue = buildSanitizedGlobalActionQueue(senderRefillPlans);
1436
+ const request = isRecord(result.request) ? result.request : {};
1437
+ const schedulerSweepAction = sanitizeTargetSchedulerSweep({
1438
+ value: target.globalActionQueue,
1439
+ selectedBySender,
1440
+ expectedWorkspaceId: stringValue(target.workspaceId),
1441
+ expectedTargetDate: stringValue(request.targetDate),
1442
+ expectedTargetShapeRevision: stringValue(result.targetShapeRevision),
1443
+ expectedStateRevision: stringValue(result.stateRevision),
1444
+ remainingProjectedGap,
1445
+ remainingReadyOrProjectedGap,
1446
+ });
1447
+ const globalActionQueue = schedulerSweepAction
1448
+ ? [schedulerSweepAction]
1449
+ : buildSanitizedGlobalActionQueue(senderRefillPlans);
979
1450
  const hasTerminalNoActionBlocker = globalActionQueue.length === 0 &&
980
1451
  blockers.some((blocker) => TERMINAL_NO_ACTION_BLOCKER_CODES.has(String(blocker.code)));
981
1452
  const status = remainingProjectedGap === 0
@@ -1012,6 +1483,7 @@ function sanitizeRefillTargetPlanResult(result) {
1012
1483
  remainingProjectedGap,
1013
1484
  remainingReadyOrProjectedGap,
1014
1485
  remainingReadyOrScheduledGap: remainingReadyOrProjectedGap,
1486
+ ...sanitizePhase98RefillContract(target),
1015
1487
  },
1016
1488
  coverage: isRecord(result.coverage)
1017
1489
  ? {
@@ -1029,7 +1501,7 @@ function sanitizeRefillTargetPlanResult(result) {
1029
1501
  export const refillTargetPlanToolDefinitions = [
1030
1502
  {
1031
1503
  name: "get_refill_target_plan",
1032
- description: "read-only refill target planner to call before any refill mutation. It identifies eligible senders and infers one implicit refill lane per sender. For unified Sales Nav cascade campaigns, the public refill lane is the paid-InMail cascade target (send_inmail_closed) with campaign classification sales_nav_cascade; the same campaign can still route prospects to Open InMail or connection-request fallback based on row eligibility and fresh paid credits >= 5. Non-cascade lanes remain 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 remain follow-up sequence actions, not refill target lanes. By default the planner computes the scheduler-forward 48-hour target window, selected sender-local days whose sending windows overlap that window, gross target, actual sent coverage, scheduler-owned scheduled coverage, projected coverage (sent + scheduled), ready buffer, remaining projected gap, paid InMail credit/threshold/freshness feasibility, same-campaign connection fallback availability, 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; loaded, awaiting scheduler targets use read-only wait/reread until projected coverage fills.",
1504
+ description: "read-only refill target planner to call before any refill mutation. It identifies eligible senders and infers one implicit refill lane per sender. For unified Sales Nav cascade campaigns with campaign classification sales_nav_cascade, Open InMail, paid InMail, and connection fallback satisfy one sales_nav_cascade goal while branch metrics remain visible; row routing still uses fresh paid credits >= 5 and reports same-campaign connection fallback availability. Campaign and sender names stay primary labels and stable IDs remain execution proof. Non-cascade lanes remain 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 remain follow-up sequence actions, not refill target lanes. By default the planner computes the scheduler-forward 48-hour target window, selected sender-local days whose sending windows overlap that window, raw coverage, projected coverage (sent + scheduled), goal-capped projected coverage, ready buffer, remaining projected gap, allocatable preparation deficit, unallocatable supply/deficit, paid InMail credit/threshold/freshness feasibility, bounded action candidates, and targetRevision drift proof. A calibrated small gap may return one scheduler_first_settle read-only receipt with a stable key/deadline before bounded preparation. 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; loaded, awaiting scheduler targets use read-only wait/reread until projected coverage fills.",
1033
1505
  inputSchema: {
1034
1506
  type: "object",
1035
1507
  properties: {