@sellable/mcp 0.1.557 → 0.1.558

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 (63) hide show
  1. package/README.md +2 -13
  2. package/agents/registry.json +2 -2
  3. package/dist/api.js +6 -3
  4. package/dist/auth.d.ts +6 -0
  5. package/dist/auth.js +44 -2
  6. package/dist/refill-contract.d.ts +157 -0
  7. package/dist/refill-contract.js +487 -0
  8. package/dist/refill-run-client.d.ts +10 -0
  9. package/dist/refill-run-client.js +22 -0
  10. package/dist/refill-run-loop.d.ts +14 -2
  11. package/dist/refill-run-loop.js +160 -15
  12. package/dist/server.js +0 -23
  13. package/dist/tools/auth.d.ts +5 -0
  14. package/dist/tools/auth.js +49 -12
  15. package/dist/tools/campaign-message-preparation.d.ts +62 -0
  16. package/dist/tools/campaign-message-preparation.js +41 -0
  17. package/dist/tools/campaigns.js +2 -2
  18. package/dist/tools/csv-dnc.js +2 -2
  19. package/dist/tools/evergreen-refill-plan.d.ts +3 -0
  20. package/dist/tools/evergreen-refill-plan.js +29 -7
  21. package/dist/tools/leads.d.ts +32 -317
  22. package/dist/tools/leads.js +10 -171
  23. package/dist/tools/model-quality.js +6 -4
  24. package/dist/tools/prompts.d.ts +3 -3
  25. package/dist/tools/prompts.js +15 -7
  26. package/dist/tools/provider-preflight.d.ts +2 -65
  27. package/dist/tools/provider-preflight.js +10 -97
  28. package/dist/tools/readiness.d.ts +5 -89
  29. package/dist/tools/readiness.js +0 -66
  30. package/dist/tools/refill-executors.d.ts +38 -0
  31. package/dist/tools/refill-executors.js +222 -3
  32. package/dist/tools/refill-sends-v2.d.ts +118 -1
  33. package/dist/tools/refill-sends-v2.js +312 -3
  34. package/dist/tools/refill-sends.d.ts +678 -32
  35. package/dist/tools/refill-sends.js +274 -13
  36. package/dist/tools/refill-target-plan.js +486 -14
  37. package/dist/tools/registry.d.ts +115 -330
  38. package/dist/tools/registry.js +1 -7
  39. package/dist/tools/scheduler-fill-capacity.js +1 -1
  40. package/dist/tools/scheduler-run.d.ts +71 -0
  41. package/dist/tools/scheduler-run.js +203 -1
  42. package/dist/tools/setup-evergreen-campaigns.js +1 -1
  43. package/dist/tools/workspace-context.d.ts +1 -1
  44. package/dist/tools/workspace-context.js +8 -3
  45. package/dist/tools/workspace-export.js +2 -2
  46. package/dist/tools/workspaces.d.ts +48 -2
  47. package/dist/tools/workspaces.js +48 -5
  48. package/package.json +1 -1
  49. package/skills/create-campaign/SKILL.md +3 -3
  50. package/skills/create-campaign-v2/SKILL.md +1 -1
  51. package/skills/create-evergreen-campaigns/SKILL.md +16 -16
  52. package/skills/find-leads/SKILL.md +630 -48
  53. package/skills/refill-sends/SKILL.md +91 -353
  54. package/skills/refill-sends-v2/SKILL.md +6 -6
  55. package/skills/refill-sends-v2-workflow/SKILL.md +5 -5
  56. package/skills/refill-sends-v2-workflow/core/flow.v1.json +8 -8
  57. package/skills/refill-sends-workflow/SKILL.md +100 -743
  58. package/skills/refill-sends-workflow/core/contract.v2.json +543 -0
  59. package/skills/refill-sends-workflow/core/flow.v1.json +185 -1
  60. package/dist/tools/find-leads-runs.d.ts +0 -151
  61. package/dist/tools/find-leads-runs.js +0 -98
  62. package/skills/find-leads-v2/SKILL.md +0 -70
  63. package/skills/find-leads-v2/core/flow.v1.json +0 -31
@@ -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);
@@ -1474,19 +1618,19 @@ function classifyCaughtError(error, ctx) {
1474
1618
  };
1475
1619
  }
1476
1620
  async function heartbeatAndContinue(input, deps, ctx) {
1477
- const heartbeat = await deps.runClient.heartbeatRefillRun({
1621
+ const yielded = await deps.runClient.yieldRefillRun({
1478
1622
  workspaceId: input.workspaceId,
1479
1623
  runId: ctx.runId,
1480
1624
  fence: ctx.fence,
1481
1625
  });
1482
- if (isLeaseLost(heartbeat))
1626
+ if (isLeaseLost(yielded))
1483
1627
  return leaseLostResult(ctx);
1484
1628
  return {
1485
1629
  status: "in_progress",
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
  }
package/dist/server.js CHANGED
@@ -29,7 +29,6 @@ import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js
29
29
  import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
30
30
  import { getRefillPlanV2 } from "./tools/evergreen-refill-plan.js";
31
31
  import { getCampaignFramework } from "./tools/framework.js";
32
- import { bootstrapFindLeads, cancelFindLeads, getFindLeadsRun, reissueFindLeadsWatchLink, updateFindLeadsRun, waitForFindLeadsRun, } from "./tools/find-leads-runs.js";
33
32
  import { confirmHarvestJobCompanies, searchHarvestJobs, } from "./tools/harvest-jobs.js";
34
33
  import { checkInboxReplyEligibility, getInboxThread, searchInboxThreads, sendInboxDraft, sendInboxManualReply, updateInboxDraft, } from "./tools/inbox.js";
35
34
  import { cancelLeadImport, confirmLeadList, confirmProspeoCompanyAccounts, getProviderPrompt, importLeads, listDncEntriesTool, loadCsvDncEntriesTool, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchProspeoCompanies, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
@@ -37,7 +36,6 @@ import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfi
37
36
  import { getCampaignNavigationState } from "./tools/navigation.js";
38
37
  import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
39
38
  import { upsertRubric } from "./tools/processing.js";
40
- import { preflightFindLeadsProvider } from "./tools/provider-preflight.js";
41
39
  import { completeSenderResearch, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, searchSubskillPrompts, } from "./tools/prompts.js";
42
40
  import { waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
43
41
  import { refreshSenderEngagementCommand } from "./tools/refresh-sender-engagement.js";
@@ -148,27 +146,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
148
146
  case "wait_for_cli_login":
149
147
  result = await handleWaitForCliLogin(args);
150
148
  break;
151
- case "bootstrap_find_leads":
152
- result = await bootstrapFindLeads(args);
153
- break;
154
- case "get_find_leads_run":
155
- result = await getFindLeadsRun(args);
156
- break;
157
- case "update_find_leads_run":
158
- result = await updateFindLeadsRun(args);
159
- break;
160
- case "wait_for_find_leads_run":
161
- result = await waitForFindLeadsRun(args);
162
- break;
163
- case "cancel_find_leads":
164
- result = await cancelFindLeads(args);
165
- break;
166
- case "reissue_find_leads_watch_link":
167
- result = await reissueFindLeadsWatchLink(args);
168
- break;
169
- case "preflight_find_leads_provider":
170
- result = await preflightFindLeadsProvider(args);
171
- break;
172
149
  case "bootstrap_engage":
173
150
  result = await bootstrapEngage(args);
174
151
  break;
@@ -11,6 +11,11 @@ export type AuthStatus = {
11
11
  tokenPresent: boolean;
12
12
  tokenPrefix: string | null;
13
13
  workspacesCount: number | null;
14
+ workspaceLock: {
15
+ enabled: boolean;
16
+ workspaceId: string | null;
17
+ required: boolean;
18
+ };
14
19
  checkedAt: string;
15
20
  /** Short message the LLM MUST show the user after auth succeeds. */
16
21
  _userNotice: string | null;
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import { getApi, SellableApiError } from "../api.js";
3
- import { getConfig, getConfigPath, getResolvedConfigsDir } from "../auth.js";
3
+ import { getConfig, getConfigPath, getEffectiveConfiguredWorkspaceId, getLockedWorkspaceId, getResolvedConfigsDir, isWorkspaceLockRequired, workspaceLockRequiredMessage, } from "../auth.js";
4
4
  import { checkForUpdates } from "../update-check.js";
5
5
  function maskToken(token) {
6
6
  if (token.length <= 10)
@@ -38,6 +38,8 @@ export async function getAuthStatus() {
38
38
  const checkedAt = new Date().toISOString();
39
39
  const update = await getUpdateStatus();
40
40
  let tokenPresent = false;
41
+ const workspaceLockRequired = isWorkspaceLockRequired();
42
+ const lockedWorkspaceId = getLockedWorkspaceId();
41
43
  const base = {
42
44
  ok: false,
43
45
  configPath,
@@ -50,27 +52,47 @@ export async function getAuthStatus() {
50
52
  tokenPresent: false,
51
53
  tokenPrefix: null,
52
54
  workspacesCount: null,
55
+ workspaceLock: {
56
+ enabled: Boolean(lockedWorkspaceId),
57
+ workspaceId: lockedWorkspaceId,
58
+ required: workspaceLockRequired,
59
+ },
53
60
  _userNotice: appendUpdateNotice(null, update),
54
61
  update,
55
62
  checkedAt,
56
63
  };
64
+ if (workspaceLockRequired && !lockedWorkspaceId) {
65
+ return {
66
+ ...base,
67
+ error: {
68
+ type: "workspace",
69
+ message: "Customer profile workspace lock is missing.",
70
+ guidance: workspaceLockRequiredMessage("get_auth_status"),
71
+ },
72
+ };
73
+ }
57
74
  try {
58
75
  const config = getConfig();
59
76
  tokenPresent = Boolean(config.token);
60
77
  base.activeEnvName = config.activeEnvName || null;
61
78
  const api = getApi();
62
79
  const { workspaces } = await api.get("/api/v3/workspaces");
63
- const activeWorkspaceId = config.activeWorkspaceId || config.workspaceId || null;
80
+ const activeWorkspaceId = getEffectiveConfiguredWorkspaceId(config);
64
81
  const activeWorkspace = activeWorkspaceId
65
82
  ? workspaces.find((ws) => ws.id === activeWorkspaceId)
66
83
  : null;
84
+ const visibleWorkspacesCount = lockedWorkspaceId
85
+ ? activeWorkspace
86
+ ? 1
87
+ : 0
88
+ : workspaces.length;
67
89
  if (!activeWorkspaceId) {
68
90
  return {
69
91
  ...base,
70
92
  apiUrl: config.apiUrl || null,
71
93
  tokenPresent,
72
94
  tokenPrefix: maskToken(config.token),
73
- workspacesCount: workspaces.length,
95
+ workspacesCount: visibleWorkspacesCount,
74
96
  error: {
75
97
  type: "workspace",
76
98
  message: "No active workspace selected.",
@@ -87,22 +109,26 @@ export async function getAuthStatus() {
87
109
  apiUrl: config.apiUrl || null,
88
110
  tokenPresent,
89
111
  tokenPrefix: maskToken(config.token),
90
- workspacesCount: workspaces.length,
112
+ workspacesCount: visibleWorkspacesCount,
91
113
  error: {
92
114
  type: "workspace",
93
115
  message: `Active workspace ${activeWorkspaceId} not found in access list.`,
94
- guidance: available
95
- ? `Available: ${available}. Run set_active_workspace with a valid ID.`
96
- : "Run list_workspaces to view available workspaces, then set_active_workspace.",
116
+ guidance: lockedWorkspaceId
117
+ ? "This profile is locked to a workspace that the configured Sellable token cannot access. Re-provision the customer profile from sellable-admin with the correct workspace token."
118
+ : available
119
+ ? `Available: ${available}. Run set_active_workspace with a valid ID.`
120
+ : "Run list_workspaces to view available workspaces, then set_active_workspace.",
97
121
  },
98
122
  };
99
123
  }
100
124
  const workspaceName = activeWorkspace.name || config.activeWorkspaceName || null;
101
125
  const envLabel = config.activeEnvName || null;
102
126
  const wsLabel = workspaceName || activeWorkspaceId;
103
- const notice = envLabel
104
- ? `You're in ${wsLabel} (${envLabel}).`
105
- : `You're in ${wsLabel}.`;
127
+ const noticePrefix = lockedWorkspaceId
128
+ ? `You're locked to ${wsLabel}.`
129
+ : envLabel
130
+ ? `You're in ${wsLabel} (${envLabel}).`
131
+ : `You're in ${wsLabel}.`;
106
132
  return {
107
133
  ok: true,
108
134
  configPath,
@@ -114,8 +140,19 @@ export async function getAuthStatus() {
114
140
  activeWorkspaceName: workspaceName,
115
141
  tokenPresent,
116
142
  tokenPrefix: maskToken(config.token),
117
- workspacesCount: workspaces.length,
118
- _userNotice: appendUpdateNotice(notice, update),
143
+ workspacesCount: visibleWorkspacesCount,
144
+ workspaceLock: lockedWorkspaceId
145
+ ? {
146
+ enabled: true,
147
+ workspaceId: lockedWorkspaceId,
148
+ required: workspaceLockRequired,
149
+ }
150
+ : {
151
+ enabled: false,
152
+ workspaceId: null,
153
+ required: workspaceLockRequired,
154
+ },
155
+ _userNotice: appendUpdateNotice(noticePrefix, update),
119
156
  update,
120
157
  checkedAt,
121
158
  error: null,
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { getApi } from "../api.js";
2
- import { getConfig } from "../auth.js";
2
+ import { getConfig, getEffectiveConfiguredWorkspaceId } from "../auth.js";
3
3
  import { assertCreateCampaignPromptLoaded, assertNetNewCreateCampaignResearchReady, } from "./flow-preflight.js";
4
4
  import { setCampaignInteractionMode, } from "./interaction-mode.js";
5
5
  import { isLinkedInProfileInput, normalizeLinkedInProfileInput, } from "./linkedin-url.js";
@@ -114,7 +114,7 @@ function normalizeLeadSourceProvider(input) {
114
114
  : null;
115
115
  }
116
116
  export function buildWatchUrl(config, path) {
117
- const workspaceId = config.activeWorkspaceId || config.workspaceId;
117
+ const workspaceId = getEffectiveConfiguredWorkspaceId(config);
118
118
  const url = new URL(path, config.apiUrl);
119
119
  if (workspaceId && !url.searchParams.has("workspaceId")) {
120
120
  url.searchParams.set("workspaceId", workspaceId);