@sellable/mcp 0.1.553 → 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.
@@ -1,4 +1,6 @@
1
1
  import { actionIds, classifyPaidInmailRefreshReceipt, collectPaidInmailRefreshActions, executeOneYoloPrimitive, firstGlobalAction, normalizeStrings, numberValue, recordValue, refreshPaidInmailCreditsWithRetry, stringValue, } from "./refill-executors.js";
2
+ import { prepareRefillSendsV2Approval, refillSendsV2Command, } from "./refill-sends-v2.js";
3
+ import { REFILL_CONTRACT_CACHE_VERSION, REFILL_CONTRACT_HASH, REFILL_CONTRACT_VERSION, renderRefillBootstrapGuidance, renderRefillSafetyGuidance, renderRefillTerminalGuidance, } from "../refill-contract.js";
2
4
  import { getRefillTargetPlan } from "./refill-target-plan.js";
3
5
  import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
4
6
  const REFILL_SEND_ACTION_TYPES = ["send_invite", "send_inmail_closed"];
@@ -94,7 +96,16 @@ function normalizeRefillSendsInput(input) {
94
96
  export const refillSendsToolDefinitions = [
95
97
  {
96
98
  name: "refill_sends",
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.",
99
+ description: [
100
+ "Public durable refill entrypoint. It preserves explicit workspace/date/selectors, renders the normal bounded packet even under yolo, executes one approved primitive, records a typed receipt, rereads, and replans.",
101
+ "An exact-date request-scoped run_scheduler_sweep may schedule eligible workspace cells through product gates; it never sends directly or raw-writes scheduler fields.",
102
+ `contractVersion=${REFILL_CONTRACT_VERSION}`,
103
+ `cacheVersion=${REFILL_CONTRACT_CACHE_VERSION}`,
104
+ `contractHash=${REFILL_CONTRACT_HASH}`,
105
+ renderRefillBootstrapGuidance(),
106
+ renderRefillSafetyGuidance(),
107
+ renderRefillTerminalGuidance(),
108
+ ].join(" "),
98
109
  inputSchema: {
99
110
  type: "object",
100
111
  properties: {
@@ -180,6 +191,14 @@ export const refillSendsToolDefinitions = [
180
191
  type: "string",
181
192
  description: "Optional actionKey from the approved packet's first global action. A mismatch returns approval_scope_changed before any refresh or mutation.",
182
193
  },
194
+ runId: { type: "string" },
195
+ fence: { type: "number" },
196
+ scopeHash: { type: "string" },
197
+ targetShapeRevision: { type: "string" },
198
+ actionFingerprint: { type: "string" },
199
+ expectedTargetsHash: { type: "string" },
200
+ approvalExpiresAt: { type: "string" },
201
+ approvalFingerprint: { type: "string" },
183
202
  },
184
203
  required: [],
185
204
  additionalProperties: false,
@@ -450,6 +469,51 @@ function paidRefreshOptionsFromApprovalPacket(packet) {
450
469
  maxStalenessSeconds: packet.maxStalenessSeconds,
451
470
  };
452
471
  }
472
+ function durableV2PlanEligible(plan) {
473
+ const root = recordValue(plan);
474
+ const target = recordValue(root?.target);
475
+ const senderPlans = Array.isArray(target?.senderRefillPlans)
476
+ ? target.senderRefillPlans
477
+ : [];
478
+ return Boolean(stringValue(root?.targetShapeRevision) &&
479
+ senderPlans.some((entry) => {
480
+ const senderPlan = recordValue(entry);
481
+ return Array.isArray(senderPlan?.laneLedgers);
482
+ }));
483
+ }
484
+ function durableApprovalEcho(input) {
485
+ const values = {
486
+ scopeHash: stringValue(input.scopeHash),
487
+ targetShapeRevision: stringValue(input.targetShapeRevision),
488
+ actionFingerprint: stringValue(input.actionFingerprint),
489
+ expectedTargetsHash: stringValue(input.expectedTargetsHash),
490
+ approvalExpiresAt: stringValue(input.approvalExpiresAt),
491
+ approvalFingerprint: stringValue(input.approvalFingerprint),
492
+ };
493
+ return Object.values(values).every(Boolean)
494
+ ? values
495
+ : null;
496
+ }
497
+ function durableYoloBaseResult(params) {
498
+ return {
499
+ ...params.command,
500
+ targetPlan: params.targetPlan,
501
+ targetPlanBeforePaidRefresh: null,
502
+ targetPlanBeforeYoloPrimitive: params.targetPlan,
503
+ autoPaidInmailRefresh: {
504
+ enabled: false,
505
+ status: "skipped_awaiting_durable_approval",
506
+ refreshedPaidInmailSenderIds: [],
507
+ failedPaidInmailRefreshes: [],
508
+ refreshReceipts: [],
509
+ attemptedSenderIds: [],
510
+ targetPlanReread: false,
511
+ workspaceId: params.workspaceId,
512
+ workspaceResolution: "explicit",
513
+ note: "Durable v2 binds approval before credit refresh or any refill primitive.",
514
+ },
515
+ };
516
+ }
453
517
  export async function executeRefillSendsCommand(input = {}) {
454
518
  const normalizedInput = normalizeRefillSendsInput(input);
455
519
  const yolo = normalizedInput.yolo === true;
@@ -486,6 +550,7 @@ export async function executeRefillSendsCommand(input = {}) {
486
550
  status: "not_run_without_yolo",
487
551
  refreshedPaidInmailSenderIds: [],
488
552
  failedPaidInmailRefreshes: [],
553
+ refreshReceipts: [],
489
554
  note: "Automatic paid InMail credit refresh requires --yolo, scheduled mode, or an explicit workspaceId so the request can refresh exact sender facts safely.",
490
555
  },
491
556
  yoloExecution: {
@@ -513,6 +578,7 @@ export async function executeRefillSendsCommand(input = {}) {
513
578
  status: "skipped_approval_scope_changed",
514
579
  refreshedPaidInmailSenderIds: [],
515
580
  failedPaidInmailRefreshes: [],
581
+ refreshReceipts: [],
516
582
  attemptedSenderIds: [],
517
583
  targetPlanReread: false,
518
584
  workspaceId,
@@ -527,6 +593,73 @@ export async function executeRefillSendsCommand(input = {}) {
527
593
  },
528
594
  };
529
595
  }
596
+ if (yolo &&
597
+ workspaceId &&
598
+ durableV2PlanEligible(targetPlanBeforePaidRefresh)) {
599
+ const base = durableYoloBaseResult({
600
+ command,
601
+ targetPlan: targetPlanBeforePaidRefresh,
602
+ workspaceId,
603
+ });
604
+ const echo = durableApprovalEcho(scopedInput);
605
+ if (!scopedInput.approvalFingerprint) {
606
+ const approval = await prepareRefillSendsV2Approval({
607
+ workspaceId,
608
+ intent: scopedInput.intent,
609
+ senderIds: normalizeStrings(scopedInput.senderIds),
610
+ approvalMode: scopedInput.approvalMode ?? "approve",
611
+ targetDate: scopedInput.targetDate ?? undefined,
612
+ untilDate: scopedInput.untilDate ?? undefined,
613
+ horizonSendDays: scopedInput.horizonSendDays,
614
+ targetPlan: targetPlanBeforePaidRefresh,
615
+ });
616
+ return {
617
+ ...base,
618
+ yoloExecution: {
619
+ enabled: true,
620
+ ...approval,
621
+ selectedAction: firstGlobalAction(targetPlanBeforePaidRefresh),
622
+ targetPlanReread: false,
623
+ },
624
+ };
625
+ }
626
+ if (!echo || !scopedInput.runId || typeof scopedInput.fence !== "number") {
627
+ return {
628
+ ...base,
629
+ ok: false,
630
+ code: "approval_scope_changed",
631
+ yoloExecution: {
632
+ enabled: true,
633
+ status: "approval_scope_changed",
634
+ blocker: "approval_echo_incomplete",
635
+ selectedAction: firstGlobalAction(targetPlanBeforePaidRefresh),
636
+ targetPlanReread: false,
637
+ },
638
+ };
639
+ }
640
+ const durable = await refillSendsV2Command({
641
+ workspaceId,
642
+ runId: scopedInput.runId,
643
+ fence: scopedInput.fence,
644
+ intent: scopedInput.intent,
645
+ senderIds: normalizeStrings(scopedInput.senderIds),
646
+ approvalMode: scopedInput.approvalMode ?? "approve",
647
+ targetDate: scopedInput.targetDate ?? undefined,
648
+ untilDate: scopedInput.untilDate ?? undefined,
649
+ horizonSendDays: scopedInput.horizonSendDays,
650
+ targetPlan: targetPlanBeforePaidRefresh,
651
+ approvalEcho: echo,
652
+ });
653
+ return {
654
+ ...base,
655
+ yoloExecution: {
656
+ enabled: true,
657
+ ...durable,
658
+ selectedAction: firstGlobalAction(targetPlanBeforePaidRefresh),
659
+ targetPlanReread: true,
660
+ },
661
+ };
662
+ }
530
663
  const refreshActions = collectPaidInmailRefreshActions(targetPlanBeforePaidRefresh);
531
664
  const refreshedPaidInmailSenderIds = [];
532
665
  const failedPaidInmailRefreshes = [];
@@ -11,6 +11,7 @@ const SAFE_PACKET_ACTION_TYPES = new Set([
11
11
  "start_paused_campaign",
12
12
  "rerun_errored_cells",
13
13
  "run_scheduler_sweep",
14
+ "scheduler_first_settle",
14
15
  "wait_for_scheduler",
15
16
  "wait_for_active_work",
16
17
  "wait_for_source_import",
@@ -84,11 +85,7 @@ const SCHEDULER_SWEEP_FORBIDDEN_SIDE_EFFECTS = [
84
85
  "change sender limits",
85
86
  "change source selection",
86
87
  ];
87
- const SCHEDULER_SWEEP_RECEIPT_GROUPS = [
88
- "campaignId",
89
- "tableId",
90
- "actionType",
91
- ];
88
+ const SCHEDULER_SWEEP_RECEIPT_GROUPS = ["campaignId", "tableId", "actionType"];
92
89
  async function postRefillTargetPlan(body, workspaceId) {
93
90
  const api = getApi();
94
91
  const requestOptions = workspaceRequestOptions(workspaceId);
@@ -215,14 +212,289 @@ function senderActionKey(senderId, actionType) {
215
212
  function sanitizeCounts(counts, selectedKeys) {
216
213
  if (!Array.isArray(counts))
217
214
  return [];
218
- return counts.filter((item) => {
215
+ return counts
216
+ .filter((item) => {
219
217
  if (!isRecord(item))
220
218
  return false;
221
219
  const actionType = allowedActionType(item.actionType);
222
220
  const key = senderActionKey(item.senderId, actionType);
223
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] : [];
224
366
  });
225
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
+ }
226
498
  function sanitizeActionCandidates(candidates, selectedKeys, paidRefreshNeededSenderIds, status, remainingReadyOrProjectedGap, remainingProjectedGap) {
227
499
  const filtered = Array.isArray(candidates)
228
500
  ? candidates
@@ -293,6 +565,7 @@ function sanitizeActionCandidate(candidate) {
293
565
  ? "get_refill_target_plan"
294
566
  : undefined,
295
567
  reason: stringValue(candidate.reason),
568
+ ...sanitizePhase98RefillContract(candidate),
296
569
  };
297
570
  }
298
571
  function sanitizeRowSelector(value) {
@@ -481,6 +754,16 @@ function sanitizeToolInput(value) {
481
754
  wait: sanitizeWait(value.wait),
482
755
  refillReceipt: sanitizeRefillReceipt(value.refillReceipt),
483
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,
484
767
  };
485
768
  }
486
769
  function sanitizePacketIds(value) {
@@ -534,6 +817,7 @@ function sanitizeStructuredAction(action, selectedBySender, mode, rank) {
534
817
  actionKey: stringValue(action.actionKey),
535
818
  capSaturated: booleanValue(action.capSaturated) === true ? true : undefined,
536
819
  prepFailureDiagnosis: sanitizePrepFailureDiagnosis(action.prepFailureDiagnosis),
820
+ ...sanitizePhase98RefillContract(action),
537
821
  };
538
822
  }
539
823
  function sanitizeLedgerBlockers(value) {
@@ -726,7 +1010,12 @@ function paidInmailRefreshActionForSenderPlan(plan, paidInmail) {
726
1010
  availableCredits: numberValue(paidInmail?.availableCredits),
727
1011
  oldThreshold: numberValue(paidInmail?.threshold),
728
1012
  maxStalenessSeconds: MCP_PAID_INMAIL_CREDITS_MAX_STALENESS_SECONDS,
729
- actionKey: ["refresh_paid_inmail_credits", senderId, campaignId, columnId].join(":"),
1013
+ actionKey: [
1014
+ "refresh_paid_inmail_credits",
1015
+ senderId,
1016
+ campaignId,
1017
+ columnId,
1018
+ ].join(":"),
730
1019
  rereadAfter: "get_refill_target_plan",
731
1020
  reason: stringValue(paidInmail?.reason) ??
732
1021
  "MCP paid InMail sender-credit facts require refresh.",
@@ -772,6 +1061,7 @@ function sanitizeStructuredSenderPlans(value, selectedBySender) {
772
1061
  senderName: stringValue(plan.senderName) ?? "",
773
1062
  status: stringValue(plan.status) ?? "needs_refill",
774
1063
  selectedLane: allowedActionType(plan.selectedLane),
1064
+ ...sanitizePhase98RefillContract(plan),
775
1065
  horizon: isRecord(plan.horizon)
776
1066
  ? {
777
1067
  selectedDays: Array.isArray(plan.horizon.selectedDays)
@@ -848,15 +1138,10 @@ function sanitizeSchedulerSweepSourceScopes(value) {
848
1138
  const tableId = stringValue(entry.tableId)?.trim();
849
1139
  const sourceLeadListId = stringValue(entry.sourceLeadListId)?.trim();
850
1140
  const leadSourceProvider = stringValue(entry.leadSourceProvider)?.trim();
851
- if (!campaignId ||
852
- !tableId ||
853
- !sourceLeadListId ||
854
- !leadSourceProvider) {
1141
+ if (!campaignId || !tableId || !sourceLeadListId || !leadSourceProvider) {
855
1142
  return [];
856
1143
  }
857
- return [
858
- { campaignId, tableId, sourceLeadListId, leadSourceProvider },
859
- ];
1144
+ return [{ campaignId, tableId, sourceLeadListId, leadSourceProvider }];
860
1145
  });
861
1146
  const unique = new Map(scopes.map((scope) => [
862
1147
  [
@@ -1100,11 +1385,17 @@ function sanitizeRefillTargetPlanResult(result) {
1100
1385
  const actionTypes = [...new Set(selectedBySender.values())];
1101
1386
  if (selectedKeys.size === 0)
1102
1387
  return emptyUnsupportedResult(result);
1103
- const senderPlans = rawSenderPlans.filter((plan) => isRecord(plan) &&
1104
- 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));
1105
1393
  const selectedDays = Array.isArray(target.selectedDays)
1106
- ? target.selectedDays.filter((day) => isRecord(day) &&
1394
+ ? target.selectedDays
1395
+ .filter((day) => isRecord(day) &&
1107
1396
  selectedKeys.has(`${String(day.senderId)}:${String(day.actionType)}`))
1397
+ .map(sanitizeSelectedDay)
1398
+ .filter((day) => Boolean(day))
1108
1399
  : [];
1109
1400
  const actionSelections = rawSelections
1110
1401
  .filter((selection) => isRecord(selection) &&
@@ -1112,15 +1403,17 @@ function sanitizeRefillTargetPlanResult(result) {
1112
1403
  selectedBySender.has(selection.senderId))
1113
1404
  .map((selection) => {
1114
1405
  const actionType = selectedBySender.get(selection.senderId);
1115
- const evidence = Array.isArray(selection.evidence)
1116
- ? selection.evidence.filter((item) => isRecord(item) && item.actionType === actionType)
1117
- : [];
1406
+ const sanitized = sanitizeActionSelection(selection);
1407
+ if (!sanitized)
1408
+ return null;
1409
+ const evidence = sanitized.evidence.filter((item) => item.actionType === actionType);
1118
1410
  return {
1119
- ...selection,
1411
+ ...sanitized,
1120
1412
  actionTypes: actionType ? [actionType] : [],
1121
1413
  evidence,
1122
1414
  };
1123
- });
1415
+ })
1416
+ .filter((selection) => Boolean(selection));
1124
1417
  const grossTarget = senderPlans.reduce((sum, plan) => sum + numberValue(plan.grossTarget), 0);
1125
1418
  const requestedTarget = senderPlans.reduce((sum, plan) => sum + numberValue(plan.requestedTarget), 0);
1126
1419
  const effectiveTarget = senderPlans.reduce((sum, plan) => {
@@ -1190,6 +1483,7 @@ function sanitizeRefillTargetPlanResult(result) {
1190
1483
  remainingProjectedGap,
1191
1484
  remainingReadyOrProjectedGap,
1192
1485
  remainingReadyOrScheduledGap: remainingReadyOrProjectedGap,
1486
+ ...sanitizePhase98RefillContract(target),
1193
1487
  },
1194
1488
  coverage: isRecord(result.coverage)
1195
1489
  ? {
@@ -1207,7 +1501,7 @@ function sanitizeRefillTargetPlanResult(result) {
1207
1501
  export const refillTargetPlanToolDefinitions = [
1208
1502
  {
1209
1503
  name: "get_refill_target_plan",
1210
- 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.",
1211
1505
  inputSchema: {
1212
1506
  type: "object",
1213
1507
  properties: {