@sellable/mcp 0.1.614 → 0.1.615-wip.121.2

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.
@@ -44,6 +44,51 @@ const USABLE_PAID_INMAIL_REFRESH_RECEIPT_STATUSES = new Set([
44
44
  "refreshed",
45
45
  "below_threshold",
46
46
  ]);
47
+ const PROVIDER_REFERENCE_ORIGINS = new Set([
48
+ "config_import_job",
49
+ "exact_table_job",
50
+ "find_leads_run",
51
+ ]);
52
+ /**
53
+ * The sole compact provider-lineage decoder used at both the MCP packet and
54
+ * executor boundaries. Legacy loose searchId/provider fields never satisfy
55
+ * this contract.
56
+ */
57
+ export function sanitizeProviderSearchReference(value) {
58
+ const reference = recordValue(value);
59
+ const owner = recordValue(reference?.owner);
60
+ const workspaceId = stringValue(reference?.workspaceId);
61
+ const provider = stringValue(reference?.provider);
62
+ const sourceTableId = stringValue(reference?.sourceTableId);
63
+ const searchId = stringValue(reference?.searchId);
64
+ const resolutionOrigin = stringValue(reference?.resolutionOrigin);
65
+ const ownerKind = stringValue(owner?.kind);
66
+ const ownerId = stringValue(owner?.id);
67
+ const basisFingerprint = stringValue(reference?.basisFingerprint);
68
+ if (!workspaceId ||
69
+ (provider !== "sales-nav" && provider !== "prospeo") ||
70
+ !sourceTableId ||
71
+ !searchId ||
72
+ !resolutionOrigin ||
73
+ !PROVIDER_REFERENCE_ORIGINS.has(resolutionOrigin) ||
74
+ (ownerKind !== "campaign" && ownerKind !== "find_leads_run") ||
75
+ !ownerId ||
76
+ !basisFingerprint) {
77
+ return null;
78
+ }
79
+ return {
80
+ workspaceId,
81
+ provider,
82
+ sourceTableId,
83
+ searchId,
84
+ resolutionOrigin: resolutionOrigin,
85
+ owner: {
86
+ kind: ownerKind,
87
+ id: ownerId,
88
+ },
89
+ basisFingerprint,
90
+ };
91
+ }
47
92
  export function normalizeStrings(values) {
48
93
  if (!Array.isArray(values))
49
94
  return [];
@@ -1687,242 +1732,456 @@ export async function broadenSignalSearch(action, workspaceId) {
1687
1732
  },
1688
1733
  };
1689
1734
  }
1690
- /**
1691
- * Phase 110-05: bounded SAME-provider-source continuation for cold-outbound
1692
- * Sales Nav / Prospeo campaigns. Mirrors continueSignalDiscoverySource's
1693
- * guardrails (campaign + provider + selected-source match) and reuses the
1694
- * EXISTING import_leads tool to add leads from the campaign's own saved
1695
- * provider search (mode "add") once eligible/prepared rows are receipt-proven
1696
- * exhausted. It never switches source families and never creates a campaign.
1697
- * The add is bounded by the packet's existing sourceRowLimit cap; when the
1698
- * packet carries no reusable provider search reference the add cannot start
1699
- * autonomously and the executor refuses with a bounded reason so the run
1700
- * records the attempt instead of looping on an unchanged mutation.
1701
- */
1702
- export async function continueProviderSource(action, workspaceId) {
1703
- const type = stringValue(action.type);
1704
- const campaignOfferId = actionCampaignId(action);
1705
- const sourceLeadListId = actionSourceLeadListId(action);
1706
- const sourceFingerprint = actionSourceFingerprint(action);
1735
+ function providerOutcomeAttempt(outcome, result = {}) {
1736
+ return {
1737
+ status: outcome.class === "executed" ? "executed_and_reread" : "refused",
1738
+ ...(outcome.class === "executed"
1739
+ ? {}
1740
+ : { refusalReason: `${outcome.code}: ${outcome.detail}` }),
1741
+ providerOutcome: outcome,
1742
+ result: { ...result, providerOutcome: outcome },
1743
+ };
1744
+ }
1745
+ function providerActionIdentity(action) {
1707
1746
  const toolInput = actionToolInput(action);
1708
- const packetProvider = stringValue(toolInput.provider);
1709
- // The owner directive authorizes cold same-source continuation for Sales Nav
1710
- // OR Prospeo only. continue_prospeo_source also spans apollo-sourced
1711
- // campaigns, which are NOT an authorized cold yolo source — refuse those.
1712
- const provider = type === "continue_sales_nav_source"
1713
- ? "sales-nav"
1714
- : type === "continue_prospeo_source" &&
1715
- packetProvider !== "apollo" &&
1716
- packetProvider !== "apollo-ai"
1717
- ? "prospeo"
1718
- : null;
1719
- if (!provider) {
1720
- return {
1721
- status: "refused",
1722
- refusalReason: "continue_provider_source only runs bounded same-source adds for Sales Nav or Prospeo cold campaigns",
1723
- };
1724
- }
1725
- if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
1726
- return {
1727
- status: "refused",
1728
- refusalReason: `${type} action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint`,
1729
- };
1730
- }
1731
- const api = getApi();
1732
- const requestOptions = workspaceRequestOptions(workspaceId);
1733
- const campaign = await api.get(`/api/v2/campaign-offers/${campaignOfferId}`, requestOptions);
1734
- const campaignProvider = campaign.leadSourceProvider ?? null;
1735
- const providerMatches = provider === "sales-nav"
1736
- ? campaignProvider === "sales-nav"
1737
- : campaignProvider === "prospeo";
1738
- if (!providerMatches) {
1747
+ return {
1748
+ actionKey: stringValue(action.actionKey),
1749
+ effectId: stringValue(action.effectId) ?? stringValue(toolInput.effectId),
1750
+ };
1751
+ }
1752
+ function exactProviderReferenceFromAction(action) {
1753
+ const rawReferences = [
1754
+ action.providerSearchReference,
1755
+ actionToolInput(action).providerSearchReference,
1756
+ actionIds(action).providerSearchReference,
1757
+ ].filter((value) => value !== undefined && value !== null);
1758
+ if (rawReferences.length === 0)
1759
+ return null;
1760
+ const references = rawReferences.map(sanitizeProviderSearchReference);
1761
+ if (references.some((reference) => !reference))
1762
+ return null;
1763
+ const [first, ...rest] = references;
1764
+ const fingerprint = JSON.stringify(first);
1765
+ return rest.every((reference) => JSON.stringify(reference) === fingerprint)
1766
+ ? first
1767
+ : null;
1768
+ }
1769
+ function providerFacts(params) {
1770
+ return {
1771
+ provider: params.provider,
1772
+ workspaceId: params.workspaceId,
1773
+ campaignOfferId: params.campaignOfferId,
1774
+ sourceLeadListId: params.sourceLeadListId,
1775
+ searchId: params.reference?.searchId ?? null,
1776
+ resolutionOrigin: params.reference?.resolutionOrigin ?? null,
1777
+ owner: params.reference?.owner ?? null,
1778
+ basisFingerprint: params.reference?.basisFingerprint ?? null,
1779
+ actionKey: params.actionKey,
1780
+ effectId: params.effectId,
1781
+ };
1782
+ }
1783
+ function authorizationOutcome(status, facts) {
1784
+ return {
1785
+ class: "authorization_blocker",
1786
+ code: status === 401
1787
+ ? "provider_authorization_global"
1788
+ : "provider_authorization_workspace",
1789
+ detail: status === 401
1790
+ ? "Provider authorization is unavailable globally."
1791
+ : "Provider authorization is unavailable for this workspace scope.",
1792
+ scope: status === 401 ? "global" : "workspace",
1793
+ status,
1794
+ facts,
1795
+ };
1796
+ }
1797
+ function numericFact(record, key) {
1798
+ const value = numberValue(record[key]);
1799
+ return value === null ? null : Math.max(0, value);
1800
+ }
1801
+ function classifyReconciledProviderJob(params) {
1802
+ const status = params.status.toUpperCase();
1803
+ const jobFacts = {
1804
+ ...params.facts,
1805
+ jobId: params.jobId,
1806
+ jobStatus: status,
1807
+ totalPages: numericFact(params.job, "totalPages"),
1808
+ fetchedPages: numericFact(params.job, "fetchedPages"),
1809
+ totalLeads: numericFact(params.job, "totalLeads"),
1810
+ fetchedLeads: numericFact(params.job, "fetchedLeads"),
1811
+ existingRows: numericFact(params.progress, "existingRows"),
1812
+ requestedRows: numericFact(params.progress, "requestedRows"),
1813
+ processed: numericFact(params.progress, "processed"),
1814
+ leadsImported: numericFact(params.progress, "leadsImported"),
1815
+ sourceRows: numericFact(params.progress, "sourceRows"),
1816
+ uniqueRows: numericFact(params.progress, "uniqueRows"),
1817
+ duplicateRowsSkipped: numericFact(params.progress, "duplicateRowsSkipped"),
1818
+ invalidRows: numericFact(params.progress, "invalidRows"),
1819
+ };
1820
+ if (["PENDING", "FETCHING", "PROCESSING", "QUEUED", "RUNNING"].includes(status)) {
1739
1821
  return {
1740
- status: "refused",
1741
- refusalReason: `campaign leadSourceProvider (${String(campaignProvider)}) is not ${provider}; refusing same-source continuation`,
1822
+ class: "executed",
1823
+ code: "provider_job_reconciled_active",
1824
+ detail: "An exact active provider job is attached to this source table.",
1825
+ jobId: params.jobId,
1826
+ facts: jobFacts,
1742
1827
  };
1743
1828
  }
1744
- if (campaign.selectedLeadListId &&
1745
- campaign.selectedLeadListId !== sourceLeadListId) {
1829
+ if (["FAILED", "ERROR", "CANCELLED", "CANCELED"].includes(status)) {
1746
1830
  return {
1747
- status: "refused",
1748
- refusalReason: "campaign selectedLeadListId changed since the plan packet; rerun get_refill_target_plan before source continuation",
1831
+ class: "transient",
1832
+ code: "provider_job_terminal_failure",
1833
+ detail: "The exact provider job ended without a successful import receipt.",
1834
+ retryable: true,
1835
+ facts: jobFacts,
1749
1836
  };
1750
1837
  }
1751
- // Bound the same-source add by the packet's existing sourceRowLimit cap.
1752
- const sourceRowLimit = Math.min(PROVIDER_SOURCE_CONTINUATION_MAX_ROWS, Math.max(1, Math.floor(numberValue(toolInput.sourceRowLimit) ??
1753
- numberValue(toolInput.targetLeadCount) ??
1754
- numberValue(toolInput.targetRows) ??
1755
- numberValue(action.targetRows) ??
1756
- PROVIDER_SOURCE_CONTINUATION_DEFAULT_ROWS)));
1757
- // Reuse the campaign's SAME saved provider search reference; never widen to a
1758
- // different source family. Without a reusable reference the same-source add
1759
- // cannot start autonomously.
1760
- const searchId = stringValue(toolInput.searchId);
1761
- if (!searchId) {
1838
+ const completed = [
1839
+ "COMPLETED",
1840
+ "COMPLETE",
1841
+ "SUCCESS",
1842
+ "SUCCEEDED",
1843
+ "DONE",
1844
+ ].includes(status);
1845
+ if (!completed) {
1762
1846
  return {
1763
- status: "refused",
1764
- refusalReason: `${provider} same-source continuation requires a reusable provider search reference on the packet; none was surfaced`,
1765
- result: {
1766
- provider,
1767
- campaignOfferId,
1768
- sourceLeadListId,
1769
- sourceFingerprint,
1770
- },
1847
+ class: "uncertain_effect",
1848
+ code: "provider_job_status_unknown",
1849
+ detail: "The exact provider job exists but its status is not recognized.",
1850
+ facts: jobFacts,
1771
1851
  };
1772
1852
  }
1773
- markProviderPromptLoaded({ provider, campaignOfferId });
1774
- const importResult = await importLeads({
1775
- campaignOfferId,
1776
- provider,
1777
- sourceLeadListId,
1778
- searchId,
1779
- mode: "add",
1780
- targetLeadCount: sourceRowLimit,
1781
- confirmed: true,
1782
- ...(workspaceId ? { workspaceId } : {}),
1783
- });
1784
- const importRecord = recordValue(importResult);
1785
- if (importRecord?.error || importRecord?.needsModeSelection === true) {
1853
+ const totalPages = jobFacts.totalPages;
1854
+ const fetchedPages = jobFacts.fetchedPages;
1855
+ const totalLeads = jobFacts.totalLeads;
1856
+ const fetchedLeads = jobFacts.fetchedLeads;
1857
+ const existingRows = jobFacts.existingRows;
1858
+ const leadsImported = jobFacts.leadsImported;
1859
+ const processed = jobFacts.processed;
1860
+ const sourceRows = jobFacts.sourceRows;
1861
+ const uniqueRows = jobFacts.uniqueRows;
1862
+ const duplicates = jobFacts.duplicateRowsSkipped;
1863
+ const invalidRows = jobFacts.invalidRows;
1864
+ const netNew = existingRows !== null && leadsImported !== null
1865
+ ? Math.max(0, leadsImported - existingRows)
1866
+ : null;
1867
+ const paginationComplete = totalPages !== null && fetchedPages !== null && fetchedPages >= totalPages;
1868
+ const processedNew = existingRows !== null && processed !== null
1869
+ ? Math.max(0, processed - existingRows)
1870
+ : null;
1871
+ const zeroUniverse = totalLeads === 0 || fetchedLeads === 0;
1872
+ const allRejected = (sourceRows !== null && uniqueRows === 0) ||
1873
+ (processedNew !== null &&
1874
+ processedNew > 0 &&
1875
+ (duplicates ?? 0) + (invalidRows ?? 0) >= processedNew);
1876
+ const zeroEligibleNetNew = netNew === 0 || zeroUniverse || allRejected;
1877
+ if (paginationComplete && zeroEligibleNetNew) {
1786
1878
  return {
1787
- status: "refused",
1788
- refusalReason: stringValue(importRecord?.message) ??
1789
- `${provider} same-source import did not start a bounded add job`,
1790
- result: { provider, campaignOfferId, sourceLeadListId, importResult },
1879
+ class: "source_terminal",
1880
+ code: "provider_pagination_exhausted",
1881
+ detail: "Completed provider pagination proved zero remaining eligible net-new yield.",
1882
+ facts: { ...jobFacts, netNewRows: netNew ?? 0, paginationComplete: true },
1791
1883
  };
1792
1884
  }
1793
1885
  return {
1794
- status: "executed_and_reread",
1795
- result: {
1796
- provider,
1797
- campaignOfferId,
1798
- previousSourceLeadListId: sourceLeadListId,
1799
- sourceFingerprint,
1800
- searchId,
1801
- sourceRowLimit,
1802
- importResult,
1803
- },
1886
+ class: "executed",
1887
+ code: netNew !== null && netNew > 0
1888
+ ? "provider_job_partial_or_positive_yield"
1889
+ : "provider_job_completed_replan",
1890
+ detail: netNew !== null && netNew > 0
1891
+ ? "The provider job produced net-new rows; replan from the refreshed source."
1892
+ : "The provider job completed without sufficient facts to prove source exhaustion; replan without consuming the source.",
1893
+ jobId: params.jobId,
1894
+ facts: { ...jobFacts, netNewRows: netNew, paginationComplete },
1804
1895
  };
1805
1896
  }
1806
- /**
1807
- * feat(110-23a): rung 3 of the automatic escalation ladder — bounded provider
1808
- * search broadening. When rungs 1-2 (same-source refill + cold continuation) are
1809
- * receipt-proven exhausted at the current search depth and schedulable capacity
1810
- * remains, this re-runs the campaign's EXISTING provider saved search one bounded
1811
- * notch wider. It does NOT invent provider filter semantics: the "notch" is a
1812
- * deeper page/depth window on the SAME searchId, expressed as a larger bounded
1813
- * targetLeadCount into the same saved search via the EXISTING import_leads path
1814
- * continueProviderSource already uses (mode "add"). One notch per campaign per run
1815
- * (the loop enforces the run-scoped bound). The receipt records prior depth, new
1816
- * depth, and the widened window; the actual rows gained surface on the next replan.
1817
- * Never switches source families, never creates a campaign, never sends.
1818
- */
1819
- export async function broadenProviderSearch(action, workspaceId) {
1820
- const campaignOfferId = actionCampaignId(action);
1821
- const sourceLeadListId = actionSourceLeadListId(action);
1822
- const sourceFingerprint = actionSourceFingerprint(action);
1897
+ async function reconcileExactProviderJob(params) {
1898
+ const api = getApi();
1899
+ const requestOptions = workspaceRequestOptions(params.workspaceId);
1900
+ try {
1901
+ const meta = await api.get(`/api/v3/workflow-tables/${encodeURIComponent(params.sourceLeadListId)}?mode=meta`, requestOptions);
1902
+ const table = recordValue(meta.table);
1903
+ const tableWorkspaceId = stringValue(table?.workspaceId);
1904
+ const config = recordValue(table?.config) ?? {};
1905
+ const jobId = stringValue(config.importJobId);
1906
+ const importProvider = stringValue(config.importProvider);
1907
+ if ((tableWorkspaceId && tableWorkspaceId !== params.workspaceId) ||
1908
+ importProvider !== params.provider ||
1909
+ !jobId ||
1910
+ (params.candidateJobId && params.candidateJobId !== jobId)) {
1911
+ return null;
1912
+ }
1913
+ const path = params.provider === "sales-nav"
1914
+ ? `/api/v3/sales-nav/export?jobId=${encodeURIComponent(jobId)}`
1915
+ : `/api/v3/lead-lists/${encodeURIComponent(params.sourceLeadListId)}/prospeo-import/status?jobId=${encodeURIComponent(jobId)}`;
1916
+ const response = await api.get(path, requestOptions);
1917
+ const job = recordValue(response.job) ?? response;
1918
+ if (stringValue(job.id) !== jobId)
1919
+ return null;
1920
+ const status = stringValue(job.status);
1921
+ if (!status)
1922
+ return null;
1923
+ if (params.activeOnly &&
1924
+ !["PENDING", "FETCHING", "PROCESSING", "QUEUED", "RUNNING"].includes(status.toUpperCase())) {
1925
+ return null;
1926
+ }
1927
+ return classifyReconciledProviderJob({
1928
+ jobId,
1929
+ status,
1930
+ job,
1931
+ progress: recordValue(config.importProgress) ?? {},
1932
+ facts: params.facts,
1933
+ });
1934
+ }
1935
+ catch {
1936
+ return null;
1937
+ }
1938
+ }
1939
+ function candidateJobIdFromError(error) {
1940
+ const body = apiErrorBody(error);
1941
+ if (!body)
1942
+ return null;
1943
+ try {
1944
+ return stringValue(recordValue(JSON.parse(body))?.jobId);
1945
+ }
1946
+ catch {
1947
+ return null;
1948
+ }
1949
+ }
1950
+ /** Shared continuation/broadening saved-search executor and classifier. */
1951
+ export async function executeProviderSavedSearch(action, workspaceId, mode) {
1952
+ const type = stringValue(action.type);
1823
1953
  const toolInput = actionToolInput(action);
1824
1954
  const packetProvider = stringValue(toolInput.provider);
1825
- // Broadening is only automatic for Sales Nav / Prospeo saved searches, mirroring
1826
- // the cold-continuation authority. apollo-sourced campaigns stay manual.
1827
- const provider = packetProvider === "sales-nav"
1955
+ const provider = type === "continue_sales_nav_source" || packetProvider === "sales-nav"
1828
1956
  ? "sales-nav"
1829
- : packetProvider === "prospeo"
1957
+ : type === "continue_prospeo_source" || packetProvider === "prospeo"
1830
1958
  ? "prospeo"
1831
1959
  : null;
1832
1960
  if (!provider) {
1833
- return {
1834
- status: "refused",
1835
- refusalReason: "broaden_provider_search only broadens Sales Nav or Prospeo saved searches",
1961
+ const outcome = {
1962
+ class: "source_terminal",
1963
+ code: "provider_search_lineage_unusable",
1964
+ detail: "The action does not identify a supported saved-search provider.",
1965
+ facts: { actionType: type, provider: packetProvider },
1836
1966
  };
1967
+ return providerOutcomeAttempt(outcome);
1837
1968
  }
1838
- if (!campaignOfferId || !sourceLeadListId || !sourceFingerprint) {
1839
- return {
1840
- status: "refused",
1841
- refusalReason: "broaden_provider_search action is missing campaignOfferId, sourceLeadListId, or sourceFingerprint",
1842
- };
1969
+ const campaignOfferId = actionCampaignId(action);
1970
+ const sourceLeadListId = actionSourceLeadListId(action);
1971
+ const reference = exactProviderReferenceFromAction(action);
1972
+ const identity = providerActionIdentity(action);
1973
+ const facts = providerFacts({
1974
+ provider,
1975
+ workspaceId: workspaceId ?? null,
1976
+ campaignOfferId,
1977
+ sourceLeadListId,
1978
+ reference,
1979
+ ...identity,
1980
+ });
1981
+ const ownerMatches = reference
1982
+ ? reference.owner.kind === "campaign"
1983
+ ? reference.owner.id === campaignOfferId
1984
+ : reference.owner.id === sourceLeadListId
1985
+ : false;
1986
+ if (!workspaceId ||
1987
+ !campaignOfferId ||
1988
+ !sourceLeadListId ||
1989
+ !reference ||
1990
+ !identity.actionKey ||
1991
+ !identity.effectId ||
1992
+ reference.provider !== provider ||
1993
+ reference.sourceTableId !== sourceLeadListId ||
1994
+ !ownerMatches) {
1995
+ return providerOutcomeAttempt({
1996
+ class: "source_terminal",
1997
+ code: "provider_search_lineage_unusable",
1998
+ detail: "Exact workspace/provider/source/search/owner lineage and actionKey/effectId are required before provider execution.",
1999
+ facts,
2000
+ });
1843
2001
  }
1844
- // Reuse the campaign's SAME saved provider search reference; never widen to a
1845
- // different source family. Without a reusable reference broadening cannot run.
1846
- const searchId = stringValue(toolInput.searchId);
1847
- if (!searchId) {
1848
- return {
1849
- status: "refused",
1850
- refusalReason: `${provider} search broadening requires a reusable provider saved-search reference on the packet; none was surfaced`,
1851
- result: {
1852
- provider,
1853
- campaignOfferId,
1854
- sourceLeadListId,
1855
- sourceFingerprint,
1856
- },
1857
- };
2002
+ if (reference.workspaceId !== workspaceId) {
2003
+ return providerOutcomeAttempt({
2004
+ class: "stale_packet_replan",
2005
+ code: "provider_workspace_changed",
2006
+ detail: "The saved-search reference belongs to a different workspace.",
2007
+ facts,
2008
+ });
1858
2009
  }
1859
2010
  const api = getApi();
1860
2011
  const requestOptions = workspaceRequestOptions(workspaceId);
1861
- const campaign = await api.get(`/api/v2/campaign-offers/${campaignOfferId}`, requestOptions);
1862
- const campaignProvider = campaign.leadSourceProvider ?? null;
1863
- if (campaignProvider !== provider) {
1864
- return {
1865
- status: "refused",
1866
- refusalReason: `campaign leadSourceProvider (${String(campaignProvider)}) is not ${provider}; refusing search broadening`,
1867
- };
2012
+ let campaign;
2013
+ try {
2014
+ campaign = await api.get(`/api/v2/campaign-offers/${campaignOfferId}`, requestOptions);
1868
2015
  }
1869
- if (campaign.selectedLeadListId &&
1870
- campaign.selectedLeadListId !== sourceLeadListId) {
1871
- return {
1872
- status: "refused",
1873
- refusalReason: "campaign selectedLeadListId changed since the plan packet; rerun get_refill_target_plan before broadening the provider search",
1874
- };
2016
+ catch (error) {
2017
+ const status = apiErrorStatus(error);
2018
+ if (status === 401 || status === 403) {
2019
+ return providerOutcomeAttempt(authorizationOutcome(status, facts));
2020
+ }
2021
+ if (status === 404) {
2022
+ return providerOutcomeAttempt({
2023
+ class: "stale_packet_replan",
2024
+ code: "provider_campaign_missing",
2025
+ detail: "The planned campaign no longer exists or is no longer visible.",
2026
+ facts,
2027
+ });
2028
+ }
2029
+ return providerOutcomeAttempt({
2030
+ class: "transient",
2031
+ code: "provider_preflight_unavailable",
2032
+ detail: "Provider preflight could not read the current campaign.",
2033
+ retryable: true,
2034
+ facts: { ...facts, status },
2035
+ });
1875
2036
  }
1876
- // Advance one depth notch: rung 2 consumed priorDepth (default 1); broadening
1877
- // requests the next deeper window (priorDepth + 1) into the SAME saved search.
1878
- const priorDepth = Math.max(1, Math.floor(numberValue(toolInput.priorSearchDepth) ?? 1));
1879
- const newDepth = priorDepth + 1;
1880
- const baseWindow = Math.max(1, Math.floor(numberValue(toolInput.targetLeadCount) ??
1881
- numberValue(toolInput.sourceRowLimit) ??
1882
- numberValue(action.targetRows) ??
1883
- PROVIDER_SOURCE_CONTINUATION_DEFAULT_ROWS));
1884
- // The wider notch reaches deeper into the same saved search's result window.
1885
- const broadenedTargetLeadCount = Math.min(PROVIDER_SEARCH_BROADEN_MAX_ROWS, baseWindow * newDepth);
1886
- markProviderPromptLoaded({ provider, campaignOfferId });
1887
- const importResult = await importLeads({
1888
- campaignOfferId,
2037
+ if (campaign.leadSourceProvider !== provider ||
2038
+ (campaign.selectedLeadListId &&
2039
+ campaign.selectedLeadListId !== sourceLeadListId)) {
2040
+ return providerOutcomeAttempt({
2041
+ class: "stale_packet_replan",
2042
+ code: "provider_source_changed",
2043
+ detail: "The campaign provider or selected source changed after planning.",
2044
+ facts: {
2045
+ ...facts,
2046
+ currentProvider: campaign.leadSourceProvider ?? null,
2047
+ currentSourceLeadListId: campaign.selectedLeadListId ?? null,
2048
+ },
2049
+ });
2050
+ }
2051
+ const existing = await reconcileExactProviderJob({
1889
2052
  provider,
2053
+ workspaceId,
1890
2054
  sourceLeadListId,
1891
- searchId,
1892
- mode: "add",
1893
- targetLeadCount: broadenedTargetLeadCount,
1894
- confirmed: true,
1895
- ...(workspaceId ? { workspaceId } : {}),
2055
+ facts,
2056
+ activeOnly: true,
1896
2057
  });
1897
- const importRecord = recordValue(importResult);
1898
- if (importRecord?.error || importRecord?.needsModeSelection === true) {
1899
- return {
1900
- status: "refused",
1901
- refusalReason: stringValue(importRecord?.message) ??
1902
- `${provider} search broadening did not start a bounded add job`,
1903
- result: { provider, campaignOfferId, sourceLeadListId, importResult },
1904
- };
2058
+ if (existing)
2059
+ return providerOutcomeAttempt(existing);
2060
+ const baseTarget = Math.max(1, Math.floor(numberValue(toolInput.sourceRowLimit) ??
2061
+ numberValue(toolInput.targetLeadCount) ??
2062
+ numberValue(toolInput.targetRows) ??
2063
+ numberValue(action.targetRows) ??
2064
+ PROVIDER_SOURCE_CONTINUATION_DEFAULT_ROWS));
2065
+ const priorDepth = Math.max(1, Math.floor(numberValue(toolInput.priorSearchDepth) ?? 1));
2066
+ const targetLeadCount = mode === "broadening"
2067
+ ? Math.min(PROVIDER_SEARCH_BROADEN_MAX_ROWS, baseTarget * (priorDepth + 1))
2068
+ : Math.min(PROVIDER_SOURCE_CONTINUATION_MAX_ROWS, baseTarget);
2069
+ markProviderPromptLoaded({ provider, campaignOfferId });
2070
+ let importResult;
2071
+ try {
2072
+ importResult = await importLeads({
2073
+ campaignOfferId,
2074
+ provider,
2075
+ sourceLeadListId,
2076
+ searchId: reference.searchId,
2077
+ mode: "add",
2078
+ targetLeadCount,
2079
+ confirmed: true,
2080
+ workspaceId,
2081
+ });
1905
2082
  }
1906
- const importCounts = recordValue(importRecord?.counts);
1907
- const rowsGained = numberValue(importCounts?.rows);
1908
- return {
1909
- status: "executed_and_reread",
1910
- result: {
2083
+ catch (error) {
2084
+ const status = apiErrorStatus(error);
2085
+ if (status === 401 || status === 403) {
2086
+ return providerOutcomeAttempt(authorizationOutcome(status, facts));
2087
+ }
2088
+ if (userAddedRowsLimitPayloadFromError(error)) {
2089
+ return providerOutcomeAttempt({
2090
+ class: "capacity",
2091
+ code: "provider_table_capacity",
2092
+ detail: "The selected source table cannot accept the bounded add.",
2093
+ facts: {
2094
+ ...facts,
2095
+ capacity: userAddedRowsLimitPayloadFromError(error),
2096
+ },
2097
+ });
2098
+ }
2099
+ if (status === 404) {
2100
+ return providerOutcomeAttempt({
2101
+ class: "stale_packet_replan",
2102
+ code: "provider_search_or_source_changed",
2103
+ detail: "The saved search or source table changed after planning.",
2104
+ facts,
2105
+ });
2106
+ }
2107
+ if (status === 429) {
2108
+ return providerOutcomeAttempt({
2109
+ class: "transient",
2110
+ code: "provider_rate_limited",
2111
+ detail: "The provider rejected this bounded attempt due to rate limiting.",
2112
+ retryable: true,
2113
+ facts: { ...facts, status },
2114
+ });
2115
+ }
2116
+ const candidateJobId = candidateJobIdFromError(error);
2117
+ const reconciled = await reconcileExactProviderJob({
1911
2118
  provider,
1912
- campaignOfferId,
1913
- previousSourceLeadListId: sourceLeadListId,
1914
- sourceFingerprint,
1915
- searchId,
1916
- priorDepth,
1917
- newDepth,
1918
- broadenedTargetLeadCount,
1919
- // Rows gained by a widened async import surface on the next replan's row
1920
- // frontier; expose the synchronous count when the import path returns one.
1921
- rowsGained: rowsGained ?? null,
1922
- rowsGainedPending: rowsGained == null,
1923
- importResult,
2119
+ workspaceId,
2120
+ sourceLeadListId,
2121
+ candidateJobId,
2122
+ facts,
2123
+ });
2124
+ if (reconciled)
2125
+ return providerOutcomeAttempt(reconciled);
2126
+ if (status !== null && status >= 500) {
2127
+ return providerOutcomeAttempt({
2128
+ class: "transient",
2129
+ code: "provider_outage",
2130
+ detail: "The provider returned a server failure and no exact active job was found during reconciliation.",
2131
+ retryable: true,
2132
+ facts: { ...facts, status },
2133
+ });
2134
+ }
2135
+ return providerOutcomeAttempt({
2136
+ class: "uncertain_effect",
2137
+ code: status === 409
2138
+ ? "provider_conflict_unreconciled"
2139
+ : "provider_timeout_or_crash_unreconciled",
2140
+ detail: "The mutation may have started, but exact active-job reconciliation did not prove its effect.",
2141
+ facts: { ...facts, status, candidateJobId },
2142
+ });
2143
+ }
2144
+ const importRecord = recordValue(importResult) ?? {};
2145
+ const jobId = stringValue(importRecord.jobId);
2146
+ if (!jobId) {
2147
+ const reconciled = await reconcileExactProviderJob({
2148
+ provider,
2149
+ workspaceId,
2150
+ sourceLeadListId,
2151
+ facts,
2152
+ });
2153
+ if (reconciled)
2154
+ return providerOutcomeAttempt(reconciled, { importResult });
2155
+ return providerOutcomeAttempt({
2156
+ class: "uncertain_effect",
2157
+ code: "provider_import_not_started",
2158
+ detail: "The provider returned without a jobId and exact active-job reconciliation found no attributable job.",
2159
+ facts,
2160
+ }, { importResult });
2161
+ }
2162
+ const rowsGained = numberValue(recordValue(importRecord.counts)?.rows);
2163
+ const outcome = {
2164
+ class: "executed",
2165
+ code: rowsGained !== null && rowsGained > 0
2166
+ ? "provider_job_partial_or_positive_yield"
2167
+ : "provider_job_started",
2168
+ detail: "The provider returned a confirmed started jobId; replan from the refreshed source receipt.",
2169
+ jobId,
2170
+ facts: {
2171
+ ...facts,
2172
+ targetLeadCount,
2173
+ priorSearchDepth: mode === "broadening" ? priorDepth : null,
2174
+ newSearchDepth: mode === "broadening" ? priorDepth + 1 : null,
2175
+ netNewRows: rowsGained,
1924
2176
  },
1925
2177
  };
2178
+ return providerOutcomeAttempt(outcome, { importResult });
2179
+ }
2180
+ export async function continueProviderSource(action, workspaceId) {
2181
+ return executeProviderSavedSearch(action, workspaceId, "continuation");
2182
+ }
2183
+ export async function broadenProviderSearch(action, workspaceId) {
2184
+ return executeProviderSavedSearch(action, workspaceId, "broadening");
1926
2185
  }
1927
2186
  /**
1928
2187
  * feat(110-23b): rung 4 of the automatic escalation ladder — bounded, reversible