@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.
@@ -1,6 +1,6 @@
1
1
  import type { appendIndexLine, appendJournalEvent, createRunJournal, renderBootstrapSection, renderCrashedAbandonedSection, renderExecutionEventSection, renderPlanSection, renderTerminalSection } from "./refill-journal.js";
2
2
  import type { advanceRefillRunGateRemote, completeRefillRun, heartbeatRefillRun, recordRunOutcome, recordRunPlanned, refillRunStatus, startRefillRunRemote } from "./refill-run-client.js";
3
- import type { getPrepareCampaignMessagesStatus } from "./tools/campaign-message-preparation.js";
3
+ import type { getPrepareCampaignMessagesStatus, stopSatisfiedPrepareCampaignMessages } from "./tools/campaign-message-preparation.js";
4
4
  import type { executeOneYoloPrimitive, executeStartCampaignPrimitive, prepareRowSelectorValue, refillPrepareRequestHash, refreshPaidInmailCreditsWithRetry } from "./tools/refill-executors.js";
5
5
  export type RefillLoopGate = "g0_bootstrap" | "g1_plan" | "g2_execute" | "g3_verify" | "g4_terminal";
6
6
  export interface RefillV2LoopInput {
@@ -13,6 +13,12 @@ export interface RefillV2LoopInput {
13
13
  };
14
14
  approvalMode?: "approve" | "mark_ready";
15
15
  dryRun?: boolean;
16
+ targetDate?: string;
17
+ untilDate?: string;
18
+ horizonSendDays?: number;
19
+ scope?: Record<string, unknown>;
20
+ scopeHash?: string;
21
+ approval?: Record<string, unknown>;
16
22
  }
17
23
  export interface RefillV2LoopBudgets {
18
24
  maxGateCyclesPerInvocation: number;
@@ -36,6 +42,7 @@ export interface RefillV2Executors {
36
42
  executeStartCampaignPrimitive: typeof executeStartCampaignPrimitive;
37
43
  refreshPaidInmailCreditsWithRetry: typeof refreshPaidInmailCreditsWithRetry;
38
44
  getPrepareCampaignMessagesStatus?: typeof getPrepareCampaignMessagesStatus;
45
+ stopSatisfiedPrepareCampaignMessages?: typeof stopSatisfiedPrepareCampaignMessages;
39
46
  refillPrepareRequestHash?: typeof refillPrepareRequestHash;
40
47
  prepareRowSelectorValue?: typeof prepareRowSelectorValue;
41
48
  }
@@ -61,6 +68,10 @@ export interface RefillV2LoopDeps {
61
68
  senderIds?: string[];
62
69
  runState?: Record<string, unknown>;
63
70
  journal?: boolean;
71
+ targetDate?: string;
72
+ untilDate?: string;
73
+ horizonSendDays?: number;
74
+ scopeHash?: string;
64
75
  }) => Promise<Record<string, unknown>>;
65
76
  executors: RefillV2Executors;
66
77
  journal: RefillV2JournalDeps;
@@ -59,6 +59,16 @@ const SCHEDULER_RUN_NEXT_ACTIONS = new Set([
59
59
  function isRecord(value) {
60
60
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
61
61
  }
62
+ function planScopeInput(input) {
63
+ return {
64
+ ...(input.targetDate ? { targetDate: input.targetDate } : {}),
65
+ ...(input.untilDate ? { untilDate: input.untilDate } : {}),
66
+ ...(input.horizonSendDays !== undefined
67
+ ? { horizonSendDays: input.horizonSendDays }
68
+ : {}),
69
+ ...(input.scopeHash ? { scopeHash: input.scopeHash } : {}),
70
+ };
71
+ }
62
72
  function stringValue(value) {
63
73
  return typeof value === "string" && value.trim() ? value.trim() : null;
64
74
  }
@@ -256,10 +266,8 @@ function sanitizeSchedulerRunReceipt(raw) {
256
266
  const prefilteredCells = numberValue(rawReceipt.prefilteredCells) ?? 0;
257
267
  const tablesFilteredForSenderMismatch = numberValue(rawReceipt.tablesFilteredForSenderMismatch) ?? 0;
258
268
  dominantSkipReason = dominantSchedulerRunSkipReason(skipReasons);
259
- dominantPrefilterReason =
260
- dominantSchedulerRunCountReason(prefilterReasons);
261
- dominantDeferredReason =
262
- dominantSchedulerRunSkipReason(deferredReasons);
269
+ dominantPrefilterReason = dominantSchedulerRunCountReason(prefilterReasons);
270
+ dominantDeferredReason = dominantSchedulerRunSkipReason(deferredReasons);
263
271
  recommendedNextAction = schedulerRunRecommendedAction({
264
272
  rawAction: stringValue(rawReceipt.nextAction),
265
273
  readyCellsFound,
@@ -429,6 +437,43 @@ function normalizeDoneReason(value) {
429
437
  function runStateProgress(ctx) {
430
438
  return recordValue(ctx.runState.progress) ?? {};
431
439
  }
440
+ function prepCircuitFrom(value) {
441
+ const root = recordValue(value);
442
+ const result = recordValue(root?.result);
443
+ const progress = recordValue(root?.progress);
444
+ return (recordValue(root?.circuit) ??
445
+ recordValue(result?.circuit) ??
446
+ recordValue(progress?.circuit) ??
447
+ null);
448
+ }
449
+ function openCircuitWaits(circuit, now) {
450
+ if (stringValue(circuit?.state) !== "open")
451
+ return false;
452
+ const nextProbeAt = stringValue(circuit?.nextProbeAt);
453
+ return Boolean(nextProbeAt && Date.parse(nextProbeAt) > now.getTime());
454
+ }
455
+ function activePreparationScopeSatisfied(plan, action) {
456
+ const packet = recordValue(plan.packet) ?? plan;
457
+ const target = recordValue(packet.target) ?? recordValue(plan.target) ?? {};
458
+ const senderId = actionSenderId(action);
459
+ const toolInput = actionToolInput(action);
460
+ const targetDate = stringValue(toolInput.targetDate);
461
+ const refillLaneKey = stringValue(toolInput.refillLaneKey);
462
+ const senderPlans = arrayValue(target.senderRefillPlans).filter(isRecord);
463
+ const ledgers = senderPlans
464
+ .filter((senderPlan) => !senderId || stringValue(senderPlan.senderId) === senderId)
465
+ .flatMap((senderPlan) => arrayValue(senderPlan.laneLedgers).filter(isRecord));
466
+ const matching = ledgers.filter((ledger) => (!senderId || stringValue(ledger.senderId) === senderId) &&
467
+ (!targetDate || stringValue(ledger.targetDate) === targetDate) &&
468
+ (!refillLaneKey || stringValue(ledger.refillLaneKey) === refillLaneKey));
469
+ return (matching.length > 0 &&
470
+ matching.every((ledger) => {
471
+ const checks = recordValue(ledger.identityChecks);
472
+ return (numberValue(ledger.initialAllocatablePrepDeficit) === 0 &&
473
+ checks != null &&
474
+ Object.values(checks).every((value) => value === true));
475
+ }));
476
+ }
432
477
  function mergeProgress(ctx, updates) {
433
478
  return {
434
479
  ...runStateProgress(ctx),
@@ -672,14 +717,28 @@ async function createJournal(input, deps, runId) {
672
717
  }
673
718
  async function startOrResume(input, deps) {
674
719
  const initialRunState = buildRunStateFromLocalHints(input.workspaceId);
720
+ const scopedInitialRunState = input.scope
721
+ ? {
722
+ ...initialRunState,
723
+ scope: input.scope,
724
+ scopeHash: input.scopeHash,
725
+ approval: input.approval,
726
+ }
727
+ : initialRunState;
675
728
  const started = await deps.runClient.startRefillRunRemote({
676
729
  workspaceId: input.workspaceId,
677
- config: {
678
- intent: input.intent ?? "auto",
679
- senderIds: input.senderIds,
680
- approvalMode: input.approvalMode ?? "approve",
681
- },
682
- runState: initialRunState,
730
+ config: input.scope
731
+ ? {
732
+ scope: input.scope,
733
+ scopeHash: input.scopeHash,
734
+ approval: input.approval,
735
+ }
736
+ : {
737
+ intent: input.intent ?? "auto",
738
+ senderIds: input.senderIds,
739
+ approvalMode: input.approvalMode ?? "approve",
740
+ },
741
+ runState: scopedInitialRunState,
683
742
  resume: input.resume,
684
743
  });
685
744
  if (isLeaseLost(started)) {
@@ -712,7 +771,7 @@ async function startOrResume(input, deps) {
712
771
  const gate = (stringValue(result.gate) ?? "g0_bootstrap");
713
772
  const gateSeq = numberValue(result.gateSeq) ?? 0;
714
773
  const runState = recordValue(result.runState) ??
715
- recordValue(initialRunState) ??
774
+ recordValue(scopedInitialRunState) ??
716
775
  {
717
776
  version: 1,
718
777
  senderCursors: [],
@@ -755,6 +814,7 @@ async function gateBootstrap(input, deps, ctx) {
755
814
  senderIds: input.senderIds,
756
815
  runState: ctx.runState,
757
816
  journal: false,
817
+ ...planScopeInput(input),
758
818
  });
759
819
  const paidCredit = arrayValue(recordValue(plan.bootstrap)?.paidCredit).filter(isRecord);
760
820
  const refreshedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.senderIds)
@@ -901,6 +961,7 @@ async function gatePlan(input, deps, ctx) {
901
961
  senderIds: input.senderIds,
902
962
  runState: ctx.runState,
903
963
  journal: false,
964
+ ...planScopeInput(input),
904
965
  });
905
966
  if (plan.blocker === "workspace_access") {
906
967
  return {
@@ -960,6 +1021,17 @@ async function gatePlan(input, deps, ctx) {
960
1021
  fallbackSummary: line.fallbackSummary,
961
1022
  }));
962
1023
  const fingerprint = fingerprintFromPlan(plan);
1024
+ const persistedCircuit = recordValue(runStateProgress(ctx).prepCircuit);
1025
+ if (stringValue(action.type) === "prepare_messages" &&
1026
+ openCircuitWaits(persistedCircuit, deps.now?.() ?? new Date())) {
1027
+ const heartbeat = await heartbeatAndContinue(input, deps, ctx);
1028
+ if (heartbeat.status !== "in_progress")
1029
+ return heartbeat;
1030
+ return {
1031
+ ...heartbeat,
1032
+ guidance: `Preparation circuit remains open until ${String(persistedCircuit?.nextProbeAt)}; re-invoke with this handle after the probe deadline.`,
1033
+ };
1034
+ }
963
1035
  const noProgress = nextNoProgressState(ctx, plan, action);
964
1036
  if (noProgress.repeatCount >= 3) {
965
1037
  return completeTerminal(input, deps, ctx, "blocked", {
@@ -970,10 +1042,17 @@ async function gatePlan(input, deps, ctx) {
970
1042
  guidance: "The same refill rung repeated three times with no coverage, frontier, or lane movement.",
971
1043
  });
972
1044
  }
1045
+ const circuitProgress = stringValue(action.type) === "prepare_messages" &&
1046
+ stringValue(persistedCircuit?.state) === "open"
1047
+ ? { ...persistedCircuit, state: "half_open" }
1048
+ : persistedCircuit;
973
1049
  const nextRunState = mergeRunState(ctx.runState, {
974
1050
  lastPlan: fingerprint,
975
1051
  headAction: action,
976
- progress: mergeProgress(ctx, { noProgress }),
1052
+ progress: mergeProgress(ctx, {
1053
+ noProgress,
1054
+ ...(circuitProgress ? { prepCircuit: circuitProgress } : {}),
1055
+ }),
977
1056
  });
978
1057
  const advanced = await deps.runClient.advanceRefillRunGateRemote({
979
1058
  workspaceId: input.workspaceId,
@@ -1010,6 +1089,7 @@ async function verifyRecoveryPoint(input, deps, ctx) {
1010
1089
  senderIds: input.senderIds,
1011
1090
  runState: ctx.runState,
1012
1091
  journal: false,
1092
+ ...planScopeInput(input),
1013
1093
  });
1014
1094
  const changed = fingerprintChanged(expected, fingerprintFromPlan(fresh));
1015
1095
  if (changed.length > 0) {
@@ -1079,6 +1159,9 @@ async function gateExecute(input, deps, ctx) {
1079
1159
  dispatchKey: stringValue(action.type),
1080
1160
  evergreenRung: stringValue(action.evergreenRung),
1081
1161
  sideEffectClass: stringValue(action.sideEffectClass),
1162
+ scopeHash: input.scopeHash ?? null,
1163
+ expectedTargetsHash: stringValue(input.scope?.expectedTargetsHash) ?? null,
1164
+ dateSelector: recordValue(input.scope?.dateSelector),
1082
1165
  };
1083
1166
  const plannedResult = await deps.runClient.recordRunPlanned({
1084
1167
  workspaceId: input.workspaceId,
@@ -1172,6 +1255,12 @@ async function advanceBackToPlan(input, deps, ctx, runStateUpdates = {}) {
1172
1255
  async function verifyPrepareAction(input, deps, ctx, action, budgets) {
1173
1256
  const getStatus = deps.executors.getPrepareCampaignMessagesStatus;
1174
1257
  const jobId = activeJobIdFromOutcome(ctx.runState.lastOutcome);
1258
+ const executionCircuit = prepCircuitFrom(ctx.runState.lastOutcome);
1259
+ if (!jobId && executionCircuit) {
1260
+ return advanceBackToPlan(input, deps, ctx, {
1261
+ progress: mergeProgress(ctx, { prepCircuit: executionCircuit }),
1262
+ });
1263
+ }
1175
1264
  if (!getStatus || !jobId) {
1176
1265
  return {
1177
1266
  status: "in_progress",
@@ -1211,6 +1300,58 @@ async function verifyPrepareAction(input, deps, ctx, action, budgets) {
1211
1300
  }),
1212
1301
  });
1213
1302
  }
1303
+ if (ownJob &&
1304
+ !isTerminalJobStatus(latestStatus) &&
1305
+ deps.executors.stopSatisfiedPrepareCampaignMessages) {
1306
+ const fresh = await deps.readPlan({
1307
+ workspaceId: input.workspaceId,
1308
+ intent: input.intent,
1309
+ senderIds: input.senderIds,
1310
+ runState: ctx.runState,
1311
+ journal: false,
1312
+ ...planScopeInput(input),
1313
+ });
1314
+ ctx.plan = fresh;
1315
+ if (activePreparationScopeSatisfied(fresh, action)) {
1316
+ const plannedStop = await deps.runClient.recordRunPlanned({
1317
+ workspaceId: input.workspaceId,
1318
+ runId: ctx.runId,
1319
+ fence: ctx.fence,
1320
+ gate: ctx.gate,
1321
+ gateSeq: ctx.gateSeq,
1322
+ planned: {
1323
+ toolName: "stop_satisfied_campaign_message_preparation",
1324
+ toolInput: { jobId },
1325
+ dispatchKey: `stop_satisfied:${jobId}`,
1326
+ sideEffectClass: "message_preparation_stop",
1327
+ scopeHash: input.scopeHash ?? null,
1328
+ },
1329
+ });
1330
+ if (isLeaseLost(plannedStop))
1331
+ return leaseLostResult(ctx);
1332
+ const stopEventId = stringValue(recordValue(plannedStop)?.eventId) ??
1333
+ `${ctx.runId}:${ctx.gateSeq}:stop`;
1334
+ const stopReceipt = await deps.executors.stopSatisfiedPrepareCampaignMessages({
1335
+ workspaceId: input.workspaceId,
1336
+ jobId,
1337
+ expectedUpdatedAt: stringValue(statusRecord.updatedAt) ?? undefined,
1338
+ });
1339
+ await deps.runClient.recordRunOutcome({
1340
+ workspaceId: input.workspaceId,
1341
+ runId: ctx.runId,
1342
+ fence: ctx.fence,
1343
+ eventId: stopEventId,
1344
+ did: { jobId },
1345
+ outcome: stopReceipt,
1346
+ });
1347
+ return advanceBackToPlan(input, deps, ctx, {
1348
+ progress: mergeProgress(ctx, {
1349
+ prepStopReceipt: stopReceipt,
1350
+ prepStoppedAt: (deps.now?.() ?? new Date()).toISOString(),
1351
+ }),
1352
+ });
1353
+ }
1354
+ }
1214
1355
  if (isTerminalJobStatus(latestStatus)) {
1215
1356
  const preparedCount = preparedCountFromStatus(latestStatus);
1216
1357
  const checkedRows = checkedRowsFromStatus(latestStatus);
@@ -1278,6 +1419,7 @@ async function verifyReadOnlyWait(input, deps, ctx, budgets, waitKind) {
1278
1419
  senderIds: input.senderIds,
1279
1420
  runState: ctx.runState,
1280
1421
  journal: false,
1422
+ ...planScopeInput(input),
1281
1423
  });
1282
1424
  ctx.plan = fresh;
1283
1425
  const doneReason = doneReasonFromPlan(fresh);
@@ -1360,6 +1502,7 @@ async function verifySchedulerWait(input, deps, ctx, action, budgets) {
1360
1502
  senderIds: input.senderIds,
1361
1503
  runState: ctx.runState,
1362
1504
  journal: false,
1505
+ ...planScopeInput(input),
1363
1506
  });
1364
1507
  ctx.plan = fresh;
1365
1508
  const doneReason = normalizeDoneReason(doneReasonFromPlan(fresh));
@@ -1422,6 +1565,7 @@ async function gateVerify(input, deps, ctx, budgets) {
1422
1565
  senderIds: input.senderIds,
1423
1566
  runState: ctx.runState,
1424
1567
  journal: false,
1568
+ ...planScopeInput(input),
1425
1569
  });
1426
1570
  ctx.plan = fresh;
1427
1571
  const doneReason = doneReasonFromPlan(fresh);
@@ -1486,7 +1630,7 @@ async function heartbeatAndContinue(input, deps, ctx) {
1486
1630
  runId: ctx.runId,
1487
1631
  fence: ctx.fence,
1488
1632
  gate: ctx.gate,
1489
- guidance: "Per-invocation gate budget reached. Re-invoke refill_sends_v2 with this runId/fence to continue.",
1633
+ guidance: "Per-invocation gate budget reached. Re-invoke public refill_sends with this durable run handle to continue.",
1490
1634
  journalPath: ctx.journalPath,
1491
1635
  };
1492
1636
  }
@@ -1500,6 +1644,7 @@ export async function runRefillV2Loop(input, deps) {
1500
1644
  intent: input.intent,
1501
1645
  senderIds: input.senderIds,
1502
1646
  journal: true,
1647
+ ...planScopeInput(input),
1503
1648
  });
1504
1649
  return { status: "dry_run", plan };
1505
1650
  }
@@ -36,9 +36,16 @@ type StartPrepareMessagesInput = PrepareMessagesBaseInput & {
36
36
  requestSource?: string;
37
37
  senderId?: string;
38
38
  actionType?: string;
39
+ targetDate?: string;
40
+ refillLaneKey?: string;
41
+ sourceEpoch?: string;
39
42
  };
40
43
  type StatusPrepareMessagesInput = PrepareMessagesBaseInput;
41
44
  type CancelPrepareMessagesInput = PrepareMessagesBaseInput;
45
+ type StopSatisfiedPrepareMessagesInput = PrepareMessagesBaseInput & {
46
+ jobId: string;
47
+ expectedUpdatedAt?: string;
48
+ };
42
49
  export declare const campaignMessagePreparationToolDefinitions: ({
43
50
  name: string;
44
51
  description: string;
@@ -117,6 +124,18 @@ export declare const campaignMessagePreparationToolDefinitions: ({
117
124
  enum: string[];
118
125
  description: string;
119
126
  };
127
+ targetDate: {
128
+ type: string;
129
+ description: string;
130
+ };
131
+ refillLaneKey: {
132
+ type: string;
133
+ description: string;
134
+ };
135
+ sourceEpoch: {
136
+ type: string;
137
+ description: string;
138
+ };
120
139
  disableLowPassRateStop: {
121
140
  type: string;
122
141
  description: string;
@@ -126,6 +145,7 @@ export declare const campaignMessagePreparationToolDefinitions: ({
126
145
  description: string;
127
146
  };
128
147
  jobId?: undefined;
148
+ expectedUpdatedAt?: undefined;
129
149
  };
130
150
  required: string[];
131
151
  additionalProperties: boolean;
@@ -160,13 +180,55 @@ export declare const campaignMessagePreparationToolDefinitions: ({
160
180
  autoContinue?: undefined;
161
181
  senderId?: undefined;
162
182
  actionType?: undefined;
183
+ targetDate?: undefined;
184
+ refillLaneKey?: undefined;
185
+ sourceEpoch?: undefined;
163
186
  disableLowPassRateStop?: undefined;
187
+ expectedUpdatedAt?: undefined;
164
188
  };
165
189
  additionalProperties: boolean;
166
190
  required?: undefined;
167
191
  };
192
+ } | {
193
+ name: string;
194
+ description: string;
195
+ inputSchema: {
196
+ type: string;
197
+ properties: {
198
+ jobId: {
199
+ type: string;
200
+ };
201
+ expectedUpdatedAt: {
202
+ type: string;
203
+ description: string;
204
+ };
205
+ workspaceId: {
206
+ type: string;
207
+ description?: undefined;
208
+ };
209
+ campaignId?: undefined;
210
+ tableId?: undefined;
211
+ targetPreparedMessages?: undefined;
212
+ maxRowsToCheck?: undefined;
213
+ batchSize?: undefined;
214
+ maxBatchRows?: undefined;
215
+ approvalMode?: undefined;
216
+ rowSelector?: undefined;
217
+ excludeRowIds?: undefined;
218
+ autoContinue?: undefined;
219
+ senderId?: undefined;
220
+ actionType?: undefined;
221
+ targetDate?: undefined;
222
+ refillLaneKey?: undefined;
223
+ sourceEpoch?: undefined;
224
+ disableLowPassRateStop?: undefined;
225
+ };
226
+ required: string[];
227
+ additionalProperties: boolean;
228
+ };
168
229
  })[];
169
230
  export declare function startPrepareCampaignMessages(input: StartPrepareMessagesInput): Promise<unknown>;
170
231
  export declare function getPrepareCampaignMessagesStatus(input: StatusPrepareMessagesInput): Promise<unknown>;
171
232
  export declare function cancelPrepareCampaignMessages(input: CancelPrepareMessagesInput): Promise<unknown>;
233
+ export declare function stopSatisfiedPrepareCampaignMessages(input: StopSatisfiedPrepareMessagesInput): Promise<unknown>;
172
234
  export {};
@@ -84,6 +84,18 @@ export const campaignMessagePreparationToolDefinitions = [
84
84
  ],
85
85
  description: "Optional refill-scope send action. InMail scopes treat open and paid InMail as one contact-reuse family.",
86
86
  },
87
+ targetDate: {
88
+ type: "string",
89
+ description: "Exact sender-local YYYY-MM-DD refill target date.",
90
+ },
91
+ refillLaneKey: {
92
+ type: "string",
93
+ description: "Normalized refill lane identity from the canonical target plan.",
94
+ },
95
+ sourceEpoch: {
96
+ type: "string",
97
+ description: "Stable source/provider epoch; only a material epoch change resets zero-yield recurrence.",
98
+ },
87
99
  disableLowPassRateStop: {
88
100
  type: "boolean",
89
101
  description: "Operator override for explicit fill/exhaust requests. When true, keep processing eligible rows even if the enriched sample pass rate is low. Still bounded by targetPreparedMessages and maxRowsToCheck.",
@@ -111,6 +123,23 @@ export const campaignMessagePreparationToolDefinitions = [
111
123
  additionalProperties: false,
112
124
  },
113
125
  },
126
+ {
127
+ name: "stop_satisfied_campaign_message_preparation",
128
+ description: "Cooperatively stop exactly one preparation job after the server rereads its persisted refill scope and proves its allocatable preparation deficit is zero. This never cancels table-wide workflow cells.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ jobId: { type: "string" },
133
+ expectedUpdatedAt: {
134
+ type: "string",
135
+ description: "Optional optimistic job updatedAt revision from status.",
136
+ },
137
+ workspaceId: { type: "string" },
138
+ },
139
+ required: ["jobId"],
140
+ additionalProperties: false,
141
+ },
142
+ },
114
143
  {
115
144
  name: "cancel_campaign_message_preparation",
116
145
  description: "Cancel an active campaign message preparation job and cancel the same pending workflow-table cells that the UI Cancel Pending Cells button would cancel. Launch remains a separate explicit start_campaign step.",
@@ -145,6 +174,9 @@ export function startPrepareCampaignMessages(input) {
145
174
  requestSource: input.requestSource,
146
175
  senderId: input.senderId,
147
176
  actionType: input.actionType,
177
+ targetDate: input.targetDate,
178
+ refillLaneKey: input.refillLaneKey,
179
+ sourceEpoch: input.sourceEpoch,
148
180
  ...(workspaceId ? { workspaceId } : {}),
149
181
  }, workspaceId ?? undefined);
150
182
  }
@@ -168,3 +200,12 @@ export function cancelPrepareCampaignMessages(input) {
168
200
  ...(workspaceId ? { workspaceId } : {}),
169
201
  }, workspaceId ?? undefined);
170
202
  }
203
+ export function stopSatisfiedPrepareCampaignMessages(input) {
204
+ const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
205
+ return postPrepareMessages({
206
+ action: "stop_if_target_satisfied",
207
+ jobId: input.jobId,
208
+ expectedUpdatedAt: input.expectedUpdatedAt,
209
+ ...(workspaceId ? { workspaceId } : {}),
210
+ }, workspaceId ?? undefined);
211
+ }
@@ -2,6 +2,9 @@ type GetRefillPlanV2Input = {
2
2
  workspaceId?: string;
3
3
  intent?: "auto" | "evergreen" | "plain" | "active";
4
4
  senderIds?: string[];
5
+ horizonSendDays?: number;
6
+ untilDate?: string;
7
+ targetDate?: string;
5
8
  runState?: Record<string, unknown>;
6
9
  journal?: boolean;
7
10
  journalNote?: string;
@@ -2,6 +2,7 @@ import * as path from "node:path";
2
2
  import { getApi, SellableApiError } from "../api.js";
3
3
  import { appendIndexLine, appendJournalEvent, createRunJournal, renderBootstrapSection, renderPlanSection, renderTerminalSection, } from "../refill-journal.js";
4
4
  import { readRefillWorkspaceState } from "../refill-local-state.js";
5
+ import { getRefillTargetPlan } from "./refill-target-plan.js";
5
6
  import { normalizeExplicitWorkspaceId, workspaceRequestOptions, } from "./workspace-context.js";
6
7
  const KNOWN_TOP_LEVEL_FIELDS = [
7
8
  "readOnly",
@@ -199,12 +200,24 @@ export async function getRefillPlanV2(input) {
199
200
  : buildRunStateFromLocalHints(workspaceId);
200
201
  let raw;
201
202
  try {
202
- raw = await postRefillPlanV2({
203
- workspaceId,
204
- intent: input.intent ?? "auto",
205
- senderIds: input.senderIds,
206
- runState,
207
- }, workspaceId);
203
+ const hasExplicitDateSelector = input.targetDate !== undefined ||
204
+ input.untilDate !== undefined ||
205
+ input.horizonSendDays !== undefined;
206
+ raw = hasExplicitDateSelector
207
+ ? await getRefillTargetPlan({
208
+ workspaceId,
209
+ intent: input.intent === "auto" ? undefined : (input.intent ?? undefined),
210
+ senderIds: input.senderIds,
211
+ targetDate: input.targetDate,
212
+ untilDate: input.untilDate,
213
+ horizonSendDays: input.horizonSendDays,
214
+ })
215
+ : await postRefillPlanV2({
216
+ workspaceId,
217
+ intent: input.intent ?? "auto",
218
+ senderIds: input.senderIds,
219
+ runState,
220
+ }, workspaceId);
208
221
  }
209
222
  catch (error) {
210
223
  if (!isWorkspaceAccessError(error))
@@ -238,7 +251,16 @@ export async function getRefillPlanV2(input) {
238
251
  workspaceResolution: "explicit",
239
252
  };
240
253
  }
241
- const sanitized = sanitizeEvergreenRefillPlanResult(raw);
254
+ const rawRecord = isRecord(raw) ? raw : {};
255
+ const target = isRecord(rawRecord.target) ? rawRecord.target : null;
256
+ const sanitized = sanitizeEvergreenRefillPlanResult(target
257
+ ? {
258
+ ...rawRecord,
259
+ plans: target.senderRefillPlans,
260
+ globalActionQueue: target.globalActionQueue,
261
+ packet: rawRecord,
262
+ }
263
+ : rawRecord);
242
264
  const warnings = Array.isArray(sanitized.warnings)
243
265
  ? [...sanitized.warnings]
244
266
  : [];
@@ -2,6 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { getSkillByName, listSkills, resolveSkillsDir, stripFrontmatter, } from "../skills.js";
4
4
  import { markCreateCampaignPromptLoaded, markResearchPromptLoaded, markSenderResearchCompleted, } from "./flow-preflight.js";
5
+ import { validateRefillContractAsset, validateRefillFlowProjection, } from "../refill-contract.js";
5
6
  // Chunk size sits below Claude Code's max-token cap on tool results
6
7
  // (empirically around ~50k chars — fetch_linkedin_profile breached at
7
8
  // 53,295 chars, which forced a python3 chunked-read fallback). 48k gives
@@ -511,6 +512,14 @@ export function getSubskillAsset(subskillName, assetPath, offset, limit) {
511
512
  throw new Error(`Unknown subskill asset: ${subskillName}/${cleanAssetPath}`);
512
513
  }
513
514
  const content = fs.readFileSync(resolved, "utf8");
515
+ if (subskillName === "refill-sends-workflow" &&
516
+ cleanAssetPath === "core/contract.v2.json") {
517
+ validateRefillContractAsset(JSON.parse(content));
518
+ }
519
+ if (subskillName === "refill-sends-workflow" &&
520
+ cleanAssetPath === "core/flow.v1.json") {
521
+ validateRefillFlowProjection(JSON.parse(content));
522
+ }
514
523
  const chunked = readChunkedContent(content, offset, limit, (nextOffset, safeLimit) => `Continue this same asset load with get_subskill_asset({ subskillName: "${subskillName}", assetPath: "${cleanAssetPath}", offset: ${nextOffset}, limit: ${safeLimit} }) until hasMore is false. Treat all chunks as one asset load; do not use saved local tool-output files or machine-specific paths.`);
515
524
  return {
516
525
  subskillName,
@@ -1,4 +1,5 @@
1
1
  import { searchSignals } from "./leads.js";
2
+ import { type SchedulerExpectedTargetInput } from "./scheduler-run.js";
2
3
  export type RefillSendsApprovalMode = "approve" | "mark_ready";
3
4
  export type PaidInmailRefreshAction = {
4
5
  senderId: string;
@@ -145,9 +146,16 @@ export type SchedulerChangedCounts = {
145
146
  truncated: false;
146
147
  total: number;
147
148
  byCampaignTableActionType: SchedulerChangedCount[];
149
+ byCampaignTableActionTypeSender: Array<SchedulerChangedCount & {
150
+ senderId: string;
151
+ }>;
148
152
  };
149
153
  export declare function normalizeSchedulerChangedCounts(value: unknown): SchedulerChangedCounts | null;
150
- export declare function normalizeSchedulerPrimitiveResult(value: unknown): {
154
+ export declare function normalizeSchedulerPrimitiveResult(value: unknown, expected?: {
155
+ expectedTargets: SchedulerExpectedTargetInput[];
156
+ expectedTargetsHash: string;
157
+ maxPlacements: number;
158
+ }): {
151
159
  status: "scheduler_receipt_incomplete";
152
160
  schedulerStatus: string;
153
161
  receipt: {} | null;