@sellable/mcp 0.1.534 → 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/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 +133 -50
- package/dist/tools/evergreen-refill-plan.d.ts +19 -0
- package/dist/tools/evergreen-refill-plan.js +19 -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.js +10 -6
- 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/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
|
|
|
@@ -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,4 +1,5 @@
|
|
|
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
4
|
import { prepareRowSelectorValue as defaultPrepareRowSelectorValue, refillPrepareRequestHash as defaultRefillPrepareRequestHash, } from "./tools/refill-executors.js";
|
|
4
5
|
const DEFAULT_BUDGETS = {
|
|
@@ -417,6 +418,41 @@ function mergeRunState(current, updates) {
|
|
|
417
418
|
...updates,
|
|
418
419
|
};
|
|
419
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
|
+
}
|
|
420
456
|
function normalizeDoneReason(value) {
|
|
421
457
|
const raw = stringValue(value);
|
|
422
458
|
if (!raw)
|
|
@@ -670,14 +706,63 @@ async function createJournal(input, deps, runId) {
|
|
|
670
706
|
return null;
|
|
671
707
|
}
|
|
672
708
|
}
|
|
673
|
-
async function
|
|
674
|
-
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);
|
|
675
759
|
const started = await deps.runClient.startRefillRunRemote({
|
|
676
760
|
workspaceId: input.workspaceId,
|
|
677
761
|
config: {
|
|
678
762
|
intent: input.intent ?? "auto",
|
|
679
763
|
senderIds: input.senderIds,
|
|
680
764
|
approvalMode: input.approvalMode ?? "approve",
|
|
765
|
+
dateWindow,
|
|
681
766
|
},
|
|
682
767
|
runState: initialRunState,
|
|
683
768
|
resume: input.resume,
|
|
@@ -719,6 +804,8 @@ async function startOrResume(input, deps) {
|
|
|
719
804
|
laneCooldowns: [],
|
|
720
805
|
passRates: [],
|
|
721
806
|
};
|
|
807
|
+
const storedDateWindow = dateWindowFromRunState(runState) ?? dateWindow;
|
|
808
|
+
const storedDateSelector = selectorFromDateWindow(storedDateWindow);
|
|
722
809
|
const journalPath = await createJournal(input, deps, runId);
|
|
723
810
|
const ctx = {
|
|
724
811
|
workspaceId: input.workspaceId,
|
|
@@ -731,6 +818,8 @@ async function startOrResume(input, deps) {
|
|
|
731
818
|
plan: null,
|
|
732
819
|
headAction: null,
|
|
733
820
|
lastFingerprint: null,
|
|
821
|
+
dateSelector: storedDateSelector,
|
|
822
|
+
dateWindow: storedDateWindow,
|
|
734
823
|
recoveryPoint: recordValue(result.recoveryPoint),
|
|
735
824
|
gateCycles: 0,
|
|
736
825
|
sleptMs: 0,
|
|
@@ -749,13 +838,8 @@ async function startOrResume(input, deps) {
|
|
|
749
838
|
return ctx;
|
|
750
839
|
}
|
|
751
840
|
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
|
-
});
|
|
841
|
+
const plan = await readLoopPlan(input, deps, ctx, false);
|
|
842
|
+
applyPlanDateWindow(ctx, plan);
|
|
759
843
|
const paidCredit = arrayValue(recordValue(plan.bootstrap)?.paidCredit).filter(isRecord);
|
|
760
844
|
const refreshedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.senderIds)
|
|
761
845
|
.map((entry) => stringValue(entry))
|
|
@@ -772,13 +856,13 @@ async function gateBootstrap(input, deps, ctx) {
|
|
|
772
856
|
receipts.push({ senderId, receipt });
|
|
773
857
|
refreshedSenderIds.add(senderId);
|
|
774
858
|
}
|
|
775
|
-
const nextRunState = mergeRunState(ctx.runState, {
|
|
859
|
+
const nextRunState = runStateWithDateWindow(mergeRunState(ctx.runState, {
|
|
776
860
|
creditTrust: {
|
|
777
861
|
refreshedAt: (deps.now?.() ?? new Date()).toISOString(),
|
|
778
862
|
senderIds: [...refreshedSenderIds],
|
|
779
863
|
receipts,
|
|
780
864
|
},
|
|
781
|
-
});
|
|
865
|
+
}), ctx.dateWindow);
|
|
782
866
|
await safeJournalAppend(ctx, deps, deps.journal.renderBootstrapSection({
|
|
783
867
|
summary: "Refill v2 execution bootstrap",
|
|
784
868
|
senderSummary: safeJson(recordValue(plan.bootstrap)?.senders ?? []),
|
|
@@ -886,13 +970,8 @@ async function completeTerminal(input, deps, ctx, doneReasonInput, reportInput =
|
|
|
886
970
|
};
|
|
887
971
|
}
|
|
888
972
|
async function gatePlan(input, deps, ctx) {
|
|
889
|
-
const plan = await deps
|
|
890
|
-
|
|
891
|
-
intent: input.intent,
|
|
892
|
-
senderIds: input.senderIds,
|
|
893
|
-
runState: ctx.runState,
|
|
894
|
-
journal: false,
|
|
895
|
-
});
|
|
973
|
+
const plan = await readLoopPlan(input, deps, ctx, false);
|
|
974
|
+
applyPlanDateWindow(ctx, plan);
|
|
896
975
|
if (plan.blocker === "workspace_access") {
|
|
897
976
|
return {
|
|
898
977
|
status: "blocked",
|
|
@@ -995,13 +1074,7 @@ async function verifyRecoveryPoint(input, deps, ctx) {
|
|
|
995
1074
|
stateRevision: stringValue(planned.stateRevision),
|
|
996
1075
|
targetShapeRevision: stringValue(planned.targetShapeRevision),
|
|
997
1076
|
};
|
|
998
|
-
const fresh = await deps
|
|
999
|
-
workspaceId: input.workspaceId,
|
|
1000
|
-
intent: input.intent,
|
|
1001
|
-
senderIds: input.senderIds,
|
|
1002
|
-
runState: ctx.runState,
|
|
1003
|
-
journal: false,
|
|
1004
|
-
});
|
|
1077
|
+
const fresh = await readLoopPlan(input, deps, ctx, false);
|
|
1005
1078
|
const changed = fingerprintChanged(expected, fingerprintFromPlan(fresh));
|
|
1006
1079
|
if (changed.length > 0) {
|
|
1007
1080
|
await deps.runClient.recordRunOutcome({
|
|
@@ -1263,13 +1336,8 @@ async function verifyPrepareAction(input, deps, ctx, action, budgets) {
|
|
|
1263
1336
|
}
|
|
1264
1337
|
async function verifyReadOnlyWait(input, deps, ctx, budgets, waitKind) {
|
|
1265
1338
|
for (let poll = 0; poll < budgets.maxPollsPerWait; poll += 1) {
|
|
1266
|
-
const fresh = await deps
|
|
1267
|
-
|
|
1268
|
-
intent: input.intent,
|
|
1269
|
-
senderIds: input.senderIds,
|
|
1270
|
-
runState: ctx.runState,
|
|
1271
|
-
journal: false,
|
|
1272
|
-
});
|
|
1339
|
+
const fresh = await readLoopPlan(input, deps, ctx, false);
|
|
1340
|
+
applyPlanDateWindow(ctx, fresh);
|
|
1273
1341
|
ctx.plan = fresh;
|
|
1274
1342
|
const doneReason = doneReasonFromPlan(fresh);
|
|
1275
1343
|
if (doneReason) {
|
|
@@ -1305,7 +1373,26 @@ async function verifySchedulerWait(input, deps, ctx, action, budgets) {
|
|
|
1305
1373
|
}
|
|
1306
1374
|
if (deps.requestSchedulerRun) {
|
|
1307
1375
|
try {
|
|
1308
|
-
|
|
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
|
+
}
|
|
1309
1396
|
}
|
|
1310
1397
|
catch {
|
|
1311
1398
|
schedulerRunReceipt = null;
|
|
@@ -1325,13 +1412,8 @@ async function verifySchedulerWait(input, deps, ctx, action, budgets) {
|
|
|
1325
1412
|
// full wait budget when the fresh receipt says nothing was placeable now.
|
|
1326
1413
|
const readbackBudget = zeroScheduledFresh ? 1 : budgets.maxSchedulerReadbacks;
|
|
1327
1414
|
for (let poll = 0; poll < readbackBudget; poll += 1) {
|
|
1328
|
-
const fresh = await deps
|
|
1329
|
-
|
|
1330
|
-
intent: input.intent,
|
|
1331
|
-
senderIds: input.senderIds,
|
|
1332
|
-
runState: ctx.runState,
|
|
1333
|
-
journal: false,
|
|
1334
|
-
});
|
|
1415
|
+
const fresh = await readLoopPlan(input, deps, ctx, false);
|
|
1416
|
+
applyPlanDateWindow(ctx, fresh);
|
|
1335
1417
|
ctx.plan = fresh;
|
|
1336
1418
|
const doneReason = normalizeDoneReason(doneReasonFromPlan(fresh));
|
|
1337
1419
|
if (doneReason === "complete" || doneReason === "capped_by_scheduler") {
|
|
@@ -1387,13 +1469,8 @@ async function gateVerify(input, deps, ctx, budgets) {
|
|
|
1387
1469
|
actionType === "wait_for_source_import") {
|
|
1388
1470
|
return verifyReadOnlyWait(input, deps, ctx, budgets, actionType);
|
|
1389
1471
|
}
|
|
1390
|
-
const fresh = await deps
|
|
1391
|
-
|
|
1392
|
-
intent: input.intent,
|
|
1393
|
-
senderIds: input.senderIds,
|
|
1394
|
-
runState: ctx.runState,
|
|
1395
|
-
journal: false,
|
|
1396
|
-
});
|
|
1472
|
+
const fresh = await readLoopPlan(input, deps, ctx, false);
|
|
1473
|
+
applyPlanDateWindow(ctx, fresh);
|
|
1397
1474
|
ctx.plan = fresh;
|
|
1398
1475
|
const doneReason = doneReasonFromPlan(fresh);
|
|
1399
1476
|
if (doneReason) {
|
|
@@ -1466,15 +1543,21 @@ export async function runRefillV2Loop(input, deps) {
|
|
|
1466
1543
|
let ctx = null;
|
|
1467
1544
|
try {
|
|
1468
1545
|
if (input.dryRun) {
|
|
1469
|
-
const
|
|
1546
|
+
const dateSelector = dateSelectorFromInput(input);
|
|
1547
|
+
const plan = await deps.readPlan(readPlanRequest({
|
|
1470
1548
|
workspaceId: input.workspaceId,
|
|
1471
1549
|
intent: input.intent,
|
|
1472
1550
|
senderIds: input.senderIds,
|
|
1473
1551
|
journal: true,
|
|
1474
|
-
|
|
1552
|
+
dateSelector,
|
|
1553
|
+
}));
|
|
1475
1554
|
return { status: "dry_run", plan };
|
|
1476
1555
|
}
|
|
1477
|
-
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);
|
|
1478
1561
|
if ("status" in started)
|
|
1479
1562
|
return started;
|
|
1480
1563
|
ctx = started;
|
|
@@ -3,6 +3,9 @@ type GetRefillPlanV2Input = {
|
|
|
3
3
|
intent?: "auto" | "evergreen" | "plain" | "active";
|
|
4
4
|
senderIds?: string[];
|
|
5
5
|
runState?: Record<string, unknown>;
|
|
6
|
+
targetDate?: string | null;
|
|
7
|
+
untilDate?: string | null;
|
|
8
|
+
horizonSendDays?: number | null;
|
|
6
9
|
journal?: boolean;
|
|
7
10
|
journalNote?: string;
|
|
8
11
|
};
|
|
@@ -38,6 +41,22 @@ export declare const refillPlanV2ToolDefinitions: {
|
|
|
38
41
|
enum: string[];
|
|
39
42
|
description: string;
|
|
40
43
|
};
|
|
44
|
+
targetDate: {
|
|
45
|
+
type: string;
|
|
46
|
+
pattern: string;
|
|
47
|
+
description: string;
|
|
48
|
+
};
|
|
49
|
+
untilDate: {
|
|
50
|
+
type: string;
|
|
51
|
+
pattern: string;
|
|
52
|
+
description: string;
|
|
53
|
+
};
|
|
54
|
+
horizonSendDays: {
|
|
55
|
+
type: string;
|
|
56
|
+
minimum: number;
|
|
57
|
+
maximum: number;
|
|
58
|
+
description: string;
|
|
59
|
+
};
|
|
41
60
|
journal: {
|
|
42
61
|
type: string;
|
|
43
62
|
description: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { getApi, SellableApiError } from "../api.js";
|
|
3
|
+
import { normalizeRefillDateSelector, refillDateSelectorBody, } from "../refill-date-window.js";
|
|
3
4
|
import { appendIndexLine, appendJournalEvent, createRunJournal, renderBootstrapSection, renderPlanSection, renderTerminalSection, } from "../refill-journal.js";
|
|
4
5
|
import { readRefillWorkspaceState } from "../refill-local-state.js";
|
|
5
6
|
import { normalizeExplicitWorkspaceId, workspaceRequestOptions, } from "./workspace-context.js";
|
|
@@ -175,6 +176,22 @@ export const refillPlanV2ToolDefinitions = [
|
|
|
175
176
|
enum: ["auto", "evergreen", "plain", "active"],
|
|
176
177
|
description: 'Planner intent. Defaults to "auto" for refill-sends-v2.',
|
|
177
178
|
},
|
|
179
|
+
targetDate: {
|
|
180
|
+
type: "string",
|
|
181
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
182
|
+
description: "Optional one exact sender-local date (YYYY-MM-DD) to plan. Takes precedence over untilDate and horizonSendDays.",
|
|
183
|
+
},
|
|
184
|
+
untilDate: {
|
|
185
|
+
type: "string",
|
|
186
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
187
|
+
description: "Optional inclusive sender-local through-date (YYYY-MM-DD) to plan.",
|
|
188
|
+
},
|
|
189
|
+
horizonSendDays: {
|
|
190
|
+
type: "number",
|
|
191
|
+
minimum: 1,
|
|
192
|
+
maximum: 7,
|
|
193
|
+
description: "Optional explicit next N sender-local send days to plan.",
|
|
194
|
+
},
|
|
178
195
|
journal: {
|
|
179
196
|
type: "boolean",
|
|
180
197
|
description: "Set false to skip the local dry-run journal write.",
|
|
@@ -197,6 +214,7 @@ export async function getRefillPlanV2(input) {
|
|
|
197
214
|
const runState = input.runState !== undefined
|
|
198
215
|
? input.runState
|
|
199
216
|
: buildRunStateFromLocalHints(workspaceId);
|
|
217
|
+
const dateSelector = normalizeRefillDateSelector(input);
|
|
200
218
|
let raw;
|
|
201
219
|
try {
|
|
202
220
|
raw = await postRefillPlanV2({
|
|
@@ -204,6 +222,7 @@ export async function getRefillPlanV2(input) {
|
|
|
204
222
|
intent: input.intent ?? "auto",
|
|
205
223
|
senderIds: input.senderIds,
|
|
206
224
|
runState,
|
|
225
|
+
...refillDateSelectorBody(dateSelector),
|
|
207
226
|
}, workspaceId);
|
|
208
227
|
}
|
|
209
228
|
catch (error) {
|
|
@@ -6,6 +6,9 @@ type RefillSendsV2Input = {
|
|
|
6
6
|
approvalMode?: "approve" | "mark_ready";
|
|
7
7
|
senderIds?: string[];
|
|
8
8
|
intent?: "auto" | "evergreen" | "plain" | "active";
|
|
9
|
+
targetDate?: string | null;
|
|
10
|
+
untilDate?: string | null;
|
|
11
|
+
horizonSendDays?: number | null;
|
|
9
12
|
};
|
|
10
13
|
export declare const refillSendsV2ToolDefinitions: {
|
|
11
14
|
name: string;
|
|
@@ -44,6 +47,22 @@ export declare const refillSendsV2ToolDefinitions: {
|
|
|
44
47
|
type: string;
|
|
45
48
|
enum: string[];
|
|
46
49
|
};
|
|
50
|
+
targetDate: {
|
|
51
|
+
type: string;
|
|
52
|
+
pattern: string;
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
untilDate: {
|
|
56
|
+
type: string;
|
|
57
|
+
pattern: string;
|
|
58
|
+
description: string;
|
|
59
|
+
};
|
|
60
|
+
horizonSendDays: {
|
|
61
|
+
type: string;
|
|
62
|
+
minimum: number;
|
|
63
|
+
maximum: number;
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
47
66
|
};
|
|
48
67
|
required: string[];
|
|
49
68
|
additionalProperties: boolean;
|
|
@@ -18,7 +18,7 @@ const BOUNDED_AUTHORITY = "Within the refill run, bounded approvals, preparation
|
|
|
18
18
|
export const refillSendsV2ToolDefinitions = [
|
|
19
19
|
{
|
|
20
20
|
name: "refill_sends_v2",
|
|
21
|
-
description: "Execute the refill sends v2 loop with run-record fencing, packet re-planning, shared refill executors, and resumable dry-run/real-run modes.",
|
|
21
|
+
description: "Execute the refill sends v2 loop with run-record fencing, packet re-planning, shared refill executors, and resumable dry-run/real-run modes. Date selectors match refill_sends: targetDate is one exact sender-local date, untilDate is inclusive, horizonSendDays is the next N sender-local send days, with precedence targetDate > untilDate > horizonSendDays > default scheduler-forward planning.",
|
|
22
22
|
inputSchema: {
|
|
23
23
|
type: "object",
|
|
24
24
|
properties: {
|
|
@@ -51,6 +51,22 @@ export const refillSendsV2ToolDefinitions = [
|
|
|
51
51
|
type: "string",
|
|
52
52
|
enum: ["auto", "evergreen", "plain", "active"],
|
|
53
53
|
},
|
|
54
|
+
targetDate: {
|
|
55
|
+
type: "string",
|
|
56
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
57
|
+
description: "Optional one exact sender-local date (YYYY-MM-DD) to refill.",
|
|
58
|
+
},
|
|
59
|
+
untilDate: {
|
|
60
|
+
type: "string",
|
|
61
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
62
|
+
description: "Optional inclusive sender-local through-date (YYYY-MM-DD) to refill.",
|
|
63
|
+
},
|
|
64
|
+
horizonSendDays: {
|
|
65
|
+
type: "number",
|
|
66
|
+
minimum: 1,
|
|
67
|
+
maximum: 7,
|
|
68
|
+
description: "Optional explicit next N sender-local send days to refill.",
|
|
69
|
+
},
|
|
54
70
|
},
|
|
55
71
|
required: ["workspaceId"],
|
|
56
72
|
additionalProperties: false,
|
|
@@ -99,6 +115,9 @@ export async function refillSendsV2Command(input) {
|
|
|
99
115
|
workspaceId,
|
|
100
116
|
intent: input.intent,
|
|
101
117
|
senderIds: input.senderIds,
|
|
118
|
+
targetDate: input.targetDate,
|
|
119
|
+
untilDate: input.untilDate,
|
|
120
|
+
horizonSendDays: input.horizonSendDays,
|
|
102
121
|
journal: true,
|
|
103
122
|
});
|
|
104
123
|
return {
|
|
@@ -113,6 +132,9 @@ export async function refillSendsV2Command(input) {
|
|
|
113
132
|
workspaceId,
|
|
114
133
|
intent: input.intent,
|
|
115
134
|
senderIds: input.senderIds,
|
|
135
|
+
targetDate: input.targetDate,
|
|
136
|
+
untilDate: input.untilDate,
|
|
137
|
+
horizonSendDays: input.horizonSendDays,
|
|
116
138
|
approvalMode: input.approvalMode ?? "approve",
|
|
117
139
|
resume: resumeHandle(input),
|
|
118
140
|
}, {
|
|
@@ -129,6 +151,9 @@ export async function refillSendsV2Command(input) {
|
|
|
129
151
|
workspaceId: planInput.workspaceId,
|
|
130
152
|
intent: planInput.intent,
|
|
131
153
|
senderIds: planInput.senderIds,
|
|
154
|
+
targetDate: planInput.targetDate,
|
|
155
|
+
untilDate: planInput.untilDate,
|
|
156
|
+
horizonSendDays: planInput.horizonSendDays,
|
|
132
157
|
runState: planInput.runState,
|
|
133
158
|
journal: planInput.journal,
|
|
134
159
|
}),
|
|
@@ -153,7 +178,11 @@ export async function refillSendsV2Command(input) {
|
|
|
153
178
|
localState: {
|
|
154
179
|
writeRefillWorkspaceState,
|
|
155
180
|
},
|
|
156
|
-
requestSchedulerRun: (workspaceId) => runSchedulerSweep({
|
|
181
|
+
requestSchedulerRun: (workspaceId, options) => runSchedulerSweep({
|
|
182
|
+
workspaceId,
|
|
183
|
+
action: "run",
|
|
184
|
+
targetDate: options?.targetDate,
|
|
185
|
+
}),
|
|
157
186
|
});
|
|
158
187
|
return {
|
|
159
188
|
...(await maybeAddLostFenceGuidance(result, { ...input, workspaceId })),
|
|
@@ -4,11 +4,15 @@ import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspa
|
|
|
4
4
|
const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
|
|
5
5
|
const MCP_PAID_INMAIL_CREDITS_MAX_STALENESS_SECONDS = 4 * 60 * 60;
|
|
6
6
|
function normalizeHorizonSendDays(value) {
|
|
7
|
-
if (value === undefined
|
|
7
|
+
if (value === undefined)
|
|
8
8
|
return null;
|
|
9
|
-
if (typeof value !== "number" ||
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
if (typeof value !== "number" ||
|
|
10
|
+
!Number.isInteger(value) ||
|
|
11
|
+
value < 1 ||
|
|
12
|
+
value > 7) {
|
|
13
|
+
throw new Error("horizonSendDays must be an integer between 1 and 7.");
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
12
16
|
}
|
|
13
17
|
function normalizeUntilDate(value) {
|
|
14
18
|
if (value === undefined || value === null || value === "")
|
|
@@ -91,7 +95,7 @@ export const refillSendsToolDefinitions = [
|
|
|
91
95
|
type: "number",
|
|
92
96
|
minimum: 1,
|
|
93
97
|
maximum: 7,
|
|
94
|
-
description: "Compatibility override for
|
|
98
|
+
description: "Compatibility override for explicit next N sender-local send days. Omit for the default scheduler-forward 48-hour target window.",
|
|
95
99
|
},
|
|
96
100
|
untilDate: {
|
|
97
101
|
type: "string",
|
|
@@ -184,7 +188,7 @@ export function refillSendsCommand(input = {}) {
|
|
|
184
188
|
horizonHours: horizonSendDays === null ? DEFAULT_SCHEDULER_FORWARD_HOURS : null,
|
|
185
189
|
description: horizonSendDays === null
|
|
186
190
|
? "Fill the default scheduler-forward 48-hour target window."
|
|
187
|
-
:
|
|
191
|
+
: `Fill the next ${horizonSendDays} sender-local send days.`,
|
|
188
192
|
},
|
|
189
193
|
approvalMode,
|
|
190
194
|
campaignId: input.campaignId ?? null,
|
|
@@ -1026,6 +1026,16 @@ function sanitizeRefillTargetPlanResult(result) {
|
|
|
1026
1026
|
mcpSanitizedRefillLanes: true,
|
|
1027
1027
|
};
|
|
1028
1028
|
}
|
|
1029
|
+
function stripInternalTargetDateKeys(value) {
|
|
1030
|
+
if (Array.isArray(value)) {
|
|
1031
|
+
return value.map((item) => stripInternalTargetDateKeys(item));
|
|
1032
|
+
}
|
|
1033
|
+
if (!isRecord(value))
|
|
1034
|
+
return value;
|
|
1035
|
+
return Object.fromEntries(Object.entries(value)
|
|
1036
|
+
.filter(([key]) => key !== "targetDateKey")
|
|
1037
|
+
.map(([key, nested]) => [key, stripInternalTargetDateKeys(nested)]));
|
|
1038
|
+
}
|
|
1029
1039
|
export const refillTargetPlanToolDefinitions = [
|
|
1030
1040
|
{
|
|
1031
1041
|
name: "get_refill_target_plan",
|
|
@@ -1042,7 +1052,7 @@ export const refillTargetPlanToolDefinitions = [
|
|
|
1042
1052
|
type: "number",
|
|
1043
1053
|
minimum: 1,
|
|
1044
1054
|
maximum: 7,
|
|
1045
|
-
description: "Compatibility override for explicit sender-local send
|
|
1055
|
+
description: "Compatibility override for explicit next N sender-local send days. Omit for the default scheduler-forward 48-hour target window; no-send days are skipped while choosing the requested send days.",
|
|
1046
1056
|
},
|
|
1047
1057
|
untilDate: {
|
|
1048
1058
|
type: "string",
|
|
@@ -1052,7 +1062,7 @@ export const refillTargetPlanToolDefinitions = [
|
|
|
1052
1062
|
targetDate: {
|
|
1053
1063
|
type: "string",
|
|
1054
1064
|
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
1055
|
-
description: "Optional exact sender-local YYYY-MM-DD
|
|
1065
|
+
description: "Optional one exact sender-local date (YYYY-MM-DD) to fill. Distinct from untilDate: targetDate asks how many scheduler-fillable slots exist on only this date.",
|
|
1056
1066
|
},
|
|
1057
1067
|
campaignId: {
|
|
1058
1068
|
type: "string",
|
|
@@ -1102,11 +1112,14 @@ export const refillTargetPlanToolDefinitions = [
|
|
|
1102
1112
|
];
|
|
1103
1113
|
export async function getRefillTargetPlan(input = {}) {
|
|
1104
1114
|
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
1115
|
+
const targetDate = input.targetDate;
|
|
1116
|
+
const untilDate = targetDate ? undefined : input.untilDate;
|
|
1117
|
+
const horizonSendDays = targetDate || untilDate ? undefined : input.horizonSendDays;
|
|
1105
1118
|
const result = await postRefillTargetPlan({
|
|
1106
1119
|
intent: input.intent,
|
|
1107
|
-
horizonSendDays
|
|
1108
|
-
untilDate
|
|
1109
|
-
targetDate
|
|
1120
|
+
horizonSendDays,
|
|
1121
|
+
untilDate,
|
|
1122
|
+
targetDate,
|
|
1110
1123
|
campaignId: input.campaignId,
|
|
1111
1124
|
tableId: input.tableId,
|
|
1112
1125
|
senderIds: input.senderIds,
|
|
@@ -1117,7 +1130,7 @@ export async function getRefillTargetPlan(input = {}) {
|
|
|
1117
1130
|
paidInmailCreditsMaxStalenessSeconds: MCP_PAID_INMAIL_CREDITS_MAX_STALENESS_SECONDS,
|
|
1118
1131
|
...(workspaceId ? { workspaceId } : {}),
|
|
1119
1132
|
}, workspaceId ?? undefined);
|
|
1120
|
-
const sanitized = sanitizeRefillTargetPlanResult(result);
|
|
1133
|
+
const sanitized = stripInternalTargetDateKeys(sanitizeRefillTargetPlanResult(result));
|
|
1121
1134
|
if (!workspaceId || !isRecord(sanitized))
|
|
1122
1135
|
return sanitized;
|
|
1123
1136
|
return {
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -2758,6 +2758,22 @@ export declare const allTools: ({
|
|
|
2758
2758
|
enum: string[];
|
|
2759
2759
|
description: string;
|
|
2760
2760
|
};
|
|
2761
|
+
targetDate: {
|
|
2762
|
+
type: string;
|
|
2763
|
+
pattern: string;
|
|
2764
|
+
description: string;
|
|
2765
|
+
};
|
|
2766
|
+
untilDate: {
|
|
2767
|
+
type: string;
|
|
2768
|
+
pattern: string;
|
|
2769
|
+
description: string;
|
|
2770
|
+
};
|
|
2771
|
+
horizonSendDays: {
|
|
2772
|
+
type: string;
|
|
2773
|
+
minimum: number;
|
|
2774
|
+
maximum: number;
|
|
2775
|
+
description: string;
|
|
2776
|
+
};
|
|
2761
2777
|
journal: {
|
|
2762
2778
|
type: string;
|
|
2763
2779
|
description: string;
|
|
@@ -7385,6 +7401,10 @@ export declare const allTools: ({
|
|
|
7385
7401
|
enum: string[];
|
|
7386
7402
|
description: string;
|
|
7387
7403
|
};
|
|
7404
|
+
targetDate: {
|
|
7405
|
+
type: string;
|
|
7406
|
+
description: string;
|
|
7407
|
+
};
|
|
7388
7408
|
};
|
|
7389
7409
|
required: string[];
|
|
7390
7410
|
additionalProperties: boolean;
|
|
@@ -7426,6 +7446,22 @@ export declare const allTools: ({
|
|
|
7426
7446
|
type: string;
|
|
7427
7447
|
enum: string[];
|
|
7428
7448
|
};
|
|
7449
|
+
targetDate: {
|
|
7450
|
+
type: string;
|
|
7451
|
+
pattern: string;
|
|
7452
|
+
description: string;
|
|
7453
|
+
};
|
|
7454
|
+
untilDate: {
|
|
7455
|
+
type: string;
|
|
7456
|
+
pattern: string;
|
|
7457
|
+
description: string;
|
|
7458
|
+
};
|
|
7459
|
+
horizonSendDays: {
|
|
7460
|
+
type: string;
|
|
7461
|
+
minimum: number;
|
|
7462
|
+
maximum: number;
|
|
7463
|
+
description: string;
|
|
7464
|
+
};
|
|
7429
7465
|
};
|
|
7430
7466
|
required: string[];
|
|
7431
7467
|
additionalProperties: boolean;
|
|
@@ -10,7 +10,7 @@ async function postSchedulerFillCapacity(body, workspaceId) {
|
|
|
10
10
|
export const schedulerFillCapacityToolDefinitions = [
|
|
11
11
|
{
|
|
12
12
|
name: "get_scheduler_fill_capacity",
|
|
13
|
-
description: "read-only scheduler capacity query for refill-sends before any refill mutation. It asks the product scheduler model how many additional cells the scheduler will try to place for exact sender/action requests, using the default scheduler-forward horizon or one exact targetDate. It returns scheduler-fillable slots, occupied scheduled/processing slots, sender sendability gates, cooldowns, Sales Navigator status for paid InMail, cached paid-InMail credit feasibility, threshold source, blockers, warnings, and explicit sideEffects false. This tool does not create rows, import leads, prepare messages, approve messages, does not schedule sends, does not refresh paid InMail credits, mutate thresholds, start campaigns, launch, or send. Use exact senderId values only; never pass sender names.",
|
|
13
|
+
description: "read-only scheduler capacity query for refill-sends before any refill mutation. It asks the product scheduler model how many additional cells the scheduler will try to place for exact sender/action requests, using the default scheduler-forward horizon or one exact targetDate. Use get_refill_target_plan for multi-day untilDate or horizonSendDays planning. It returns scheduler-fillable slots, occupied scheduled/processing slots, sender sendability gates, cooldowns, Sales Navigator status for paid InMail, cached paid-InMail credit feasibility, threshold source, blockers, warnings, and explicit sideEffects false. This tool does not create rows, import leads, prepare messages, approve messages, does not schedule sends, does not refresh paid InMail credits, mutate thresholds, start campaigns, launch, or send. Use exact senderId values only; never pass sender names.",
|
|
14
14
|
inputSchema: {
|
|
15
15
|
type: "object",
|
|
16
16
|
properties: {
|
|
@@ -2,6 +2,7 @@ type SchedulerRunAction = "run" | "status";
|
|
|
2
2
|
type RunSchedulerSweepInput = {
|
|
3
3
|
workspaceId: string;
|
|
4
4
|
action?: SchedulerRunAction;
|
|
5
|
+
targetDate?: string | null;
|
|
5
6
|
};
|
|
6
7
|
export declare const schedulerRunToolDefinitions: {
|
|
7
8
|
name: string;
|
|
@@ -18,6 +19,10 @@ export declare const schedulerRunToolDefinitions: {
|
|
|
18
19
|
enum: string[];
|
|
19
20
|
description: string;
|
|
20
21
|
};
|
|
22
|
+
targetDate: {
|
|
23
|
+
type: string;
|
|
24
|
+
description: string;
|
|
25
|
+
};
|
|
21
26
|
};
|
|
22
27
|
required: string[];
|
|
23
28
|
additionalProperties: boolean;
|
|
@@ -10,7 +10,7 @@ async function postSchedulerRun(body, workspaceId) {
|
|
|
10
10
|
export const schedulerRunToolDefinitions = [
|
|
11
11
|
{
|
|
12
12
|
name: "run_scheduler_sweep",
|
|
13
|
-
description: 'Trigger the product scheduler placement sweep for one explicit workspace now, or read the last on-demand scheduler run status with action "status".
|
|
13
|
+
description: 'Trigger the product scheduler placement sweep for one explicit workspace now, or read the last on-demand scheduler run status with action "status". By default a run is workspace-wide, not a scoped run for one campaign/table. Pass targetDate to scope placement to one exact sender-local date while still remaining workspace-wide inside that date. Multi-day untilDate or horizonSendDays planning should come from get_refill_target_plan, then use exact per-date run_scheduler_sweep calls only for dates that still need scheduler placement. The synchronous envelope includes status ran, attached, backoff, window_closed_noop, or failed; retryAfterMs for matching-scope backoff replays; and a backward-compatible receipt with public scope metadata. In v2 receipts, readyCellsFound is total scheduler-ready supply found before filters, cellsConsidered is the allocation-attempt count that survived prefilters, prefiltered means ready cells removed before allocation such as no capacity, stale paid InMail credit facts, or sender mismatch, skipped means considered cells rejected by hard scheduler gates, and deferred means considered cells waiting on windows/capacity/cooldown. campaignScopeSummary lists bounded tables/campaigns the workspace-wide run inspected; absence there is not a scoped-run guarantee that the target was ready. The receipt can include readyCellsByType, consideredCellsByType, prefilterReasons, skippedReasons, deferredReasons, summary, and nextAction. It places cells within existing scheduler gates only: it can move placement earlier, but it cannot bypass sending windows, daily limits, cooldowns, sender gates, billing, or credit thresholds, and it never sends messages directly. Repeated calls inside the backoff window replay the last receipt only when the recorded public scope matches this request. Status is read-only and scoped to the last on-demand run only; cron sweeps are not recorded here.',
|
|
14
14
|
inputSchema: {
|
|
15
15
|
type: "object",
|
|
16
16
|
properties: {
|
|
@@ -23,12 +23,33 @@ export const schedulerRunToolDefinitions = [
|
|
|
23
23
|
enum: ["run", "status"],
|
|
24
24
|
description: 'Use "run" to trigger a placement sweep now. Use "status" for a read-only view of the last on-demand run. Defaults to "run".',
|
|
25
25
|
},
|
|
26
|
+
targetDate: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Optional one exact sender-local date (YYYY-MM-DD) to scope scheduler placement/status. The run remains workspace-wide inside that date.",
|
|
29
|
+
},
|
|
26
30
|
},
|
|
27
31
|
required: ["workspaceId"],
|
|
28
32
|
additionalProperties: false,
|
|
29
33
|
},
|
|
30
34
|
},
|
|
31
35
|
];
|
|
36
|
+
function isValidDateKey(value) {
|
|
37
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
38
|
+
return false;
|
|
39
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
40
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
41
|
+
return (date.getUTCFullYear() === year &&
|
|
42
|
+
date.getUTCMonth() === month - 1 &&
|
|
43
|
+
date.getUTCDate() === day);
|
|
44
|
+
}
|
|
45
|
+
function normalizeTargetDate(value) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (typeof value !== "string" || !isValidDateKey(value)) {
|
|
49
|
+
throw new Error("targetDate must be a valid YYYY-MM-DD calendar date.");
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
32
53
|
export async function runSchedulerSweep(input) {
|
|
33
54
|
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
34
55
|
if (!workspaceId) {
|
|
@@ -38,8 +59,10 @@ export async function runSchedulerSweep(input) {
|
|
|
38
59
|
if (action !== "run" && action !== "status") {
|
|
39
60
|
throw new Error('action must be "run" or "status" for run_scheduler_sweep.');
|
|
40
61
|
}
|
|
62
|
+
const targetDate = normalizeTargetDate(input.targetDate);
|
|
41
63
|
return postSchedulerRun({
|
|
42
64
|
workspaceId,
|
|
43
65
|
action,
|
|
66
|
+
targetDate,
|
|
44
67
|
}, workspaceId);
|
|
45
68
|
}
|
package/package.json
CHANGED
|
@@ -87,6 +87,12 @@ Accepted invocation flags in the same user request:
|
|
|
87
87
|
preserves natural-language arguments better than shell-style flags.
|
|
88
88
|
- `targetDate: YYYY-MM-DD`: exact-date selector alternative when the host
|
|
89
89
|
preserves natural-language arguments better than shell-style flags.
|
|
90
|
+
- `horizonSendDays: N`: compatibility selector for the next N sender-local send
|
|
91
|
+
days when no exact or through-date selector is present.
|
|
92
|
+
|
|
93
|
+
Date selector precedence is `targetDate > untilDate > horizonSendDays`. Use
|
|
94
|
+
`targetDate` for one exact sender-local date, `untilDate` for an inclusive
|
|
95
|
+
through-date, and `horizonSendDays` for the next N sender-local send days.
|
|
90
96
|
|
|
91
97
|
When the host can call typed MCP tools, start with:
|
|
92
98
|
|
|
@@ -272,7 +278,9 @@ same sender/action/date; it tells the MCP how many cells the product scheduler
|
|
|
272
278
|
will try to place and does not import, approve, schedule, refresh credits, or
|
|
273
279
|
mutate.
|
|
274
280
|
When the refill loop has ready rows and needs scheduler pickup now, use
|
|
275
|
-
`run_scheduler_sweep
|
|
281
|
+
`run_scheduler_sweep({ workspaceId, targetDate })` for exact-date requests. For
|
|
282
|
+
`untilDate` or `horizonSendDays` requests, sweep each planner-selected
|
|
283
|
+
`targetDate` with the same explicit `workspaceId`. The sweep can place cells
|
|
276
284
|
within existing scheduler gates and returns the receipt, but it never sends or
|
|
277
285
|
bypasses limits.
|
|
278
286
|
Scheduler-run receipt interpretation: `cellsConsidered is allocation-attempt
|
|
@@ -37,16 +37,21 @@ active workspace readback. Scheduled or autonomous usage must carry
|
|
|
37
37
|
`workspaceId` on every tool call. If the workspace is missing or ambiguous, stop
|
|
38
38
|
with `WORKSPACE_REQUIRED`; do not switch the shared active workspace.
|
|
39
39
|
|
|
40
|
+
Date selectors are sender-local and mutually exclusive by precedence:
|
|
41
|
+
`targetDate > untilDate > horizonSendDays`. Use `targetDate` for one exact sender-local date, `untilDate` for an inclusive through-date, and `horizonSendDays` for the next N sender-local send days. If none is provided, the planner uses the default scheduler-forward 48-hour behavior.
|
|
42
|
+
|
|
40
43
|
For a real refill run:
|
|
41
44
|
|
|
42
45
|
```text
|
|
43
|
-
refill_sends_v2({ workspaceId, intent:"auto", senderIds?, approvalMode? })
|
|
46
|
+
refill_sends_v2({ workspaceId, intent:"auto", senderIds?, approvalMode?, targetDate?, untilDate?, horizonSendDays? })
|
|
47
|
+
refill_sends_v2({ workspaceId, targetDate:"2026-07-07" })
|
|
48
|
+
refill_sends_v2({ workspaceId, untilDate:"2026-07-09" })
|
|
44
49
|
```
|
|
45
50
|
|
|
46
51
|
For read-only inspection:
|
|
47
52
|
|
|
48
53
|
```text
|
|
49
|
-
refill_sends_v2({ workspaceId, dryRun:true, intent:"auto", senderIds? })
|
|
54
|
+
refill_sends_v2({ workspaceId, dryRun:true, intent:"auto", senderIds?, targetDate?, untilDate?, horizonSendDays? })
|
|
50
55
|
```
|
|
51
56
|
|
|
52
57
|
To resume an in-progress run, pass the handle back exactly:
|
|
@@ -55,6 +60,11 @@ To resume an in-progress run, pass the handle back exactly:
|
|
|
55
60
|
refill_sends_v2({ workspaceId, runId, fence })
|
|
56
61
|
```
|
|
57
62
|
|
|
63
|
+
The original run's date window is stored in the run record. On resume, the
|
|
64
|
+
stored window is reused and conflicting resume input is blocked; do not add,
|
|
65
|
+
change, or remove `targetDate`, `untilDate`, or `horizonSendDays` while passing
|
|
66
|
+
an existing `{ runId, fence }`.
|
|
67
|
+
|
|
58
68
|
If a stale handle loses the lease, the tool reports the holder status and the
|
|
59
69
|
approximately 10 minute lockout window. Reinvoke with the current handle or wait
|
|
60
70
|
for lease expiry; never guess a fence.
|
|
@@ -125,9 +135,12 @@ host guard, heartbeats the run lease, and may return:
|
|
|
125
135
|
```
|
|
126
136
|
|
|
127
137
|
Reinvoke with that handle. The approximately five minute scheduler budget is at
|
|
128
|
-
the run level, not one tool call.
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
the run level, not one tool call. Exact-date runs request pickup with
|
|
139
|
+
`run_scheduler_sweep({ workspaceId, targetDate })`; `untilDate` and
|
|
140
|
+
`horizonSendDays` runs sweep each planner-selected exact target date. A
|
|
141
|
+
window-closed or loaded-awaiting scheduler report must include remaining-ready
|
|
142
|
+
count, exact expected pickup time, and the resume handle; never treat bare
|
|
143
|
+
"awaiting scheduler" copy as a final answer.
|
|
131
144
|
Scheduler-run receipt interpretation still applies to terminal progress:
|
|
132
145
|
`cellsConsidered is allocation-attempt count`, not total ready supply, while
|
|
133
146
|
`readyCellsFound` is ready inventory found before prefilters. Inspect
|
|
@@ -33,10 +33,16 @@ local files or memory.
|
|
|
33
33
|
Resolve request-scoped `workspaceId` using read-only auth and workspace tools.
|
|
34
34
|
Scheduled or autonomous runs must not rely on implicit workspace state.
|
|
35
35
|
|
|
36
|
+
Carry the request date selector through bootstrap, planning, execution, and
|
|
37
|
+
resume. Date selector precedence is `targetDate > untilDate > horizonSendDays`:
|
|
38
|
+
`targetDate` is one exact sender-local date, `untilDate` is an inclusive
|
|
39
|
+
through-date, and `horizonSendDays` is the next N sender-local send days. If no
|
|
40
|
+
selector is present, use the default scheduler-forward 48-hour behavior.
|
|
41
|
+
|
|
36
42
|
Start or resume the execution loop:
|
|
37
43
|
|
|
38
44
|
```text
|
|
39
|
-
refill_sends_v2({ workspaceId, intent:"auto", senderIds?, approvalMode?, runId?, fence? })
|
|
45
|
+
refill_sends_v2({ workspaceId, intent:"auto", senderIds?, approvalMode?, targetDate?, untilDate?, horizonSendDays?, runId?, fence? })
|
|
40
46
|
```
|
|
41
47
|
|
|
42
48
|
Use `dryRun:true` only when the user asked for read-only inspection. A dry run
|
|
@@ -44,6 +50,11 @@ writes a journal only. A real run creates or resumes a run record, refreshes
|
|
|
44
50
|
paid-credit trust only when packet facts require it, and returns a resume handle
|
|
45
51
|
when the current invocation must pause.
|
|
46
52
|
|
|
53
|
+
The run record stores the selected date window. On resume, reuse that stored
|
|
54
|
+
window and block conflicting resume input; do not accept a changed
|
|
55
|
+
`targetDate`, `untilDate`, or `horizonSendDays` with an existing `{ runId,
|
|
56
|
+
fence }`.
|
|
57
|
+
|
|
47
58
|
## G1 Plan
|
|
48
59
|
|
|
49
60
|
The loop reads `get_refill_plan_v2` with runState fed back from the run record.
|
|
@@ -116,7 +127,10 @@ outcome, then verifies before replanning.
|
|
|
116
127
|
Scheduler wait is cross-invocation. If the tool returns `in_progress` with
|
|
117
128
|
sweep guidance, re-invoke `refill_sends_v2` with `{runId, fence}`. The scheduler
|
|
118
129
|
budget is cumulative at the run level. Zero pickup after a confirmed sweep is
|
|
119
|
-
debugged from `pipelineDiagnosis`; it is not waited out forever.
|
|
130
|
+
debugged from `pipelineDiagnosis`; it is not waited out forever. Exact-date
|
|
131
|
+
runs request pickup with `run_scheduler_sweep({ workspaceId, targetDate })`;
|
|
132
|
+
`untilDate` and `horizonSendDays` runs sweep each planner-selected exact target
|
|
133
|
+
date.
|
|
120
134
|
Scheduler-run receipt interpretation still applies to terminal progress:
|
|
121
135
|
`cellsConsidered is allocation-attempt count`, not total ready supply, while
|
|
122
136
|
`readyCellsFound` is ready inventory found before prefilters. Inspect
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "v1.0",
|
|
3
3
|
"workflow": "refill-sends-v2-workflow",
|
|
4
|
-
"principle": "Execute refill sends v2 through a fenced run record: bootstrap, plan, execute, verify, and report. Dry runs stay read-only; real runs execute only packet-named bounded work.",
|
|
4
|
+
"principle": "Execute refill sends v2 through a fenced run record: bootstrap, plan, execute, verify, and report. Dry runs stay read-only; real runs execute only packet-named bounded work. Date selectors use targetDate > untilDate > horizonSendDays precedence.",
|
|
5
5
|
"laneSources": [
|
|
6
6
|
"managed_waterfall",
|
|
7
7
|
"dashboard_evergreen",
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
"repair rungs come from the plan packet; the loop bounds and refuses, never self-emits repair actions",
|
|
51
51
|
"capped_by_scheduler is complete, never poll",
|
|
52
52
|
"awaiting scheduler is not an end state; window-closed reports name remaining-ready, expected pickup time, and resume handle",
|
|
53
|
+
"date selectors resolve by targetDate > untilDate > horizonSendDays; targetDate is one exact sender-local date, untilDate is an inclusive through-date, and horizonSendDays means next N sender-local send days",
|
|
54
|
+
"dateWindow is stored on the run record and conflicting resume input is blocked",
|
|
55
|
+
"exact-date scheduler pickup uses run_scheduler_sweep({ workspaceId, targetDate }); untilDate and horizonSendDays runs sweep each planner-selected exact targetDate",
|
|
53
56
|
"honest-rubric-fail means a human updates the rubric or changes lead source",
|
|
54
57
|
"source replenishment beyond same-source-automatic is a scoped create-campaign handoff, never refill-owned provider search",
|
|
55
58
|
"no campaign creation, provider-family switch, threshold lowering, scheduler writes, sends, archives, deletes, or brief/filter/message/sequence/sender mutation without a separate approval packet"
|
|
@@ -143,7 +146,16 @@
|
|
|
143
146
|
},
|
|
144
147
|
{
|
|
145
148
|
"tool": "refill_sends_v2",
|
|
146
|
-
"requiredFields": ["workspaceId"]
|
|
149
|
+
"requiredFields": ["workspaceId"],
|
|
150
|
+
"optionalFields": [
|
|
151
|
+
"senderIds",
|
|
152
|
+
"approvalMode",
|
|
153
|
+
"targetDate",
|
|
154
|
+
"untilDate",
|
|
155
|
+
"horizonSendDays",
|
|
156
|
+
"runId",
|
|
157
|
+
"fence"
|
|
158
|
+
]
|
|
147
159
|
},
|
|
148
160
|
{
|
|
149
161
|
"tool": "get_subskill_prompt",
|
|
@@ -176,6 +188,9 @@
|
|
|
176
188
|
"optionalFields": [
|
|
177
189
|
"intent",
|
|
178
190
|
"senderIds",
|
|
191
|
+
"targetDate",
|
|
192
|
+
"untilDate",
|
|
193
|
+
"horizonSendDays",
|
|
179
194
|
"runState",
|
|
180
195
|
"journal",
|
|
181
196
|
"journalNote"
|
|
@@ -311,7 +326,7 @@
|
|
|
311
326
|
},
|
|
312
327
|
{
|
|
313
328
|
"id": "verify",
|
|
314
|
-
"description": "Verify whole-row outcomes before another plan read. Reattach to an own active prep job by requestSource/requestHash; never re-dispatch prep while a matching job is active. Foreign prep waits until clear or blocks foreign_prep_active. wait_for_active_work and wait_for_source_import reread the plan on bounded cadence. wait_for_scheduler is cross-invocation: each tool call polls briefly under the host guard, heartbeats the lease, and returns in_progress sweep guidance so the agent re-invokes refill_sends_v2 with {runId,fence}. The approximately five minute scheduler budget is cumulative at the run level.",
|
|
329
|
+
"description": "Verify whole-row outcomes before another plan read. Reattach to an own active prep job by requestSource/requestHash; never re-dispatch prep while a matching job is active. Foreign prep waits until clear or blocks foreign_prep_active. wait_for_active_work and wait_for_source_import reread the plan on bounded cadence. wait_for_scheduler is cross-invocation: each tool call polls briefly under the host guard, heartbeats the lease, and returns in_progress sweep guidance so the agent re-invokes refill_sends_v2 with {runId,fence}. Exact-date scheduler pickup uses run_scheduler_sweep({ workspaceId, targetDate }); untilDate and horizonSendDays runs sweep each planner-selected exact targetDate. The approximately five minute scheduler budget is cumulative at the run level.",
|
|
315
330
|
"allowedTools": ["refill_sends_v2", "get_refill_plan_v2"],
|
|
316
331
|
"onEnter": [
|
|
317
332
|
{
|
|
@@ -322,6 +337,7 @@
|
|
|
322
337
|
"hardRules": [
|
|
323
338
|
"do not re-dispatch prep when an own active job exists; reattach and poll status",
|
|
324
339
|
"capped_by_scheduler is complete",
|
|
340
|
+
"resume uses the stored dateWindow; conflicting resume input is blocked",
|
|
325
341
|
"replan-driven repair: the planner names rerun_errored_cells, approve_messages, start_campaign, or source work; the loop bounds and refuses",
|
|
326
342
|
"conversion verdict is journaled from packet facts after the first prep batch; the plan gate withholds a second unproven prep batch",
|
|
327
343
|
"zero pickup after a confirmed sweep is debugged from pipelineDiagnosis, not waited out forever",
|
|
@@ -189,9 +189,11 @@ files or memory.
|
|
|
189
189
|
does not create rows, import, approve, schedule, refresh paid-InMail credits,
|
|
190
190
|
or mutate thresholds.
|
|
191
191
|
When ready rows exist and the wait is for scheduler pickup, call
|
|
192
|
-
`run_scheduler_sweep
|
|
193
|
-
product scheduler placement pass now and read its receipt.
|
|
194
|
-
|
|
192
|
+
`run_scheduler_sweep({ workspaceId, targetDate })` for exact-date requests
|
|
193
|
+
to request the product scheduler placement pass now and read its receipt. For
|
|
194
|
+
`untilDate` or `horizonSendDays` requests, sweep each planner-selected
|
|
195
|
+
`targetDate` with the same explicit `workspaceId`. This may place cells
|
|
196
|
+
within existing gates, never sends messages, and never bypasses limits.
|
|
195
197
|
Scheduler-run receipt interpretation: `cellsConsidered is
|
|
196
198
|
allocation-attempt count`, not total ready supply, while `readyCellsFound`
|
|
197
199
|
is ready inventory found before prefilters. Inspect `campaignScopeSummary`
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
],
|
|
203
203
|
"rules": [
|
|
204
204
|
"Poll every 60-120 seconds or on the host continuation interval.",
|
|
205
|
-
"Use run_scheduler_sweep
|
|
205
|
+
"Use run_scheduler_sweep({ workspaceId, targetDate }) for exact-date scheduler pickup; for untilDate or horizonSendDays requests, sweep each planner-selected targetDate with the explicit workspaceId. It places cells only inside existing gates and never sends.",
|
|
206
206
|
"cellsConsidered is allocation-attempt count; inspect campaignScopeSummary before assuming the selected refill campaign/table was included or ready-but-blocked.",
|
|
207
207
|
"Read prefiltered, skipped, and deferred separately: prefiltered ready closed-InMail cells with stale paid-credit facts require refresh_paid_inmail_credits_then_rerun once, then rerun/status.",
|
|
208
208
|
"wait_for_capacity_or_window means report loaded/capped/waiting and do not source or prep more rows; no_ready_cells_continue_refill_prep means return to the refill/prep ladder.",
|