@sellable/mcp 0.1.533 → 0.1.535

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/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
@@ -1,6 +1,6 @@
1
1
  import { SellableApiError } from "./api.js";
2
2
  import { buildRunStateFromLocalHints } from "./tools/evergreen-refill-plan.js";
3
- import { prepareRowSelectorValue as defaultPrepareRowSelectorValue, refillPrepareRequestHash as defaultRefillPrepareRequestHash, } from "./tools/refill-executors.js";
3
+ import { classifyPaidInmailRefreshReceipt, prepareRowSelectorValue as defaultPrepareRowSelectorValue, refillPrepareRequestHash as defaultRefillPrepareRequestHash, } from "./tools/refill-executors.js";
4
4
  const DEFAULT_BUDGETS = {
5
5
  maxGateCyclesPerInvocation: 8,
6
6
  pollIntervalMs: 15_000,
@@ -19,34 +19,6 @@ const REFILL_DONE_REASONS = new Set([
19
19
  "not_an_evergreen_workspace",
20
20
  "no_refillable_campaigns",
21
21
  ]);
22
- const SCHEDULER_RUN_ENVELOPE_STATUSES = new Set([
23
- "ran",
24
- "attached",
25
- "backoff",
26
- "window_closed_noop",
27
- "failed",
28
- ]);
29
- const SCHEDULER_RUN_RECEIPT_STATUSES = new Set([
30
- "ran",
31
- "window_closed_noop",
32
- "failed",
33
- ]);
34
- const SCHEDULER_RUN_SKIP_REASONS = [
35
- "window_closed",
36
- "daily_limit",
37
- "cooldown",
38
- "sender_gate",
39
- "credit_threshold",
40
- "billing_blocked",
41
- "duplicate_lead",
42
- "no_senders",
43
- "other",
44
- ];
45
- const SCHEDULER_RUN_HARD_BLOCK_REASONS = new Set([
46
- "billing_blocked",
47
- "credit_threshold",
48
- "cooldown",
49
- ]);
50
22
  function isRecord(value) {
51
23
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
52
24
  }
@@ -68,89 +40,6 @@ function arrayValue(value) {
68
40
  function hasBlocker(value, blocker) {
69
41
  return isRecord(value) && value.blocker === blocker;
70
42
  }
71
- function schedulerRunSkipReasons(value) {
72
- const raw = recordValue(value) ?? {};
73
- const normalized = {};
74
- for (const reason of SCHEDULER_RUN_SKIP_REASONS) {
75
- normalized[reason] = numberValue(raw[reason]) ?? 0;
76
- }
77
- return normalized;
78
- }
79
- function dominantSchedulerRunSkipReason(skipReasons) {
80
- let dominant = null;
81
- let dominantCount = 0;
82
- for (const reason of SCHEDULER_RUN_SKIP_REASONS) {
83
- const count = skipReasons[reason] ?? 0;
84
- if (count > dominantCount) {
85
- dominant = reason;
86
- dominantCount = count;
87
- }
88
- }
89
- return dominantCount > 0 ? dominant : null;
90
- }
91
- function sanitizeSchedulerRunReceipt(raw) {
92
- const envelope = recordValue(raw);
93
- if (!envelope)
94
- return null;
95
- const status = stringValue(envelope.status);
96
- if (!status || !SCHEDULER_RUN_ENVELOPE_STATUSES.has(status))
97
- return null;
98
- const retryAfterMs = envelope.retryAfterMs == null ? null : numberValue(envelope.retryAfterMs);
99
- if (envelope.retryAfterMs != null && retryAfterMs == null)
100
- return null;
101
- const receiptInput = envelope.receipt;
102
- let receipt = null;
103
- let dominantSkipReason = null;
104
- if (receiptInput != null) {
105
- const rawReceipt = recordValue(receiptInput);
106
- if (!rawReceipt)
107
- return null;
108
- const receiptStatus = stringValue(rawReceipt.status);
109
- if (!receiptStatus || !SCHEDULER_RUN_RECEIPT_STATUSES.has(receiptStatus)) {
110
- return null;
111
- }
112
- const cellsConsidered = numberValue(rawReceipt.cellsConsidered);
113
- const cellsScheduled = numberValue(rawReceipt.cellsScheduled);
114
- const cellsSkipped = numberValue(rawReceipt.cellsSkipped);
115
- const cellsDeferred = numberValue(rawReceipt.cellsDeferred);
116
- const tablesFilteredForNoCapacity = numberValue(rawReceipt.tablesFilteredForNoCapacity);
117
- if (cellsConsidered == null ||
118
- cellsScheduled == null ||
119
- cellsSkipped == null ||
120
- cellsDeferred == null ||
121
- tablesFilteredForNoCapacity == null) {
122
- return null;
123
- }
124
- const skipReasons = schedulerRunSkipReasons(rawReceipt.skipReasons);
125
- dominantSkipReason = dominantSchedulerRunSkipReason(skipReasons);
126
- receipt = {
127
- status: receiptStatus,
128
- cellsConsidered,
129
- cellsScheduled,
130
- cellsSkipped,
131
- cellsDeferred,
132
- tablesFilteredForNoCapacity,
133
- skipReasons,
134
- };
135
- }
136
- return {
137
- status,
138
- retryAfterMs,
139
- receipt,
140
- dominantSkipReason,
141
- hardBlocked: dominantSkipReason != null &&
142
- SCHEDULER_RUN_HARD_BLOCK_REASONS.has(dominantSkipReason),
143
- };
144
- }
145
- function schedulerRunReceiptIsFreshZeroScheduled(schedulerRunReceipt) {
146
- const status = stringValue(schedulerRunReceipt?.status);
147
- const fresh = status === "ran" || status === "window_closed_noop";
148
- if (!fresh)
149
- return false;
150
- const receipt = recordValue(schedulerRunReceipt?.receipt);
151
- return (status === "window_closed_noop" ||
152
- numberValue(receipt?.cellsScheduled) === 0);
153
- }
154
43
  function isLeaseLost(value) {
155
44
  return hasBlocker(value, "lease_lost");
156
45
  }
@@ -587,22 +476,31 @@ async function gateBootstrap(input, deps, ctx) {
587
476
  const refreshedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.senderIds)
588
477
  .map((entry) => stringValue(entry))
589
478
  .filter((entry) => Boolean(entry)));
479
+ const attemptedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.attemptedSenderIds)
480
+ .map((entry) => stringValue(entry))
481
+ .filter((entry) => Boolean(entry)));
590
482
  const receipts = [];
591
483
  for (const entry of paidCredit) {
592
484
  const senderId = stringValue(entry.senderId);
593
485
  if (!senderId ||
594
486
  !entry.plannedJitRefresh ||
595
- refreshedSenderIds.has(senderId)) {
487
+ refreshedSenderIds.has(senderId) ||
488
+ attemptedSenderIds.has(senderId)) {
596
489
  continue;
597
490
  }
598
491
  const receipt = await deps.executors.refreshPaidInmailCreditsWithRetry(senderId, input.workspaceId);
599
- receipts.push({ senderId, receipt });
600
- refreshedSenderIds.add(senderId);
492
+ attemptedSenderIds.add(senderId);
493
+ const classification = classifyPaidInmailRefreshReceipt(receipt.receipt);
494
+ receipts.push({ senderId, receipt, classification });
495
+ if (classification.usableCurrentFacts) {
496
+ refreshedSenderIds.add(senderId);
497
+ }
601
498
  }
602
499
  const nextRunState = mergeRunState(ctx.runState, {
603
500
  creditTrust: {
604
501
  refreshedAt: (deps.now?.() ?? new Date()).toISOString(),
605
502
  senderIds: [...refreshedSenderIds],
503
+ attemptedSenderIds: [...attemptedSenderIds],
606
504
  receipts,
607
505
  },
608
506
  });
@@ -1124,34 +1022,42 @@ async function verifySchedulerWait(input, deps, ctx, action, budgets) {
1124
1022
  const enteredAt = stringValue(progress.schedulerWaitEnteredAt) ??
1125
1023
  (deps.now?.() ?? new Date()).toISOString();
1126
1024
  const jitFired = booleanValue(progress.schedulerJitFired) ?? false;
1127
- let schedulerRunReceipt = recordValue(progress.schedulerRunReceipt) ?? null;
1128
1025
  if (!jitFired) {
1129
1026
  const senderId = actionSenderId(action);
1130
1027
  if (senderId) {
1131
- await deps.executors.refreshPaidInmailCreditsWithRetry(senderId, input.workspaceId);
1028
+ const refresh = await deps.executors.refreshPaidInmailCreditsWithRetry(senderId, input.workspaceId);
1029
+ const classification = classifyPaidInmailRefreshReceipt(refresh.receipt);
1030
+ if (!classification.usableCurrentFacts) {
1031
+ return {
1032
+ status: "blocked",
1033
+ blocker: "paid_inmail_refresh_failed",
1034
+ runId: ctx.runId,
1035
+ fence: ctx.fence,
1036
+ gate: ctx.gate,
1037
+ guidance: "Paid InMail credit refresh did not return usable current facts, so scheduler JIT was not requested.",
1038
+ report: {
1039
+ senderId,
1040
+ receiptStatus: classification.status,
1041
+ error: classification.error,
1042
+ receipt: refresh.receipt,
1043
+ attempts: refresh.attempts,
1044
+ errors: refresh.errors,
1045
+ },
1046
+ journalPath: ctx.journalPath,
1047
+ };
1048
+ }
1132
1049
  }
1133
1050
  if (deps.requestSchedulerRun) {
1134
- try {
1135
- schedulerRunReceipt = sanitizeSchedulerRunReceipt(await deps.requestSchedulerRun(input.workspaceId));
1136
- }
1137
- catch {
1138
- schedulerRunReceipt = null;
1139
- }
1051
+ await deps.requestSchedulerRun(input.workspaceId);
1140
1052
  }
1141
1053
  ctx.runState = mergeRunState(ctx.runState, {
1142
1054
  progress: mergeProgress(ctx, {
1143
1055
  schedulerWaitEnteredAt: enteredAt,
1144
1056
  schedulerJitFired: true,
1145
- ...(schedulerRunReceipt ? { schedulerRunReceipt } : {}),
1146
1057
  }),
1147
1058
  });
1148
1059
  }
1149
- const zeroScheduledFresh = schedulerRunReceiptIsFreshZeroScheduled(schedulerRunReceipt);
1150
- // EDGE-2: the on-demand run only executes placement; cron can still process
1151
- // due/timed-out cells later. We still do one readback, then avoid burning the
1152
- // full wait budget when the fresh receipt says nothing was placeable now.
1153
- const readbackBudget = zeroScheduledFresh ? 1 : budgets.maxSchedulerReadbacks;
1154
- for (let poll = 0; poll < readbackBudget; poll += 1) {
1060
+ for (let poll = 0; poll < budgets.maxSchedulerReadbacks; poll += 1) {
1155
1061
  const fresh = await deps.readPlan({
1156
1062
  workspaceId: input.workspaceId,
1157
1063
  intent: input.intent,
package/dist/server.js CHANGED
@@ -45,7 +45,6 @@ import { allTools } from "./tools/registry.js";
45
45
  import { getRows, getTableRows, getTableRowsMinimal } from "./tools/rows.js";
46
46
  import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
47
47
  import { getSchedulerFillCapacity } from "./tools/scheduler-fill-capacity.js";
48
- import { runSchedulerSweep } from "./tools/scheduler-run.js";
49
48
  import { getSenderRoutingTool, setSenderRoutingTool, } from "./tools/sender-routing.js";
50
49
  import { getSender, listSenders, refreshPaidInmailCredits, } from "./tools/senders.js";
51
50
  import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from "./tools/sequencer.js";
@@ -236,9 +235,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
236
235
  case "get_scheduler_fill_capacity":
237
236
  result = await getSchedulerFillCapacity(args);
238
237
  break;
239
- case "run_scheduler_sweep":
240
- result = await runSchedulerSweep(args);
241
- break;
242
238
  case "refill_sends":
243
239
  result = await executeRefillSendsCommand(args);
244
240
  break;
@@ -14,6 +14,12 @@ export type PaidInmailCreditRefreshOptions = {
14
14
  maxStalenessSeconds?: number | null;
15
15
  paidInmailCreditsMaxStalenessSeconds?: number | null;
16
16
  };
17
+ export type PaidInmailRefreshReceiptClassification = {
18
+ usableCurrentFacts: boolean;
19
+ status: string;
20
+ error: string | null;
21
+ refreshed: boolean;
22
+ };
17
23
  export type YoloPrimitiveExecution = {
18
24
  enabled: true;
19
25
  status: "no_action" | "read_only_reread" | "executed_and_reread" | "refused" | "skipped_after_paid_refresh";
@@ -135,6 +141,7 @@ export declare function userAddedRowsLimitPayloadFromError(error: unknown): User
135
141
  export declare function sourceImportInProgressPayloadFromError(error: unknown): SourceImportInProgressPayload | null;
136
142
  export declare function paidRefreshActionFrom(value: unknown): PaidInmailRefreshAction | null;
137
143
  export declare function collectPaidInmailRefreshActions(plan: unknown): PaidInmailRefreshAction[];
144
+ export declare function classifyPaidInmailRefreshReceipt(value: unknown): PaidInmailRefreshReceiptClassification;
138
145
  export declare function firstGlobalAction(plan: unknown): Record<string, unknown> | null;
139
146
  export declare function actionIds(action: Record<string, unknown>): Record<string, unknown>;
140
147
  export declare function actionToolInput(action: Record<string, unknown>): Record<string, unknown>;
@@ -10,6 +10,11 @@ const PAID_INMAIL_REFRESH_RETRY_DELAY_MS = Number(process.env.SELLABLE_MCP_PAID_
10
10
  const SIGNAL_DISCOVERY_MIN_REFILL_POSTS = 3;
11
11
  const SIGNAL_DISCOVERY_MAX_REFILL_POSTS = 10;
12
12
  const SIGNAL_DISCOVERY_TARGET_ROWS_PER_POST = 150;
13
+ const USABLE_PAID_INMAIL_REFRESH_RECEIPT_STATUSES = new Set([
14
+ "fresh",
15
+ "refreshed",
16
+ "below_threshold",
17
+ ]);
13
18
  export function normalizeStrings(values) {
14
19
  if (!Array.isArray(values))
15
20
  return [];
@@ -206,6 +211,23 @@ export function collectPaidInmailRefreshActions(plan) {
206
211
  add(action);
207
212
  return [...bySender.values()];
208
213
  }
214
+ export function classifyPaidInmailRefreshReceipt(value) {
215
+ const response = recordValue(value);
216
+ const nestedReceipt = recordValue(response?.receipt);
217
+ const status = stringValue(nestedReceipt?.status) ??
218
+ stringValue(response?.status) ??
219
+ (response?.refreshed === true ? "refreshed" : "not_refreshed");
220
+ const refreshed = response?.refreshed === true || nestedReceipt?.refreshed === true;
221
+ const usableCurrentFacts = refreshed || USABLE_PAID_INMAIL_REFRESH_RECEIPT_STATUSES.has(status);
222
+ return {
223
+ usableCurrentFacts,
224
+ status,
225
+ refreshed,
226
+ error: stringValue(response?.error) ??
227
+ stringValue(nestedReceipt?.error) ??
228
+ (usableCurrentFacts ? null : "paid InMail credit refresh did not return usable current facts"),
229
+ };
230
+ }
209
231
  export function firstGlobalAction(plan) {
210
232
  const root = recordValue(plan);
211
233
  const target = recordValue(root?.target);
@@ -5,7 +5,6 @@ import { runRefillV2Loop } from "../refill-run-loop.js";
5
5
  import { getPrepareCampaignMessagesStatus } from "./campaign-message-preparation.js";
6
6
  import { getRefillPlanV2 } from "./evergreen-refill-plan.js";
7
7
  import { executeOneYoloPrimitive, executeStartCampaignPrimitive, prepareRowSelectorValue, refillPrepareRequestHash, refreshPaidInmailCreditsWithRetry, } from "./refill-executors.js";
8
- import { runSchedulerSweep } from "./scheduler-run.js";
9
8
  const FORBIDDEN_ACTIONS = [
10
9
  "Do not schedule sends.",
11
10
  "Do not send messages.",
@@ -153,7 +152,6 @@ export async function refillSendsV2Command(input) {
153
152
  localState: {
154
153
  writeRefillWorkspaceState,
155
154
  },
156
- requestSchedulerRun: (workspaceId) => runSchedulerSweep({ workspaceId, action: "run" }),
157
155
  });
158
156
  return {
159
157
  ...(await maybeAddLostFenceGuidance(result, { ...input, workspaceId })),
@@ -292,6 +292,8 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
292
292
  attempts?: number;
293
293
  error: string;
294
294
  errors?: string[];
295
+ receiptStatus?: string;
296
+ receipt?: unknown;
295
297
  }[];
296
298
  refreshReceipts: {
297
299
  senderId: string;
@@ -301,6 +303,8 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
301
303
  approvalSource: string;
302
304
  approvalPacket: PaidInmailRefreshApprovalPacket;
303
305
  receipt: import("./senders.js").RefreshPaidInmailCreditsResponse;
306
+ receiptStatus: string;
307
+ usableCurrentFacts: boolean;
304
308
  }[];
305
309
  attemptedSenderIds: string[];
306
310
  targetPlanReread: boolean;
@@ -309,6 +313,14 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
309
313
  note: string;
310
314
  };
311
315
  yoloExecution: {
316
+ enabled: false;
317
+ status: "not_run_without_yolo";
318
+ selectedAction: null;
319
+ targetPlanReread: false;
320
+ result?: undefined;
321
+ refusalReason?: undefined;
322
+ postActionFirstAction?: undefined;
323
+ } | {
312
324
  enabled: true;
313
325
  status: "skipped_after_paid_refresh";
314
326
  selectedAction: null;
@@ -1,4 +1,4 @@
1
- import { actionIds, collectPaidInmailRefreshActions, executeOneYoloPrimitive, firstGlobalAction, normalizeStrings, numberValue, recordValue, refreshPaidInmailCreditsWithRetry, stringValue, } from "./refill-executors.js";
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
4
  const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
@@ -231,7 +231,7 @@ export function refillSendsCommand(input = {}) {
231
231
  "Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
232
232
  "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
233
  "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 --yolo, this tool automatically maintains a run-local refreshedPaidInmailSenderIds set: when the first target plan returns refresh_paid_inmail_credits candidates for selected paid-InMail lanes, it refreshes each exact sender at most once, reruns get_refill_target_plan, and returns the post-refresh targetPlan before any prep/source-copy/bounded-approval/read-only wait action is chosen.",
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 wait action is chosen.",
235
235
  "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
236
  "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.",
237
237
  "Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh and the post-refresh targetPlan.",
@@ -374,7 +374,11 @@ export async function executeRefillSendsCommand(input = {}) {
374
374
  ? { ...input, workspaceId: workspaceContext.context.workspaceId }
375
375
  : input;
376
376
  const command = refillSendsCommand(scopedInput);
377
- if (!yolo) {
377
+ const workspaceId = workspaceContext && workspaceContext.ok
378
+ ? workspaceContext.context.workspaceId
379
+ : normalizeExplicitWorkspaceId(scopedInput.workspaceId);
380
+ const canRefreshPaidInmailFacts = yolo || executionMode === "scheduled" || Boolean(workspaceId);
381
+ if (!canRefreshPaidInmailFacts) {
378
382
  return {
379
383
  ...command,
380
384
  autoPaidInmailRefresh: {
@@ -382,7 +386,7 @@ export async function executeRefillSendsCommand(input = {}) {
382
386
  status: "not_run_without_yolo",
383
387
  refreshedPaidInmailSenderIds: [],
384
388
  failedPaidInmailRefreshes: [],
385
- note: "Automatic paid InMail credit refresh only runs for --yolo refill_sends calls.",
389
+ note: "Automatic paid InMail credit refresh requires --yolo, scheduled mode, or an explicit workspaceId so the request can refresh exact sender facts safely.",
386
390
  },
387
391
  yoloExecution: {
388
392
  enabled: false,
@@ -392,9 +396,6 @@ export async function executeRefillSendsCommand(input = {}) {
392
396
  },
393
397
  };
394
398
  }
395
- const workspaceId = workspaceContext && workspaceContext.ok
396
- ? workspaceContext.context.workspaceId
397
- : normalizeExplicitWorkspaceId(scopedInput.workspaceId);
398
399
  const targetPlanInput = targetPlanInputFor(scopedInput);
399
400
  const targetPlanBeforePaidRefresh = await getRefillTargetPlan(targetPlanInput);
400
401
  const refreshActions = collectPaidInmailRefreshActions(targetPlanBeforePaidRefresh);
@@ -420,7 +421,7 @@ export async function executeRefillSendsCommand(input = {}) {
420
421
  }
421
422
  const refreshResult = await refreshPaidInmailCreditsWithRetry(action.senderId, workspaceId, paidRefreshOptionsFromApprovalPacket(approvalPacket));
422
423
  if (refreshResult.receipt) {
423
- refreshedPaidInmailSenderIds.push(action.senderId);
424
+ const classification = classifyPaidInmailRefreshReceipt(refreshResult.receipt);
424
425
  refreshReceipts.push({
425
426
  senderId: action.senderId,
426
427
  actionKey: action.actionKey ?? null,
@@ -429,7 +430,23 @@ export async function executeRefillSendsCommand(input = {}) {
429
430
  approvalSource: "explicit_yolo_flag",
430
431
  approvalPacket,
431
432
  receipt: refreshResult.receipt,
433
+ receiptStatus: classification.status,
434
+ usableCurrentFacts: classification.usableCurrentFacts,
432
435
  });
436
+ if (classification.usableCurrentFacts) {
437
+ refreshedPaidInmailSenderIds.push(action.senderId);
438
+ }
439
+ else {
440
+ failedPaidInmailRefreshes.push({
441
+ senderId: action.senderId,
442
+ attempts: refreshResult.attempts,
443
+ error: classification.error ??
444
+ "paid InMail credit refresh did not return usable current facts",
445
+ errors: refreshResult.errors,
446
+ receiptStatus: classification.status,
447
+ receipt: refreshResult.receipt,
448
+ });
449
+ }
433
450
  }
434
451
  else {
435
452
  failedPaidInmailRefreshes.push({
@@ -444,8 +461,8 @@ export async function executeRefillSendsCommand(input = {}) {
444
461
  const targetPlan = refreshActions.length > 0
445
462
  ? await getRefillTargetPlan(targetPlanInput)
446
463
  : targetPlanBeforePaidRefresh;
447
- const selectedAction = refreshActions.length > 0 ? null : firstGlobalAction(targetPlan);
448
- const primitiveAttempt = refreshActions.length > 0
464
+ const selectedAction = yolo && refreshActions.length === 0 ? firstGlobalAction(targetPlan) : null;
465
+ const primitiveAttempt = !yolo || refreshActions.length > 0
449
466
  ? null
450
467
  : await executeOneYoloPrimitive(selectedAction, workspaceId ?? undefined);
451
468
  const shouldRereadAfterPrimitive = primitiveAttempt?.status === "executed_and_reread" ||
@@ -458,7 +475,9 @@ export async function executeRefillSendsCommand(input = {}) {
458
475
  ...command,
459
476
  targetPlan: finalTargetPlan,
460
477
  targetPlanBeforePaidRefresh: refreshActions.length > 0 ? targetPlanBeforePaidRefresh : null,
461
- targetPlanBeforeYoloPrimitive: refreshActions.length === 0 && postActionTargetPlan ? targetPlan : null,
478
+ targetPlanBeforeYoloPrimitive: yolo && refreshActions.length === 0 && postActionTargetPlan
479
+ ? targetPlan
480
+ : null,
462
481
  autoPaidInmailRefresh: {
463
482
  enabled: true,
464
483
  status: refreshActions.length === 0
@@ -477,23 +496,30 @@ export async function executeRefillSendsCommand(input = {}) {
477
496
  ? "refill_sends refreshed stale paid InMail credit facts internally with bounded retries, once per sender, then returned the post-refresh targetPlan."
478
497
  : "No stale or missing paid InMail credit refresh candidates were present in the initial target plan.",
479
498
  },
480
- yoloExecution: refreshActions.length > 0
499
+ yoloExecution: !yolo
481
500
  ? {
482
- enabled: true,
483
- status: "skipped_after_paid_refresh",
501
+ enabled: false,
502
+ status: "not_run_without_yolo",
484
503
  selectedAction: null,
485
- targetPlanReread: true,
504
+ targetPlanReread: false,
486
505
  }
487
- : {
488
- enabled: true,
489
- status: primitiveAttempt?.status ?? "no_action",
490
- selectedAction,
491
- result: primitiveAttempt?.result,
492
- targetPlanReread: shouldRereadAfterPrimitive,
493
- refusalReason: primitiveAttempt?.refusalReason,
494
- postActionFirstAction: postActionTargetPlan
495
- ? firstGlobalAction(postActionTargetPlan)
496
- : null,
497
- },
506
+ : refreshActions.length > 0
507
+ ? {
508
+ enabled: true,
509
+ status: "skipped_after_paid_refresh",
510
+ selectedAction: null,
511
+ targetPlanReread: true,
512
+ }
513
+ : {
514
+ enabled: true,
515
+ status: primitiveAttempt?.status ?? "no_action",
516
+ selectedAction,
517
+ result: primitiveAttempt?.result,
518
+ targetPlanReread: shouldRereadAfterPrimitive,
519
+ refusalReason: primitiveAttempt?.refusalReason,
520
+ postActionFirstAction: postActionTargetPlan
521
+ ? firstGlobalAction(postActionTargetPlan)
522
+ : null,
523
+ },
498
524
  };
499
525
  }
@@ -7370,25 +7370,6 @@ export declare const allTools: ({
7370
7370
  };
7371
7371
  required: string[];
7372
7372
  };
7373
- } | {
7374
- name: string;
7375
- description: string;
7376
- inputSchema: {
7377
- type: string;
7378
- properties: {
7379
- workspaceId: {
7380
- type: string;
7381
- description: string;
7382
- };
7383
- action: {
7384
- type: string;
7385
- enum: string[];
7386
- description: string;
7387
- };
7388
- };
7389
- required: string[];
7390
- additionalProperties: boolean;
7391
- };
7392
7373
  } | {
7393
7374
  name: string;
7394
7375
  description: string;
@@ -40,7 +40,6 @@ import { refillTargetPlanToolDefinitions } from "./refill-target-plan.js";
40
40
  import { rowToolDefinitions } from "./rows.js";
41
41
  import { rubricToolDefinitions } from "./rubrics.js";
42
42
  import { schedulerFillCapacityToolDefinitions } from "./scheduler-fill-capacity.js";
43
- import { schedulerRunToolDefinitions } from "./scheduler-run.js";
44
43
  import { senderRoutingToolDefinitions } from "./sender-routing.js";
45
44
  import { senderToolDefinitions } from "./senders.js";
46
45
  import { sequencerToolDefinitions } from "./sequencer.js";
@@ -58,7 +57,6 @@ export const allTools = [
58
57
  ...refillPlanV2ToolDefinitions,
59
58
  ...refillTargetPlanToolDefinitions,
60
59
  ...schedulerFillCapacityToolDefinitions,
61
- ...schedulerRunToolDefinitions,
62
60
  ...refillSendsToolDefinitions,
63
61
  ...refillSendsV2ToolDefinitions,
64
62
  ...setupEvergreenCampaignsToolDefinitions,
@@ -52,16 +52,22 @@ export type PaidInmailCreditStatus = {
52
52
  };
53
53
  export type RefreshPaidInmailCreditsResponse = {
54
54
  senderId: string;
55
- refreshed: true;
55
+ refreshed: boolean;
56
+ error?: string | null;
56
57
  credits: PaidInmailCreditStatus;
57
- receipt?: unknown;
58
+ receipt?: {
59
+ status?: string;
60
+ refreshed?: boolean;
61
+ error?: string;
62
+ [key: string]: unknown;
63
+ } | null;
58
64
  sideEffects: {
59
- refreshedLinkedInDerivedCreditFacts: true;
60
- updatedSenderCreditCache: true;
61
- campaignMutation: false;
62
- schedulerMutation: false;
63
- thresholdMutation: false;
64
- sendMutation: false;
65
+ refreshedLinkedInDerivedCreditFacts: boolean;
66
+ updatedSenderCreditCache: boolean;
67
+ campaignMutation: boolean;
68
+ schedulerMutation: boolean;
69
+ thresholdMutation: boolean;
70
+ sendMutation: boolean;
65
71
  };
66
72
  };
67
73
  export declare const senderToolDefinitions: ({
@@ -237,6 +237,21 @@ function normalizeCreditStatus(raw) {
237
237
  needsRefresh: raw?.needsRefresh === true,
238
238
  };
239
239
  }
240
+ function normalizeRefreshReceipt(raw) {
241
+ if (!raw || typeof raw !== "object")
242
+ return null;
243
+ return raw;
244
+ }
245
+ function normalizeCreditRefreshSideEffects(raw, receiptRefreshed) {
246
+ return {
247
+ refreshedLinkedInDerivedCreditFacts: raw?.refreshedLinkedInDerivedCreditFacts === true || receiptRefreshed,
248
+ updatedSenderCreditCache: raw?.updatedSenderCreditCache === true || receiptRefreshed,
249
+ campaignMutation: raw?.campaignMutation === true,
250
+ schedulerMutation: raw?.schedulerMutation === true,
251
+ thresholdMutation: raw?.thresholdMutation === true,
252
+ sendMutation: raw?.sendMutation === true,
253
+ };
254
+ }
240
255
  export async function refreshPaidInmailCredits(input) {
241
256
  const senderId = input.senderId?.trim();
242
257
  if (!senderId) {
@@ -268,18 +283,14 @@ export async function refreshPaidInmailCredits(input) {
268
283
  if (threshold !== null)
269
284
  body.threshold = threshold;
270
285
  const result = await api.post(`/api/v3/mcp/senders/${encodeURIComponent(senderId)}/refresh-inmail-credits`, body, requestOptions);
286
+ const receipt = normalizeRefreshReceipt(result?.receipt);
287
+ const receiptRefreshed = result?.refreshed === true || receipt?.refreshed === true;
271
288
  return {
272
289
  senderId,
273
- refreshed: true,
290
+ refreshed: receiptRefreshed,
291
+ error: pickString(result?.error, receipt?.error),
274
292
  credits: normalizeCreditStatus(result?.credits),
275
- receipt: result?.receipt,
276
- sideEffects: {
277
- refreshedLinkedInDerivedCreditFacts: true,
278
- updatedSenderCreditCache: true,
279
- campaignMutation: false,
280
- schedulerMutation: false,
281
- thresholdMutation: false,
282
- sendMutation: false,
283
- },
293
+ receipt,
294
+ sideEffects: normalizeCreditRefreshSideEffects(result?.sideEffects, receiptRefreshed),
284
295
  };
285
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.533",
3
+ "version": "0.1.535",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -6,7 +6,6 @@ allowed-tools:
6
6
  - mcp__sellable__refill_sends
7
7
  - mcp__sellable__get_refill_target_plan
8
8
  - mcp__sellable__get_scheduler_fill_capacity
9
- - mcp__sellable__run_scheduler_sweep
10
9
  - mcp__sellable__refresh_paid_inmail_credits
11
10
  - mcp__sellable__get_subskill_asset
12
11
  - mcp__sellable__get_auth_status
@@ -115,11 +114,10 @@ or install-time workspace mapping. Pass `workspaceId` on every scheduled or
115
114
  `--yolo` refill tool call, including setup/read calls such as
116
115
  `refill_sends`, `get_refill_target_plan`, `list_senders`,
117
116
  `get_sender_routing`, `resolve_campaign_fill_route`,
118
- `get_campaign_refill_state`, `get_scheduler_fill_capacity`,
119
- `run_scheduler_sweep`, and any later refill mutation covered by the packet.
120
- Missing `workspaceId` in scheduled or `--yolo` mode is a blocker; stop with
121
- `WORKSPACE_REQUIRED` instead of running against an implicit or guessed
122
- workspace.
117
+ `get_campaign_refill_state`, `get_scheduler_fill_capacity`, and any later
118
+ refill mutation covered by the packet. Missing `workspaceId` in scheduled or
119
+ `--yolo` mode is a blocker; stop with `WORKSPACE_REQUIRED` instead of running
120
+ against an implicit or guessed workspace.
123
121
 
124
122
  Do not solve scheduled or `--yolo` workspace uncertainty by changing the shared
125
123
  active workspace. Manual interactive workspace switching remains a separate
@@ -271,10 +269,6 @@ need raw proof, call the read-only `get_scheduler_fill_capacity` query for the
271
269
  same sender/action/date; it tells the MCP how many cells the product scheduler
272
270
  will try to place and does not import, approve, schedule, refresh credits, or
273
271
  mutate.
274
- When the refill loop has ready rows and needs scheduler pickup now, use
275
- `run_scheduler_sweep` with the same explicit `workspaceId`; it can place cells
276
- within existing scheduler gates and returns the receipt, but it never sends or
277
- bypasses limits.
278
272
  If the target plan is complete by projected coverage, report that the selected
279
273
  target is already filled and no-op without asking for approval. If the ready
280
274
  buffer covers the projected gap, paid InMail credit facts are fresh for every
@@ -6,7 +6,6 @@ allowed-tools:
6
6
  - mcp__sellable__get_subskill_asset
7
7
  - mcp__sellable__get_refill_target_plan
8
8
  - mcp__sellable__get_scheduler_fill_capacity
9
- - mcp__sellable__run_scheduler_sweep
10
9
  - mcp__sellable__refresh_paid_inmail_credits
11
10
  - mcp__sellable__list_senders
12
11
  - mcp__sellable__get_sender_routing
@@ -68,12 +67,11 @@ request-scoped `workspaceId`. Pass that same `workspaceId` on every refill tool
68
67
  call in this workflow: `get_refill_target_plan`, `list_senders`,
69
68
  `get_sender_routing`, `resolve_campaign_fill_route`,
70
69
  `get_campaign_refill_state`, `get_scheduler_fill_capacity`,
71
- `run_scheduler_sweep`, `refresh_paid_inmail_credits`, source import/readiness
72
- calls, preparation calls, approval calls, and campaign start calls. Missing
73
- `workspaceId` in scheduled or `--yolo` mode is a blocker; return or report
74
- `WORKSPACE_REQUIRED` instead of falling back to shared config state. Manual
75
- interactive workspace switching is diagnostic setup only and is not an
76
- automation control path.
70
+ `refresh_paid_inmail_credits`, source import/readiness calls, preparation calls,
71
+ approval calls, and campaign start calls. Missing `workspaceId` in scheduled or
72
+ `--yolo` mode is a blocker; return or report `WORKSPACE_REQUIRED` instead of
73
+ falling back to shared config state. Manual interactive workspace switching is
74
+ diagnostic setup only and is not an automation control path.
77
75
 
78
76
  Goal-mode continuation: a skill cannot create or invoke `/goal` by itself. When
79
77
  this workflow is already running inside an active Codex goal, keep that goal
@@ -188,10 +186,6 @@ files or memory.
188
186
  how many cells the product scheduler will try to place for that sender; it
189
187
  does not create rows, import, approve, schedule, refresh paid-InMail credits,
190
188
  or mutate thresholds.
191
- When ready rows exist and the wait is for scheduler pickup, call
192
- `run_scheduler_sweep` with the same explicit `workspaceId` to request the
193
- product scheduler placement pass now and read its receipt. This may place
194
- cells within existing gates, never sends messages, and never bypasses limits.
195
189
  If `status:"complete"`, report the target, selected dates, sent count,
196
190
  scheduled count, projected count, campaign ids, and no-op proof without
197
191
  asking for approval or mutating.
@@ -1,28 +0,0 @@
1
- type RefillSendsEvergreenInput = {
2
- workspaceId?: string;
3
- };
4
- export declare const refillSendsEvergreenToolDefinitions: {
5
- name: string;
6
- description: string;
7
- inputSchema: {
8
- type: string;
9
- properties: {
10
- workspaceId: {
11
- type: string;
12
- description: string;
13
- };
14
- };
15
- required: string[];
16
- additionalProperties: boolean;
17
- };
18
- }[];
19
- export declare function refillSendsEvergreenCommand(input: RefillSendsEvergreenInput): {
20
- readOnly: boolean;
21
- workspaceId: string | null;
22
- firstOperationalSteps: string[];
23
- approvalContract: string;
24
- forbiddenActions: string[];
25
- fillWindow: string;
26
- hostExamples: string[];
27
- };
28
- export {};
@@ -1,47 +0,0 @@
1
- export const refillSendsEvergreenToolDefinitions = [
2
- {
3
- name: "refill_sends_evergreen",
4
- description: "Read-only Phase 85 evergreen refill command contract. It performs no mutations and only tells the operator to call get_evergreen_refill_plan for a dry-run packet and journal.",
5
- inputSchema: {
6
- type: "object",
7
- properties: {
8
- workspaceId: {
9
- type: "string",
10
- description: "Explicit request-scoped workspace id.",
11
- },
12
- },
13
- required: ["workspaceId"],
14
- additionalProperties: false,
15
- },
16
- },
17
- ];
18
- export function refillSendsEvergreenCommand(input) {
19
- return {
20
- readOnly: true,
21
- workspaceId: input.workspaceId ?? null,
22
- firstOperationalSteps: [
23
- "Call get_evergreen_refill_plan with the explicit workspaceId.",
24
- "Read the returned packet, globalActionQueue, per-sender plans, and itinerary before taking any action.",
25
- "Review the dry-run journal file path returned by get_evergreen_refill_plan.",
26
- "Phase 85 is PLAN-ONLY; execution arrives in Phase 86.",
27
- ],
28
- approvalContract: "Nothing is approved or executable in Phase 85. The evergreen command is read-only; Phase 86 introduces execution approval.",
29
- forbiddenActions: [
30
- "Do not schedule sends.",
31
- "Do not send messages.",
32
- "Do not approve messages.",
33
- "Do not prepare messages.",
34
- "Do not start or launch campaigns.",
35
- "Do not create campaigns.",
36
- "Do not switch providers or source families.",
37
- "Do not lower paid InMail thresholds.",
38
- "Do not refresh paid InMail credits.",
39
- "Do not write scheduler fields.",
40
- ],
41
- fillWindow: "Use only the target window and caps returned by get_evergreen_refill_plan.",
42
- hostExamples: [
43
- "refill_sends_evergreen({ workspaceId })",
44
- "get_evergreen_refill_plan({ workspaceId })",
45
- ],
46
- };
47
- }
@@ -1,27 +0,0 @@
1
- type SchedulerRunAction = "run" | "status";
2
- type RunSchedulerSweepInput = {
3
- workspaceId: string;
4
- action?: SchedulerRunAction;
5
- };
6
- export declare const schedulerRunToolDefinitions: {
7
- name: string;
8
- description: string;
9
- inputSchema: {
10
- type: string;
11
- properties: {
12
- workspaceId: {
13
- type: string;
14
- description: string;
15
- };
16
- action: {
17
- type: string;
18
- enum: string[];
19
- description: string;
20
- };
21
- };
22
- required: string[];
23
- additionalProperties: boolean;
24
- };
25
- }[];
26
- export declare function runSchedulerSweep(input: RunSchedulerSweepInput): Promise<unknown>;
27
- export {};
@@ -1,45 +0,0 @@
1
- import { getApi } from "../api.js";
2
- import { normalizeExplicitWorkspaceId, workspaceRequestOptions, } from "./workspace-context.js";
3
- async function postSchedulerRun(body, workspaceId) {
4
- const api = getApi();
5
- const requestOptions = workspaceRequestOptions(workspaceId);
6
- return requestOptions
7
- ? api.post("/api/v3/mcp/scheduler-run", body, requestOptions)
8
- : api.post("/api/v3/mcp/scheduler-run", body);
9
- }
10
- export const schedulerRunToolDefinitions = [
11
- {
12
- name: "run_scheduler_sweep",
13
- description: 'Trigger the product scheduler placement sweep for one explicit workspace now, or read the last on-demand scheduler run status with action "status". A run returns a synchronous envelope with status ran, attached, backoff, window_closed_noop, or failed; retryAfterMs for backoff replays; and a receipt with cellsConsidered, cellsScheduled, cellsSkipped, cellsDeferred, deterministic skipReasons, and tablesFilteredForNoCapacity. It places cells within existing scheduler gates only: it can move placement earlier, but it cannot bypass sending windows, daily limits, cooldowns, sender gates, billing, or credit thresholds, and it never sends messages directly. Repeated calls inside the backoff window replay the last receipt verbatim. Status is read-only and scoped to the last on-demand run only; cron sweeps are not recorded here.',
14
- inputSchema: {
15
- type: "object",
16
- properties: {
17
- workspaceId: {
18
- type: "string",
19
- description: "Explicit request-scoped workspace id for scheduled/yolo refill automation. Pass this instead of switching the shared active workspace.",
20
- },
21
- action: {
22
- type: "string",
23
- enum: ["run", "status"],
24
- description: 'Use "run" to trigger a placement sweep now. Use "status" for a read-only view of the last on-demand run. Defaults to "run".',
25
- },
26
- },
27
- required: ["workspaceId"],
28
- additionalProperties: false,
29
- },
30
- },
31
- ];
32
- export async function runSchedulerSweep(input) {
33
- const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
34
- if (!workspaceId) {
35
- throw new Error("workspaceId is required for run_scheduler_sweep.");
36
- }
37
- const action = input.action ?? "run";
38
- if (action !== "run" && action !== "status") {
39
- throw new Error('action must be "run" or "status" for run_scheduler_sweep.');
40
- }
41
- return postSchedulerRun({
42
- workspaceId,
43
- action,
44
- }, workspaceId);
45
- }