@sellable/mcp 0.1.520 → 0.1.521

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.
@@ -365,6 +365,79 @@ export function refillPrepareRequestHash(params) {
365
365
  : "no-row-selector",
366
366
  ].join(":");
367
367
  }
368
+ function prepReceiptFromResult(result) {
369
+ const root = recordValue(result);
370
+ if (!root)
371
+ return null;
372
+ const status = recordValue(root.status);
373
+ const progress = recordValue(root.progress) ?? recordValue(status?.progress);
374
+ return (recordValue(root.prepReceipt) ??
375
+ recordValue(progress?.receipt) ??
376
+ recordValue(root.receipt));
377
+ }
378
+ function normalizePrepareMessagesResult(result) {
379
+ const receipt = prepReceiptFromResult(result);
380
+ const root = recordValue(result);
381
+ if (!receipt || !root)
382
+ return result;
383
+ const world = recordValue(receipt.world);
384
+ const batchStop = stringValue(receipt.batchStop);
385
+ const brokenByReason = recordValue(world?.brokenByReason) ?? {};
386
+ const approvalCandidates = numberValue(world?.approvalCandidates) ?? 0;
387
+ const hasMoreFrontierRows = world?.hasMoreFrontierRows === true;
388
+ const stuckActiveCells = Array.isArray(world?.stuckActiveCells)
389
+ ? world.stuckActiveCells
390
+ : [];
391
+ const approvedNotDispatched = Array.isArray(world?.approvedNotDispatched)
392
+ ? world.approvedNotDispatched
393
+ : [];
394
+ return {
395
+ ...root,
396
+ prepReceipt: receipt,
397
+ exhaustionEvidence: {
398
+ clean: batchStop === "frontier_exhausted" &&
399
+ hasMoreFrontierRows === false &&
400
+ stuckActiveCells.length === 0 &&
401
+ approvedNotDispatched.every((entry) => {
402
+ const row = recordValue(entry);
403
+ return row?.terminal === true || approvedNotDispatched.length === 0;
404
+ }),
405
+ batchStop,
406
+ hasMoreFrontierRows,
407
+ approvalCandidates,
408
+ brokenByReason,
409
+ stuckActiveCells,
410
+ approvedNotDispatched,
411
+ },
412
+ };
413
+ }
414
+ function waitReceiptFromAction(action) {
415
+ const toolInput = actionToolInput(action);
416
+ const wait = recordValue(toolInput.wait) ??
417
+ recordValue(action.wait) ??
418
+ recordValue(recordValue(toolInput.refillReceipt)?.wait);
419
+ if (!wait)
420
+ return null;
421
+ const deadlineAt = stringValue(wait.deadlineAt);
422
+ const onExpiry = stringValue(wait.onExpiry);
423
+ if (!deadlineAt || !onExpiry)
424
+ return null;
425
+ return { deadlineAt, onExpiry };
426
+ }
427
+ function waitResultForAction(action) {
428
+ const wait = waitReceiptFromAction(action);
429
+ if (!wait)
430
+ return { waited: true };
431
+ const deadlineMs = Date.parse(wait.deadlineAt);
432
+ if (Number.isFinite(deadlineMs) && deadlineMs <= Date.now()) {
433
+ return {
434
+ waited: false,
435
+ reason: "wait_deadline_expired",
436
+ wait,
437
+ };
438
+ }
439
+ return { waited: true, wait };
440
+ }
368
441
  export function boundedApprovalLimit(action) {
369
442
  const toolInput = actionToolInput(action);
370
443
  const rowSelector = recordValue(toolInput.rowSelector);
@@ -673,7 +746,7 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
673
746
  case "wait_for_scheduler":
674
747
  case "wait_for_active_work":
675
748
  case "wait_for_source_import":
676
- return { status: "read_only_reread", result: { waited: true } };
749
+ return { status: "read_only_reread", result: waitResultForAction(action) };
677
750
  case "prepare_messages": {
678
751
  const campaignId = actionCampaignId(action);
679
752
  const tableId = actionTableId(action) ?? undefined;
@@ -712,7 +785,10 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
712
785
  }),
713
786
  requestSource: "refill_sends",
714
787
  });
715
- return { status: "executed_and_reread", result };
788
+ return {
789
+ status: "executed_and_reread",
790
+ result: normalizePrepareMessagesResult(result),
791
+ };
716
792
  }
717
793
  case "copy_selected_source_rows": {
718
794
  const campaignOfferId = actionCampaignId(action);
@@ -50,7 +50,7 @@ function normalizeTargetDate(value) {
50
50
  export const refillSendsToolDefinitions = [
51
51
  {
52
52
  name: "refill_sends",
53
- 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 same-source copy, bounded generated-message approval, or read-only wait) and reread. It does not run unbounded approval, lower paid InMail thresholds, switch source families, create campaigns, schedule sends, launch, archive, delete, or write scheduler rows.",
53
+ description: "Typed command entrypoint for Sellable refill sends. Accepts --yolo semantics and optional sender selectors, then returns the bounded execution contract for the skill-led refill workflow. In --yolo, it may refresh stale paid InMail credit cache facts once, or execute exactly one safe primitive from target.globalActionQueue[0] (bounded existing-row preparation, bounded generated-message approval, receipt-proven same-source copy, or read-only wait) and reread. Same-source copy/source fallback is only safe after receipt-proven exhaustion: hasMoreFrontierRows:false, zero approvalCandidates, no stuckActiveCells, and no non-terminal approvedNotDispatched work. Wait primitives are gates, not competing goals, and carry absolute wait.deadlineAt when present. It does not run unbounded approval, lower paid InMail thresholds, switch source families, create campaigns, schedule sends, launch, archive, delete, or write scheduler rows.",
54
54
  inputSchema: {
55
55
  type: "object",
56
56
  properties: {
@@ -209,6 +209,7 @@ export function refillSendsCommand(input = {}) {
209
209
  : `, horizonSendDays: ${horizonSendDays}`}, approvalMode: "${approvalMode}" }) before any import, prep, approval, start, or schedule-affecting action.`,
210
210
  "Render target.eligibleSenderLedger and target.senderRefillPlans before mutation. Preserve the coverage labels Need to prepare, Goal, Already sent, Scheduled, Ready and waiting to be scheduled, and Still need.",
211
211
  "Use target.globalActionQueue as the only cross-sender yolo queue: execute only target.globalActionQueue[0], one globally ranked primitive, then rerun get_refill_target_plan before choosing another action.",
212
+ "Read target.senderRefillPlans[].refillReceipt as the public ladder receipt: it carries the selected campaign/sender/lane summary, skippedRungs, existingRowFrontier, and any absolute wait.deadlineAt. Do not choose source/copy/fallback work until that receipt proves the earlier ready/prep/approval rungs are exhausted.",
212
213
  "If get_refill_target_plan returns status complete, report eligible sender ledger, target.senderRefillPlans, gross target, selected days, sent count, scheduled count, projected count, campaign ids, targetShapeRevision, and no-op proof without asking for approval.",
213
214
  "Refill target lanes are connection invites (send_invite), standalone paid InMails (send_inmail_closed), or unified Sales Nav cascades represented publicly as send_inmail_closed with campaign classification sales_nav_cascade. For a Sales Nav cascade, refill the selected campaign first; its sequence can route prospects to Open InMail, paid InMail while fresh credits are >= 5, or same-campaign connection fallback without asking for separate open/paid/connection campaigns. Do not count send_dm as horizon target capacity.",
214
215
  "If remainingReadyOrProjectedGap is 0 but remainingProjectedGap is positive, run only a persistent read-only scheduler wait/reread loop; do not ask for prep/import/approval and do not close out while scheduler pickup is the only remaining state.",
@@ -228,15 +229,17 @@ export function refillSendsCommand(input = {}) {
228
229
  "Treat current dashboard-active PAUSED campaign-backed sequence campaigns as start-eligible candidates: read refill state before deciding whether to prep, approve, start, or skip.",
229
230
  "Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
230
231
  "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.",
232
+ "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.",
231
233
  "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.",
232
234
  "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.",
235
+ "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.",
233
236
  "Do not present paid-credit refresh as the next operator action after refill_sends returns autoPaidInmailRefresh and the post-refresh targetPlan.",
234
237
  "If paid InMail feasibility remains below threshold after that automatic refresh, report the exact sender/campaign/table/column threshold action or same-campaign connection fallback; --yolo must not lower paid InMail thresholds or create campaigns.",
235
238
  "In --yolo or after one Accept, continue through every safe selected sender/campaign action covered by the rendered target packet; sent/scheduled-count progress changes stateRevision and should continue while targetShapeRevision is stable.",
236
239
  "Do not complete a fill/schedule request until a final get_refill_target_plan or refill-state reread proves projected coverage (sent + scheduled) fills the scheduler-forward target window. Treat awaiting_scheduler_after_ready_buffer as loaded, awaiting scheduler when ready buffer covers the gap; do not import/prep more rows in that state, and keep polling unless Christian stops/statuses the run or a concrete non-scheduler blocker such as paid_inmail_below_threshold appears.",
237
240
  ],
238
241
  approvalContract: yolo
239
- ? "Auto-accept only the rendered bounded refill target packet after get_refill_target_plan and fresh reread. Execute one globally ranked primitive from target.globalActionQueue at a time, then rerun get_refill_target_plan before taking another action. Continue through every safe prep/source-copy/bounded-approval/read-only wait action inside that packet. Unbounded approval, start_campaign, source-family switches, threshold changes, and campaign creation require their own explicit gates. After each safe action, rerun get_refill_target_plan; keep going while targetShapeRevision is stable and projected coverage (sent + scheduled) progresses toward the target, even though stateRevision changes. Ready-to-schedule rows are buffer, not completion. If ready buffer covers the projected gap, use only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Stop immediately if targetShapeRevision changes because sender set, route, ids, caps, dates, blockers, action class, paid InMail threshold feasibility, or side-effect class drifts. --yolo does not authorize lowering paid InMail thresholds or creating connection campaigns."
242
+ ? "Auto-accept only the rendered bounded refill target packet after get_refill_target_plan and fresh reread. Execute one globally ranked primitive from target.globalActionQueue at a time, then rerun get_refill_target_plan before taking another action. Continue through every safe prep/source-copy/bounded-approval/read-only wait action inside that packet, but source-copy/fallback requires receipt-proven exhaustion of earlier ready/prep/approval rungs. Unbounded approval, start_campaign, source-family switches, threshold changes, and campaign creation require their own explicit gates. After each safe action, rerun get_refill_target_plan; keep going while targetShapeRevision is stable and projected coverage (sent + scheduled) progresses toward the target, even though stateRevision changes. Ready-to-schedule rows are buffer, not completion. If ready buffer covers the projected gap, use only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Stop immediately if targetShapeRevision changes because sender set, route, ids, caps, dates, blockers, action class, paid InMail threshold feasibility, side-effect class, or receipt exhaustion proof drifts. --yolo does not authorize lowering paid InMail thresholds or creating connection campaigns."
240
243
  : hasSenderSelectors
241
244
  ? "Before mutation, post the full bounded refill packet in normal chat as Markdown, including workspace, sender scope, campaign table, exact ids, caps/dates, gross target, sent count, scheduled count, projected count, ready buffer, remaining projected gap, paid InMail threshold feasibility, targetShapeRevision/stateRevision, side effects, forbidden actions, and stop condition. Then ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet instead of duplicating it. Only ask when get_refill_target_plan reports a positive remaining projected/ready gap. If target is complete by projected coverage, no-op without approval. If ready buffer covers the projected gap, run only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Threshold changes and campaign creation need separate explicit approval."
242
245
  : "Ask which eligible enrolled senders to refill first, then, before mutation, post the full bounded refill packet in normal chat as Markdown and ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet.",
@@ -249,7 +252,7 @@ export function refillSendsCommand(input = {}) {
249
252
  "direct scheduler writes",
250
253
  "sender reassignment",
251
254
  "campaigns outside the rendered eligible sender packet",
252
- "on-demand or unrelated active-campaign fallback unless the refill workflow proves the selected same-sender lane is blocked or exhausted",
255
+ "on-demand or unrelated active-campaign fallback unless the refill workflow shows receipt-proven exhaustion for the selected same-sender lane",
253
256
  ],
254
257
  hostExamples: {
255
258
  claude: [
@@ -79,6 +79,11 @@ function stringValue(value) {
79
79
  function booleanValue(value) {
80
80
  return typeof value === "boolean" ? value : undefined;
81
81
  }
82
+ function numericRecord(value) {
83
+ if (!isRecord(value))
84
+ return {};
85
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "number" && Number.isFinite(entry[1])));
86
+ }
82
87
  function campaignClassificationValue(value) {
83
88
  return value === "sales_nav_cascade" ? "sales_nav_cascade" : undefined;
84
89
  }
@@ -190,6 +195,8 @@ function sanitizeActionCandidate(candidate) {
190
195
  columnRole: stringValue(candidate.columnRole),
191
196
  rowSelector: sanitizeRowSelector(candidate.rowSelector),
192
197
  actionKey: stringValue(candidate.actionKey),
198
+ wait: sanitizeWait(candidate.wait),
199
+ refillReceipt: sanitizeRefillReceipt(candidate.refillReceipt),
193
200
  rereadAfter: candidate.rereadAfter === "get_refill_target_plan"
194
201
  ? "get_refill_target_plan"
195
202
  : undefined,
@@ -217,6 +224,98 @@ function sanitizeRowSelector(value) {
217
224
  : {}),
218
225
  };
219
226
  }
227
+ function sanitizeWait(value) {
228
+ if (!isRecord(value))
229
+ return undefined;
230
+ const deadlineAt = stringValue(value.deadlineAt);
231
+ const onExpiry = stringValue(value.onExpiry);
232
+ if (!deadlineAt && !onExpiry)
233
+ return undefined;
234
+ return { deadlineAt, onExpiry };
235
+ }
236
+ function sanitizeStuckActiveCells(value) {
237
+ if (!Array.isArray(value))
238
+ return [];
239
+ return value
240
+ .filter((cell) => isRecord(cell))
241
+ .map((cell) => ({
242
+ cellId: stringValue(cell.cellId),
243
+ rowId: stringValue(cell.rowId),
244
+ status: stringValue(cell.status),
245
+ ageMs: typeof cell.ageMs === "number" ? cell.ageMs : undefined,
246
+ }));
247
+ }
248
+ function sanitizeApprovedNotDispatched(value) {
249
+ if (!Array.isArray(value))
250
+ return [];
251
+ return value
252
+ .filter((row) => isRecord(row))
253
+ .map((row) => ({
254
+ rowId: stringValue(row.rowId),
255
+ attempts: typeof row.attempts === "number" ? row.attempts : undefined,
256
+ reason: stringValue(row.reason),
257
+ terminal: booleanValue(row.terminal),
258
+ }));
259
+ }
260
+ function sanitizeExistingRowFrontier(value) {
261
+ if (!isRecord(value))
262
+ return undefined;
263
+ return {
264
+ proof: stringValue(value.proof),
265
+ batchStop: stringValue(value.batchStop),
266
+ hasMoreFrontierRows: booleanValue(value.hasMoreFrontierRows),
267
+ stageCounts: numericRecord(value.stageCounts),
268
+ activeCellCount: typeof value.activeCellCount === "number"
269
+ ? value.activeCellCount
270
+ : undefined,
271
+ approvalCandidates: typeof value.approvalCandidates === "number"
272
+ ? value.approvalCandidates
273
+ : undefined,
274
+ brokenByReason: numericRecord(value.brokenByReason),
275
+ stuckActiveCells: sanitizeStuckActiveCells(value.stuckActiveCells),
276
+ approvedNotDispatched: sanitizeApprovedNotDispatched(value.approvedNotDispatched),
277
+ wait: sanitizeWait(value.wait),
278
+ };
279
+ }
280
+ function sanitizeSkippedRungs(value) {
281
+ if (!Array.isArray(value))
282
+ return [];
283
+ return value
284
+ .filter((rung) => isRecord(rung))
285
+ .map((rung) => ({
286
+ rung: stringValue(rung.rung),
287
+ reason: stringValue(rung.reason),
288
+ detail: stringValue(rung.detail),
289
+ }));
290
+ }
291
+ function sanitizeRefillReceipt(value) {
292
+ if (!isRecord(value))
293
+ return undefined;
294
+ return {
295
+ summary: stringValue(value.summary),
296
+ schedulableGap: typeof value.schedulableGap === "number"
297
+ ? value.schedulableGap
298
+ : undefined,
299
+ schedulerFillableGap: typeof value.schedulerFillableGap === "number"
300
+ ? value.schedulerFillableGap
301
+ : undefined,
302
+ selectedCampaignId: stringValue(value.selectedCampaignId),
303
+ selectedCampaignName: stringValue(value.selectedCampaignName),
304
+ selectedTableId: stringValue(value.selectedTableId),
305
+ senderId: stringValue(value.senderId),
306
+ senderName: stringValue(value.senderName),
307
+ actionType: allowedActionType(value.actionType),
308
+ dateWindow: isRecord(value.dateWindow)
309
+ ? { selectedDates: stringArray(value.dateWindow.selectedDates) }
310
+ : undefined,
311
+ readyBuffer: typeof value.readyBuffer === "number" ? value.readyBuffer : undefined,
312
+ approvalCandidates: typeof value.approvalCandidates === "number"
313
+ ? value.approvalCandidates
314
+ : undefined,
315
+ existingRowFrontier: sanitizeExistingRowFrontier(value.existingRowFrontier),
316
+ skippedRungs: sanitizeSkippedRungs(value.skippedRungs),
317
+ };
318
+ }
220
319
  function sanitizeRerunErroredCellsOperations(value) {
221
320
  if (!Array.isArray(value))
222
321
  return [];
@@ -285,6 +384,8 @@ function sanitizeToolInput(value) {
285
384
  : undefined,
286
385
  threshold: typeof value.threshold === "number" ? value.threshold : undefined,
287
386
  operations: sanitizeRerunErroredCellsOperations(value.operations),
387
+ wait: sanitizeWait(value.wait),
388
+ refillReceipt: sanitizeRefillReceipt(value.refillReceipt),
288
389
  };
289
390
  }
290
391
  function sanitizePacketIds(value) {
@@ -328,6 +429,7 @@ function sanitizeStructuredAction(action, selectedBySender, mode, rank) {
328
429
  toolInput: sanitizeToolInput(action.toolInput),
329
430
  inputSummary: stringValue(action.inputSummary) ?? "",
330
431
  reason: stringValue(action.reason) ?? "",
432
+ refillReceipt: sanitizeRefillReceipt(action.refillReceipt),
331
433
  prerequisites: stringArray(action.prerequisites),
332
434
  rereadAfter: action.rereadAfter === "get_refill_target_plan"
333
435
  ? "get_refill_target_plan"
@@ -514,6 +616,7 @@ function sanitizeStructuredSenderPlans(value, selectedBySender) {
514
616
  campaignRanking: sanitizeCampaignRanking(plan.campaignRanking),
515
617
  paidInmail: sanitizePaidInmail(plan.paidInmail),
516
618
  sourcePlan: sanitizeSourcePlan(plan.sourcePlan),
619
+ refillReceipt: sanitizeRefillReceipt(plan.refillReceipt),
517
620
  nextActions,
518
621
  manualAlternates,
519
622
  ...(isRecord(plan.emptyState)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.520",
3
+ "version": "0.1.521",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -97,8 +97,11 @@ That command helper normalizes arguments and returns the execution contract. In
97
97
  non-yolo mode it does not mutate. In `--yolo`, it may execute exactly one safe
98
98
  bounded primitive from the fresh `target.globalActionQueue[0]`, then reread and
99
99
  return the new target plan; currently safe primitives are paid-credit refresh,
100
- existing-row message preparation, same-source row copy, and read-only wait
101
- rereads. It does not run unbounded approval, lower
100
+ existing-row message preparation, generated-message approval, receipt-proven
101
+ same-source row copy, and read-only wait rereads. Same-source copy/source
102
+ fallback is safe only after receipt-proven exhaustion:
103
+ `hasMoreFrontierRows:false`, zero `approvalCandidates`, no `stuckActiveCells`,
104
+ and no non-terminal `approvedNotDispatched` work. It does not run unbounded approval, lower
102
105
  paid-InMail thresholds, switch source families, create campaigns, launch, send,
103
106
  or write scheduler rows. Continue with the workflow below for route selection,
104
107
  state rereads, approval gating, source import, preparation, and bounded
@@ -198,7 +201,10 @@ Structured planner packet:
198
201
  - `target.senderRefillPlans[]` is the canonical sender-level packet; read and
199
202
  display it before mutation.
200
203
  - Each sender packet includes `campaignRanking.options`, `sourcePlan`,
201
- `nextActions`, and `manualAlternates`.
204
+ `refillReceipt`, `nextActions`, and `manualAlternates`.
205
+ - `refillReceipt` is the public ladder receipt. It carries the selected
206
+ campaign/sender/lane summary, skipped rungs, existing-row frontier proof, and
207
+ any absolute `wait.deadlineAt`.
202
208
  - Preserve these coverage labels exactly: `Need to prepare`, `Goal`,
203
209
  `Already sent`, `Scheduled`, `Ready and waiting to be scheduled`, and
204
210
  `Still need`.
@@ -217,6 +223,13 @@ from the selected source (`selectedLeadListId`, provider, and source
217
223
  fingerprint preserved), then use provider-aligned source-more. A new source or
218
224
  provider switch changes the reply-rate baseline and is a manual alternate, not a
219
225
  `--yolo` side effect.
226
+ Source/copy/fallback requires receipt-proven exhaustion of earlier rungs:
227
+ `existingRowFrontier.hasMoreFrontierRows:false`, zero `approvalCandidates`, no
228
+ fresh active prep, no `stuckActiveCells`, and no non-terminal
229
+ `approvedNotDispatched` rows. Treat anomalies, `stuckActiveCells`, and
230
+ non-terminal `approvedNotDispatched` as diagnose-and-report gates, not
231
+ exhaustion. Terminal `approvedNotDispatched` blockers may be reported, then the
232
+ ladder can proceed.
220
233
 
221
234
  Run-local paid-credit guard: in `--yolo`, the `refill_sends` MCP command
222
235
  automatically maintains a `refreshedPaidInmailSenderIds` set for the current
@@ -266,6 +279,10 @@ interval, until projected coverage fills the target, a concrete non-scheduler
266
279
  blocker appears, or Christian explicitly asks to stop or only receive a status
267
280
  report. Treat `awaiting_scheduler_after_ready_buffer` as an in-progress wait
268
281
  state, not a close-out condition.
282
+ Wait actions are gates, not competing goals. When `wait_for_active_work` or
283
+ `wait_for_scheduler` includes receipt `wait.deadlineAt`, honor that absolute
284
+ deadline; if it is expired on this call, escalate to diagnostics with the
285
+ receipt evidence instead of issuing another blind wait.
269
286
  If paid InMail credit facts are stale or missing and the first target plan
270
287
  contains `refresh_paid_inmail_credits`, do not present that as the operator's
271
288
  next action in `--yolo`. Do not present paid-credit refresh as the next operator
@@ -119,7 +119,10 @@ files or memory.
119
119
  - `target.senderRefillPlans[]` is the canonical sender-level packet; read and
120
120
  display it before mutation.
121
121
  - Each sender packet includes `campaignRanking.options`, `sourcePlan`,
122
- `nextActions`, and `manualAlternates`.
122
+ `refillReceipt`, `nextActions`, and `manualAlternates`.
123
+ - `refillReceipt` is the public ladder receipt. It carries the selected
124
+ campaign/sender/lane summary, skipped rungs, existing-row frontier proof,
125
+ and any absolute `wait.deadlineAt`.
123
126
  - Preserve these coverage labels exactly: `Need to prepare`, `Goal`,
124
127
  `Already sent`, `Scheduled`, `Ready and waiting to be scheduled`, and
125
128
  `Still need`.
@@ -138,6 +141,14 @@ files or memory.
138
141
  provider, and source fingerprint preserved), then use provider-aligned
139
142
  source-more. A new source or provider switch changes the reply-rate baseline
140
143
  and is a manual alternate, not a `--yolo` side effect.
144
+ Source/copy/fallback requires receipt-proven exhaustion of earlier rungs:
145
+ `existingRowFrontier.hasMoreFrontierRows:false`, zero
146
+ `approvalCandidates`, no fresh active prep, no `stuckActiveCells`, and no
147
+ non-terminal `approvedNotDispatched` rows. Treat anomalies,
148
+ `stuckActiveCells`, and non-terminal `approvedNotDispatched` as
149
+ diagnose-and-report gates, not exhaustion. Terminal
150
+ `approvedNotDispatched` blockers may be reported, then the ladder can
151
+ proceed.
141
152
  Run-local paid-credit guard: in `--yolo`, the `refill_sends` MCP command
142
153
  automatically maintains a `refreshedPaidInmailSenderIds` set for the current
143
154
  command call. If its first target plan has stale/missing paid-InMail credit
@@ -187,6 +198,10 @@ files or memory.
187
198
  stop or only receive a status report. Treat
188
199
  `awaiting_scheduler_after_ready_buffer` as an in-progress wait state, not a
189
200
  close-out condition.
201
+ Wait actions are gates, not competing goals. When `wait_for_active_work` or
202
+ `wait_for_scheduler` includes receipt `wait.deadlineAt`, honor that absolute
203
+ deadline; if it is expired on this call, escalate to diagnostics with the
204
+ receipt evidence instead of issuing another blind wait.
190
205
  If paid InMail credit facts are stale or missing and the first target plan
191
206
  contains `refresh_paid_inmail_credits`, do not present that as the operator's
192
207
  next action in `--yolo`. Do not present paid-credit refresh as the next