@sellable/mcp 0.1.553 → 0.1.555

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.
@@ -3,7 +3,7 @@ import { startPrepareCampaignMessages } from "./campaign-message-preparation.js"
3
3
  import { startCampaign } from "./campaigns.js";
4
4
  import { confirmLeadList, importLeads, searchSignals, selectPromisingPosts, } from "./leads.js";
5
5
  import { markProviderPromptLoaded } from "./provider-preflight.js";
6
- import { runSchedulerSweep } from "./scheduler-run.js";
6
+ import { runSchedulerSweep, } from "./scheduler-run.js";
7
7
  import { refreshPaidInmailCredits } from "./senders.js";
8
8
  import { workspaceRequestOptions } from "./workspace-context.js";
9
9
  const PAID_INMAIL_REFRESH_MAX_ATTEMPTS = 3;
@@ -86,19 +86,87 @@ export function normalizeSchedulerChangedCounts(value) {
86
86
  if (groups.reduce((sum, group) => sum + group.count, 0) !== total) {
87
87
  return null;
88
88
  }
89
+ if (!Array.isArray(changedCounts.byCampaignTableActionTypeSender)) {
90
+ return null;
91
+ }
92
+ const senderGroups = [];
93
+ const senderSeen = new Set();
94
+ for (const rawGroup of changedCounts.byCampaignTableActionTypeSender) {
95
+ const group = recordValue(rawGroup);
96
+ const campaignId = stringValue(group?.campaignId);
97
+ const tableId = stringValue(group?.tableId);
98
+ const actionType = stringValue(group?.actionType);
99
+ const senderId = stringValue(group?.senderId);
100
+ const count = numberValue(group?.count);
101
+ if (!campaignId ||
102
+ !tableId ||
103
+ !actionType ||
104
+ !senderId ||
105
+ count === null ||
106
+ count <= 0 ||
107
+ !Number.isInteger(count)) {
108
+ return null;
109
+ }
110
+ const key = `${campaignId}:${tableId}:${actionType}:${senderId}`;
111
+ if (senderSeen.has(key))
112
+ return null;
113
+ senderSeen.add(key);
114
+ senderGroups.push({ campaignId, tableId, actionType, senderId, count });
115
+ }
116
+ if (senderGroups.reduce((sum, group) => sum + group.count, 0) !== total) {
117
+ return null;
118
+ }
89
119
  return {
90
120
  complete: true,
91
121
  truncated: false,
92
122
  total,
93
123
  byCampaignTableActionType: groups,
124
+ byCampaignTableActionTypeSender: senderGroups,
94
125
  };
95
126
  }
96
- export function normalizeSchedulerPrimitiveResult(value) {
127
+ function completeExpectedTargetSummary(value, expectedTargets, maxPlacements) {
128
+ const summary = recordValue(value);
129
+ const items = Array.isArray(summary?.items) ? summary.items : [];
130
+ if (summary?.complete !== true ||
131
+ summary.truncated !== false ||
132
+ numberValue(summary.requested) !== expectedTargets.length ||
133
+ numberValue(summary.included) == null ||
134
+ numberValue(summary.omitted) == null ||
135
+ numberValue(summary.included) + numberValue(summary.omitted) !==
136
+ expectedTargets.length ||
137
+ numberValue(summary.maxPlacements) !== maxPlacements ||
138
+ items.length !== expectedTargets.length) {
139
+ return false;
140
+ }
141
+ return items.every((rawItem, index) => {
142
+ const item = recordValue(rawItem);
143
+ const target = expectedTargets[index];
144
+ const senderIds = stringArray(item?.senderIds).sort();
145
+ const bySender = Array.isArray(item?.bySender) ? item.bySender : [];
146
+ const accounted = bySender
147
+ .map((entry) => stringValue(recordValue(entry)?.senderId))
148
+ .filter((senderId) => Boolean(senderId))
149
+ .sort();
150
+ return (Boolean(target) &&
151
+ item?.campaignId === target?.campaignId &&
152
+ item?.tableId === target?.tableId &&
153
+ item?.refillLaneKey === target?.refillLaneKey &&
154
+ (item?.outcome === "included" || item?.outcome === "omitted") &&
155
+ JSON.stringify(senderIds) === JSON.stringify(target?.senderIds) &&
156
+ JSON.stringify(accounted) === JSON.stringify(target?.senderIds));
157
+ });
158
+ }
159
+ export function normalizeSchedulerPrimitiveResult(value, expected) {
97
160
  const response = recordValue(value);
98
161
  const receipt = recordValue(response?.receipt);
99
162
  const changedCounts = normalizeSchedulerChangedCounts(receipt?.changedCounts ?? response?.changedCounts);
163
+ const targetAuditComplete = !expected ||
164
+ (receipt?.receiptVersion === 3 &&
165
+ receipt.expectedTargetsHash === expected.expectedTargetsHash &&
166
+ receipt.maxPlacements === expected.maxPlacements &&
167
+ completeExpectedTargetSummary(receipt.expectedTargetSummary, expected.expectedTargets, expected.maxPlacements));
100
168
  const schedulerStatus = stringValue(response?.status) ?? stringValue(receipt?.status) ?? "unknown";
101
- if (!changedCounts) {
169
+ if (!changedCounts || !targetAuditComplete) {
102
170
  return {
103
171
  status: "scheduler_receipt_incomplete",
104
172
  schedulerStatus,
@@ -312,7 +380,9 @@ export function classifyPaidInmailRefreshReceipt(value) {
312
380
  refreshed,
313
381
  error: stringValue(response?.error) ??
314
382
  stringValue(nestedReceipt?.error) ??
315
- (usableCurrentFacts ? null : "paid InMail credit refresh did not return usable current facts"),
383
+ (usableCurrentFacts
384
+ ? null
385
+ : "paid InMail credit refresh did not return usable current facts"),
316
386
  };
317
387
  }
318
388
  export function firstGlobalAction(plan) {
@@ -856,7 +926,10 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
856
926
  case "wait_for_scheduler":
857
927
  case "wait_for_active_work":
858
928
  case "wait_for_source_import":
859
- return { status: "read_only_reread", result: waitResultForAction(action) };
929
+ return {
930
+ status: "read_only_reread",
931
+ result: waitResultForAction(action),
932
+ };
860
933
  case "run_scheduler_sweep": {
861
934
  const toolInput = actionToolInput(action);
862
935
  const actionWorkspaceId = stringValue(toolInput.workspaceId);
@@ -867,6 +940,11 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
867
940
  const targetShapeRevision = stringValue(toolInput.targetShapeRevision);
868
941
  const receiptRequirements = recordValue(action.receiptRequirements);
869
942
  const receiptGroups = stringArray(receiptRequirements?.groupBy);
943
+ const expectedTargets = Array.isArray(toolInput.expectedTargets)
944
+ ? toolInput.expectedTargets
945
+ : [];
946
+ const expectedTargetsHash = stringValue(toolInput.expectedTargetsHash);
947
+ const maxPlacements = numberValue(toolInput.maxPlacements);
870
948
  if (!workspaceId ||
871
949
  !actionWorkspaceId ||
872
950
  actionWorkspaceId !== workspaceId ||
@@ -878,7 +956,13 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
878
956
  !targetShapeRevision ||
879
957
  action.workspaceWide !== true ||
880
958
  receiptRequirements?.nonTruncated !== true ||
881
- receiptGroups.join(":") !== "campaignId:tableId:actionType") {
959
+ receiptGroups.join(":") !== "campaignId:tableId:actionType:senderId" ||
960
+ receiptRequirements?.expectedTargetSummary !== true ||
961
+ expectedTargets.length === 0 ||
962
+ !expectedTargetsHash ||
963
+ maxPlacements === null ||
964
+ !Number.isInteger(maxPlacements) ||
965
+ maxPlacements <= 0) {
882
966
  return {
883
967
  status: "refused",
884
968
  refusalReason: "run_scheduler_sweep action is missing exact workspace/date/revision scope or complete receipt requirements",
@@ -890,10 +974,17 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
890
974
  targetDate,
891
975
  requestKey,
892
976
  targetShapeRevision,
977
+ expectedTargets,
978
+ expectedTargetsHash,
979
+ maxPlacements,
893
980
  });
894
981
  return {
895
982
  status: "executed_and_reread",
896
- result: normalizeSchedulerPrimitiveResult(result),
983
+ result: normalizeSchedulerPrimitiveResult(result, {
984
+ expectedTargets,
985
+ expectedTargetsHash,
986
+ maxPlacements,
987
+ }),
897
988
  };
898
989
  }
899
990
  case "prepare_messages": {
@@ -917,7 +1008,9 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
917
1008
  tableId,
918
1009
  ...(workspaceId ? { workspaceId } : {}),
919
1010
  targetPreparedMessages,
920
- maxRowsToCheck: numberValue(toolInput.maxRowsToCheck) ?? 300,
1011
+ ...(numberValue(toolInput.maxRowsToCheck) != null
1012
+ ? { maxRowsToCheck: numberValue(toolInput.maxRowsToCheck) }
1013
+ : {}),
921
1014
  approvalMode,
922
1015
  rowSelector,
923
1016
  ...(excludeRowIds.length > 0 ? { excludeRowIds } : {}),
@@ -1,4 +1,4 @@
1
- type RefillSendsV2Input = {
1
+ export type RefillSendsV2Input = {
2
2
  workspaceId?: string;
3
3
  runId?: string;
4
4
  fence?: number;
@@ -6,6 +6,39 @@ type RefillSendsV2Input = {
6
6
  approvalMode?: "approve" | "mark_ready";
7
7
  senderIds?: string[];
8
8
  intent?: "auto" | "evergreen" | "plain" | "active";
9
+ targetDate?: string;
10
+ untilDate?: string;
11
+ horizonSendDays?: number;
12
+ scope?: Record<string, unknown>;
13
+ scopeHash?: string;
14
+ approval?: Record<string, unknown>;
15
+ targetPlan?: Record<string, unknown>;
16
+ approvalEcho?: {
17
+ scopeHash: string;
18
+ targetShapeRevision: string;
19
+ actionFingerprint: string;
20
+ expectedTargetsHash: string;
21
+ approvalExpiresAt: string;
22
+ approvalFingerprint: string;
23
+ };
24
+ };
25
+ type RefillV2ApprovalPreparationInput = Pick<RefillSendsV2Input, "workspaceId" | "intent" | "senderIds" | "approvalMode" | "targetDate" | "untilDate" | "horizonSendDays"> & {
26
+ targetPlan: Record<string, unknown>;
27
+ now?: Date;
28
+ approvalExpiresAt?: string;
29
+ };
30
+ export declare const REFILL_V2_RUNTIME_IDENTITY: {
31
+ readonly contract: {
32
+ readonly version: "2.0.0";
33
+ readonly hash: string;
34
+ readonly cacheKey: "refill-contract-v1";
35
+ };
36
+ readonly installed: {
37
+ readonly mcpVersion: "phase98-local";
38
+ readonly installVersion: "phase98-local";
39
+ readonly pluginVersion: "phase98-local";
40
+ readonly cacheIdentity: "phase98-local-plugin-cache";
41
+ };
9
42
  };
10
43
  export declare const refillSendsV2ToolDefinitions: {
11
44
  name: string;
@@ -44,11 +77,89 @@ export declare const refillSendsV2ToolDefinitions: {
44
77
  type: string;
45
78
  enum: string[];
46
79
  };
80
+ targetDate: {
81
+ type: string;
82
+ description: string;
83
+ };
84
+ untilDate: {
85
+ type: string;
86
+ description: string;
87
+ };
88
+ horizonSendDays: {
89
+ type: string;
90
+ description: string;
91
+ };
47
92
  };
48
93
  required: string[];
49
94
  additionalProperties: boolean;
50
95
  };
51
96
  }[];
97
+ export declare function prepareRefillSendsV2Approval(input: RefillV2ApprovalPreparationInput): Promise<{
98
+ status: "blocked";
99
+ blocker: string;
100
+ response: unknown;
101
+ } | {
102
+ status: "awaiting_approval";
103
+ runId: string;
104
+ fence: number;
105
+ scope: Record<string, unknown> | {
106
+ version: 2;
107
+ workspaceId: string;
108
+ intent: "active" | "plain" | "evergreen" | "auto";
109
+ senderIds: string[];
110
+ campaignIds: string[];
111
+ tableIds: string[];
112
+ actionIds: string[];
113
+ approvalMode: "mark_ready" | "approve";
114
+ dateSelector: {
115
+ kind: "target_date";
116
+ targetDate: string;
117
+ untilDate?: undefined;
118
+ horizonSendDays?: undefined;
119
+ } | {
120
+ kind: "until_date";
121
+ untilDate: string;
122
+ targetDate?: undefined;
123
+ horizonSendDays?: undefined;
124
+ } | {
125
+ kind: "horizon_send_days";
126
+ horizonSendDays: number;
127
+ targetDate?: undefined;
128
+ untilDate?: undefined;
129
+ } | {
130
+ kind: "scheduler_default";
131
+ targetDate?: undefined;
132
+ untilDate?: undefined;
133
+ horizonSendDays?: undefined;
134
+ };
135
+ senderTimezones: {
136
+ [k: string]: string;
137
+ };
138
+ expectedTargetsHash: string;
139
+ maxPlacements: number;
140
+ contract: {
141
+ version: "2.0.0";
142
+ hash: string;
143
+ cacheKey: "refill-contract-v1";
144
+ };
145
+ installed: {
146
+ mcpVersion: "phase98-local";
147
+ installVersion: "phase98-local";
148
+ pluginVersion: "phase98-local";
149
+ cacheIdentity: "phase98-local-plugin-cache";
150
+ };
151
+ };
152
+ approvalFingerprint: string;
153
+ scopeHash: string;
154
+ targetShapeRevision: string;
155
+ actionFingerprint: string;
156
+ expectedTargetsHash: string;
157
+ sideEffectEnvelope: string[];
158
+ maxPlacements: number;
159
+ approvalExpiresAt: string;
160
+ blocker?: undefined;
161
+ response?: undefined;
162
+ }>;
52
163
  export declare function refillSendsV2Command(input: RefillSendsV2Input): Promise<{
53
164
  readOnly: boolean;
54
165
  mode: string;
@@ -1,11 +1,26 @@
1
+ import { createHash } from "node:crypto";
2
+ import { REFILL_CONTRACT_CACHE_VERSION, REFILL_CONTRACT_HASH, REFILL_CONTRACT_VERSION, renderRefillSafetyGuidance, } from "../refill-contract.js";
1
3
  import { appendIndexLine, appendJournalEvent, createRunJournal, renderBootstrapSection, renderCrashedAbandonedSection, renderExecutionEventSection, renderPlanSection, renderTerminalSection, } from "../refill-journal.js";
2
4
  import { writeRefillWorkspaceState } from "../refill-local-state.js";
3
5
  import { advanceRefillRunGateRemote, completeRefillRun, heartbeatRefillRun, recordRunOutcome, recordRunPlanned, refillRunStatus, startRefillRunRemote, } from "../refill-run-client.js";
4
6
  import { runRefillV2Loop } from "../refill-run-loop.js";
5
- import { getPrepareCampaignMessagesStatus } from "./campaign-message-preparation.js";
7
+ import { getPrepareCampaignMessagesStatus, stopSatisfiedPrepareCampaignMessages, } from "./campaign-message-preparation.js";
6
8
  import { getRefillPlanV2 } from "./evergreen-refill-plan.js";
7
9
  import { executeOneYoloPrimitive, executeStartCampaignPrimitive, prepareRowSelectorValue, refillPrepareRequestHash, refreshPaidInmailCreditsWithRetry, } from "./refill-executors.js";
8
10
  import { runSchedulerSweep } from "./scheduler-run.js";
11
+ export const REFILL_V2_RUNTIME_IDENTITY = {
12
+ contract: {
13
+ version: REFILL_CONTRACT_VERSION,
14
+ hash: REFILL_CONTRACT_HASH,
15
+ cacheKey: REFILL_CONTRACT_CACHE_VERSION,
16
+ },
17
+ installed: {
18
+ mcpVersion: "phase98-local",
19
+ installVersion: "phase98-local",
20
+ pluginVersion: "phase98-local",
21
+ cacheIdentity: "phase98-local-plugin-cache",
22
+ },
23
+ };
9
24
  const FORBIDDEN_ACTIONS = [
10
25
  "Do not schedule sends.",
11
26
  "Do not send messages.",
@@ -18,7 +33,11 @@ const BOUNDED_AUTHORITY = "Within the refill run, bounded approvals, preparation
18
33
  export const refillSendsV2ToolDefinitions = [
19
34
  {
20
35
  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.",
36
+ description: [
37
+ "Execute the server-internal refill loop behind public refill_sends; it is retained for dispatch compatibility but is not advertised.",
38
+ `contractHash=${REFILL_CONTRACT_HASH}`,
39
+ renderRefillSafetyGuidance(),
40
+ ].join(" "),
22
41
  inputSchema: {
23
42
  type: "object",
24
43
  properties: {
@@ -51,6 +70,18 @@ export const refillSendsV2ToolDefinitions = [
51
70
  type: "string",
52
71
  enum: ["auto", "evergreen", "plain", "active"],
53
72
  },
73
+ targetDate: {
74
+ type: "string",
75
+ description: "Exact sender-local YYYY-MM-DD date.",
76
+ },
77
+ untilDate: {
78
+ type: "string",
79
+ description: "Inclusive sender-local YYYY-MM-DD through-date.",
80
+ },
81
+ horizonSendDays: {
82
+ type: "number",
83
+ description: "Positive sender-local send-day horizon.",
84
+ },
54
85
  },
55
86
  required: ["workspaceId"],
56
87
  additionalProperties: false,
@@ -68,6 +99,234 @@ function resumeHandle(input) {
68
99
  ? { runId: input.runId, fence: input.fence }
69
100
  : undefined;
70
101
  }
102
+ function validDate(value, field) {
103
+ const trimmed = value.trim();
104
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed) ||
105
+ new Date(`${trimmed}T00:00:00.000Z`).toISOString().slice(0, 10) !== trimmed) {
106
+ throw new Error(`${field} must be a valid YYYY-MM-DD date.`);
107
+ }
108
+ return trimmed;
109
+ }
110
+ function normalizeDateSelector(input) {
111
+ const selected = [
112
+ input.targetDate !== undefined,
113
+ input.untilDate !== undefined,
114
+ input.horizonSendDays !== undefined,
115
+ ].filter(Boolean).length;
116
+ if (selected > 1) {
117
+ throw new Error("Provide exactly one refill date selector.");
118
+ }
119
+ if (input.targetDate !== undefined) {
120
+ const targetDate = validDate(input.targetDate, "targetDate");
121
+ return {
122
+ dateSelector: { kind: "target_date", targetDate },
123
+ targetDate,
124
+ };
125
+ }
126
+ if (input.untilDate !== undefined) {
127
+ const untilDate = validDate(input.untilDate, "untilDate");
128
+ return {
129
+ dateSelector: { kind: "until_date", untilDate },
130
+ untilDate,
131
+ };
132
+ }
133
+ if (input.horizonSendDays !== undefined) {
134
+ if (!Number.isInteger(input.horizonSendDays) ||
135
+ input.horizonSendDays <= 0) {
136
+ throw new Error("horizonSendDays must be a positive integer.");
137
+ }
138
+ return {
139
+ dateSelector: {
140
+ kind: "horizon_send_days",
141
+ horizonSendDays: input.horizonSendDays,
142
+ },
143
+ horizonSendDays: input.horizonSendDays,
144
+ };
145
+ }
146
+ return { dateSelector: { kind: "scheduler_default" } };
147
+ }
148
+ function record(value) {
149
+ return value && typeof value === "object" && !Array.isArray(value)
150
+ ? value
151
+ : null;
152
+ }
153
+ function text(value) {
154
+ return typeof value === "string" && value.trim() ? value.trim() : null;
155
+ }
156
+ function list(value) {
157
+ return Array.isArray(value) ? value : [];
158
+ }
159
+ function sortedUnique(values) {
160
+ return [
161
+ ...new Set(values.filter((value) => Boolean(value))),
162
+ ].sort((left, right) => left.localeCompare(right));
163
+ }
164
+ function sha256(value) {
165
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
166
+ }
167
+ function targetPlanParts(targetPlan) {
168
+ const target = record(targetPlan.target) ?? {};
169
+ const plans = list(target.senderRefillPlans)
170
+ .map(record)
171
+ .filter((value) => value != null);
172
+ const actions = list(target.globalActionQueue)
173
+ .map(record)
174
+ .filter((value) => value != null);
175
+ return { target, plans, actions };
176
+ }
177
+ function stableIdsFromTargetPlan(targetPlan, input) {
178
+ const { plans, actions } = targetPlanParts(targetPlan);
179
+ const actionRecords = actions.map((action) => ({
180
+ action,
181
+ ids: record(action.ids) ?? {},
182
+ toolInput: record(action.toolInput) ?? {},
183
+ }));
184
+ const senderIds = sortedUnique([
185
+ ...(input.senderIds ?? []),
186
+ ...plans.map((plan) => text(plan.senderId)),
187
+ ...actionRecords.map(({ ids, toolInput }) => text(ids.senderId) ?? text(toolInput.senderId)),
188
+ ]);
189
+ const campaignIds = sortedUnique(actionRecords.map(({ ids, toolInput }) => text(ids.campaignId) ?? text(toolInput.campaignId)));
190
+ const tableIds = sortedUnique(actionRecords.map(({ ids, toolInput }) => text(ids.tableId) ?? text(toolInput.tableId)));
191
+ const actionIds = sortedUnique(actionRecords.map(({ action }) => text(action.actionKey) ?? text(action.type)));
192
+ const senderTimezones = Object.fromEntries(plans
193
+ .flatMap((plan) => list(plan.selectedDays))
194
+ .map(record)
195
+ .filter((day) => day != null)
196
+ .map((day) => [
197
+ text(day.senderId),
198
+ text(day.senderTimeZone) ?? text(day.timeZone),
199
+ ])
200
+ .filter((entry) => entry[0] != null && entry[1] != null)
201
+ .sort(([left], [right]) => left.localeCompare(right)));
202
+ return {
203
+ senderIds,
204
+ campaignIds,
205
+ tableIds,
206
+ actionIds,
207
+ senderTimezones,
208
+ actions: actionRecords,
209
+ };
210
+ }
211
+ function buildApprovalEnvelope(input) {
212
+ const workspaceId = normalizeWorkspaceId(input.workspaceId);
213
+ const date = normalizeDateSelector(input);
214
+ const stable = stableIdsFromTargetPlan(input.targetPlan, input);
215
+ const schedulerAction = stable.actions.find(({ action }) => action.type === "run_scheduler_sweep");
216
+ const schedulerInput = schedulerAction?.toolInput ?? {};
217
+ const expectedTargets = list(schedulerInput.expectedTargets);
218
+ const expectedTargetsHash = text(schedulerInput.expectedTargetsHash) ?? sha256(expectedTargets);
219
+ const maxPlacements = typeof schedulerInput.maxPlacements === "number" &&
220
+ Number.isInteger(schedulerInput.maxPlacements) &&
221
+ schedulerInput.maxPlacements > 0
222
+ ? schedulerInput.maxPlacements
223
+ : 150;
224
+ const scope = {
225
+ version: 2,
226
+ workspaceId,
227
+ intent: input.intent ?? "auto",
228
+ senderIds: stable.senderIds,
229
+ campaignIds: stable.campaignIds,
230
+ tableIds: stable.tableIds,
231
+ actionIds: stable.actionIds,
232
+ approvalMode: input.approvalMode ?? "approve",
233
+ dateSelector: date.dateSelector,
234
+ senderTimezones: stable.senderTimezones,
235
+ expectedTargetsHash,
236
+ maxPlacements,
237
+ contract: { ...REFILL_V2_RUNTIME_IDENTITY.contract },
238
+ installed: { ...REFILL_V2_RUNTIME_IDENTITY.installed },
239
+ };
240
+ const scopeHash = sha256(scope);
241
+ const targetShapeRevision = text(input.targetPlan.targetShapeRevision) ??
242
+ text(record(input.targetPlan.target)?.targetShapeRevision) ??
243
+ "missing_target_shape_revision";
244
+ const actionFingerprint = text(stable.actions[0]?.action.actionKey) ??
245
+ text(stable.actions[0]?.action.type) ??
246
+ "no_action";
247
+ const sideEffectEnvelope = sortedUnique(stable.actions.map(({ action }) => text(action.type)));
248
+ const approvalExpiresAt = input.approvalExpiresAt ??
249
+ new Date((input.now ?? new Date()).getTime() + 15 * 60_000).toISOString();
250
+ const fingerprintFields = {
251
+ scopeHash,
252
+ targetShapeRevision,
253
+ actionFingerprint,
254
+ expectedTargetsHash,
255
+ sideEffectEnvelope,
256
+ maxPlacements,
257
+ approvalExpiresAt,
258
+ };
259
+ const approval = {
260
+ status: "awaiting_approval",
261
+ ...fingerprintFields,
262
+ approvalFingerprint: sha256(fingerprintFields),
263
+ };
264
+ return { scope, scopeHash, approval, date };
265
+ }
266
+ export async function prepareRefillSendsV2Approval(input) {
267
+ const envelope = buildApprovalEnvelope(input);
268
+ const started = await startRefillRunRemote({
269
+ workspaceId: envelope.scope.workspaceId,
270
+ config: {
271
+ scope: envelope.scope,
272
+ scopeHash: envelope.scopeHash,
273
+ approval: envelope.approval,
274
+ },
275
+ runState: {
276
+ version: 1,
277
+ scope: envelope.scope,
278
+ scopeHash: envelope.scopeHash,
279
+ approval: envelope.approval,
280
+ senderCursors: [],
281
+ laneCooldowns: [],
282
+ passRates: [],
283
+ },
284
+ });
285
+ const result = record(started) ?? {};
286
+ const runId = text(result.runId);
287
+ const fence = typeof result.fence === "number" && Number.isFinite(result.fence)
288
+ ? result.fence
289
+ : null;
290
+ if (!runId || fence == null) {
291
+ return {
292
+ status: "blocked",
293
+ blocker: text(result.blocker) ?? "approval_run_unavailable",
294
+ response: started,
295
+ };
296
+ }
297
+ const persistedConfig = record(result.config);
298
+ const persistedScope = record(persistedConfig?.scope) ?? envelope.scope;
299
+ const persistedApproval = record(persistedConfig?.approval) ??
300
+ envelope.approval;
301
+ return {
302
+ ...persistedApproval,
303
+ status: "awaiting_approval",
304
+ runId,
305
+ fence,
306
+ scope: persistedScope,
307
+ };
308
+ }
309
+ function validateApprovalEcho(envelope, echo, now = new Date()) {
310
+ const fields = [
311
+ "scopeHash",
312
+ "targetShapeRevision",
313
+ "actionFingerprint",
314
+ "expectedTargetsHash",
315
+ "approvalExpiresAt",
316
+ "approvalFingerprint",
317
+ ];
318
+ const changed = fields.filter((field) => echo[field] !== envelope.approval[field]);
319
+ if (Date.parse(envelope.approval.approvalExpiresAt) <= now.getTime()) {
320
+ return { ok: false, blocker: "approval_expired", changed };
321
+ }
322
+ return changed.length === 0
323
+ ? { ok: true }
324
+ : {
325
+ ok: false,
326
+ blocker: "approval_fingerprint_mismatch",
327
+ changed,
328
+ };
329
+ }
71
330
  async function maybeAddLostFenceGuidance(result, input) {
72
331
  if (result.status !== "blocked" ||
73
332
  result.blocker !== "lease_lost" ||
@@ -94,12 +353,45 @@ async function maybeAddLostFenceGuidance(result, input) {
94
353
  }
95
354
  export async function refillSendsV2Command(input) {
96
355
  const workspaceId = normalizeWorkspaceId(input.workspaceId);
356
+ const date = normalizeDateSelector(input);
357
+ let resolvedScope = input.scope;
358
+ let resolvedScopeHash = input.scopeHash;
359
+ let resolvedApproval = input.approval;
360
+ if (input.targetPlan && input.approvalEcho) {
361
+ const envelope = buildApprovalEnvelope({
362
+ workspaceId,
363
+ intent: input.intent,
364
+ senderIds: input.senderIds,
365
+ approvalMode: input.approvalMode,
366
+ targetDate: input.targetDate,
367
+ untilDate: input.untilDate,
368
+ horizonSendDays: input.horizonSendDays,
369
+ targetPlan: input.targetPlan,
370
+ approvalExpiresAt: input.approvalEcho.approvalExpiresAt,
371
+ });
372
+ const validation = validateApprovalEcho(envelope, input.approvalEcho);
373
+ if (!validation.ok) {
374
+ return {
375
+ status: "blocked",
376
+ blocker: validation.blocker,
377
+ guidance: "The durable refill approval no longer matches fresh scope; render and approve a new packet before mutation.",
378
+ report: { changed: validation.changed, fresh: envelope.approval },
379
+ readOnly: false,
380
+ forbiddenActions: FORBIDDEN_ACTIONS,
381
+ boundedAuthority: BOUNDED_AUTHORITY,
382
+ };
383
+ }
384
+ resolvedScope = envelope.scope;
385
+ resolvedScopeHash = envelope.scopeHash;
386
+ resolvedApproval = envelope.approval;
387
+ }
97
388
  if (input.dryRun) {
98
389
  const plan = await getRefillPlanV2({
99
390
  workspaceId,
100
391
  intent: input.intent,
101
392
  senderIds: input.senderIds,
102
393
  journal: true,
394
+ ...date,
103
395
  });
104
396
  return {
105
397
  ...plan,
@@ -115,6 +407,10 @@ export async function refillSendsV2Command(input) {
115
407
  senderIds: input.senderIds,
116
408
  approvalMode: input.approvalMode ?? "approve",
117
409
  resume: resumeHandle(input),
410
+ ...date,
411
+ scope: resolvedScope,
412
+ scopeHash: resolvedScopeHash,
413
+ approval: resolvedApproval,
118
414
  }, {
119
415
  runClient: {
120
416
  startRefillRunRemote,
@@ -131,12 +427,16 @@ export async function refillSendsV2Command(input) {
131
427
  senderIds: planInput.senderIds,
132
428
  runState: planInput.runState,
133
429
  journal: planInput.journal,
430
+ targetDate: planInput.targetDate,
431
+ untilDate: planInput.untilDate,
432
+ horizonSendDays: planInput.horizonSendDays,
134
433
  }),
135
434
  executors: {
136
435
  executeOneYoloPrimitive,
137
436
  executeStartCampaignPrimitive,
138
437
  refreshPaidInmailCreditsWithRetry,
139
438
  getPrepareCampaignMessagesStatus,
439
+ stopSatisfiedPrepareCampaignMessages,
140
440
  refillPrepareRequestHash,
141
441
  prepareRowSelectorValue,
142
442
  },