@sellable/mcp 0.1.756 → 0.1.758

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.
@@ -61,6 +61,14 @@ const DEFAULT_BUDGETS = {
61
61
  // refuses to promote unversioned legacy creditTrust packets into scheduling
62
62
  // authority, even when they contain a fresh provider response.
63
63
  const REFILL_CREDIT_TRUST_VERSION = 2;
64
+ const PAID_INMAIL_CREDIT_RECEIPT_VERSION = "paid_inmail_credit_evaluation.v2";
65
+ const PAID_INMAIL_CREDIT_POLICY_VERSION = 1;
66
+ const PAID_INMAIL_CREDIT_POLICY_TTL_MS = 6 * 60 * 60 * 1000;
67
+ const USABLE_CONTEXTUAL_CREDIT_RECEIPT_STATUSES = new Set([
68
+ "fresh",
69
+ "refreshed",
70
+ "below_threshold",
71
+ ]);
64
72
  // Mirrors RefillDoneReason in src/lib/workflow-tables/refill-target-plan.ts,
65
73
  // plus loop-authored terminals the planner never emits:
66
74
  // - abandoned_stale_checkpoint (fix 110-20d): a resumed wait the loop declares
@@ -885,6 +893,39 @@ function runScopedSignalDiscoveryAttemptUpdate(action, runState, now) {
885
893
  ],
886
894
  };
887
895
  }
896
+ function runScopedSignalSourceValidationUpdate(action, runState, now) {
897
+ const toolInput = actionToolInput(action);
898
+ const provider = stringValue(toolInput.leadSourceProvider);
899
+ if (provider !== "signal-discovery" && provider !== "campaign-tracked-post") {
900
+ return {};
901
+ }
902
+ const campaignId = actionCampaignId(action);
903
+ const payload = resultPayload(runState.lastOutcome);
904
+ const reviewBatch = recordValue(payload.reviewBatch);
905
+ const reviewBatchBasisHash = stringValue(reviewBatch?.basisHash);
906
+ const reviewBatchRowCount = numberValue(reviewBatch?.rowCount);
907
+ if (!campaignId || !reviewBatchBasisHash || !reviewBatchRowCount)
908
+ return {};
909
+ const attempts = arrayValue(runState.signalSourceValidationAttempts).filter(isRecord);
910
+ return {
911
+ signalSourceValidationAttempts: [
912
+ ...attempts.filter((attempt) => stringValue(attempt.campaignId) !== campaignId),
913
+ {
914
+ campaignId,
915
+ tableId: actionTableId(action),
916
+ senderId: actionSenderId(action),
917
+ sourceLeadListId: stringValue(toolInput.sourceLeadListId),
918
+ sourceFingerprint: actionSourceFingerprint(action),
919
+ reviewBatchBasisHash,
920
+ reviewBatchRowCount,
921
+ threshold: 0.1,
922
+ status: "pending",
923
+ copiedAt: now.toISOString(),
924
+ scope: "refill_run",
925
+ },
926
+ ],
927
+ };
928
+ }
888
929
  // Phase 110-05: Sales Nav / Prospeo same-source continuation gets exactly one
889
930
  // bounded add per (campaign, sender) per run, mirroring the Signal Discovery
890
931
  // run-scoped attempt bound so the loop never repeats an unchanged failed
@@ -2274,6 +2315,116 @@ async function startOrResume(input, deps) {
2274
2315
  }
2275
2316
  return ctx;
2276
2317
  }
2318
+ function sha256ReceiptParts(parts) {
2319
+ return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
2320
+ }
2321
+ function sameReceiptContext(value, expected) {
2322
+ return (stringValue(value.workspaceId) === expected.workspaceId &&
2323
+ stringValue(value.senderId) === expected.senderId &&
2324
+ stringValue(value.runId) === expected.runId &&
2325
+ stringValue(value.campaignId) === expected.campaignId &&
2326
+ stringValue(value.tableId) === expected.tableId &&
2327
+ stringValue(value.columnId) === expected.columnId &&
2328
+ (stringValue(value.cellId) ?? null) === expected.cellId &&
2329
+ stringValue(value.actionType) === expected.actionType &&
2330
+ numberValue(value.threshold) === expected.threshold);
2331
+ }
2332
+ /**
2333
+ * Decode the backend-owned Phase 123 receipt enough to prove that the client
2334
+ * is handing the exact fact/context identity back to the planner. The backend
2335
+ * performs the same validation again when parsing run state; this client-side
2336
+ * check prevents advancing g0 with bytes that are already known to fail there.
2337
+ */
2338
+ function decodeContextualPaidInmailReceipt(value, expected, now) {
2339
+ const response = recordValue(value);
2340
+ const receipt = recordValue(response?.receipt) ??
2341
+ (stringValue(response?.receiptVersion) ===
2342
+ PAID_INMAIL_CREDIT_RECEIPT_VERSION
2343
+ ? response
2344
+ : null);
2345
+ if (!receipt ||
2346
+ stringValue(receipt.receiptVersion) !==
2347
+ PAID_INMAIL_CREDIT_RECEIPT_VERSION ||
2348
+ numberValue(receipt.policyVersion) !== PAID_INMAIL_CREDIT_POLICY_VERSION ||
2349
+ numberValue(receipt.policyTtlMs) !== PAID_INMAIL_CREDIT_POLICY_TTL_MS ||
2350
+ numberValue(receipt.maxStalenessMs) !== PAID_INMAIL_CREDIT_POLICY_TTL_MS ||
2351
+ !USABLE_CONTEXTUAL_CREDIT_RECEIPT_STATUSES.has(stringValue(receipt.status) ?? "")) {
2352
+ return null;
2353
+ }
2354
+ const evaluationContext = recordValue(receipt.evaluationContext);
2355
+ if (!evaluationContext ||
2356
+ !sameReceiptContext(evaluationContext, expected) ||
2357
+ !sameReceiptContext(receipt, expected)) {
2358
+ return null;
2359
+ }
2360
+ const evaluatedAt = new Date(stringValue(receipt.evaluatedAt) ?? "");
2361
+ const after = recordValue(receipt.after);
2362
+ const checkedAt = new Date(stringValue(after?.checkedAt) ?? "");
2363
+ const balance = numberValue(after?.balance);
2364
+ const sentSince = numberValue(after?.sentSince);
2365
+ const available = numberValue(after?.available);
2366
+ const logicalRefreshId = stringValue(receipt.logicalRefreshId);
2367
+ const transportAttempt = recordValue(receipt.transportAttempt);
2368
+ if (!Number.isFinite(evaluatedAt.getTime()) ||
2369
+ !Number.isFinite(checkedAt.getTime()) ||
2370
+ evaluatedAt.getTime() > now.getTime() ||
2371
+ checkedAt.getTime() > now.getTime() ||
2372
+ balance === null ||
2373
+ !Number.isInteger(balance) ||
2374
+ balance < 0 ||
2375
+ sentSince === null ||
2376
+ !Number.isInteger(sentSince) ||
2377
+ sentSince < 0 ||
2378
+ available === null ||
2379
+ !Number.isInteger(available) ||
2380
+ available < 0 ||
2381
+ !logicalRefreshId ||
2382
+ stringValue(transportAttempt?.logicalRefreshId) !== logicalRefreshId) {
2383
+ return null;
2384
+ }
2385
+ const factIdentity = sha256ReceiptParts([
2386
+ "paid-inmail-credit-fact",
2387
+ PAID_INMAIL_CREDIT_POLICY_VERSION,
2388
+ expected.senderId,
2389
+ checkedAt.toISOString(),
2390
+ balance,
2391
+ ]);
2392
+ if (stringValue(receipt.factIdentity) !== factIdentity ||
2393
+ stringValue(after?.factIdentity) !== factIdentity) {
2394
+ return null;
2395
+ }
2396
+ const snapshotIdentity = sha256ReceiptParts([
2397
+ "paid-inmail-credit-evaluation-snapshot",
2398
+ factIdentity,
2399
+ sentSince,
2400
+ available,
2401
+ evaluatedAt.toISOString(),
2402
+ ]);
2403
+ if (stringValue(receipt.evaluationSnapshotIdentity) !== snapshotIdentity) {
2404
+ return null;
2405
+ }
2406
+ const contextIdentity = sha256ReceiptParts([
2407
+ expected.workspaceId,
2408
+ expected.senderId,
2409
+ expected.runId,
2410
+ expected.campaignId,
2411
+ expected.tableId,
2412
+ expected.columnId,
2413
+ expected.cellId,
2414
+ expected.actionType,
2415
+ expected.threshold,
2416
+ ]);
2417
+ const receiptId = sha256ReceiptParts([
2418
+ PAID_INMAIL_CREDIT_RECEIPT_VERSION,
2419
+ PAID_INMAIL_CREDIT_POLICY_VERSION,
2420
+ contextIdentity,
2421
+ factIdentity,
2422
+ snapshotIdentity,
2423
+ logicalRefreshId,
2424
+ stringValue(receipt.status),
2425
+ ]);
2426
+ return stringValue(receipt.receiptId) === receiptId ? receipt : null;
2427
+ }
2277
2428
  async function gateBootstrap(input, deps, ctx) {
2278
2429
  const plan = await deps.readPlan(refillPlanReadInput(input, { ctx, journal: false }));
2279
2430
  const issuedReportingContext = sanitizeRefillReportingContextProjection(plan.reportingContext);
@@ -2294,46 +2445,127 @@ async function gateBootstrap(input, deps, ctx) {
2294
2445
  if (issuedReportingContext) {
2295
2446
  input.reportingContext = issuedReportingContext;
2296
2447
  }
2297
- const paidCredit = arrayValue(recordValue(plan.bootstrap)?.paidCredit).filter(isRecord);
2298
- const refreshedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.senderIds)
2299
- .map((entry) => stringValue(entry))
2300
- .filter((entry) => Boolean(entry)));
2301
- const attemptedSenderIds = new Set(arrayValue(recordValue(ctx.runState.creditTrust)?.attemptedSenderIds)
2302
- .map((entry) => stringValue(entry))
2303
- .filter((entry) => Boolean(entry)));
2304
- const receipts = [];
2305
- for (const entry of paidCredit) {
2306
- const senderId = stringValue(entry.senderId);
2307
- if (!senderId ||
2308
- !entry.plannedJitRefresh ||
2309
- refreshedSenderIds.has(senderId) ||
2310
- attemptedSenderIds.has(senderId)) {
2311
- continue;
2448
+ const pinnedRunState = pinInitialSenderLaneChains(plan, ctx.runState);
2449
+ // Exact fenced plans are projected by projectedV2Packet(), whose legacy
2450
+ // bootstrap.paidCredit array is intentionally empty. The canonical refresh
2451
+ // authority is packet.target.globalActionQueue; reading the bootstrap array
2452
+ // minted `{ version: 2, receipts: [] }` on every run, which made the next
2453
+ // exact planner read receipt-aware without giving it a receipt to validate.
2454
+ const packet = recordValue(plan.packet);
2455
+ const packetTarget = recordValue(packet?.target);
2456
+ const paidCreditActions = arrayValue(packetTarget?.globalActionQueue)
2457
+ .filter(isRecord)
2458
+ .filter((action) => stringValue(action.type) === "refresh_paid_inmail_credits");
2459
+ const contextualReceipts = [];
2460
+ const refreshedSenderIds = [];
2461
+ const now = deps.now?.() ?? new Date();
2462
+ for (const action of paidCreditActions) {
2463
+ const toolInput = actionToolInput(action);
2464
+ const ids = actionIds(action);
2465
+ const senderId = actionSenderId(action);
2466
+ const campaignId = actionCampaignId(action);
2467
+ const tableId = actionTableId(action);
2468
+ const columnId = stringValue(toolInput.columnId) ??
2469
+ stringValue(ids.columnId) ??
2470
+ stringValue(action.columnId);
2471
+ const actionType = stringValue(action.actionType) ?? stringValue(toolInput.actionType);
2472
+ const threshold = numberValue(toolInput.threshold) ??
2473
+ numberValue(toolInput.oldThreshold) ??
2474
+ numberValue(action.threshold) ??
2475
+ numberValue(action.oldThreshold);
2476
+ const completeContext = senderId &&
2477
+ campaignId &&
2478
+ tableId &&
2479
+ columnId &&
2480
+ actionType === "send_inmail_closed" &&
2481
+ threshold !== null &&
2482
+ Number.isInteger(threshold) &&
2483
+ threshold >= 0
2484
+ ? {
2485
+ workspaceId: input.workspaceId,
2486
+ senderId,
2487
+ runId: ctx.runId,
2488
+ campaignId,
2489
+ tableId,
2490
+ columnId,
2491
+ cellId: null,
2492
+ actionType: "INMAIL_CLOSED",
2493
+ threshold,
2494
+ }
2495
+ : null;
2496
+ if (!completeContext) {
2497
+ return {
2498
+ status: "blocked",
2499
+ blocker: "paid_inmail_refresh_failed",
2500
+ runId: ctx.runId,
2501
+ fence: ctx.fence,
2502
+ gate: ctx.gate,
2503
+ guidance: "The exact paid InMail refresh action did not carry a complete sender/campaign/table/column/action/threshold context. The run stayed at bootstrap and no refill primitive executed.",
2504
+ report: { action },
2505
+ journalPath: ctx.journalPath,
2506
+ targetConfig: ctx.targetConfig ?? undefined,
2507
+ runHandle: ctx.runHandle,
2508
+ };
2312
2509
  }
2313
- const receipt = await deps.executors.refreshPaidInmailCreditsWithRetry(senderId, input.workspaceId);
2314
- attemptedSenderIds.add(senderId);
2315
- const classification = classifyPaidInmailRefreshReceipt(receipt.receipt);
2316
- receipts.push({ senderId, receipt, classification });
2317
- if (classification.usableCurrentFacts) {
2318
- refreshedSenderIds.add(senderId);
2510
+ const refresh = await deps.executors.refreshPaidInmailCreditsWithRetry(completeContext.senderId, completeContext.workspaceId, {
2511
+ runId: completeContext.runId,
2512
+ actionType: "send_inmail_closed",
2513
+ campaignId: completeContext.campaignId,
2514
+ tableId: completeContext.tableId,
2515
+ columnId: completeContext.columnId,
2516
+ threshold: completeContext.threshold,
2517
+ });
2518
+ const classification = classifyPaidInmailRefreshReceipt(refresh.receipt);
2519
+ const decodedReceipt = decodeContextualPaidInmailReceipt(refresh.receipt, completeContext, now);
2520
+ if (!classification.usableCurrentFacts || !decodedReceipt) {
2521
+ return {
2522
+ status: "blocked",
2523
+ blocker: "paid_inmail_refresh_failed",
2524
+ runId: ctx.runId,
2525
+ fence: ctx.fence,
2526
+ gate: ctx.gate,
2527
+ guidance: "Paid InMail credit refresh did not return one usable receipt matching the exact fenced context. The run stayed at bootstrap and did not advance with invalid authority.",
2528
+ report: {
2529
+ senderId: completeContext.senderId,
2530
+ receiptStatus: classification.status,
2531
+ error: classification.error,
2532
+ attempts: refresh.attempts,
2533
+ errors: refresh.errors,
2534
+ },
2535
+ journalPath: ctx.journalPath,
2536
+ targetConfig: ctx.targetConfig ?? undefined,
2537
+ runHandle: ctx.runHandle,
2538
+ };
2319
2539
  }
2320
- }
2321
- const pinnedRunState = pinInitialSenderLaneChains(plan, ctx.runState);
2322
- const nextRunState = mergeRunState(pinnedRunState, {
2323
- creditTrust: {
2324
- version: REFILL_CREDIT_TRUST_VERSION,
2325
- refreshedAt: (deps.now?.() ?? new Date()).toISOString(),
2326
- senderIds: [...refreshedSenderIds],
2327
- attemptedSenderIds: [...attemptedSenderIds],
2328
- receipts,
2329
- },
2330
- });
2540
+ contextualReceipts.push(decodedReceipt);
2541
+ refreshedSenderIds.push(completeContext.senderId);
2542
+ }
2543
+ // Never promote an empty or legacy packet into receipt authority. With no
2544
+ // refresh head, the exact planner remains cache-fact based and can execute
2545
+ // the source/preparation action it already selected.
2546
+ const { creditTrust: _discardedCreditTrust, ...runStateWithoutCreditTrust } = pinnedRunState;
2547
+ const nextRunState = contextualReceipts.length > 0
2548
+ ? mergeRunState(runStateWithoutCreditTrust, {
2549
+ creditTrust: {
2550
+ version: REFILL_CREDIT_TRUST_VERSION,
2551
+ receiptId: stringValue(contextualReceipts[0]?.receiptId),
2552
+ refreshedAt: now.toISOString(),
2553
+ senderIds: [...new Set(refreshedSenderIds)],
2554
+ attemptedSenderIds: [...new Set(refreshedSenderIds)],
2555
+ receipts: contextualReceipts,
2556
+ },
2557
+ })
2558
+ : runStateWithoutCreditTrust;
2331
2559
  await safeJournalAppend(ctx, deps, deps.journal.renderBootstrapSection({
2332
2560
  summary: "Refill v2 execution bootstrap",
2333
2561
  senderSummary: safeJson(recordValue(plan.bootstrap)?.senders ?? []),
2334
2562
  laneSummary: safeJson(recordValue(plan.bootstrap)?.laneOrder ?? []),
2335
2563
  targetSummary: safeJson(recordValue(plan.bootstrap)?.target ?? []),
2336
- creditSummary: safeJson(recordValue(plan.bootstrap)?.paidCredit ?? []),
2564
+ creditSummary: safeJson({
2565
+ refreshActionCount: paidCreditActions.length,
2566
+ persistedReceiptCount: contextualReceipts.length,
2567
+ refreshedSenderIds: [...new Set(refreshedSenderIds)],
2568
+ }),
2337
2569
  }));
2338
2570
  const advanced = await deps.runClient.advanceRefillRunGateRemote({
2339
2571
  workspaceId: input.workspaceId,
@@ -4314,6 +4546,14 @@ async function gateVerify(input, deps, ctx, budgets) {
4314
4546
  // keep going — instead of terminalizing the invite lane with confirmed-fillable
4315
4547
  // slots still open. Once a broadening round proves the universe dry this run, this
4316
4548
  // no longer intercepts and the refusal terminalizes typed below.
4549
+ if (actionType === "copy_selected_source_rows" &&
4550
+ stringValue(recordValue(ctx.runState.lastOutcome)?.status) ===
4551
+ "executed_and_reread") {
4552
+ return advanceBackToPlan(input, deps, ctx, {
4553
+ ...runScopedSignalSourceValidationUpdate(action, ctx.runState, deps.now?.() ?? new Date()),
4554
+ ...clearRunScopedDrySourcePrepAttempts(actionCampaignId(action), ctx.runState),
4555
+ });
4556
+ }
4317
4557
  if (actionType === "continue_signal_discovery_source" &&
4318
4558
  isSignalSelectionBroadenableRefusal(ctx.runState.lastOutcome) &&
4319
4559
  !hasRunScopedSignalUniverseExhausted(action, ctx.runState)) {
@@ -12,6 +12,7 @@ type RowSelector = {
12
12
  } | {
13
13
  type: "reviewBatch";
14
14
  limit?: number;
15
+ basisHash?: string;
15
16
  } | {
16
17
  type: "staleGeneratedMessages";
17
18
  limit?: number;
@@ -121,6 +122,11 @@ export declare const campaignMessagePreparationToolDefinitions: ({
121
122
  minimum: number;
122
123
  maximum: number;
123
124
  };
125
+ basisHash: {
126
+ type: string;
127
+ minLength: number;
128
+ maxLength: number;
129
+ };
124
130
  rowIds: {
125
131
  type: string;
126
132
  items: {
@@ -75,6 +75,7 @@ export const campaignMessagePreparationToolDefinitions = [
75
75
  ],
76
76
  },
77
77
  limit: { type: "number", minimum: 1, maximum: 500 },
78
+ basisHash: { type: "string", minLength: 1, maxLength: 200 },
78
79
  rowIds: {
79
80
  type: "array",
80
81
  items: { type: "string", minLength: 1 },
@@ -17,6 +17,40 @@ type GetRefillPlanV2Input = {
17
17
  journalNote?: string;
18
18
  };
19
19
  export declare function sanitizeEvergreenRefillPlanResult(value: unknown): Record<string, unknown>;
20
+ export declare function projectedV2Packet(packet: Record<string, unknown>): {
21
+ sideEffects: {
22
+ scheduled: boolean;
23
+ mutated: boolean;
24
+ sent: boolean;
25
+ refreshedPaidInmailCredits: boolean;
26
+ };
27
+ warnings: unknown;
28
+ reporting?: Record<string, unknown> | undefined;
29
+ refillReporting?: Record<string, unknown> | undefined;
30
+ reportingContext?: RefillReportingContextV2 | undefined;
31
+ readOnly: boolean;
32
+ generatedAt: unknown;
33
+ bootstrap: {
34
+ senders: never[];
35
+ laneOrder: never[];
36
+ target: never[];
37
+ paidCredit: never[];
38
+ laneMemoryHints: {
39
+ appliedFromRunState: boolean;
40
+ cooldownsApplied: never[];
41
+ };
42
+ };
43
+ plans: {};
44
+ globalActionQueue: {};
45
+ evergreen: unknown;
46
+ planRevision: unknown;
47
+ stateRevision: unknown;
48
+ packet: Record<string, unknown>;
49
+ targetConfig: unknown;
50
+ targetConfigBytes: unknown;
51
+ targetConfigDigest: unknown;
52
+ runHandle: {} | null;
53
+ };
20
54
  export declare function buildRunStateFromLocalHints(workspaceId: string): {
21
55
  version: number;
22
56
  senderCursors: never[];
@@ -76,7 +76,7 @@ export function sanitizeEvergreenRefillPlanResult(value) {
76
76
  }
77
77
  return sanitized;
78
78
  }
79
- function projectedV2Packet(packet) {
79
+ export function projectedV2Packet(packet) {
80
80
  const target = isRecord(packet.target) ? packet.target : {};
81
81
  const reporting = sanitizeRefillReportingProjection(packet.reporting);
82
82
  const reportingContext = sanitizeRefillReportingContextProjection(packet.reportingContext);
@@ -7,6 +7,7 @@ export type PaidInmailRefreshAction = {
7
7
  action?: Record<string, unknown>;
8
8
  };
9
9
  export type PaidInmailCreditRefreshOptions = {
10
+ runId?: string | null;
10
11
  actionType?: string | null;
11
12
  campaignId?: string | null;
12
13
  tableId?: string | null;
@@ -119,6 +120,7 @@ export type PrepareRowSelector = {
119
120
  } | {
120
121
  type: "reviewBatch";
121
122
  limit?: number;
123
+ basisHash?: string;
122
124
  } | {
123
125
  type: "staleGeneratedMessages";
124
126
  limit?: number;
@@ -450,9 +450,11 @@ export function prepareRowSelectorValue(value) {
450
450
  return undefined;
451
451
  }
452
452
  const limit = numberValue(selector?.limit);
453
+ const basisHash = type === "reviewBatch" ? stringValue(selector?.basisHash) : null;
453
454
  return {
454
455
  type,
455
456
  ...(limit && limit > 0 ? { limit: Math.floor(limit) } : {}),
457
+ ...(type === "reviewBatch" && basisHash ? { basisHash } : {}),
456
458
  };
457
459
  }
458
460
  export function uniqueStrings(values) {
@@ -1052,7 +1054,9 @@ export function refillPrepareRequestHash(params) {
1052
1054
  ? `rowIds:${createHash("sha256")
1053
1055
  .update(JSON.stringify([...params.rowSelector.rowIds].sort()))
1054
1056
  .digest("hex")}`
1055
- : `${params.rowSelector.type}:${params.rowSelector.limit ?? "no-limit"}`
1057
+ : params.rowSelector.type === "reviewBatch"
1058
+ ? `reviewBatch:${params.rowSelector.limit ?? "no-limit"}:${params.rowSelector.basisHash ?? "no-basis"}`
1059
+ : `${params.rowSelector.type}:${params.rowSelector.limit ?? "no-limit"}`
1056
1060
  : "no-row-selector";
1057
1061
  // Request hashes are persisted in the preparation job's normalized config.
1058
1062
  // Keep the public identity fixed-width: the former concatenated form could
@@ -3221,6 +3225,16 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
3221
3225
  const allowPartialSourceList = toolInput.allowPartialSourceList === true;
3222
3226
  const sourceFingerprint = stringValue(toolInput.sourceFingerprint) ??
3223
3227
  stringValue(actionIds(action).sourceFingerprint);
3228
+ const signalSource = actionLeadSourceProvider(action) === "signal-discovery" ||
3229
+ actionLeadSourceProvider(action) === "campaign-tracked-post";
3230
+ const requestedReviewBatchLimit = sourceRowIds.length > 0
3231
+ ? sourceRowIds.length
3232
+ : (sourceRowLimit ?? undefined);
3233
+ const reviewBatchLimit = requestedReviewBatchLimit
3234
+ ? signalSource
3235
+ ? Math.min(15, requestedReviewBatchLimit)
3236
+ : requestedReviewBatchLimit
3237
+ : undefined;
3224
3238
  if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
3225
3239
  return {
3226
3240
  status: "refused",
@@ -3248,9 +3262,7 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
3248
3262
  ...(workspaceId ? { workspaceId } : {}),
3249
3263
  sourceRowIds: sourceRowIds.length > 0 ? sourceRowIds : undefined,
3250
3264
  sourceRowLimit: sourceRowIds.length > 0 ? undefined : (sourceRowLimit ?? undefined),
3251
- reviewBatchLimit: sourceRowIds.length > 0
3252
- ? sourceRowIds.length
3253
- : (sourceRowLimit ?? undefined),
3265
+ reviewBatchLimit,
3254
3266
  ...(allowPartialSourceList ? { allowPartialSourceList: true } : {}),
3255
3267
  };
3256
3268
  let result;
@@ -3288,16 +3300,19 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
3288
3300
  const retrySourceRowLimit = retrySourceRowIds.length > 0
3289
3301
  ? undefined
3290
3302
  : Math.min(sourceRowLimit ?? remainingRows, remainingRows);
3291
- const retryReviewBatchLimit = retrySourceRowIds.length > 0
3303
+ const retryCopyLimit = retrySourceRowIds.length > 0
3292
3304
  ? retrySourceRowIds.length
3293
3305
  : retrySourceRowLimit;
3294
- if (!retryReviewBatchLimit || retryReviewBatchLimit <= 0) {
3306
+ if (!retryCopyLimit || retryCopyLimit <= 0) {
3295
3307
  return {
3296
3308
  status: "refused",
3297
3309
  refusalReason: rowLimitPayload.error ??
3298
3310
  "selected campaign table cannot accept more workflow rows",
3299
3311
  };
3300
3312
  }
3313
+ const retryReviewBatchLimit = signalSource
3314
+ ? Math.min(15, retryCopyLimit)
3315
+ : retryCopyLimit;
3301
3316
  const retryResult = await confirmLeadList({
3302
3317
  ...copyInput,
3303
3318
  sourceRowIds: retrySourceRowIds.length > 0 ? retrySourceRowIds : undefined,
@@ -247,6 +247,7 @@ const WORKSPACE_ATTEMPT_LEDGER_KEYS = [
247
247
  "signalDiscoveryAttempts",
248
248
  "providerSourceAttempts",
249
249
  "broadenSignalSearchAttempts",
250
+ "signalSourceValidationAttempts",
250
251
  "broadenProviderSearchAttempts",
251
252
  "rubricRelaxationAttempts",
252
253
  "senderPostRefreshAttempts",
@@ -790,8 +791,29 @@ function sameWorkspaceExactAction(left, right) {
790
791
  Boolean(left?.actionKey) &&
791
792
  left?.actionKey === right.actionKey);
792
793
  }
794
+ function coordinatorSenderIds(input) {
795
+ const explicit = normalizeStrings(input.senderIds);
796
+ if (explicit.length > 0)
797
+ return explicit;
798
+ const targetConfigSenderIds = normalizeStrings(input.targetConfig?.senderScope.senderIds);
799
+ if (targetConfigSenderIds.length > 0)
800
+ return targetConfigSenderIds;
801
+ const runState = recordValue(input.workspaceRunState);
802
+ const senderCursors = Array.isArray(runState?.senderCursors)
803
+ ? runState.senderCursors
804
+ : [];
805
+ return normalizeStrings(senderCursors
806
+ .map((entry) => stringValue(recordValue(entry)?.senderId))
807
+ .filter((senderId) => Boolean(senderId)));
808
+ }
793
809
  function workspaceCoordinatorInput(input) {
794
810
  const coordinatorInput = { ...input };
811
+ const senderIds = coordinatorSenderIds(input);
812
+ if (senderIds.length > 0) {
813
+ coordinatorInput.senderIds = senderIds;
814
+ coordinatorInput.senders = [];
815
+ coordinatorInput.senderNames = [];
816
+ }
795
817
  delete coordinatorInput.campaignId;
796
818
  delete coordinatorInput.tableId;
797
819
  delete coordinatorInput.runId;
@@ -843,7 +865,9 @@ function withKeywordsHandoffScope(result, scopedInput) {
843
865
  (handleRunId && typeof handleFence === "number"
844
866
  ? { runId: handleRunId, fence: handleFence }
845
867
  : null);
868
+ const senderIds = coordinatorSenderIds(scopedInput);
846
869
  const scopeAdditions = {
870
+ ...(senderIds.length > 0 ? { senderIds } : {}),
847
871
  ...(scopedInput.targetDate ? { targetDate: scopedInput.targetDate } : {}),
848
872
  ...(scopedInput.untilDate ? { untilDate: scopedInput.untilDate } : {}),
849
873
  ...(typeof scopedInput.horizonSendDays === "number"
@@ -4150,7 +4174,7 @@ export async function executeRefillSendsCommand(input = {}) {
4150
4174
  intent: targetConfigIntent(continuationTargetConfig) ??
4151
4175
  scopedInput.intent ??
4152
4176
  "auto",
4153
- senderIds: normalizeStrings(scopedInput.senderIds),
4177
+ senderIds: coordinatorSenderIds(scopedInput),
4154
4178
  ...(scopedInput.actionTypes
4155
4179
  ? { actionTypes: scopedInput.actionTypes }
4156
4180
  : {}),
@@ -6453,6 +6453,11 @@ export declare const allTools: ({
6453
6453
  minimum: number;
6454
6454
  maximum: number;
6455
6455
  };
6456
+ basisHash: {
6457
+ type: string;
6458
+ minLength: number;
6459
+ maxLength: number;
6460
+ };
6456
6461
  rowIds: {
6457
6462
  type: string;
6458
6463
  items: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.756",
3
+ "version": "0.1.758",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",