@sellable/mcp 0.1.535 → 0.1.536

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +5 -2
  2. package/dist/index-dev.js +0 -0
  3. package/dist/index.js +0 -0
  4. package/dist/refill-date-window.d.ts +34 -0
  5. package/dist/refill-date-window.js +210 -0
  6. package/dist/refill-run-loop.d.ts +9 -1
  7. package/dist/refill-run-loop.js +435 -85
  8. package/dist/server.js +4 -0
  9. package/dist/tools/evergreen-refill-plan.d.ts +19 -0
  10. package/dist/tools/evergreen-refill-plan.js +19 -0
  11. package/dist/tools/prompts.d.ts +2 -0
  12. package/dist/tools/prompts.js +1 -0
  13. package/dist/tools/refill-executors.d.ts +0 -7
  14. package/dist/tools/refill-executors.js +0 -22
  15. package/dist/tools/refill-sends-evergreen.d.ts +28 -0
  16. package/dist/tools/refill-sends-evergreen.js +47 -0
  17. package/dist/tools/refill-sends-v2.d.ts +19 -0
  18. package/dist/tools/refill-sends-v2.js +32 -1
  19. package/dist/tools/refill-sends.d.ts +0 -12
  20. package/dist/tools/refill-sends.js +37 -58
  21. package/dist/tools/refill-target-plan.js +19 -6
  22. package/dist/tools/registry.d.ts +55 -0
  23. package/dist/tools/registry.js +2 -0
  24. package/dist/tools/scheduler-fill-capacity.js +1 -1
  25. package/dist/tools/scheduler-run.d.ts +32 -0
  26. package/dist/tools/scheduler-run.js +68 -0
  27. package/dist/tools/senders.d.ts +8 -14
  28. package/dist/tools/senders.js +10 -21
  29. package/package.json +1 -1
  30. package/skills/refill-sends/SKILL.md +31 -4
  31. package/skills/refill-sends-v2/SKILL.md +29 -5
  32. package/skills/refill-sends-v2-workflow/SKILL.md +27 -2
  33. package/skills/refill-sends-v2-workflow/core/flow.v1.json +19 -3
  34. package/skills/refill-sends-workflow/SKILL.md +27 -5
  35. package/skills/refill-sends-workflow/core/flow.v1.json +6 -1
@@ -1,6 +1,7 @@
1
1
  import { SellableApiError } from "./api.js";
2
+ import { dateSelectorMatchesWindow, dateWindowFromPlan, dateWindowFromRunState, dateWindowFromSelector, normalizeRefillDateSelector, refillDateSelectorBody, schedulerSweepTargetDates, selectorFromDateWindow, } from "./refill-date-window.js";
2
3
  import { buildRunStateFromLocalHints } from "./tools/evergreen-refill-plan.js";
3
- import { classifyPaidInmailRefreshReceipt, prepareRowSelectorValue as defaultPrepareRowSelectorValue, refillPrepareRequestHash as defaultRefillPrepareRequestHash, } from "./tools/refill-executors.js";
4
+ import { prepareRowSelectorValue as defaultPrepareRowSelectorValue, refillPrepareRequestHash as defaultRefillPrepareRequestHash, } from "./tools/refill-executors.js";
4
5
  const DEFAULT_BUDGETS = {
5
6
  maxGateCyclesPerInvocation: 8,
6
7
  pollIntervalMs: 15_000,
@@ -19,6 +20,43 @@ const REFILL_DONE_REASONS = new Set([
19
20
  "not_an_evergreen_workspace",
20
21
  "no_refillable_campaigns",
21
22
  ]);
23
+ const SCHEDULER_RUN_ENVELOPE_STATUSES = new Set([
24
+ "ran",
25
+ "attached",
26
+ "backoff",
27
+ "window_closed_noop",
28
+ "failed",
29
+ ]);
30
+ const SCHEDULER_RUN_RECEIPT_STATUSES = new Set([
31
+ "ran",
32
+ "window_closed_noop",
33
+ "failed",
34
+ ]);
35
+ const SCHEDULER_RUN_SKIP_REASONS = [
36
+ "window_closed",
37
+ "daily_limit",
38
+ "cooldown",
39
+ "sender_gate",
40
+ "credit_threshold",
41
+ "billing_blocked",
42
+ "duplicate_lead",
43
+ "no_senders",
44
+ "other",
45
+ ];
46
+ const SCHEDULER_RUN_HARD_BLOCK_REASONS = new Set([
47
+ "billing_blocked",
48
+ "credit_threshold",
49
+ "cooldown",
50
+ ]);
51
+ const SCHEDULER_RUN_NEXT_ACTIONS = new Set([
52
+ "refresh_paid_inmail_credits_then_rerun",
53
+ "wait_for_capacity_or_window",
54
+ "inspect_sender_mismatch",
55
+ "no_ready_cells_continue_refill_prep",
56
+ "scheduled_cells",
57
+ "investigate_deferred_reasons",
58
+ "investigate_scheduler_failure",
59
+ ]);
22
60
  function isRecord(value) {
23
61
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
24
62
  }
@@ -40,6 +78,253 @@ function arrayValue(value) {
40
78
  function hasBlocker(value, blocker) {
41
79
  return isRecord(value) && value.blocker === blocker;
42
80
  }
81
+ function schedulerRunSkipReasons(value) {
82
+ const raw = recordValue(value) ?? {};
83
+ const normalized = {};
84
+ for (const reason of SCHEDULER_RUN_SKIP_REASONS) {
85
+ normalized[reason] = numberValue(raw[reason]) ?? 0;
86
+ }
87
+ return normalized;
88
+ }
89
+ function schedulerRunCountMap(value) {
90
+ const raw = recordValue(value) ?? {};
91
+ return Object.fromEntries(Object.entries(raw)
92
+ .map(([key, rawCount]) => [key, numberValue(rawCount) ?? 0])
93
+ .filter(([, count]) => count > 0)
94
+ .sort(([left], [right]) => left.localeCompare(right)));
95
+ }
96
+ function schedulerRunNestedCountMap(value) {
97
+ const raw = recordValue(value) ?? {};
98
+ return Object.fromEntries(Object.entries(raw)
99
+ .map(([key, nested]) => [key, schedulerRunCountMap(nested)])
100
+ .filter(([, nested]) => Object.keys(nested).length > 0)
101
+ .sort(([left], [right]) => left.localeCompare(right)));
102
+ }
103
+ function dominantSchedulerRunSkipReason(skipReasons) {
104
+ let dominant = null;
105
+ let dominantCount = 0;
106
+ for (const reason of SCHEDULER_RUN_SKIP_REASONS) {
107
+ const count = skipReasons[reason] ?? 0;
108
+ if (count > dominantCount) {
109
+ dominant = reason;
110
+ dominantCount = count;
111
+ }
112
+ }
113
+ return dominantCount > 0 ? dominant : null;
114
+ }
115
+ function dominantSchedulerRunCountReason(counts) {
116
+ let dominant = null;
117
+ let dominantCount = 0;
118
+ for (const reason of Object.keys(counts).sort()) {
119
+ const count = counts[reason] ?? 0;
120
+ if (count > dominantCount) {
121
+ dominant = reason;
122
+ dominantCount = count;
123
+ }
124
+ }
125
+ return dominantCount > 0 ? dominant : null;
126
+ }
127
+ function schedulerRunReasonLooksLike(reasons, pattern) {
128
+ return Object.keys(reasons).some((reason) => (reasons[reason] ?? 0) > 0 && pattern.test(reason));
129
+ }
130
+ function schedulerRunHasReason(reasons, reason) {
131
+ return (reasons[reason] ?? 0) > 0;
132
+ }
133
+ function schedulerRunRecommendedAction(params) {
134
+ if (params.rawAction && SCHEDULER_RUN_NEXT_ACTIONS.has(params.rawAction)) {
135
+ return params.rawAction;
136
+ }
137
+ if (schedulerRunReasonLooksLike(params.prefilterReasons, /(credit|paid[_ -]?inmail|stale[_ -]?credit|missing[_ -]?credit)/i) ||
138
+ schedulerRunHasReason(params.skippedReasons, "credit_threshold") ||
139
+ schedulerRunHasReason(params.deferredReasons, "credit_threshold")) {
140
+ return "refresh_paid_inmail_credits_then_rerun";
141
+ }
142
+ if (schedulerRunReasonLooksLike(params.prefilterReasons, /sender[_ -]?mismatch/i)) {
143
+ return "inspect_sender_mismatch";
144
+ }
145
+ if (schedulerRunReasonLooksLike(params.prefilterReasons, /(no[_ -]?capacity|capacity|window|daily[_ -]?limit|cooldown)/i) ||
146
+ schedulerRunHasReason(params.skippedReasons, "window_closed") ||
147
+ schedulerRunHasReason(params.skippedReasons, "daily_limit") ||
148
+ schedulerRunHasReason(params.skippedReasons, "cooldown") ||
149
+ schedulerRunHasReason(params.deferredReasons, "window_closed") ||
150
+ schedulerRunHasReason(params.deferredReasons, "daily_limit") ||
151
+ schedulerRunHasReason(params.deferredReasons, "cooldown")) {
152
+ return "wait_for_capacity_or_window";
153
+ }
154
+ if (params.cellsScheduled > 0)
155
+ return "scheduled_cells";
156
+ if (params.readyCellsFound === 0)
157
+ return "no_ready_cells_continue_refill_prep";
158
+ if (params.cellsConsidered > 0)
159
+ return "investigate_deferred_reasons";
160
+ return null;
161
+ }
162
+ function schedulerRunCampaignScopeSummary(value) {
163
+ const raw = recordValue(value);
164
+ if (!raw) {
165
+ return {
166
+ totalEligibleTables: 0,
167
+ tablesWithReadyCells: 0,
168
+ returnedTables: 0,
169
+ omittedTables: 0,
170
+ truncated: false,
171
+ tables: [],
172
+ };
173
+ }
174
+ const rawTables = arrayValue(raw.tables).slice(0, 25);
175
+ const tables = rawTables
176
+ .map((entry) => {
177
+ const table = recordValue(entry);
178
+ if (!table)
179
+ return null;
180
+ const tableId = stringValue(table.tableId);
181
+ if (!tableId)
182
+ return null;
183
+ return {
184
+ tableId,
185
+ tableName: stringValue(table.tableName),
186
+ workspaceId: stringValue(table.workspaceId),
187
+ campaignOfferId: stringValue(table.campaignOfferId),
188
+ campaignName: stringValue(table.campaignName),
189
+ readyCells: numberValue(table.readyCells) ?? 0,
190
+ readyCellsByType: schedulerRunCountMap(table.readyCellsByType),
191
+ consideredCells: numberValue(table.consideredCells) ?? 0,
192
+ consideredCellsByType: schedulerRunCountMap(table.consideredCellsByType),
193
+ prefilteredCells: numberValue(table.prefilteredCells) ?? 0,
194
+ prefilterReasons: schedulerRunCountMap(table.prefilterReasons),
195
+ prefilterReasonsByType: schedulerRunNestedCountMap(table.prefilterReasonsByType),
196
+ };
197
+ })
198
+ .filter((entry) => entry != null);
199
+ return {
200
+ totalEligibleTables: numberValue(raw.totalEligibleTables) ?? 0,
201
+ tablesWithReadyCells: numberValue(raw.tablesWithReadyCells) ?? tables.length,
202
+ returnedTables: tables.length,
203
+ omittedTables: numberValue(raw.omittedTables) ?? 0,
204
+ truncated: booleanValue(raw.truncated) ?? false,
205
+ tables,
206
+ };
207
+ }
208
+ function sanitizeSchedulerRunReceipt(raw) {
209
+ const envelope = recordValue(raw);
210
+ if (!envelope)
211
+ return null;
212
+ const status = stringValue(envelope.status);
213
+ if (!status || !SCHEDULER_RUN_ENVELOPE_STATUSES.has(status))
214
+ return null;
215
+ const retryAfterMs = envelope.retryAfterMs == null ? null : numberValue(envelope.retryAfterMs);
216
+ if (envelope.retryAfterMs != null && retryAfterMs == null)
217
+ return null;
218
+ const receiptInput = envelope.receipt;
219
+ let receipt = null;
220
+ let dominantSkipReason = null;
221
+ let dominantPrefilterReason = null;
222
+ let dominantDeferredReason = null;
223
+ let recommendedNextAction = null;
224
+ if (receiptInput != null) {
225
+ const rawReceipt = recordValue(receiptInput);
226
+ if (!rawReceipt)
227
+ return null;
228
+ const receiptStatus = stringValue(rawReceipt.status);
229
+ if (!receiptStatus || !SCHEDULER_RUN_RECEIPT_STATUSES.has(receiptStatus)) {
230
+ return null;
231
+ }
232
+ const cellsConsidered = numberValue(rawReceipt.cellsConsidered);
233
+ const cellsScheduled = numberValue(rawReceipt.cellsScheduled);
234
+ const cellsSkipped = numberValue(rawReceipt.cellsSkipped);
235
+ const cellsDeferred = numberValue(rawReceipt.cellsDeferred);
236
+ const tablesFilteredForNoCapacity = numberValue(rawReceipt.tablesFilteredForNoCapacity);
237
+ if (cellsConsidered == null ||
238
+ cellsScheduled == null ||
239
+ cellsSkipped == null ||
240
+ cellsDeferred == null ||
241
+ tablesFilteredForNoCapacity == null) {
242
+ return null;
243
+ }
244
+ const skipReasons = schedulerRunSkipReasons(rawReceipt.skipReasons);
245
+ const skippedReasons = rawReceipt.skippedReasons
246
+ ? schedulerRunSkipReasons(rawReceipt.skippedReasons)
247
+ : skipReasons;
248
+ const deferredReasons = rawReceipt.deferredReasons
249
+ ? schedulerRunSkipReasons(rawReceipt.deferredReasons)
250
+ : schedulerRunSkipReasons(null);
251
+ const prefilterReasons = schedulerRunCountMap(rawReceipt.prefilterReasons);
252
+ const prefilterReasonsByType = schedulerRunNestedCountMap(rawReceipt.prefilterReasonsByType);
253
+ const readyCellsByType = schedulerRunCountMap(rawReceipt.readyCellsByType);
254
+ const consideredCellsByType = schedulerRunCountMap(rawReceipt.consideredCellsByType);
255
+ const prefilteredCellsByType = schedulerRunCountMap(rawReceipt.prefilteredCellsByType);
256
+ const readyCellsFound = numberValue(rawReceipt.readyCellsFound) ?? cellsConsidered;
257
+ const prefilteredCells = numberValue(rawReceipt.prefilteredCells) ?? 0;
258
+ const tablesFilteredForSenderMismatch = numberValue(rawReceipt.tablesFilteredForSenderMismatch) ?? 0;
259
+ dominantSkipReason = dominantSchedulerRunSkipReason(skipReasons);
260
+ dominantPrefilterReason =
261
+ dominantSchedulerRunCountReason(prefilterReasons);
262
+ dominantDeferredReason =
263
+ dominantSchedulerRunSkipReason(deferredReasons);
264
+ recommendedNextAction = schedulerRunRecommendedAction({
265
+ rawAction: stringValue(rawReceipt.nextAction),
266
+ readyCellsFound,
267
+ cellsConsidered,
268
+ cellsScheduled,
269
+ prefilterReasons,
270
+ skippedReasons,
271
+ deferredReasons,
272
+ });
273
+ receipt = {
274
+ status: receiptStatus,
275
+ ...(numberValue(rawReceipt.receiptVersion) != null
276
+ ? { receiptVersion: numberValue(rawReceipt.receiptVersion) }
277
+ : {}),
278
+ ...(numberValue(rawReceipt.diagnosticsVersion) != null
279
+ ? { diagnosticsVersion: numberValue(rawReceipt.diagnosticsVersion) }
280
+ : {}),
281
+ readyCellsFound,
282
+ readyCellsByType,
283
+ cellsConsidered,
284
+ consideredCellsByType,
285
+ cellsScheduled,
286
+ cellsSkipped,
287
+ cellsDeferred,
288
+ prefilteredCells,
289
+ prefilteredCellsByType,
290
+ tablesFilteredForNoCapacity,
291
+ tablesFilteredForSenderMismatch,
292
+ prefilterReasons,
293
+ prefilterReasonsByType,
294
+ skippedReasons,
295
+ deferredReasons,
296
+ skipReasons,
297
+ campaignScopeSummary: schedulerRunCampaignScopeSummary(rawReceipt.campaignScopeSummary),
298
+ ...(stringValue(rawReceipt.summary)
299
+ ? { summary: stringValue(rawReceipt.summary) }
300
+ : {}),
301
+ ...(recommendedNextAction ? { nextAction: recommendedNextAction } : {}),
302
+ };
303
+ }
304
+ return {
305
+ status,
306
+ retryAfterMs,
307
+ receipt,
308
+ dominantSkipReason,
309
+ dominantPrefilterReason,
310
+ dominantDeferredReason,
311
+ recommendedNextAction,
312
+ hardBlocked: (dominantSkipReason != null &&
313
+ SCHEDULER_RUN_HARD_BLOCK_REASONS.has(dominantSkipReason)) ||
314
+ (dominantDeferredReason != null &&
315
+ SCHEDULER_RUN_HARD_BLOCK_REASONS.has(dominantDeferredReason)) ||
316
+ recommendedNextAction === "refresh_paid_inmail_credits_then_rerun",
317
+ };
318
+ }
319
+ function schedulerRunReceiptIsFreshZeroScheduled(schedulerRunReceipt) {
320
+ const status = stringValue(schedulerRunReceipt?.status);
321
+ const fresh = status === "ran" || status === "window_closed_noop";
322
+ if (!fresh)
323
+ return false;
324
+ const receipt = recordValue(schedulerRunReceipt?.receipt);
325
+ return (status === "window_closed_noop" ||
326
+ numberValue(receipt?.cellsScheduled) === 0);
327
+ }
43
328
  function isLeaseLost(value) {
44
329
  return hasBlocker(value, "lease_lost");
45
330
  }
@@ -133,6 +418,41 @@ function mergeRunState(current, updates) {
133
418
  ...updates,
134
419
  };
135
420
  }
421
+ function dateSelectorFromInput(input) {
422
+ return normalizeRefillDateSelector({
423
+ targetDate: input.targetDate,
424
+ untilDate: input.untilDate,
425
+ horizonSendDays: input.horizonSendDays,
426
+ });
427
+ }
428
+ function runStateWithDateWindow(runState, dateWindow) {
429
+ return mergeRunState(runState, { dateWindow });
430
+ }
431
+ function readPlanRequest(input) {
432
+ return {
433
+ workspaceId: input.workspaceId,
434
+ intent: input.intent,
435
+ senderIds: input.senderIds,
436
+ runState: input.runState,
437
+ journal: input.journal,
438
+ ...refillDateSelectorBody(input.dateSelector),
439
+ };
440
+ }
441
+ async function readLoopPlan(input, deps, ctx, journal) {
442
+ return deps.readPlan(readPlanRequest({
443
+ workspaceId: input.workspaceId,
444
+ intent: input.intent,
445
+ senderIds: input.senderIds,
446
+ runState: ctx.runState,
447
+ journal,
448
+ dateSelector: ctx.dateSelector,
449
+ }));
450
+ }
451
+ function applyPlanDateWindow(ctx, plan) {
452
+ ctx.dateWindow = dateWindowFromPlan(plan, ctx.dateSelector);
453
+ ctx.dateSelector = selectorFromDateWindow(ctx.dateWindow);
454
+ ctx.runState = runStateWithDateWindow(ctx.runState, ctx.dateWindow);
455
+ }
136
456
  function normalizeDoneReason(value) {
137
457
  const raw = stringValue(value);
138
458
  if (!raw)
@@ -386,14 +706,63 @@ async function createJournal(input, deps, runId) {
386
706
  return null;
387
707
  }
388
708
  }
389
- async function startOrResume(input, deps) {
390
- const initialRunState = buildRunStateFromLocalHints(input.workspaceId);
709
+ async function preflightResumeDateWindow(input, deps, requestedSelector) {
710
+ const requestedWindow = dateWindowFromSelector(requestedSelector);
711
+ if (!input.resume) {
712
+ return { ok: true, selector: requestedSelector, window: requestedWindow };
713
+ }
714
+ const status = await deps.runClient.refillRunStatus({
715
+ workspaceId: input.workspaceId,
716
+ runId: input.resume.runId,
717
+ });
718
+ if (hasBlocker(status, "lease_lost")) {
719
+ return {
720
+ status: "blocked",
721
+ blocker: "lease_lost",
722
+ runId: input.resume.runId,
723
+ fence: input.resume.fence,
724
+ guidance: "Refill run lease was lost before resume date-window preflight.",
725
+ };
726
+ }
727
+ const storedWindow = dateWindowFromRunState(recordValue(status)?.runState);
728
+ const explicitSelector = requestedSelector.source !== "default_scheduler_forward";
729
+ if (!storedWindow) {
730
+ if (explicitSelector) {
731
+ return {
732
+ status: "blocked",
733
+ blocker: "date_window_conflict",
734
+ runId: input.resume.runId,
735
+ fence: input.resume.fence,
736
+ guidance: "This legacy refill_sends_v2 run has no stored date window. Resume it without a new date selector or start a new run.",
737
+ report: { requestedDateWindow: requestedWindow },
738
+ };
739
+ }
740
+ return { ok: true, selector: requestedSelector, window: requestedWindow };
741
+ }
742
+ if (explicitSelector && !dateSelectorMatchesWindow(requestedSelector, storedWindow)) {
743
+ return {
744
+ status: "blocked",
745
+ blocker: "date_window_conflict",
746
+ runId: input.resume.runId,
747
+ fence: input.resume.fence,
748
+ guidance: "Resume date selector conflicts with the stored refill run date window.",
749
+ report: { requestedDateWindow: requestedWindow, storedDateWindow: storedWindow },
750
+ };
751
+ }
752
+ const selector = explicitSelector
753
+ ? requestedSelector
754
+ : selectorFromDateWindow(storedWindow);
755
+ return { ok: true, selector, window: storedWindow };
756
+ }
757
+ async function startOrResume(input, deps, dateWindow) {
758
+ const initialRunState = runStateWithDateWindow(buildRunStateFromLocalHints(input.workspaceId), dateWindow);
391
759
  const started = await deps.runClient.startRefillRunRemote({
392
760
  workspaceId: input.workspaceId,
393
761
  config: {
394
762
  intent: input.intent ?? "auto",
395
763
  senderIds: input.senderIds,
396
764
  approvalMode: input.approvalMode ?? "approve",
765
+ dateWindow,
397
766
  },
398
767
  runState: initialRunState,
399
768
  resume: input.resume,
@@ -435,6 +804,8 @@ async function startOrResume(input, deps) {
435
804
  laneCooldowns: [],
436
805
  passRates: [],
437
806
  };
807
+ const storedDateWindow = dateWindowFromRunState(runState) ?? dateWindow;
808
+ const storedDateSelector = selectorFromDateWindow(storedDateWindow);
438
809
  const journalPath = await createJournal(input, deps, runId);
439
810
  const ctx = {
440
811
  workspaceId: input.workspaceId,
@@ -447,6 +818,8 @@ async function startOrResume(input, deps) {
447
818
  plan: null,
448
819
  headAction: null,
449
820
  lastFingerprint: null,
821
+ dateSelector: storedDateSelector,
822
+ dateWindow: storedDateWindow,
450
823
  recoveryPoint: recordValue(result.recoveryPoint),
451
824
  gateCycles: 0,
452
825
  sleptMs: 0,
@@ -465,45 +838,31 @@ async function startOrResume(input, deps) {
465
838
  return ctx;
466
839
  }
467
840
  async function gateBootstrap(input, deps, ctx) {
468
- const plan = await deps.readPlan({
469
- workspaceId: input.workspaceId,
470
- intent: input.intent,
471
- senderIds: input.senderIds,
472
- runState: ctx.runState,
473
- journal: false,
474
- });
841
+ const plan = await readLoopPlan(input, deps, ctx, false);
842
+ applyPlanDateWindow(ctx, plan);
475
843
  const paidCredit = arrayValue(recordValue(plan.bootstrap)?.paidCredit).filter(isRecord);
476
844
  const refreshedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.senderIds)
477
845
  .map((entry) => stringValue(entry))
478
846
  .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)));
482
847
  const receipts = [];
483
848
  for (const entry of paidCredit) {
484
849
  const senderId = stringValue(entry.senderId);
485
850
  if (!senderId ||
486
851
  !entry.plannedJitRefresh ||
487
- refreshedSenderIds.has(senderId) ||
488
- attemptedSenderIds.has(senderId)) {
852
+ refreshedSenderIds.has(senderId)) {
489
853
  continue;
490
854
  }
491
855
  const receipt = await deps.executors.refreshPaidInmailCreditsWithRetry(senderId, input.workspaceId);
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
- }
856
+ receipts.push({ senderId, receipt });
857
+ refreshedSenderIds.add(senderId);
498
858
  }
499
- const nextRunState = mergeRunState(ctx.runState, {
859
+ const nextRunState = runStateWithDateWindow(mergeRunState(ctx.runState, {
500
860
  creditTrust: {
501
861
  refreshedAt: (deps.now?.() ?? new Date()).toISOString(),
502
862
  senderIds: [...refreshedSenderIds],
503
- attemptedSenderIds: [...attemptedSenderIds],
504
863
  receipts,
505
864
  },
506
- });
865
+ }), ctx.dateWindow);
507
866
  await safeJournalAppend(ctx, deps, deps.journal.renderBootstrapSection({
508
867
  summary: "Refill v2 execution bootstrap",
509
868
  senderSummary: safeJson(recordValue(plan.bootstrap)?.senders ?? []),
@@ -611,13 +970,8 @@ async function completeTerminal(input, deps, ctx, doneReasonInput, reportInput =
611
970
  };
612
971
  }
613
972
  async function gatePlan(input, deps, ctx) {
614
- const plan = await deps.readPlan({
615
- workspaceId: input.workspaceId,
616
- intent: input.intent,
617
- senderIds: input.senderIds,
618
- runState: ctx.runState,
619
- journal: false,
620
- });
973
+ const plan = await readLoopPlan(input, deps, ctx, false);
974
+ applyPlanDateWindow(ctx, plan);
621
975
  if (plan.blocker === "workspace_access") {
622
976
  return {
623
977
  status: "blocked",
@@ -720,13 +1074,7 @@ async function verifyRecoveryPoint(input, deps, ctx) {
720
1074
  stateRevision: stringValue(planned.stateRevision),
721
1075
  targetShapeRevision: stringValue(planned.targetShapeRevision),
722
1076
  };
723
- const fresh = await deps.readPlan({
724
- workspaceId: input.workspaceId,
725
- intent: input.intent,
726
- senderIds: input.senderIds,
727
- runState: ctx.runState,
728
- journal: false,
729
- });
1077
+ const fresh = await readLoopPlan(input, deps, ctx, false);
730
1078
  const changed = fingerprintChanged(expected, fingerprintFromPlan(fresh));
731
1079
  if (changed.length > 0) {
732
1080
  await deps.runClient.recordRunOutcome({
@@ -988,13 +1336,8 @@ async function verifyPrepareAction(input, deps, ctx, action, budgets) {
988
1336
  }
989
1337
  async function verifyReadOnlyWait(input, deps, ctx, budgets, waitKind) {
990
1338
  for (let poll = 0; poll < budgets.maxPollsPerWait; poll += 1) {
991
- const fresh = await deps.readPlan({
992
- workspaceId: input.workspaceId,
993
- intent: input.intent,
994
- senderIds: input.senderIds,
995
- runState: ctx.runState,
996
- journal: false,
997
- });
1339
+ const fresh = await readLoopPlan(input, deps, ctx, false);
1340
+ applyPlanDateWindow(ctx, fresh);
998
1341
  ctx.plan = fresh;
999
1342
  const doneReason = doneReasonFromPlan(fresh);
1000
1343
  if (doneReason) {
@@ -1022,49 +1365,55 @@ async function verifySchedulerWait(input, deps, ctx, action, budgets) {
1022
1365
  const enteredAt = stringValue(progress.schedulerWaitEnteredAt) ??
1023
1366
  (deps.now?.() ?? new Date()).toISOString();
1024
1367
  const jitFired = booleanValue(progress.schedulerJitFired) ?? false;
1368
+ let schedulerRunReceipt = recordValue(progress.schedulerRunReceipt) ?? null;
1025
1369
  if (!jitFired) {
1026
1370
  const senderId = actionSenderId(action);
1027
1371
  if (senderId) {
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
- }
1372
+ await deps.executors.refreshPaidInmailCreditsWithRetry(senderId, input.workspaceId);
1049
1373
  }
1050
1374
  if (deps.requestSchedulerRun) {
1051
- await deps.requestSchedulerRun(input.workspaceId);
1375
+ try {
1376
+ const targetDates = schedulerSweepTargetDates(ctx.dateWindow);
1377
+ const sweepReceipts = [];
1378
+ if (targetDates.length === 0) {
1379
+ schedulerRunReceipt = sanitizeSchedulerRunReceipt(await deps.requestSchedulerRun(input.workspaceId));
1380
+ }
1381
+ else {
1382
+ for (const targetDate of targetDates) {
1383
+ const receipt = sanitizeSchedulerRunReceipt(await deps.requestSchedulerRun(input.workspaceId, { targetDate }));
1384
+ if (receipt)
1385
+ sweepReceipts.push({ targetDate, ...receipt });
1386
+ }
1387
+ schedulerRunReceipt = sweepReceipts[0] ?? null;
1388
+ }
1389
+ if (sweepReceipts.length > 1) {
1390
+ ctx.runState = mergeRunState(ctx.runState, {
1391
+ progress: mergeProgress(ctx, {
1392
+ schedulerRunReceipts: sweepReceipts,
1393
+ }),
1394
+ });
1395
+ }
1396
+ }
1397
+ catch {
1398
+ schedulerRunReceipt = null;
1399
+ }
1052
1400
  }
1053
1401
  ctx.runState = mergeRunState(ctx.runState, {
1054
1402
  progress: mergeProgress(ctx, {
1055
1403
  schedulerWaitEnteredAt: enteredAt,
1056
1404
  schedulerJitFired: true,
1405
+ ...(schedulerRunReceipt ? { schedulerRunReceipt } : {}),
1057
1406
  }),
1058
1407
  });
1059
1408
  }
1060
- for (let poll = 0; poll < budgets.maxSchedulerReadbacks; poll += 1) {
1061
- const fresh = await deps.readPlan({
1062
- workspaceId: input.workspaceId,
1063
- intent: input.intent,
1064
- senderIds: input.senderIds,
1065
- runState: ctx.runState,
1066
- journal: false,
1067
- });
1409
+ const zeroScheduledFresh = schedulerRunReceiptIsFreshZeroScheduled(schedulerRunReceipt);
1410
+ // EDGE-2: the on-demand run only executes placement; cron can still process
1411
+ // due/timed-out cells later. We still do one readback, then avoid burning the
1412
+ // full wait budget when the fresh receipt says nothing was placeable now.
1413
+ const readbackBudget = zeroScheduledFresh ? 1 : budgets.maxSchedulerReadbacks;
1414
+ for (let poll = 0; poll < readbackBudget; poll += 1) {
1415
+ const fresh = await readLoopPlan(input, deps, ctx, false);
1416
+ applyPlanDateWindow(ctx, fresh);
1068
1417
  ctx.plan = fresh;
1069
1418
  const doneReason = normalizeDoneReason(doneReasonFromPlan(fresh));
1070
1419
  if (doneReason === "complete" || doneReason === "capped_by_scheduler") {
@@ -1120,13 +1469,8 @@ async function gateVerify(input, deps, ctx, budgets) {
1120
1469
  actionType === "wait_for_source_import") {
1121
1470
  return verifyReadOnlyWait(input, deps, ctx, budgets, actionType);
1122
1471
  }
1123
- const fresh = await deps.readPlan({
1124
- workspaceId: input.workspaceId,
1125
- intent: input.intent,
1126
- senderIds: input.senderIds,
1127
- runState: ctx.runState,
1128
- journal: false,
1129
- });
1472
+ const fresh = await readLoopPlan(input, deps, ctx, false);
1473
+ applyPlanDateWindow(ctx, fresh);
1130
1474
  ctx.plan = fresh;
1131
1475
  const doneReason = doneReasonFromPlan(fresh);
1132
1476
  if (doneReason) {
@@ -1199,15 +1543,21 @@ export async function runRefillV2Loop(input, deps) {
1199
1543
  let ctx = null;
1200
1544
  try {
1201
1545
  if (input.dryRun) {
1202
- const plan = await deps.readPlan({
1546
+ const dateSelector = dateSelectorFromInput(input);
1547
+ const plan = await deps.readPlan(readPlanRequest({
1203
1548
  workspaceId: input.workspaceId,
1204
1549
  intent: input.intent,
1205
1550
  senderIds: input.senderIds,
1206
1551
  journal: true,
1207
- });
1552
+ dateSelector,
1553
+ }));
1208
1554
  return { status: "dry_run", plan };
1209
1555
  }
1210
- const started = await startOrResume(input, deps);
1556
+ const requestedSelector = dateSelectorFromInput(input);
1557
+ const preflight = await preflightResumeDateWindow(input, deps, requestedSelector);
1558
+ if ("status" in preflight)
1559
+ return preflight;
1560
+ const started = await startOrResume(input, deps, preflight.window);
1211
1561
  if ("status" in started)
1212
1562
  return started;
1213
1563
  ctx = started;
package/dist/server.js CHANGED
@@ -45,6 +45,7 @@ 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";
48
49
  import { getSenderRoutingTool, setSenderRoutingTool, } from "./tools/sender-routing.js";
49
50
  import { getSender, listSenders, refreshPaidInmailCredits, } from "./tools/senders.js";
50
51
  import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from "./tools/sequencer.js";
@@ -235,6 +236,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
235
236
  case "get_scheduler_fill_capacity":
236
237
  result = await getSchedulerFillCapacity(args);
237
238
  break;
239
+ case "run_scheduler_sweep":
240
+ result = await runSchedulerSweep(args);
241
+ break;
238
242
  case "refill_sends":
239
243
  result = await executeRefillSendsCommand(args);
240
244
  break;