@sellable/mcp 0.1.793 → 0.1.794

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.
Files changed (54) hide show
  1. package/dist/agent-tool-names.d.ts +2 -2
  2. package/dist/agent-tool-names.js +14 -0
  3. package/dist/scheduler-envelope.d.ts +190 -0
  4. package/dist/scheduler-envelope.js +412 -0
  5. package/dist/server.js +28 -2
  6. package/dist/tools/campaign-message-preparation.d.ts +25 -0
  7. package/dist/tools/campaign-message-preparation.js +37 -0
  8. package/dist/tools/campaign-processing.d.ts +74 -1
  9. package/dist/tools/campaign-processing.js +57 -1
  10. package/dist/tools/prompts.d.ts +15 -3
  11. package/dist/tools/prompts.js +25 -0
  12. package/dist/tools/refill-executors.d.ts +43 -70
  13. package/dist/tools/refill-executors.js +201 -273
  14. package/dist/tools/refill-sends.d.ts +3 -3
  15. package/dist/tools/refill-sends.js +1 -1
  16. package/dist/tools/refill-target-plan.js +1 -1
  17. package/dist/tools/refill-v3-advance-contract.d.ts +618 -0
  18. package/dist/tools/refill-v3-advance-contract.js +920 -0
  19. package/dist/tools/refill-v3-advance.d.ts +397 -0
  20. package/dist/tools/refill-v3-advance.js +75 -0
  21. package/dist/tools/refill-v3-attention-packet-contract.d.ts +608 -0
  22. package/dist/tools/refill-v3-attention-packet-contract.js +911 -0
  23. package/dist/tools/refill-v3-campaign-work.d.ts +324 -0
  24. package/dist/tools/refill-v3-campaign-work.js +1015 -0
  25. package/dist/tools/refill-v3-continue-contract.d.ts +433 -0
  26. package/dist/tools/refill-v3-continue-contract.js +112 -0
  27. package/dist/tools/refill-v3-continue.d.ts +501 -0
  28. package/dist/tools/refill-v3-continue.js +654 -0
  29. package/dist/tools/refill-v3-edit-selected-campaign-contract.d.ts +208 -0
  30. package/dist/tools/refill-v3-edit-selected-campaign-contract.js +351 -0
  31. package/dist/tools/refill-v3-edit-selected-campaign.d.ts +232 -0
  32. package/dist/tools/refill-v3-edit-selected-campaign.js +147 -0
  33. package/dist/tools/refill-v3-fill-ready-contract.d.ts +198 -0
  34. package/dist/tools/refill-v3-fill-ready-contract.js +324 -0
  35. package/dist/tools/refill-v3-fill-ready.d.ts +199 -0
  36. package/dist/tools/refill-v3-fill-ready.js +136 -0
  37. package/dist/tools/refill-v3-source-family-dispatch.d.ts +249 -0
  38. package/dist/tools/refill-v3-source-family-dispatch.js +514 -0
  39. package/dist/tools/refill-v3-waterfall-contract.d.ts +156 -0
  40. package/dist/tools/refill-v3-waterfall-contract.js +181 -0
  41. package/dist/tools/refill-v3-waterfall.d.ts +136 -0
  42. package/dist/tools/refill-v3-waterfall.js +161 -0
  43. package/dist/tools/refill-v3-world-state-contract.d.ts +666 -0
  44. package/dist/tools/refill-v3-world-state-contract.js +790 -0
  45. package/dist/tools/refill-v3-world-state.d.ts +592 -0
  46. package/dist/tools/refill-v3-world-state.js +137 -0
  47. package/dist/tools/registry.d.ts +1710 -3
  48. package/dist/tools/registry.js +19 -0
  49. package/package.json +1 -1
  50. package/skills/refill-sends/SKILL.md +89 -4
  51. package/skills/refill-sends-waterfall-order/SKILL.md +174 -0
  52. package/skills/refill-sends-work-campaign/SKILL.md +253 -0
  53. package/skills/refill-sends-workflow/SKILL.md +112 -5
  54. package/skills/refill-sends-workflow/core/flow.v3.json +209 -0
@@ -1,11 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { getApi, SellableApiError } from "../api.js";
3
3
  import { decodeRefillReadinessAuthority, decodeRefillSchedulerDateGuards, refillReadinessExpectationInput, refillReadinessFingerprint, } from "../refill-readiness.js";
4
+ import { hasTerminalSchedulerReceipt, normalizeSchedulerChangedCounts, normalizeSchedulerPrimitiveResult, numberValue, recordValue, SCHEDULER_ATTACHING_ENVELOPE_STATUSES, schedulerEnvelopeIsTerminal, stringArray, stringValue, } from "../scheduler-envelope.js";
4
5
  import { startPrepareCampaignMessages } from "./campaign-message-preparation.js";
5
6
  import { normalizeMessageTemplateRevision, normalizeRefillRunHandle, normalizeRefillTargetConfig, reviseMessageTemplateAndRerun, } from "./campaign-processing.js";
6
7
  import { startCampaign } from "./campaigns.js";
7
8
  import { confirmLeadList, importLeads, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, } from "./leads.js";
8
- import { markProviderPromptLoaded } from "./provider-preflight.js";
9
9
  import { refreshSenderEngagementCommand } from "./refresh-sender-engagement.js";
10
10
  import { runSchedulerSweep, } from "./scheduler-run.js";
11
11
  import { refreshPaidInmailCredits } from "./senders.js";
@@ -51,6 +51,80 @@ const USABLE_PAID_INMAIL_REFRESH_RECEIPT_STATUSES = new Set([
51
51
  "refreshed",
52
52
  "below_threshold",
53
53
  ]);
54
+ // ───────────────────────────────────────────────────────────────────────────
55
+ // Refill V3 source-dispatch seam (Phase 146.5 Plan 09 T3, RV3PRM-002).
56
+ //
57
+ // T3 is the SOLE STRUCTURAL owner of this seam (`A09-EXECUTORS-SERIALIZATION`):
58
+ // it introduces the receipt-shape extension and the dispatcher call seam, and
59
+ // T4/T5 make only additive table-entry and characterization changes. There is
60
+ // EXACTLY ONE receipt-normalization path for source dispatch, because a second
61
+ // one would let two families' receipts drift apart silently (error law 2).
62
+ // ───────────────────────────────────────────────────────────────────────────
63
+ /**
64
+ * Normalize ONE existing family owner's `YoloPrimitiveAttempt` into the closed
65
+ * dispatch outcome. Missing or uncertain effect evidence is NEVER success:
66
+ * `no_action` is a bounded zero-yield success only when its receipt carries
67
+ * before/after evidence, and a refusal or an absent receipt is a typed failure.
68
+ */
69
+ /**
70
+ * The V3 source path is SINGLE-PASS by construction.
71
+ *
72
+ * V1 keeps its two-attempt signal helper and its depth-scaled provider target;
73
+ * V3 removes both. `searchSignalsWithProviderRetry` hides TWO transport attempts
74
+ * inside one "bounded" owner call, and `priorSearchDepth` is retry/expansion
75
+ * MEMORY that multiplies the target the AI never asked for. Under V3 a provider
76
+ * outcome is a typed no-effect receipt followed by a reread, and only a later
77
+ * fresh attention iteration may choose another action.
78
+ */
79
+ export const REFILL_V3_SIGNAL_SEARCH_ATTEMPTS = 1;
80
+ export function isRefillV3SinglePassAction(action) {
81
+ return actionToolInput(action).refillV3SinglePass === true;
82
+ }
83
+ export function normalizeRefillV3SourceOwnerReceipt(params) {
84
+ const { attempt, ownerKey, requestFingerprint, idempotencyKey } = params;
85
+ if (attempt.status === "refused") {
86
+ return {
87
+ kind: "failure",
88
+ failureClass: "owner_threw",
89
+ detail: attempt.refusalReason ?? `${ownerKey} refused without a reason`,
90
+ };
91
+ }
92
+ const record = recordValue(attempt.result) ?? {};
93
+ const before = stringValue(record.sourceBeforeFingerprint);
94
+ const after = stringValue(record.sourceAfterFingerprint);
95
+ if (!before || !after) {
96
+ return { kind: "timeout", effectIdentity: null };
97
+ }
98
+ const importedCount = Math.max(0, numberValue(record.importedCount) ?? 0);
99
+ const receipt = {
100
+ ownerKey,
101
+ requestFingerprint,
102
+ idempotencyKey,
103
+ sourceBeforeFingerprint: before,
104
+ sourceAfterFingerprint: after,
105
+ sourceBeforeCount: Math.max(0, numberValue(record.sourceBeforeCount) ?? 0),
106
+ sourceAfterCount: Math.max(0, numberValue(record.sourceAfterCount) ?? 0),
107
+ importedCount,
108
+ effects: stringArray(record.effects),
109
+ replayState: stringValue(record.replayState) === "exact_replay"
110
+ ? "exact_replay"
111
+ : importedCount > 0
112
+ ? "first"
113
+ : "not_applicable",
114
+ rollback: importedCount > 0
115
+ ? {
116
+ available: true,
117
+ input: recordValue(record.rollbackInput) ?? {},
118
+ result: stringValue(record.rollbackResult),
119
+ }
120
+ : { available: false, reason: "not_applicable" },
121
+ postRationales: null,
122
+ yieldEvidence: null,
123
+ };
124
+ return importedCount > 0
125
+ ? { kind: "receipt", receipt }
126
+ : { kind: "no_effect", receipt };
127
+ }
54
128
  function isSenderOwnedPostEngagersCampaign(campaign) {
55
129
  return Boolean((campaign.leadSourceProvider === "signal-discovery" ||
56
130
  campaign.leadSourceProvider === "campaign-tracked-post") &&
@@ -148,152 +222,6 @@ export function sleep(ms) {
148
222
  return Promise.resolve();
149
223
  return new Promise((resolve) => setTimeout(resolve, ms));
150
224
  }
151
- export function recordValue(value) {
152
- return value && typeof value === "object" && !Array.isArray(value)
153
- ? value
154
- : null;
155
- }
156
- export function stringValue(value) {
157
- return typeof value === "string" && value.trim() ? value.trim() : null;
158
- }
159
- export function numberValue(value) {
160
- return typeof value === "number" && Number.isFinite(value) ? value : null;
161
- }
162
- export function normalizeSchedulerChangedCounts(value) {
163
- const changedCounts = recordValue(value);
164
- if (!changedCounts ||
165
- changedCounts.complete !== true ||
166
- changedCounts.truncated !== false) {
167
- return null;
168
- }
169
- const total = numberValue(changedCounts.total);
170
- if (total === null || total < 0 || !Number.isInteger(total))
171
- return null;
172
- if (!Array.isArray(changedCounts.byCampaignTableActionType))
173
- return null;
174
- const groups = [];
175
- const seen = new Set();
176
- for (const rawGroup of changedCounts.byCampaignTableActionType) {
177
- const group = recordValue(rawGroup);
178
- const campaignId = stringValue(group?.campaignId);
179
- const tableId = stringValue(group?.tableId);
180
- const actionType = stringValue(group?.actionType);
181
- const count = numberValue(group?.count);
182
- if (!campaignId ||
183
- !tableId ||
184
- !actionType ||
185
- count === null ||
186
- count <= 0 ||
187
- !Number.isInteger(count)) {
188
- return null;
189
- }
190
- const key = `${campaignId}:${tableId}:${actionType}`;
191
- if (seen.has(key))
192
- return null;
193
- seen.add(key);
194
- groups.push({ campaignId, tableId, actionType, count });
195
- }
196
- groups.sort((a, b) => {
197
- if (a.campaignId !== b.campaignId) {
198
- return a.campaignId.localeCompare(b.campaignId);
199
- }
200
- if (a.tableId !== b.tableId)
201
- return a.tableId.localeCompare(b.tableId);
202
- return a.actionType.localeCompare(b.actionType);
203
- });
204
- if (groups.reduce((sum, group) => sum + group.count, 0) !== total) {
205
- return null;
206
- }
207
- if (!Array.isArray(changedCounts.byCampaignTableActionTypeSender)) {
208
- return null;
209
- }
210
- const senderGroups = [];
211
- const senderSeen = new Set();
212
- for (const rawGroup of changedCounts.byCampaignTableActionTypeSender) {
213
- const group = recordValue(rawGroup);
214
- const campaignId = stringValue(group?.campaignId);
215
- const tableId = stringValue(group?.tableId);
216
- const actionType = stringValue(group?.actionType);
217
- const senderId = stringValue(group?.senderId);
218
- const count = numberValue(group?.count);
219
- if (!campaignId ||
220
- !tableId ||
221
- !actionType ||
222
- !senderId ||
223
- count === null ||
224
- count <= 0 ||
225
- !Number.isInteger(count)) {
226
- return null;
227
- }
228
- const key = `${campaignId}:${tableId}:${actionType}:${senderId}`;
229
- if (senderSeen.has(key))
230
- return null;
231
- senderSeen.add(key);
232
- senderGroups.push({ campaignId, tableId, actionType, senderId, count });
233
- }
234
- if (senderGroups.reduce((sum, group) => sum + group.count, 0) !== total) {
235
- return null;
236
- }
237
- return {
238
- complete: true,
239
- truncated: false,
240
- total,
241
- byCampaignTableActionType: groups,
242
- byCampaignTableActionTypeSender: senderGroups,
243
- };
244
- }
245
- function completeExpectedTargetSummary(value, expectedTargets, maxPlacements) {
246
- const summary = recordValue(value);
247
- const items = Array.isArray(summary?.items) ? summary.items : [];
248
- if (summary?.complete !== true ||
249
- summary.truncated !== false ||
250
- numberValue(summary.requested) !== expectedTargets.length ||
251
- numberValue(summary.included) == null ||
252
- numberValue(summary.omitted) == null ||
253
- numberValue(summary.included) + numberValue(summary.omitted) !==
254
- expectedTargets.length ||
255
- numberValue(summary.maxPlacements) !== maxPlacements ||
256
- items.length !== expectedTargets.length) {
257
- return false;
258
- }
259
- return items.every((rawItem, index) => {
260
- const item = recordValue(rawItem);
261
- const target = expectedTargets[index];
262
- const senderIds = stringArray(item?.senderIds).sort();
263
- const bySender = Array.isArray(item?.bySender) ? item.bySender : [];
264
- const accounted = bySender
265
- .map((entry) => stringValue(recordValue(entry)?.senderId))
266
- .filter((senderId) => Boolean(senderId))
267
- .sort();
268
- return (Boolean(target) &&
269
- item?.campaignId === target?.campaignId &&
270
- item?.tableId === target?.tableId &&
271
- item?.refillLaneKey === target?.refillLaneKey &&
272
- (item?.outcome === "included" || item?.outcome === "omitted") &&
273
- JSON.stringify(senderIds) === JSON.stringify(target?.senderIds) &&
274
- JSON.stringify(accounted) === JSON.stringify(target?.senderIds));
275
- });
276
- }
277
- // fix(112ao-2): the scheduler's ATTACHING envelope statuses. These describe a
278
- // dispatch that joined an existing run rather than producing its own outcome.
279
- // They are terminal ONLY when the attach actually yielded a terminal receipt.
280
- const SCHEDULER_ATTACHING_ENVELOPE_STATUSES = new Set(["accepted", "attached"]);
281
- // The envelope statuses that report an outcome of this dispatch directly.
282
- const SCHEDULER_TERMINAL_ENVELOPE_STATUSES = new Set([
283
- "ran",
284
- "window_closed_noop",
285
- ]);
286
- // The only receipt statuses that constitute a settled outcome. A receipt
287
- // without one of these is not evidence that anything finished.
288
- const SCHEDULER_TERMINAL_RECEIPT_STATUSES = new Set([
289
- "ran",
290
- "window_closed_noop",
291
- "failed",
292
- ]);
293
- function hasTerminalSchedulerReceipt(receipt) {
294
- const status = receipt ? stringValue(receipt.status) : null;
295
- return Boolean(status && SCHEDULER_TERMINAL_RECEIPT_STATUSES.has(status));
296
- }
297
225
  // fix(112ap): poll ONLY a genuinely in-flight attach. `!terminal` is the wrong
298
226
  // test — it is also true for `backoff` and `failed`, and re-polling those would
299
227
  // both waste the budget and misrepresent a refused dispatch as a pending one.
@@ -322,103 +250,12 @@ export function setSweepSettlePollDelayMsForTests(ms) {
322
250
  export function resetSweepSettlePollDelayMsForTests() {
323
251
  sweepSettlePollDelayMs = SWEEP_SETTLE_POLL_DEFAULT_DELAY_MS;
324
252
  }
325
- export function schedulerEnvelopeIsTerminal(schedulerStatus, receipt = null) {
326
- if (SCHEDULER_TERMINAL_ENVELOPE_STATUSES.has(schedulerStatus))
327
- return true;
328
- return (SCHEDULER_ATTACHING_ENVELOPE_STATUSES.has(schedulerStatus) &&
329
- hasTerminalSchedulerReceipt(receipt));
330
- }
331
- export function normalizeSchedulerPrimitiveResult(value, expected) {
332
- const response = recordValue(value);
333
- const receipt = recordValue(response?.receipt);
334
- const schedulerStatus = stringValue(response?.status) ?? stringValue(receipt?.status) ?? "unknown";
335
- const errorCode = stringValue(recordValue(response?.error)?.code);
336
- // fix(112ao-2): whether this dispatch finished is decided explicitly from the
337
- // envelope status plus the RECEIPT'S OWN STATUS — never from the bare
338
- // presence of a receipt field.
339
- //
340
- // The previous guard was `!receipt && (accepted || attached)`. Truthiness is
341
- // the wrong test: an attach that carries a receipt with no terminal status is
342
- // not evidence that anything settled, yet it skipped this branch, ran the v3
343
- // audit on that non-evidence, and could return auditComplete: true — a
344
- // completion claim for a run still in flight, which the coordinator then
345
- // treats as done rather than awaiting (Dotwork 2026-07-29: 21 exact runs,
346
- // coverage frozen at sent:0/scheduled:0).
347
- //
348
- // The attach is NOT always in flight, and this deliberately does not treat it
349
- // so: scheduler-run.ts:466-483 only returns a receipt from
350
- // waitForSchedulerRunReceipt when it is scope-matched to THIS request, so an
351
- // attach carrying a terminal receipt is a legitimate idempotent completion of
352
- // this exact operation and must keep its counters. Discarding it would report
353
- // zero placements for a sweep that really placed cells.
354
- //
355
- // `ran`/`window_closed_noop` remain terminal. `backoff`/`failed` keep their
356
- // existing handling — the run loop already unwraps a backoff envelope's
357
- // replayed receipt deliberately (schedulerRunReceiptIsFreshZeroScheduled).
358
- if (SCHEDULER_ATTACHING_ENVELOPE_STATUSES.has(schedulerStatus) &&
359
- !hasTerminalSchedulerReceipt(receipt)) {
360
- return {
361
- status: "matching_scheduler_active",
362
- schedulerStatus,
363
- terminal: false,
364
- disposition: "bounded_reread_wait",
365
- receipt: null,
366
- // A non-terminal receipt is not this dispatch's effect. Surfaced under a
367
- // distinct name so it stays readable for diagnostics but can never be
368
- // mistaken for a settled outcome.
369
- ...(receipt ? { unsettledReceipt: receipt } : {}),
370
- retryAfterMs: numberValue(response?.retryAfterMs) ?? 30_000,
371
- changedCounts: null,
372
- auditComplete: false,
373
- };
374
- }
375
- if (errorCode === "scheduler_scope_busy" ||
376
- errorCode === "uncertain_outcome" ||
377
- schedulerStatus === "uncertain_outcome") {
378
- const status = errorCode === "scheduler_scope_busy"
379
- ? "scheduler_scope_busy"
380
- : "uncertain_outcome";
381
- return {
382
- status,
383
- schedulerStatus,
384
- disposition: status === "scheduler_scope_busy"
385
- ? "bounded_backoff_wait"
386
- : "diagnostic_stop",
387
- receipt: response?.receipt ?? null,
388
- retryAfterMs: numberValue(response?.retryAfterMs),
389
- changedCounts: null,
390
- auditComplete: false,
391
- blocker: status,
392
- };
393
- }
394
- const changedCounts = normalizeSchedulerChangedCounts(receipt?.changedCounts ?? response?.changedCounts);
395
- const targetAuditComplete = !expected ||
396
- (receipt?.receiptVersion === 3 &&
397
- receipt.expectedTargetsHash === expected.expectedTargetsHash &&
398
- receipt.maxPlacements === expected.maxPlacements &&
399
- completeExpectedTargetSummary(receipt.expectedTargetSummary, expected.expectedTargets, expected.maxPlacements));
400
- if (!changedCounts || !targetAuditComplete) {
401
- return {
402
- status: "scheduler_receipt_incomplete",
403
- schedulerStatus,
404
- receipt: response?.receipt ?? null,
405
- retryAfterMs: numberValue(response?.retryAfterMs),
406
- changedCounts: null,
407
- auditComplete: false,
408
- blocker: "scheduler_receipt_incomplete",
409
- };
410
- }
411
- return {
412
- status: schedulerStatus,
413
- // fix(112ao-2): the completion decision, carried explicitly instead of
414
- // leaving each consumer to re-infer it from the status string.
415
- terminal: schedulerEnvelopeIsTerminal(schedulerStatus, receipt),
416
- receipt: response?.receipt ?? null,
417
- retryAfterMs: numberValue(response?.retryAfterMs),
418
- changedCounts,
419
- auditComplete: true,
420
- };
421
- }
253
+ // The scheduler envelope normalizer, the terminality predicate, and the
254
+ // primitive value readers now live in the dependency-free package module
255
+ // `mcp/sellable/src/scheduler-envelope.ts` so the Next app service can import
256
+ // them without pulling this executor graph. They are re-exported here so every
257
+ // existing caller and pin keeps its exact import path.
258
+ export { hasTerminalSchedulerReceipt, normalizeSchedulerChangedCounts, normalizeSchedulerPrimitiveResult, numberValue, recordValue, schedulerEnvelopeIsTerminal, stringArray, stringValue, };
422
259
  function exactDateValue(value) {
423
260
  const targetDate = stringValue(value);
424
261
  if (!targetDate || !/^\d{4}-\d{2}-\d{2}$/.test(targetDate))
@@ -429,11 +266,6 @@ function exactDateValue(value) {
429
266
  ? targetDate
430
267
  : null;
431
268
  }
432
- export function stringArray(value) {
433
- if (!Array.isArray(value))
434
- return [];
435
- return value.filter((item) => typeof item === "string");
436
- }
437
269
  export function prepareRowSelectorValue(value) {
438
270
  const selector = recordValue(value);
439
271
  const type = stringValue(selector?.type);
@@ -687,7 +519,9 @@ function rerunErroredCellsOperations(action) {
687
519
  return operations.flatMap((raw) => {
688
520
  const operation = recordValue(raw);
689
521
  const columnRole = stringValue(operation?.columnRole);
690
- if (columnRole !== "icpScore" && columnRole !== "generateMessage") {
522
+ if (columnRole !== "enrich" &&
523
+ columnRole !== "icpScore" &&
524
+ columnRole !== "generateMessage") {
691
525
  return [];
692
526
  }
693
527
  const rowSelector = recordValue(operation?.rowSelector);
@@ -792,6 +626,19 @@ function signalSearchProviderErrors(summary) {
792
626
  : [];
793
627
  return uniqueStrings(keywordResults.map((result) => stringValue(result?.error)));
794
628
  }
629
+ /**
630
+ * V3-aware single-pass entry. Under V3 `searchSignals` is called AT MOST ONCE
631
+ * per campaign-attention iteration with the exact validated AI keywords; a
632
+ * provider-unavailable result becomes a typed no-effect receipt rather than a
633
+ * hidden second transport attempt.
634
+ */
635
+ async function searchSignalsBoundedForAction(input, action) {
636
+ if (isRefillV3SinglePassAction(action)) {
637
+ const summary = await searchSignals(input);
638
+ return { summary, attempts: [summary] };
639
+ }
640
+ return searchSignalsWithProviderRetry(input);
641
+ }
795
642
  async function searchSignalsWithProviderRetry(input) {
796
643
  const attempts = [];
797
644
  for (let attempt = 0; attempt < SIGNAL_SEARCH_PROVIDER_MAX_ATTEMPTS; attempt += 1) {
@@ -1361,12 +1208,13 @@ export async function continueSignalDiscoverySource(action, workspaceId) {
1361
1208
  },
1362
1209
  };
1363
1210
  }
1364
- markProviderPromptLoaded({
1365
- provider: "signal-discovery",
1366
- campaignOfferId,
1367
- });
1211
+ // Phase 146.5 Plan 09 T5: the owner self-mark is REMOVED. Only the existing
1212
+ // `get_provider_prompt` tool may mark the exact `(campaignOfferId, provider)`
1213
+ // preflight; an owner that marks its own preflight satisfies the gate it is
1214
+ // supposed to be gated by, which makes the active-provider-only rule
1215
+ // unenforceable. The assertion side of the gate is unchanged.
1368
1216
  const searchAttempt = keywords.length > 0
1369
- ? await searchSignalsWithProviderRetry({
1217
+ ? await searchSignalsBoundedForAction({
1370
1218
  type: "keywords",
1371
1219
  keywords: keywords.map((keyword) => ({
1372
1220
  keyword,
@@ -1379,7 +1227,7 @@ export async function continueSignalDiscoverySource(action, workspaceId) {
1379
1227
  confirmed: true,
1380
1228
  limit: 50,
1381
1229
  ...(workspaceId ? { workspaceId } : {}),
1382
- })
1230
+ }, action)
1383
1231
  : null;
1384
1232
  const searchSummary = searchAttempt?.summary ?? null;
1385
1233
  if (searchSummary && signalSearchProviderUnavailable(searchSummary)) {
@@ -1713,10 +1561,11 @@ export async function broadenSignalSearch(action, workspaceId) {
1713
1561
  searchKeywords = staleKeywords;
1714
1562
  effectiveKeywordSource = "stale_researched";
1715
1563
  }
1716
- markProviderPromptLoaded({
1717
- provider: "signal-discovery",
1718
- campaignOfferId,
1719
- });
1564
+ // Phase 146.5 Plan 09 T5: the owner self-mark is REMOVED. Only the existing
1565
+ // `get_provider_prompt` tool may mark the exact `(campaignOfferId, provider)`
1566
+ // preflight; an owner that marks its own preflight satisfies the gate it is
1567
+ // supposed to be gated by, which makes the active-provider-only rule
1568
+ // unenforceable. The assertion side of the gate is unchanged.
1720
1569
  let searchSummary = null;
1721
1570
  let providerSearchAttempts = 0;
1722
1571
  let runScopedDerivedTabIds = [];
@@ -1739,7 +1588,7 @@ export async function broadenSignalSearch(action, workspaceId) {
1739
1588
  carriedRemainder = freshCarriedPostIds.slice(scrapeCap);
1740
1589
  }
1741
1590
  else {
1742
- const searchAttempt = await searchSignalsWithProviderRetry({
1591
+ const searchAttempt = await searchSignalsBoundedForAction({
1743
1592
  type: "keywords",
1744
1593
  keywords: searchKeywords.map((keyword) => ({
1745
1594
  keyword,
@@ -1756,7 +1605,7 @@ export async function broadenSignalSearch(action, workspaceId) {
1756
1605
  confirmed: true,
1757
1606
  limit: 50,
1758
1607
  ...(workspaceId ? { workspaceId } : {}),
1759
- });
1608
+ }, action);
1760
1609
  searchSummary = searchAttempt.summary;
1761
1610
  providerSearchAttempts = searchAttempt.attempts.length;
1762
1611
  // Derived/agent broadening keywords are transient (not the campaign's persistent
@@ -2077,10 +1926,11 @@ async function startBoundedProviderSearchClone(params) {
2077
1926
  receipt: { originalSearchId: snapshot.searchId },
2078
1927
  };
2079
1928
  }
2080
- markProviderPromptLoaded({
2081
- provider: params.provider,
2082
- campaignOfferId: params.campaignOfferId,
2083
- });
1929
+ // Phase 146.5 Plan 09 T5: the owner self-mark is REMOVED. Only the existing
1930
+ // `get_provider_prompt` tool may mark the exact `(campaignOfferId, provider)`
1931
+ // preflight; an owner that marks its own preflight satisfies the gate it is
1932
+ // supposed to be gated by, which makes the active-provider-only rule
1933
+ // unenforceable. The assertion side of the gate is unchanged.
2084
1934
  const searchName = `${snapshot.searchName ?? "Provider search"} · refill trial`;
2085
1935
  const result = params.provider === "sales-nav"
2086
1936
  ? await searchSalesNav({
@@ -2451,9 +2301,15 @@ export async function executeProviderSavedSearch(action, workspaceId, mode) {
2451
2301
  numberValue(toolInput.targetRows) ??
2452
2302
  numberValue(action.targetRows) ??
2453
2303
  PROVIDER_SOURCE_CONTINUATION_DEFAULT_ROWS));
2454
- const priorDepth = Math.max(1, Math.floor(numberValue(toolInput.priorSearchDepth) ?? 1));
2304
+ // V3 removes `priorSearchDepth` as retry/expansion memory: the target comes
2305
+ // ONLY from the authoritative remaining gap and the provider caps, never from
2306
+ // how many times the campaign has been broadened before.
2307
+ const singlePass = isRefillV3SinglePassAction(action);
2308
+ const priorDepth = singlePass
2309
+ ? 1
2310
+ : Math.max(1, Math.floor(numberValue(toolInput.priorSearchDepth) ?? 1));
2455
2311
  const targetLeadCount = mode === "broadening"
2456
- ? Math.min(PROVIDER_SEARCH_BROADEN_MAX_ROWS, baseTarget * (priorDepth + 1))
2312
+ ? Math.min(PROVIDER_SEARCH_BROADEN_MAX_ROWS, singlePass ? baseTarget : baseTarget * (priorDepth + 1))
2457
2313
  : Math.min(PROVIDER_SOURCE_CONTINUATION_MAX_ROWS, baseTarget);
2458
2314
  let searchIdForImport = reference.searchId;
2459
2315
  let broadenReceipt = null;
@@ -2511,9 +2367,11 @@ export async function executeProviderSavedSearch(action, workspaceId, mode) {
2511
2367
  });
2512
2368
  }
2513
2369
  }
2514
- else {
2515
- markProviderPromptLoaded({ provider, campaignOfferId });
2516
- }
2370
+ // Phase 146.5 Plan 09 T5: the owner self-mark is REMOVED. Only the existing
2371
+ // `get_provider_prompt` tool may mark the exact `(campaignOfferId, provider)`
2372
+ // preflight; an owner that marks its own preflight satisfies the gate it is
2373
+ // supposed to be gated by, which makes the active-provider-only rule
2374
+ // unenforceable. The assertion side of the gate is unchanged.
2517
2375
  let importResult;
2518
2376
  try {
2519
2377
  importResult = await importLeads({
@@ -2620,8 +2478,8 @@ export async function executeProviderSavedSearch(action, workspaceId, mode) {
2620
2478
  ...(broadenReceipt ?? {}),
2621
2479
  targetLeadCount,
2622
2480
  importSearchId: searchIdForImport,
2623
- priorSearchDepth: mode === "broadening" ? priorDepth : null,
2624
- newSearchDepth: mode === "broadening" ? priorDepth + 1 : null,
2481
+ priorSearchDepth: mode === "broadening" && !singlePass ? priorDepth : null,
2482
+ newSearchDepth: mode === "broadening" && !singlePass ? priorDepth + 1 : null,
2625
2483
  netNewRows: rowsGained,
2626
2484
  },
2627
2485
  };
@@ -2636,6 +2494,76 @@ export async function continueProviderSource(action, workspaceId) {
2636
2494
  export async function broadenProviderSearch(action, workspaceId) {
2637
2495
  return executeProviderSavedSearch(action, workspaceId, "broadening");
2638
2496
  }
2497
+ export function buildRefillV3SourceOwnerAction(request) {
2498
+ const actionKey = `refill-v3-source:${request.scope.idempotencyKey}`;
2499
+ const effectId = `refill-v3-source-effect:${request.scope.idempotencyKey}`;
2500
+ const providerSearchReference = request.scope.providerSearchReference ?? undefined;
2501
+ return {
2502
+ actionKey,
2503
+ effectId,
2504
+ ...(providerSearchReference ? { providerSearchReference } : {}),
2505
+ ids: {
2506
+ campaignId: request.scope.campaignId,
2507
+ tableId: request.scope.tableId,
2508
+ sourceLeadListId: request.scope.sourceLeadListId,
2509
+ senderId: request.scope.senderId,
2510
+ sourceFingerprint: request.scope.priorSourceFingerprint,
2511
+ ...(providerSearchReference ? { providerSearchReference } : {}),
2512
+ },
2513
+ toolInput: {
2514
+ // The V3 marker. It is the ONLY thing that changes owner behaviour, and
2515
+ // it removes policy rather than adding any.
2516
+ refillV3SinglePass: true,
2517
+ priorSearchDepth: null,
2518
+ leadSourceProvider: request.command.provider,
2519
+ provider: request.command.provider,
2520
+ sourceRowLimit: request.scope.count,
2521
+ sourceLeadListId: request.scope.sourceLeadListId,
2522
+ query: request.command.query,
2523
+ filters: request.command.filterDelta,
2524
+ keywords: request.command.keywordBatch
2525
+ ? [...request.command.keywordBatch]
2526
+ : undefined,
2527
+ selectedPostIds: request.command.selectedPostIds
2528
+ ? [...request.command.selectedPostIds]
2529
+ : undefined,
2530
+ sourceFingerprint: request.command.requestFingerprint,
2531
+ effectId,
2532
+ ...(providerSearchReference ? { providerSearchReference } : {}),
2533
+ },
2534
+ };
2535
+ }
2536
+ export function refillV3SourceFamilyOwners(workspaceId) {
2537
+ async function run(ownerKey, owner, request) {
2538
+ const action = buildRefillV3SourceOwnerAction(request);
2539
+ let attempt;
2540
+ try {
2541
+ attempt = await owner(action, workspaceId);
2542
+ }
2543
+ catch (error) {
2544
+ // An owner THROW is a typed scoped failure, never a crash that takes the
2545
+ // outer queue down and never a generic no-effect success.
2546
+ return {
2547
+ kind: "failure",
2548
+ failureClass: "owner_threw",
2549
+ detail: `${ownerKey}: ${error instanceof Error ? error.message : "unknown"}`,
2550
+ };
2551
+ }
2552
+ return normalizeRefillV3SourceOwnerReceipt({
2553
+ ownerKey,
2554
+ attempt,
2555
+ requestFingerprint: request.command.requestFingerprint,
2556
+ idempotencyKey: request.scope.idempotencyKey,
2557
+ });
2558
+ }
2559
+ return Object.freeze({
2560
+ refreshSenderPostEngagers: (request) => run("refreshSenderPostEngagers", refreshSenderPostEngagers, request),
2561
+ continueSignalDiscoverySource: (request) => run("continueSignalDiscoverySource", continueSignalDiscoverySource, request),
2562
+ broadenSignalSearch: (request) => run("broadenSignalSearch", broadenSignalSearch, request),
2563
+ continueProviderSource: (request) => run("continueProviderSource", continueProviderSource, request),
2564
+ broadenProviderSearch: (request) => run("broadenProviderSearch", broadenProviderSearch, request),
2565
+ });
2566
+ }
2639
2567
  export async function refreshSenderPostEngagers(action, workspaceId) {
2640
2568
  const senderId = actionSenderId(action);
2641
2569
  const campaignId = actionCampaignId(action);
@@ -14273,13 +14273,13 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
14273
14273
  planReadCount: number;
14274
14274
  refusalReason?: string | undefined;
14275
14275
  enabled: true;
14276
- status: "blocked" | "no_action" | "read_only_reread" | "executed_and_reread" | "refused";
14276
+ status: "blocked" | "refused" | "no_action" | "read_only_reread" | "executed_and_reread";
14277
14277
  selectedAction: null;
14278
14278
  result: unknown;
14279
14279
  targetPlanReread: boolean;
14280
14280
  } | {
14281
14281
  enabled: true;
14282
- status: "blocked" | "no_action" | "read_only_reread" | "executed_and_reread" | "refused";
14282
+ status: "blocked" | "refused" | "no_action" | "read_only_reread" | "executed_and_reread";
14283
14283
  selectedAction: Record<string, unknown>;
14284
14284
  result: unknown;
14285
14285
  targetPlanReread: boolean;
@@ -14293,7 +14293,7 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
14293
14293
  planReadCount: number;
14294
14294
  refusalReason?: string | undefined;
14295
14295
  enabled: true;
14296
- status: "blocked" | "no_action" | "read_only_reread" | "executed_and_reread" | "refused";
14296
+ status: "blocked" | "refused" | "no_action" | "read_only_reread" | "executed_and_reread";
14297
14297
  selectedAction: Record<string, unknown>;
14298
14298
  result: unknown;
14299
14299
  targetPlanReread: true;
@@ -293,7 +293,7 @@ function mergeAttemptLedgersIntoWorkspaceRunState(workspaceRunState, fenceRunSta
293
293
  export const refillSendsToolDefinitions = [
294
294
  {
295
295
  name: "refill_sends",
296
- description: "Typed command entrypoint for Sellable refill sends. Every mode begins with a read-only visible plan pinned to expectedTargetShapeRevision and expectedActionKey; no credit refresh, refill mutation, or run fence may exist before that render. A first manual call without those pins returns status plan_ready and requires explicit acceptance before its planned_manual_run continuation. A first explicit workspace-wide --yolo coordinator call without pins also returns status plan_ready with the fresh sender/campaign waterfall, selected exact target, and a planned_workspace_run continuation; yolo auto-accepts the displayed packet, then traverses its planner-ranked existing targets autonomously. Execution preflights globalActionQueue[0] against the rendered authority and executes one fenced checkpointed run slice before rereading workspace scope for the next exact target. An active_exact_run continuation carries targetConfig plus exact runId/fence; a next_exact_target continuation carries the next preflighted targetConfig and opens a new fence. Exact target configs bind the issued semantic action and revisions. After a terminal exact run, two fresh read-only preflights stabilize the workspace queue; if the same terminal action remains visible, the coordinator excludes only that exact sender/lane edge and preflights the next planner-ranked existing edge instead of reopening an identical fence. A retryable Signal Discovery provider outage defers that exact signal-search action family within its campaign/table/lane for the remainder of the current workspace command, even if the eligible sender cohort changes, records the blocker in workspaceRunState, and continues independent scheduler, credit, or source-family work; a fresh user command retries the deferred family. If no other exact edge exists while the workspace gap remains positive, it returns the concrete deferred provider blocker or a classified typed terminal: new_campaign_required when receipts prove every existing lane/source family is spent, actionable_supply_not_queued (retryable) when actionable row frontiers remain but no executable exact edge surfaced, or workspace_exact_target_unavailable for the residual case. Once a scheduler sweep is dispatched, the exact fence remains active and every in_progress continuation polls only that request key until its official terminal receipt. A host wait budget never converts the active sweep into loaded_awaiting_scheduler. The post-receipt target reread returns complete, another planner action, or a receipt-backed scheduler_terminal_with_remaining_gap blocker. loaded_awaiting_scheduler is a workspace terminal only when no refill-owned scheduler request is active. After the plan is rendered, the packaged workflow must immediately invoke yolo continuation arguments until workspace projected coverage fills, the workspace scheduler-loaded terminal is returned, or no independent exact work remains behind concrete blockers, without asking the user to continue. Every primitive records actionKey/requestId/effectId before mutation and rereads the fresh target plan afterward. It may refresh stale paid InMail facts once, prepare/approve bounded existing rows, reuse/import receipt-proven source rows, continue or broaden a selected source, traverse any planner-ranked EXISTING campaign/lane/source family, start a route-resolver start-eligible PAUSED existing campaign, run a request-scoped product scheduler sweep, and wait/reread. Each bounded primitive is followed by a full replan. It never sends directly, lowers paid InMail thresholds/floors, creates NEW campaigns, archives/deletes, or invents source selection outside the planner. Provider failure is retryable and never proves source exhaustion. Unselected PAUSED campaigns that are not start-eligible remain paused. Scheduler placement stays product-native; the command never raw-writes scheduler fields.",
296
+ description: "SUPERSEDED V1 REFILL COORDINATOR — CHECK THE ROUTE BEFORE CALLING. If this server also exposes `refill_v3_advance`, that tool owns refill execution and this one must NOT be called: every ordinary request to refill, top up, fill, max out, or load up sends goes to `refill_v3_advance` with exactly {workspaceId, scope}. Call this tool only on servers where `refill_v3_advance` is absent. Typed command entrypoint for Sellable refill sends. Every mode begins with a read-only visible plan pinned to expectedTargetShapeRevision and expectedActionKey; no credit refresh, refill mutation, or run fence may exist before that render. A first manual call without those pins returns status plan_ready and requires explicit acceptance before its planned_manual_run continuation. A first explicit workspace-wide --yolo coordinator call without pins also returns status plan_ready with the fresh sender/campaign waterfall, selected exact target, and a planned_workspace_run continuation; yolo auto-accepts the displayed packet, then traverses its planner-ranked existing targets autonomously. Execution preflights globalActionQueue[0] against the rendered authority and executes one fenced checkpointed run slice before rereading workspace scope for the next exact target. An active_exact_run continuation carries targetConfig plus exact runId/fence; a next_exact_target continuation carries the next preflighted targetConfig and opens a new fence. Exact target configs bind the issued semantic action and revisions. After a terminal exact run, two fresh read-only preflights stabilize the workspace queue; if the same terminal action remains visible, the coordinator excludes only that exact sender/lane edge and preflights the next planner-ranked existing edge instead of reopening an identical fence. A retryable Signal Discovery provider outage defers that exact signal-search action family within its campaign/table/lane for the remainder of the current workspace command, even if the eligible sender cohort changes, records the blocker in workspaceRunState, and continues independent scheduler, credit, or source-family work; a fresh user command retries the deferred family. If no other exact edge exists while the workspace gap remains positive, it returns the concrete deferred provider blocker or a classified typed terminal: new_campaign_required when receipts prove every existing lane/source family is spent, actionable_supply_not_queued (retryable) when actionable row frontiers remain but no executable exact edge surfaced, or workspace_exact_target_unavailable for the residual case. Once a scheduler sweep is dispatched, the exact fence remains active and every in_progress continuation polls only that request key until its official terminal receipt. A host wait budget never converts the active sweep into loaded_awaiting_scheduler. The post-receipt target reread returns complete, another planner action, or a receipt-backed scheduler_terminal_with_remaining_gap blocker. loaded_awaiting_scheduler is a workspace terminal only when no refill-owned scheduler request is active. After the plan is rendered, the packaged workflow must immediately invoke yolo continuation arguments until workspace projected coverage fills, the workspace scheduler-loaded terminal is returned, or no independent exact work remains behind concrete blockers, without asking the user to continue. Every primitive records actionKey/requestId/effectId before mutation and rereads the fresh target plan afterward. It may refresh stale paid InMail facts once, prepare/approve bounded existing rows, reuse/import receipt-proven source rows, continue or broaden a selected source, traverse any planner-ranked EXISTING campaign/lane/source family, start a route-resolver start-eligible PAUSED existing campaign, run a request-scoped product scheduler sweep, and wait/reread. Each bounded primitive is followed by a full replan. It never sends directly, lowers paid InMail thresholds/floors, creates NEW campaigns, archives/deletes, or invents source selection outside the planner. Provider failure is retryable and never proves source exhaustion. Unselected PAUSED campaigns that are not start-eligible remain paused. Scheduler placement stays product-native; the command never raw-writes scheduler fields.",
297
297
  inputSchema: {
298
298
  type: "object",
299
299
  properties: {
@@ -2279,7 +2279,7 @@ function attachExplicitMessageTemplateRevision(params) {
2279
2279
  export const refillTargetPlanToolDefinitions = [
2280
2280
  {
2281
2281
  name: "get_refill_target_plan",
2282
- description: "read-only refill target planner to call before any refill mutation. It identifies eligible senders and infers one implicit refill lane per sender. For unified Sales Nav cascade campaigns, the public refill lane is the paid-InMail cascade target (send_inmail_closed) with campaign classification sales_nav_cascade; the same campaign can still route prospects to Open InMail or connection-request fallback based on row eligibility and fresh paid credits >= 5. Non-cascade lanes remain connection invites or paid InMails, chosen from future scheduled, recent scheduled, and ready-to-schedule evidence in active campaign-backed sequence campaigns before falling back to campaign sequences. DMs remain follow-up sequence actions, not refill target lanes. By default the planner computes the scheduler-forward 48-hour target window, selected sender-local days whose sending windows overlap that window, gross target, actual sent coverage, scheduler-owned scheduled coverage, projected coverage (sent + scheduled), ready buffer, remaining projected gap, paid InMail credit/threshold/freshness feasibility, same-campaign connection fallback availability, bounded action candidates, and targetRevision drift proof. This tool does not create rows, import leads, prepare messages, approve messages, does not schedule sends, start campaigns, lower paid InMail thresholds, create campaigns, launch, spend InMail credits, or write scheduler fields. Complete projected targets no-op without approval; loaded, awaiting scheduler targets use read-only wait/reread until projected coverage fills.",
2282
+ description: "V1-ROUTE PLANNER — CHECK THE ROUTE BEFORE CALLING. If this server also exposes `refill_v3_advance`, that tool owns refill execution and makes its own fresh observation, so a refill must NOT be opened here; send every ordinary refill, top up, fill, max out, or load up request to `refill_v3_advance` instead. Call this tool only on servers where `refill_v3_advance` is absent. read-only refill target planner to call before any refill mutation. It identifies eligible senders and infers one implicit refill lane per sender. For unified Sales Nav cascade campaigns, the public refill lane is the paid-InMail cascade target (send_inmail_closed) with campaign classification sales_nav_cascade; the same campaign can still route prospects to Open InMail or connection-request fallback based on row eligibility and fresh paid credits >= 5. Non-cascade lanes remain connection invites or paid InMails, chosen from future scheduled, recent scheduled, and ready-to-schedule evidence in active campaign-backed sequence campaigns before falling back to campaign sequences. DMs remain follow-up sequence actions, not refill target lanes. By default the planner computes the scheduler-forward 48-hour target window, selected sender-local days whose sending windows overlap that window, gross target, actual sent coverage, scheduler-owned scheduled coverage, projected coverage (sent + scheduled), ready buffer, remaining projected gap, paid InMail credit/threshold/freshness feasibility, same-campaign connection fallback availability, bounded action candidates, and targetRevision drift proof. This tool does not create rows, import leads, prepare messages, approve messages, does not schedule sends, start campaigns, lower paid InMail thresholds, create campaigns, launch, spend InMail credits, or write scheduler fields. Complete projected targets no-op without approval; loaded, awaiting scheduler targets use read-only wait/reread until projected coverage fills.",
2283
2283
  inputSchema: {
2284
2284
  type: "object",
2285
2285
  properties: {