@sellable/mcp 0.1.537 → 0.1.538
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 +147 -86
- package/dist/tools/evergreen-refill-plan.d.ts +19 -0
- package/dist/tools/evergreen-refill-plan.js +19 -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 +31 -2
- package/dist/tools/refill-sends.d.ts +0 -12
- package/dist/tools/refill-sends.js +36 -58
- package/dist/tools/refill-target-plan.js +19 -6
- package/dist/tools/registry.d.ts +36 -0
- package/dist/tools/scheduler-fill-capacity.js +1 -1
- package/dist/tools/scheduler-run.d.ts +5 -0
- package/dist/tools/scheduler-run.js +24 -1
- 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 +9 -1
- package/skills/refill-sends-v2/SKILL.md +18 -5
- package/skills/refill-sends-v2-workflow/SKILL.md +16 -2
- package/skills/refill-sends-v2-workflow/core/flow.v1.json +19 -3
- package/skills/refill-sends-workflow/SKILL.md +5 -3
- package/skills/refill-sends-workflow/core/flow.v1.json +1 -1
package/README.md
CHANGED
|
@@ -74,9 +74,12 @@ sending from:
|
|
|
74
74
|
|
|
75
75
|
The refill-sends public command wrapper plans and executes approval-gated send
|
|
76
76
|
refills. It supports `--yolo` and optional sender selectors such as
|
|
77
|
-
`--sender "Christian Reyes"`,
|
|
77
|
+
`--sender "Christian Reyes"`, exact-date selectors such as
|
|
78
|
+
`--target-date 2026-06-30`, optional through-date selectors such as
|
|
78
79
|
`--until 2026-06-30`, or typed MCP calls to `refill_sends({ yolo, senders,
|
|
79
|
-
untilDate })
|
|
80
|
+
horizonSendDays, targetDate, untilDate })`. Date selectors resolve by
|
|
81
|
+
`targetDate > untilDate > horizonSendDays`, where `targetDate` means one exact
|
|
82
|
+
sender-local date and `untilDate` means an inclusive through-date, from:
|
|
80
83
|
|
|
81
84
|
- `mcp/sellable/skills/refill-sends/SKILL.md`
|
|
82
85
|
|
package/dist/index-dev.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type RefillDateWindowSource = "default_scheduler_forward" | "horizon_send_days" | "until_date" | "target_date";
|
|
2
|
+
export type NormalizedRefillDateSelector = {
|
|
3
|
+
source: RefillDateWindowSource;
|
|
4
|
+
targetDate: string | null;
|
|
5
|
+
untilDate: string | null;
|
|
6
|
+
horizonSendDays: number | null;
|
|
7
|
+
};
|
|
8
|
+
export type RefillRunDateWindow = NormalizedRefillDateSelector & {
|
|
9
|
+
version: 1;
|
|
10
|
+
selectedDates: string[];
|
|
11
|
+
senderWindows?: Array<{
|
|
12
|
+
senderId: string | null;
|
|
13
|
+
timeZone: string | null;
|
|
14
|
+
selectedDates: string[];
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
type DateSelectorInput = {
|
|
18
|
+
targetDate?: unknown;
|
|
19
|
+
untilDate?: unknown;
|
|
20
|
+
horizonSendDays?: unknown;
|
|
21
|
+
};
|
|
22
|
+
export declare function normalizeRefillDateSelector(input: DateSelectorInput): NormalizedRefillDateSelector;
|
|
23
|
+
export declare function refillDateSelectorBody(selector: NormalizedRefillDateSelector): {
|
|
24
|
+
targetDate?: string;
|
|
25
|
+
untilDate?: string;
|
|
26
|
+
horizonSendDays?: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function dateWindowFromSelector(selector: NormalizedRefillDateSelector): RefillRunDateWindow;
|
|
29
|
+
export declare function dateWindowFromPlan(plan: Record<string, unknown>, selector: NormalizedRefillDateSelector): RefillRunDateWindow;
|
|
30
|
+
export declare function dateWindowFromRunState(runState: unknown): RefillRunDateWindow | null;
|
|
31
|
+
export declare function selectorFromDateWindow(window: RefillRunDateWindow): NormalizedRefillDateSelector;
|
|
32
|
+
export declare function dateSelectorMatchesWindow(selector: NormalizedRefillDateSelector, window: RefillRunDateWindow): boolean;
|
|
33
|
+
export declare function schedulerSweepTargetDates(window: RefillRunDateWindow): string[];
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function stringValue(value) {
|
|
5
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
6
|
+
}
|
|
7
|
+
function numberValue(value) {
|
|
8
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
9
|
+
}
|
|
10
|
+
function arrayValue(value) {
|
|
11
|
+
return Array.isArray(value) ? value : [];
|
|
12
|
+
}
|
|
13
|
+
function isValidDateKey(value) {
|
|
14
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
15
|
+
return false;
|
|
16
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
17
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
18
|
+
return (date.getUTCFullYear() === year &&
|
|
19
|
+
date.getUTCMonth() === month - 1 &&
|
|
20
|
+
date.getUTCDate() === day);
|
|
21
|
+
}
|
|
22
|
+
function normalizeDateKey(value, field) {
|
|
23
|
+
if (value === undefined || value === null || value === "")
|
|
24
|
+
return null;
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
throw new Error(`${field} must be a string in YYYY-MM-DD format.`);
|
|
27
|
+
}
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!isValidDateKey(trimmed)) {
|
|
30
|
+
throw new Error(`${field} must be a valid YYYY-MM-DD calendar date.`);
|
|
31
|
+
}
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
function normalizeHorizonSendDays(value) {
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
if (typeof value !== "number" ||
|
|
38
|
+
!Number.isInteger(value) ||
|
|
39
|
+
value < 1 ||
|
|
40
|
+
value > 7) {
|
|
41
|
+
throw new Error("horizonSendDays must be an integer between 1 and 7.");
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
export function normalizeRefillDateSelector(input) {
|
|
46
|
+
const targetDate = normalizeDateKey(input.targetDate, "targetDate");
|
|
47
|
+
const untilDate = normalizeDateKey(input.untilDate, "untilDate");
|
|
48
|
+
const horizonSendDays = normalizeHorizonSendDays(input.horizonSendDays);
|
|
49
|
+
if (targetDate) {
|
|
50
|
+
return {
|
|
51
|
+
source: "target_date",
|
|
52
|
+
targetDate,
|
|
53
|
+
untilDate: null,
|
|
54
|
+
horizonSendDays: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (untilDate) {
|
|
58
|
+
return {
|
|
59
|
+
source: "until_date",
|
|
60
|
+
targetDate: null,
|
|
61
|
+
untilDate,
|
|
62
|
+
horizonSendDays: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (horizonSendDays !== null) {
|
|
66
|
+
return {
|
|
67
|
+
source: "horizon_send_days",
|
|
68
|
+
targetDate: null,
|
|
69
|
+
untilDate: null,
|
|
70
|
+
horizonSendDays,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
source: "default_scheduler_forward",
|
|
75
|
+
targetDate: null,
|
|
76
|
+
untilDate: null,
|
|
77
|
+
horizonSendDays: null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function refillDateSelectorBody(selector) {
|
|
81
|
+
if (selector.source === "target_date" && selector.targetDate) {
|
|
82
|
+
return { targetDate: selector.targetDate };
|
|
83
|
+
}
|
|
84
|
+
if (selector.source === "until_date" && selector.untilDate) {
|
|
85
|
+
return { untilDate: selector.untilDate };
|
|
86
|
+
}
|
|
87
|
+
if (selector.source === "horizon_send_days" &&
|
|
88
|
+
selector.horizonSendDays !== null) {
|
|
89
|
+
return { horizonSendDays: selector.horizonSendDays };
|
|
90
|
+
}
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
export function dateWindowFromSelector(selector) {
|
|
94
|
+
return {
|
|
95
|
+
version: 1,
|
|
96
|
+
...selector,
|
|
97
|
+
selectedDates: selector.targetDate ? [selector.targetDate] : [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function selectedDatesFromPlan(plan) {
|
|
101
|
+
const packet = isRecord(plan.packet) ? plan.packet : {};
|
|
102
|
+
const target = isRecord(packet.target) ? packet.target : {};
|
|
103
|
+
const window = isRecord(target.window) ? target.window : {};
|
|
104
|
+
const senderWindows = arrayValue(window.senderWindows).filter(isRecord);
|
|
105
|
+
const fromWindows = senderWindows.flatMap((senderWindow) => arrayValue(senderWindow.selectedDates)
|
|
106
|
+
.map((entry) => stringValue(entry))
|
|
107
|
+
.filter((entry) => Boolean(entry)));
|
|
108
|
+
if (fromWindows.length > 0)
|
|
109
|
+
return Array.from(new Set(fromWindows)).sort();
|
|
110
|
+
const senderRefillPlans = arrayValue(target.senderRefillPlans).filter(isRecord);
|
|
111
|
+
return Array.from(new Set(senderRefillPlans.flatMap((senderPlan) => {
|
|
112
|
+
const horizon = isRecord(senderPlan.horizon) ? senderPlan.horizon : {};
|
|
113
|
+
return arrayValue(horizon.selectedDays)
|
|
114
|
+
.filter(isRecord)
|
|
115
|
+
.map((day) => stringValue(day.date))
|
|
116
|
+
.filter((entry) => Boolean(entry));
|
|
117
|
+
}))).sort();
|
|
118
|
+
}
|
|
119
|
+
function senderWindowsFromPlan(plan) {
|
|
120
|
+
const packet = isRecord(plan.packet) ? plan.packet : {};
|
|
121
|
+
const target = isRecord(packet.target) ? packet.target : {};
|
|
122
|
+
const window = isRecord(target.window) ? target.window : {};
|
|
123
|
+
return arrayValue(window.senderWindows)
|
|
124
|
+
.filter(isRecord)
|
|
125
|
+
.map((senderWindow) => ({
|
|
126
|
+
senderId: stringValue(senderWindow.senderId),
|
|
127
|
+
timeZone: stringValue(senderWindow.timeZone),
|
|
128
|
+
selectedDates: arrayValue(senderWindow.selectedDates)
|
|
129
|
+
.map((entry) => stringValue(entry))
|
|
130
|
+
.filter((entry) => Boolean(entry)),
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
export function dateWindowFromPlan(plan, selector) {
|
|
134
|
+
const packet = isRecord(plan.packet) ? plan.packet : {};
|
|
135
|
+
const request = isRecord(packet.request) ? packet.request : {};
|
|
136
|
+
const targetDate = stringValue(request.targetDate) ?? selector.targetDate ?? null;
|
|
137
|
+
const untilDate = stringValue(request.untilDate) ?? selector.untilDate ?? null;
|
|
138
|
+
const horizonSendDays = numberValue(request.horizonSendDays) ?? selector.horizonSendDays ?? null;
|
|
139
|
+
const source = targetDate
|
|
140
|
+
? "target_date"
|
|
141
|
+
: untilDate
|
|
142
|
+
? "until_date"
|
|
143
|
+
: horizonSendDays !== null
|
|
144
|
+
? "horizon_send_days"
|
|
145
|
+
: selector.source;
|
|
146
|
+
const selectedDates = selectedDatesFromPlan(plan);
|
|
147
|
+
return {
|
|
148
|
+
version: 1,
|
|
149
|
+
source,
|
|
150
|
+
targetDate: source === "target_date" ? targetDate : null,
|
|
151
|
+
untilDate: source === "until_date" ? untilDate : null,
|
|
152
|
+
horizonSendDays: source === "horizon_send_days" ? horizonSendDays : null,
|
|
153
|
+
selectedDates: selectedDates.length > 0
|
|
154
|
+
? selectedDates
|
|
155
|
+
: targetDate
|
|
156
|
+
? [targetDate]
|
|
157
|
+
: [],
|
|
158
|
+
senderWindows: senderWindowsFromPlan(plan),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export function dateWindowFromRunState(runState) {
|
|
162
|
+
const state = isRecord(runState) ? runState : {};
|
|
163
|
+
const raw = isRecord(state.dateWindow) ? state.dateWindow : null;
|
|
164
|
+
if (!raw || raw.version !== 1)
|
|
165
|
+
return null;
|
|
166
|
+
const source = raw.source === "target_date" ||
|
|
167
|
+
raw.source === "until_date" ||
|
|
168
|
+
raw.source === "horizon_send_days" ||
|
|
169
|
+
raw.source === "default_scheduler_forward"
|
|
170
|
+
? raw.source
|
|
171
|
+
: null;
|
|
172
|
+
if (!source)
|
|
173
|
+
return null;
|
|
174
|
+
return {
|
|
175
|
+
version: 1,
|
|
176
|
+
source,
|
|
177
|
+
targetDate: source === "target_date" ? stringValue(raw.targetDate) : null,
|
|
178
|
+
untilDate: source === "until_date" ? stringValue(raw.untilDate) : null,
|
|
179
|
+
horizonSendDays: source === "horizon_send_days"
|
|
180
|
+
? numberValue(raw.horizonSendDays)
|
|
181
|
+
: null,
|
|
182
|
+
selectedDates: arrayValue(raw.selectedDates)
|
|
183
|
+
.map((entry) => stringValue(entry))
|
|
184
|
+
.filter((entry) => Boolean(entry)),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export function selectorFromDateWindow(window) {
|
|
188
|
+
return {
|
|
189
|
+
source: window.source,
|
|
190
|
+
targetDate: window.targetDate,
|
|
191
|
+
untilDate: window.untilDate,
|
|
192
|
+
horizonSendDays: window.horizonSendDays,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export function dateSelectorMatchesWindow(selector, window) {
|
|
196
|
+
return (selector.source === window.source &&
|
|
197
|
+
selector.targetDate === window.targetDate &&
|
|
198
|
+
selector.untilDate === window.untilDate &&
|
|
199
|
+
selector.horizonSendDays === window.horizonSendDays);
|
|
200
|
+
}
|
|
201
|
+
export function schedulerSweepTargetDates(window) {
|
|
202
|
+
if (window.source === "target_date" && window.targetDate) {
|
|
203
|
+
return [window.targetDate];
|
|
204
|
+
}
|
|
205
|
+
if (window.source === "until_date" ||
|
|
206
|
+
window.source === "horizon_send_days") {
|
|
207
|
+
return window.selectedDates;
|
|
208
|
+
}
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
@@ -13,6 +13,9 @@ export interface RefillV2LoopInput {
|
|
|
13
13
|
};
|
|
14
14
|
approvalMode?: "approve" | "mark_ready";
|
|
15
15
|
dryRun?: boolean;
|
|
16
|
+
targetDate?: string | null;
|
|
17
|
+
untilDate?: string | null;
|
|
18
|
+
horizonSendDays?: number | null;
|
|
16
19
|
}
|
|
17
20
|
export interface RefillV2LoopBudgets {
|
|
18
21
|
maxGateCyclesPerInvocation: number;
|
|
@@ -60,6 +63,9 @@ export interface RefillV2LoopDeps {
|
|
|
60
63
|
intent?: string;
|
|
61
64
|
senderIds?: string[];
|
|
62
65
|
runState?: Record<string, unknown>;
|
|
66
|
+
targetDate?: string;
|
|
67
|
+
untilDate?: string;
|
|
68
|
+
horizonSendDays?: number;
|
|
63
69
|
journal?: boolean;
|
|
64
70
|
}) => Promise<Record<string, unknown>>;
|
|
65
71
|
executors: RefillV2Executors;
|
|
@@ -68,7 +74,9 @@ export interface RefillV2LoopDeps {
|
|
|
68
74
|
budgets?: Partial<RefillV2LoopBudgets>;
|
|
69
75
|
now?: () => Date;
|
|
70
76
|
sleep?: (ms: number) => Promise<void>;
|
|
71
|
-
requestSchedulerRun?: (workspaceId: string
|
|
77
|
+
requestSchedulerRun?: (workspaceId: string, options?: {
|
|
78
|
+
targetDate?: string;
|
|
79
|
+
}) => Promise<unknown>;
|
|
72
80
|
}
|
|
73
81
|
export type RefillV2LoopResult = {
|
|
74
82
|
status: "dry_run";
|
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,
|
|
@@ -379,9 +380,16 @@ function firstAction(plan) {
|
|
|
379
380
|
}
|
|
380
381
|
function doneReasonFromPlan(plan) {
|
|
381
382
|
const action = firstAction(plan);
|
|
383
|
+
const packet = recordValue(plan.packet) ?? {};
|
|
384
|
+
const status = stringValue(plan.status) ?? stringValue(packet.status);
|
|
382
385
|
return (stringValue(action?.doneReason) ??
|
|
383
|
-
stringValue(
|
|
384
|
-
stringValue(plan.doneReason)
|
|
386
|
+
stringValue(packet.doneReason) ??
|
|
387
|
+
stringValue(plan.doneReason) ??
|
|
388
|
+
(status === "complete" ? "complete" : null) ??
|
|
389
|
+
(status === "awaiting_scheduler_after_ready_buffer"
|
|
390
|
+
? "loaded_awaiting_scheduler"
|
|
391
|
+
: null) ??
|
|
392
|
+
(status === "blocked" ? "blocked" : null));
|
|
385
393
|
}
|
|
386
394
|
function selectedLaneAuthority(plan, action) {
|
|
387
395
|
const senderId = actionSenderId(action);
|
|
@@ -417,6 +425,41 @@ function mergeRunState(current, updates) {
|
|
|
417
425
|
...updates,
|
|
418
426
|
};
|
|
419
427
|
}
|
|
428
|
+
function dateSelectorFromInput(input) {
|
|
429
|
+
return normalizeRefillDateSelector({
|
|
430
|
+
targetDate: input.targetDate,
|
|
431
|
+
untilDate: input.untilDate,
|
|
432
|
+
horizonSendDays: input.horizonSendDays,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function runStateWithDateWindow(runState, dateWindow) {
|
|
436
|
+
return mergeRunState(runState, { dateWindow });
|
|
437
|
+
}
|
|
438
|
+
function readPlanRequest(input) {
|
|
439
|
+
return {
|
|
440
|
+
workspaceId: input.workspaceId,
|
|
441
|
+
intent: input.intent,
|
|
442
|
+
senderIds: input.senderIds,
|
|
443
|
+
runState: input.runState,
|
|
444
|
+
journal: input.journal,
|
|
445
|
+
...refillDateSelectorBody(input.dateSelector),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function readLoopPlan(input, deps, ctx, journal) {
|
|
449
|
+
return deps.readPlan(readPlanRequest({
|
|
450
|
+
workspaceId: input.workspaceId,
|
|
451
|
+
intent: input.intent,
|
|
452
|
+
senderIds: input.senderIds,
|
|
453
|
+
runState: ctx.runState,
|
|
454
|
+
journal,
|
|
455
|
+
dateSelector: ctx.dateSelector,
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
function applyPlanDateWindow(ctx, plan) {
|
|
459
|
+
ctx.dateWindow = dateWindowFromPlan(plan, ctx.dateSelector);
|
|
460
|
+
ctx.dateSelector = selectorFromDateWindow(ctx.dateWindow);
|
|
461
|
+
ctx.runState = runStateWithDateWindow(ctx.runState, ctx.dateWindow);
|
|
462
|
+
}
|
|
420
463
|
function normalizeDoneReason(value) {
|
|
421
464
|
const raw = stringValue(value);
|
|
422
465
|
if (!raw)
|
|
@@ -670,14 +713,63 @@ async function createJournal(input, deps, runId) {
|
|
|
670
713
|
return null;
|
|
671
714
|
}
|
|
672
715
|
}
|
|
673
|
-
async function
|
|
674
|
-
const
|
|
716
|
+
async function preflightResumeDateWindow(input, deps, requestedSelector) {
|
|
717
|
+
const requestedWindow = dateWindowFromSelector(requestedSelector);
|
|
718
|
+
if (!input.resume) {
|
|
719
|
+
return { ok: true, selector: requestedSelector, window: requestedWindow };
|
|
720
|
+
}
|
|
721
|
+
const status = await deps.runClient.refillRunStatus({
|
|
722
|
+
workspaceId: input.workspaceId,
|
|
723
|
+
runId: input.resume.runId,
|
|
724
|
+
});
|
|
725
|
+
if (hasBlocker(status, "lease_lost")) {
|
|
726
|
+
return {
|
|
727
|
+
status: "blocked",
|
|
728
|
+
blocker: "lease_lost",
|
|
729
|
+
runId: input.resume.runId,
|
|
730
|
+
fence: input.resume.fence,
|
|
731
|
+
guidance: "Refill run lease was lost before resume date-window preflight.",
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const storedWindow = dateWindowFromRunState(recordValue(status)?.runState);
|
|
735
|
+
const explicitSelector = requestedSelector.source !== "default_scheduler_forward";
|
|
736
|
+
if (!storedWindow) {
|
|
737
|
+
if (explicitSelector) {
|
|
738
|
+
return {
|
|
739
|
+
status: "blocked",
|
|
740
|
+
blocker: "date_window_conflict",
|
|
741
|
+
runId: input.resume.runId,
|
|
742
|
+
fence: input.resume.fence,
|
|
743
|
+
guidance: "This legacy refill_sends_v2 run has no stored date window. Resume it without a new date selector or start a new run.",
|
|
744
|
+
report: { requestedDateWindow: requestedWindow },
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return { ok: true, selector: requestedSelector, window: requestedWindow };
|
|
748
|
+
}
|
|
749
|
+
if (explicitSelector && !dateSelectorMatchesWindow(requestedSelector, storedWindow)) {
|
|
750
|
+
return {
|
|
751
|
+
status: "blocked",
|
|
752
|
+
blocker: "date_window_conflict",
|
|
753
|
+
runId: input.resume.runId,
|
|
754
|
+
fence: input.resume.fence,
|
|
755
|
+
guidance: "Resume date selector conflicts with the stored refill run date window.",
|
|
756
|
+
report: { requestedDateWindow: requestedWindow, storedDateWindow: storedWindow },
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const selector = explicitSelector
|
|
760
|
+
? requestedSelector
|
|
761
|
+
: selectorFromDateWindow(storedWindow);
|
|
762
|
+
return { ok: true, selector, window: storedWindow };
|
|
763
|
+
}
|
|
764
|
+
async function startOrResume(input, deps, dateWindow) {
|
|
765
|
+
const initialRunState = runStateWithDateWindow(buildRunStateFromLocalHints(input.workspaceId), dateWindow);
|
|
675
766
|
const started = await deps.runClient.startRefillRunRemote({
|
|
676
767
|
workspaceId: input.workspaceId,
|
|
677
768
|
config: {
|
|
678
769
|
intent: input.intent ?? "auto",
|
|
679
770
|
senderIds: input.senderIds,
|
|
680
771
|
approvalMode: input.approvalMode ?? "approve",
|
|
772
|
+
dateWindow,
|
|
681
773
|
},
|
|
682
774
|
runState: initialRunState,
|
|
683
775
|
resume: input.resume,
|
|
@@ -719,6 +811,8 @@ async function startOrResume(input, deps) {
|
|
|
719
811
|
laneCooldowns: [],
|
|
720
812
|
passRates: [],
|
|
721
813
|
};
|
|
814
|
+
const storedDateWindow = dateWindowFromRunState(runState) ?? dateWindow;
|
|
815
|
+
const storedDateSelector = selectorFromDateWindow(storedDateWindow);
|
|
722
816
|
const journalPath = await createJournal(input, deps, runId);
|
|
723
817
|
const ctx = {
|
|
724
818
|
workspaceId: input.workspaceId,
|
|
@@ -731,6 +825,8 @@ async function startOrResume(input, deps) {
|
|
|
731
825
|
plan: null,
|
|
732
826
|
headAction: null,
|
|
733
827
|
lastFingerprint: null,
|
|
828
|
+
dateSelector: storedDateSelector,
|
|
829
|
+
dateWindow: storedDateWindow,
|
|
734
830
|
recoveryPoint: recordValue(result.recoveryPoint),
|
|
735
831
|
gateCycles: 0,
|
|
736
832
|
sleptMs: 0,
|
|
@@ -749,45 +845,31 @@ async function startOrResume(input, deps) {
|
|
|
749
845
|
return ctx;
|
|
750
846
|
}
|
|
751
847
|
async function gateBootstrap(input, deps, ctx) {
|
|
752
|
-
const plan = await deps
|
|
753
|
-
|
|
754
|
-
intent: input.intent,
|
|
755
|
-
senderIds: input.senderIds,
|
|
756
|
-
runState: ctx.runState,
|
|
757
|
-
journal: false,
|
|
758
|
-
});
|
|
848
|
+
const plan = await readLoopPlan(input, deps, ctx, false);
|
|
849
|
+
applyPlanDateWindow(ctx, plan);
|
|
759
850
|
const paidCredit = arrayValue(recordValue(plan.bootstrap)?.paidCredit).filter(isRecord);
|
|
760
851
|
const refreshedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.senderIds)
|
|
761
852
|
.map((entry) => stringValue(entry))
|
|
762
853
|
.filter((entry) => Boolean(entry)));
|
|
763
|
-
const attemptedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.attemptedSenderIds)
|
|
764
|
-
.map((entry) => stringValue(entry))
|
|
765
|
-
.filter((entry) => Boolean(entry)));
|
|
766
854
|
const receipts = [];
|
|
767
855
|
for (const entry of paidCredit) {
|
|
768
856
|
const senderId = stringValue(entry.senderId);
|
|
769
857
|
if (!senderId ||
|
|
770
858
|
!entry.plannedJitRefresh ||
|
|
771
|
-
refreshedSenderIds.has(senderId)
|
|
772
|
-
attemptedSenderIds.has(senderId)) {
|
|
859
|
+
refreshedSenderIds.has(senderId)) {
|
|
773
860
|
continue;
|
|
774
861
|
}
|
|
775
862
|
const receipt = await deps.executors.refreshPaidInmailCreditsWithRetry(senderId, input.workspaceId);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
receipts.push({ senderId, receipt, classification });
|
|
779
|
-
if (classification.usableCurrentFacts) {
|
|
780
|
-
refreshedSenderIds.add(senderId);
|
|
781
|
-
}
|
|
863
|
+
receipts.push({ senderId, receipt });
|
|
864
|
+
refreshedSenderIds.add(senderId);
|
|
782
865
|
}
|
|
783
|
-
const nextRunState = mergeRunState(ctx.runState, {
|
|
866
|
+
const nextRunState = runStateWithDateWindow(mergeRunState(ctx.runState, {
|
|
784
867
|
creditTrust: {
|
|
785
868
|
refreshedAt: (deps.now?.() ?? new Date()).toISOString(),
|
|
786
869
|
senderIds: [...refreshedSenderIds],
|
|
787
|
-
attemptedSenderIds: [...attemptedSenderIds],
|
|
788
870
|
receipts,
|
|
789
871
|
},
|
|
790
|
-
});
|
|
872
|
+
}), ctx.dateWindow);
|
|
791
873
|
await safeJournalAppend(ctx, deps, deps.journal.renderBootstrapSection({
|
|
792
874
|
summary: "Refill v2 execution bootstrap",
|
|
793
875
|
senderSummary: safeJson(recordValue(plan.bootstrap)?.senders ?? []),
|
|
@@ -895,13 +977,8 @@ async function completeTerminal(input, deps, ctx, doneReasonInput, reportInput =
|
|
|
895
977
|
};
|
|
896
978
|
}
|
|
897
979
|
async function gatePlan(input, deps, ctx) {
|
|
898
|
-
const plan = await deps
|
|
899
|
-
|
|
900
|
-
intent: input.intent,
|
|
901
|
-
senderIds: input.senderIds,
|
|
902
|
-
runState: ctx.runState,
|
|
903
|
-
journal: false,
|
|
904
|
-
});
|
|
980
|
+
const plan = await readLoopPlan(input, deps, ctx, false);
|
|
981
|
+
applyPlanDateWindow(ctx, plan);
|
|
905
982
|
if (plan.blocker === "workspace_access") {
|
|
906
983
|
return {
|
|
907
984
|
status: "blocked",
|
|
@@ -1004,13 +1081,7 @@ async function verifyRecoveryPoint(input, deps, ctx) {
|
|
|
1004
1081
|
stateRevision: stringValue(planned.stateRevision),
|
|
1005
1082
|
targetShapeRevision: stringValue(planned.targetShapeRevision),
|
|
1006
1083
|
};
|
|
1007
|
-
const fresh = await deps
|
|
1008
|
-
workspaceId: input.workspaceId,
|
|
1009
|
-
intent: input.intent,
|
|
1010
|
-
senderIds: input.senderIds,
|
|
1011
|
-
runState: ctx.runState,
|
|
1012
|
-
journal: false,
|
|
1013
|
-
});
|
|
1084
|
+
const fresh = await readLoopPlan(input, deps, ctx, false);
|
|
1014
1085
|
const changed = fingerprintChanged(expected, fingerprintFromPlan(fresh));
|
|
1015
1086
|
if (changed.length > 0) {
|
|
1016
1087
|
await deps.runClient.recordRunOutcome({
|
|
@@ -1272,13 +1343,8 @@ async function verifyPrepareAction(input, deps, ctx, action, budgets) {
|
|
|
1272
1343
|
}
|
|
1273
1344
|
async function verifyReadOnlyWait(input, deps, ctx, budgets, waitKind) {
|
|
1274
1345
|
for (let poll = 0; poll < budgets.maxPollsPerWait; poll += 1) {
|
|
1275
|
-
const fresh = await deps
|
|
1276
|
-
|
|
1277
|
-
intent: input.intent,
|
|
1278
|
-
senderIds: input.senderIds,
|
|
1279
|
-
runState: ctx.runState,
|
|
1280
|
-
journal: false,
|
|
1281
|
-
});
|
|
1346
|
+
const fresh = await readLoopPlan(input, deps, ctx, false);
|
|
1347
|
+
applyPlanDateWindow(ctx, fresh);
|
|
1282
1348
|
ctx.plan = fresh;
|
|
1283
1349
|
const doneReason = doneReasonFromPlan(fresh);
|
|
1284
1350
|
if (doneReason) {
|
|
@@ -1310,31 +1376,30 @@ async function verifySchedulerWait(input, deps, ctx, action, budgets) {
|
|
|
1310
1376
|
if (!jitFired) {
|
|
1311
1377
|
const senderId = actionSenderId(action);
|
|
1312
1378
|
if (senderId) {
|
|
1313
|
-
|
|
1314
|
-
const classification = classifyPaidInmailRefreshReceipt(refresh.receipt);
|
|
1315
|
-
if (!classification.usableCurrentFacts) {
|
|
1316
|
-
return {
|
|
1317
|
-
status: "blocked",
|
|
1318
|
-
blocker: "paid_inmail_refresh_failed",
|
|
1319
|
-
runId: ctx.runId,
|
|
1320
|
-
fence: ctx.fence,
|
|
1321
|
-
gate: ctx.gate,
|
|
1322
|
-
guidance: "Paid InMail credit refresh did not return usable current facts, so scheduler JIT was not requested.",
|
|
1323
|
-
report: {
|
|
1324
|
-
senderId,
|
|
1325
|
-
receiptStatus: classification.status,
|
|
1326
|
-
error: classification.error,
|
|
1327
|
-
receipt: refresh.receipt,
|
|
1328
|
-
attempts: refresh.attempts,
|
|
1329
|
-
errors: refresh.errors,
|
|
1330
|
-
},
|
|
1331
|
-
journalPath: ctx.journalPath,
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1379
|
+
await deps.executors.refreshPaidInmailCreditsWithRetry(senderId, input.workspaceId);
|
|
1334
1380
|
}
|
|
1335
1381
|
if (deps.requestSchedulerRun) {
|
|
1336
1382
|
try {
|
|
1337
|
-
|
|
1383
|
+
const targetDates = schedulerSweepTargetDates(ctx.dateWindow);
|
|
1384
|
+
const sweepReceipts = [];
|
|
1385
|
+
if (targetDates.length === 0) {
|
|
1386
|
+
schedulerRunReceipt = sanitizeSchedulerRunReceipt(await deps.requestSchedulerRun(input.workspaceId));
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
for (const targetDate of targetDates) {
|
|
1390
|
+
const receipt = sanitizeSchedulerRunReceipt(await deps.requestSchedulerRun(input.workspaceId, { targetDate }));
|
|
1391
|
+
if (receipt)
|
|
1392
|
+
sweepReceipts.push({ targetDate, ...receipt });
|
|
1393
|
+
}
|
|
1394
|
+
schedulerRunReceipt = sweepReceipts[0] ?? null;
|
|
1395
|
+
}
|
|
1396
|
+
if (sweepReceipts.length > 1) {
|
|
1397
|
+
ctx.runState = mergeRunState(ctx.runState, {
|
|
1398
|
+
progress: mergeProgress(ctx, {
|
|
1399
|
+
schedulerRunReceipts: sweepReceipts,
|
|
1400
|
+
}),
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1338
1403
|
}
|
|
1339
1404
|
catch {
|
|
1340
1405
|
schedulerRunReceipt = null;
|
|
@@ -1354,13 +1419,8 @@ async function verifySchedulerWait(input, deps, ctx, action, budgets) {
|
|
|
1354
1419
|
// full wait budget when the fresh receipt says nothing was placeable now.
|
|
1355
1420
|
const readbackBudget = zeroScheduledFresh ? 1 : budgets.maxSchedulerReadbacks;
|
|
1356
1421
|
for (let poll = 0; poll < readbackBudget; poll += 1) {
|
|
1357
|
-
const fresh = await deps
|
|
1358
|
-
|
|
1359
|
-
intent: input.intent,
|
|
1360
|
-
senderIds: input.senderIds,
|
|
1361
|
-
runState: ctx.runState,
|
|
1362
|
-
journal: false,
|
|
1363
|
-
});
|
|
1422
|
+
const fresh = await readLoopPlan(input, deps, ctx, false);
|
|
1423
|
+
applyPlanDateWindow(ctx, fresh);
|
|
1364
1424
|
ctx.plan = fresh;
|
|
1365
1425
|
const doneReason = normalizeDoneReason(doneReasonFromPlan(fresh));
|
|
1366
1426
|
if (doneReason === "complete" || doneReason === "capped_by_scheduler") {
|
|
@@ -1416,13 +1476,8 @@ async function gateVerify(input, deps, ctx, budgets) {
|
|
|
1416
1476
|
actionType === "wait_for_source_import") {
|
|
1417
1477
|
return verifyReadOnlyWait(input, deps, ctx, budgets, actionType);
|
|
1418
1478
|
}
|
|
1419
|
-
const fresh = await deps
|
|
1420
|
-
|
|
1421
|
-
intent: input.intent,
|
|
1422
|
-
senderIds: input.senderIds,
|
|
1423
|
-
runState: ctx.runState,
|
|
1424
|
-
journal: false,
|
|
1425
|
-
});
|
|
1479
|
+
const fresh = await readLoopPlan(input, deps, ctx, false);
|
|
1480
|
+
applyPlanDateWindow(ctx, fresh);
|
|
1426
1481
|
ctx.plan = fresh;
|
|
1427
1482
|
const doneReason = doneReasonFromPlan(fresh);
|
|
1428
1483
|
if (doneReason) {
|
|
@@ -1495,15 +1550,21 @@ export async function runRefillV2Loop(input, deps) {
|
|
|
1495
1550
|
let ctx = null;
|
|
1496
1551
|
try {
|
|
1497
1552
|
if (input.dryRun) {
|
|
1498
|
-
const
|
|
1553
|
+
const dateSelector = dateSelectorFromInput(input);
|
|
1554
|
+
const plan = await deps.readPlan(readPlanRequest({
|
|
1499
1555
|
workspaceId: input.workspaceId,
|
|
1500
1556
|
intent: input.intent,
|
|
1501
1557
|
senderIds: input.senderIds,
|
|
1502
1558
|
journal: true,
|
|
1503
|
-
|
|
1559
|
+
dateSelector,
|
|
1560
|
+
}));
|
|
1504
1561
|
return { status: "dry_run", plan };
|
|
1505
1562
|
}
|
|
1506
|
-
const
|
|
1563
|
+
const requestedSelector = dateSelectorFromInput(input);
|
|
1564
|
+
const preflight = await preflightResumeDateWindow(input, deps, requestedSelector);
|
|
1565
|
+
if ("status" in preflight)
|
|
1566
|
+
return preflight;
|
|
1567
|
+
const started = await startOrResume(input, deps, preflight.window);
|
|
1507
1568
|
if ("status" in started)
|
|
1508
1569
|
return started;
|
|
1509
1570
|
ctx = started;
|