@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.
- package/README.md +5 -2
- package/dist/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/refill-date-window.d.ts +34 -0
- package/dist/refill-date-window.js +210 -0
- package/dist/refill-run-loop.d.ts +9 -1
- package/dist/refill-run-loop.js +435 -85
- package/dist/server.js +4 -0
- package/dist/tools/evergreen-refill-plan.d.ts +19 -0
- package/dist/tools/evergreen-refill-plan.js +19 -0
- package/dist/tools/prompts.d.ts +2 -0
- package/dist/tools/prompts.js +1 -0
- package/dist/tools/refill-executors.d.ts +0 -7
- package/dist/tools/refill-executors.js +0 -22
- package/dist/tools/refill-sends-evergreen.d.ts +28 -0
- package/dist/tools/refill-sends-evergreen.js +47 -0
- package/dist/tools/refill-sends-v2.d.ts +19 -0
- package/dist/tools/refill-sends-v2.js +32 -1
- package/dist/tools/refill-sends.d.ts +0 -12
- package/dist/tools/refill-sends.js +37 -58
- package/dist/tools/refill-target-plan.js +19 -6
- package/dist/tools/registry.d.ts +55 -0
- package/dist/tools/registry.js +2 -0
- package/dist/tools/scheduler-fill-capacity.js +1 -1
- package/dist/tools/scheduler-run.d.ts +32 -0
- package/dist/tools/scheduler-run.js +68 -0
- package/dist/tools/senders.d.ts +8 -14
- package/dist/tools/senders.js +10 -21
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +31 -4
- package/skills/refill-sends-v2/SKILL.md +29 -5
- package/skills/refill-sends-v2-workflow/SKILL.md +27 -2
- package/skills/refill-sends-v2-workflow/core/flow.v1.json +19 -3
- package/skills/refill-sends-workflow/SKILL.md +27 -5
- package/skills/refill-sends-workflow/core/flow.v1.json +6 -1
package/dist/refill-run-loop.js
CHANGED
|
@@ -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 {
|
|
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
|
|
390
|
-
const
|
|
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
|
|
469
|
-
|
|
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
|
-
|
|
493
|
-
|
|
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
|
|
615
|
-
|
|
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
|
|
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
|
|
992
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
|
1124
|
-
|
|
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
|
|
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
|
|
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;
|