@canonry/canonry 4.143.0 → 4.145.0

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 (29) hide show
  1. package/assets/assets/{AuditHistoryPanel--kNrRAQm.js → AuditHistoryPanel-CV9g5mLR.js} +1 -1
  2. package/assets/assets/{BacklinksPage-CtW1w9lh.js → BacklinksPage-RTcYKA5I.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-C-nMEqdu.js → ChartPrimitives-BMR0Dqnf.js} +1 -1
  4. package/assets/assets/{HistoryPage-CAPJDSbc.js → HistoryPage-DAN9QO6x.js} +1 -1
  5. package/assets/assets/ProjectPage-aFV2nddp.js +9 -0
  6. package/assets/assets/{RunRow-aDzTzGSK.js → RunRow-WDCIS9MJ.js} +1 -1
  7. package/assets/assets/{RunsPage-Dy9oWMBK.js → RunsPage-D3Zxd6oc.js} +1 -1
  8. package/assets/assets/SettingsPage-DcCFVeA7.js +1 -0
  9. package/assets/assets/{TrafficPage-B4ADqeKG.js → TrafficPage-D0tPBu5O.js} +1 -1
  10. package/assets/assets/{TrafficSourceDetailPage-CFcoL_PR.js → TrafficSourceDetailPage-CtyEE9eJ.js} +1 -1
  11. package/assets/assets/{arrow-left-DV9xcX1v.js → arrow-left-hBxXIGS1.js} +1 -1
  12. package/assets/assets/{extract-error-message-DXtgw8Q8.js → extract-error-message-DRu89RHb.js} +1 -1
  13. package/assets/assets/index-ANFzZ3nU.css +1 -0
  14. package/assets/assets/index-DSOfEiTH.js +213 -0
  15. package/assets/assets/{trash-2-DMV6uGU1.js → trash-2-DP0ukgbl.js} +1 -1
  16. package/assets/index.html +2 -2
  17. package/dist/{chunk-KUPTGRME.js → chunk-EFBTZ6ZT.js} +4653 -3599
  18. package/dist/{chunk-VN3CI6K6.js → chunk-GTBCGJNT.js} +7436 -2479
  19. package/dist/{chunk-3G5B3IMZ.js → chunk-MBLATP5L.js} +1303 -71
  20. package/dist/{chunk-TJ6SDLXL.js → chunk-XNAJXSZ2.js} +346 -97
  21. package/dist/cli.js +409 -149
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-V6JYDYGF.js → intelligence-service-DVMEVHAG.js} +2 -2
  24. package/dist/mcp.js +5 -5
  25. package/package.json +10 -10
  26. package/assets/assets/ProjectPage-79R3wJW6.js +0 -9
  27. package/assets/assets/SettingsPage-DTAs1BrZ.js +0 -1
  28. package/assets/assets/index-CYA3jd24.css +0 -1
  29. package/assets/assets/index-H2iFFrr1.js +0 -213
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-3G5B3IMZ.js";
13
+ } from "./chunk-MBLATP5L.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -27,6 +27,7 @@ import {
27
27
  agentSessions,
28
28
  agentToolEvents,
29
29
  aiUserFetchEventsHourly,
30
+ anyUsersExist,
30
31
  apiKeys,
31
32
  apiRoutes,
32
33
  auditLog,
@@ -97,6 +98,7 @@ import {
97
98
  llmUsageEvents,
98
99
  loadDuckdb,
99
100
  markSessionFailed,
101
+ measurementRunCompleteness,
100
102
  migrate,
101
103
  nextRunFromCron,
102
104
  notifications,
@@ -115,6 +117,7 @@ import {
115
117
  researchRunQueries,
116
118
  researchRuns,
117
119
  resolveDestination,
120
+ resolveTrustProxy,
118
121
  resolveVercelSyncDeadlineMs,
119
122
  resolveWebhookTarget,
120
123
  runs,
@@ -123,7 +126,7 @@ import {
123
126
  siteAuditSnapshots,
124
127
  toAlertView,
125
128
  usageCounters
126
- } from "./chunk-VN3CI6K6.js";
129
+ } from "./chunk-GTBCGJNT.js";
127
130
  import {
128
131
  AGENT_MEMORY_VALUE_MAX_BYTES,
129
132
  AGENT_PROVIDER_IDS,
@@ -181,6 +184,7 @@ import {
181
184
  embedClientConfigForRequest,
182
185
  factorStatusFromScore,
183
186
  filterCapturedCitedUrls,
187
+ forbidden,
184
188
  formatIsoDateInTimeZone,
185
189
  frameAncestorsHeaderValue,
186
190
  hostMatchesAnyDomain,
@@ -193,11 +197,14 @@ import {
193
197
  isVertexGroundingRedirect,
194
198
  mapWithConcurrency,
195
199
  normalizeIdTokens,
200
+ normalizeMeasurementExecutionQueryText,
196
201
  normalizeServedModel,
197
202
  normalizeUrlPath,
198
203
  notFound,
204
+ parseMeasurementRunManifestV1,
199
205
  parseOriginList,
200
206
  providerError,
207
+ providerSupportsLocationContext,
201
208
  registrableDomain,
202
209
  resolveSnapshotRequestQueries,
203
210
  serializeForInlineScript,
@@ -210,7 +217,7 @@ import {
210
217
  validationError,
211
218
  winnabilityClassLabel,
212
219
  withRetry
213
- } from "./chunk-KUPTGRME.js";
220
+ } from "./chunk-EFBTZ6ZT.js";
214
221
 
215
222
  // src/telemetry.ts
216
223
  import crypto from "crypto";
@@ -917,6 +924,8 @@ var geminiAdapter = {
917
924
  name: "gemini",
918
925
  displayName: "Gemini",
919
926
  mode: "api",
927
+ // normalize.ts folds the location into the prompt the model receives.
928
+ supportsLocationContext: true,
920
929
  keyUrl: "https://aistudio.google.com/apikey",
921
930
  // Upstream model list: https://ai.google.dev/gemini-api/docs/models
922
931
  modelRegistry: {
@@ -1265,6 +1274,8 @@ var openaiAdapter = {
1265
1274
  name: "openai",
1266
1275
  displayName: "OpenAI",
1267
1276
  mode: "api",
1277
+ // normalize.ts sets `user_location` on the web_search tool.
1278
+ supportsLocationContext: true,
1268
1279
  keyUrl: "https://platform.openai.com/api-keys",
1269
1280
  // Upstream model list: https://platform.openai.com/docs/models
1270
1281
  modelRegistry: {
@@ -1656,6 +1667,8 @@ var claudeAdapter = {
1656
1667
  name: "claude",
1657
1668
  displayName: "Claude",
1658
1669
  mode: "api",
1670
+ // normalize.ts sets `user_location` on the web_search tool.
1671
+ supportsLocationContext: true,
1659
1672
  keyUrl: "https://platform.claude.com/settings/keys",
1660
1673
  // Upstream model list: https://platform.claude.com/docs/en/about-claude/models/overview
1661
1674
  modelRegistry: {
@@ -1901,6 +1914,8 @@ var localAdapter = {
1901
1914
  name: "local",
1902
1915
  displayName: "Local",
1903
1916
  mode: "api",
1917
+ // normalize.ts folds the location into the system message.
1918
+ supportsLocationContext: true,
1904
1919
  modelRegistry: {
1905
1920
  defaultModel: "llama3",
1906
1921
  validationPattern: /./,
@@ -2444,6 +2459,9 @@ var cdpChatgptAdapter = {
2444
2459
  name: "cdp:chatgpt",
2445
2460
  displayName: "ChatGPT (Browser)",
2446
2461
  mode: "browser",
2462
+ // The browser session's own geolocation decides; the configured location
2463
+ // never reaches the request, so a row from this adapter claims no place.
2464
+ supportsLocationContext: false,
2447
2465
  modelRegistry: {
2448
2466
  defaultModel: "chatgpt-web",
2449
2467
  validationPattern: /./,
@@ -2798,6 +2816,8 @@ var perplexityAdapter = {
2798
2816
  name: "perplexity",
2799
2817
  displayName: "Perplexity",
2800
2818
  mode: "api",
2819
+ // normalize.ts folds the location into the prompt the model receives.
2820
+ supportsLocationContext: true,
2801
2821
  keyUrl: "https://www.perplexity.ai/settings/api",
2802
2822
  // Upstream model list: https://docs.perplexity.ai/guides/model-cards
2803
2823
  modelRegistry: {
@@ -3281,6 +3301,15 @@ var ProviderExecutionGate = class {
3281
3301
  }
3282
3302
  }
3283
3303
  };
3304
+ var sharedGates = /* @__PURE__ */ new Map();
3305
+ function getSharedProviderExecutionGate(providerName, maxConcurrency, maxPerMinute) {
3306
+ const key = providerName.trim().toLocaleLowerCase("en");
3307
+ const existing = sharedGates.get(key);
3308
+ if (existing) return existing;
3309
+ const gate = new ProviderExecutionGate(maxConcurrency, maxPerMinute);
3310
+ sharedGates.set(key, gate);
3311
+ return gate;
3312
+ }
3284
3313
 
3285
3314
  // src/usage-quota.ts
3286
3315
  import crypto3 from "crypto";
@@ -3421,6 +3450,35 @@ async function runWithConcurrency(items, limit, worker) {
3421
3450
  await Promise.all(Array.from({ length: cap }, next));
3422
3451
  }
3423
3452
  var PROVIDER_FANOUT_DEFAULT = 8;
3453
+ function resolvePlanExecution(run, projectQueries) {
3454
+ if (!run.measurementPlanVersionId || !run.measurementManifest) return null;
3455
+ const manifest = parseMeasurementRunManifestV1(run.measurementManifest);
3456
+ const queryIdByText = /* @__PURE__ */ new Map();
3457
+ for (const row of projectQueries) {
3458
+ const key = normalizeMeasurementExecutionQueryText(row.query);
3459
+ if (!queryIdByText.has(key)) queryIdByText.set(key, row.id);
3460
+ }
3461
+ const unitsByProvider = /* @__PURE__ */ new Map();
3462
+ const nodes = /* @__PURE__ */ new Set();
3463
+ for (const slot of manifest.expectedSlots) {
3464
+ nodes.add(slot.executionId);
3465
+ const units = unitsByProvider.get(slot.provider) ?? [];
3466
+ units.push({
3467
+ executionId: slot.executionId,
3468
+ queryText: slot.queryText,
3469
+ context: slot.context,
3470
+ queryId: queryIdByText.get(normalizeMeasurementExecutionQueryText(slot.queryText)) ?? null,
3471
+ requestedModel: slot.requestedModel
3472
+ });
3473
+ unitsByProvider.set(slot.provider, units);
3474
+ }
3475
+ return {
3476
+ manifest,
3477
+ unitsByProvider,
3478
+ nodeCount: nodes.size,
3479
+ maxUnitsPerProvider: Math.max(0, ...[...unitsByProvider.values()].map((units) => units.length))
3480
+ };
3481
+ }
3424
3482
  function resolveProviderFanout() {
3425
3483
  const raw = process.env.CANONRY_PROVIDER_FANOUT;
3426
3484
  if (!raw) return PROVIDER_FANOUT_DEFAULT;
@@ -3504,6 +3562,7 @@ var JobRunner = class {
3504
3562
  let runLocation;
3505
3563
  let activeProviders = [];
3506
3564
  let projectQueries = [];
3565
+ let planExecution = null;
3507
3566
  let runTrigger;
3508
3567
  let canonicalDomain;
3509
3568
  const providerDispatchCounts = /* @__PURE__ */ new Map();
@@ -3545,17 +3604,24 @@ var JobRunner = class {
3545
3604
  runLocation = projectLocations.find((l) => l.label === project.defaultLocation);
3546
3605
  }
3547
3606
  }
3548
- const projectProviders = providerOverride ?? project.providers;
3549
- activeProviders = this.registry.getForProject(projectProviders).map((entry) => {
3550
- const model = project.providerModels[entry.adapter.name];
3551
- return model === void 0 ? entry : { ...entry, config: { ...entry.config, model } };
3552
- });
3607
+ const scopedQueryNames = existingRun.queries;
3608
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and2(eq2(queries.projectId, projectId), inArray(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq2(queries.projectId, projectId)).all();
3609
+ planExecution = resolvePlanExecution(existingRun, projectQueries);
3610
+ if (planExecution) {
3611
+ const plan = planExecution;
3612
+ const manifestProviders = [...plan.unitsByProvider.keys()];
3613
+ activeProviders = manifestProviders.map((name) => this.registry.get(name)).filter((entry) => entry !== void 0);
3614
+ } else {
3615
+ const projectProviders = providerOverride ?? project.providers;
3616
+ activeProviders = this.registry.getForProject(projectProviders).map((entry) => {
3617
+ const model = project.providerModels[entry.adapter.name];
3618
+ return model === void 0 ? entry : { ...entry, config: { ...entry.config, model } };
3619
+ });
3620
+ }
3553
3621
  if (activeProviders.length === 0) {
3554
3622
  throw new Error("No providers configured. Add at least one provider API key.");
3555
3623
  }
3556
3624
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
3557
- const scopedQueryNames = existingRun.queries;
3558
- projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and2(eq2(queries.projectId, projectId), inArray(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq2(queries.projectId, projectId)).all();
3559
3625
  const projectCompetitors = this.db.select().from(competitors).where(eq2(competitors.projectId, projectId)).all();
3560
3626
  const competitorDomains = projectCompetitors.map((c) => c.domain);
3561
3627
  const allDomains = effectiveDomains({
@@ -3569,12 +3635,12 @@ var JobRunner = class {
3569
3635
  const executionContext = {
3570
3636
  providerCount: activeProviders.length,
3571
3637
  providers: activeProviders.map((provider) => provider.adapter.name),
3572
- queryCount: projectQueries.length,
3638
+ queryCount: planExecution?.nodeCount ?? projectQueries.length,
3573
3639
  ...runLocation ? { location: runLocation.label } : {},
3574
3640
  ...runTrigger ? { trigger: runTrigger } : {},
3575
3641
  ...canonicalDomain ? { canonicalDomain } : {}
3576
3642
  };
3577
- const queriesPerProvider = projectQueries.length;
3643
+ const queriesPerProvider = planExecution?.maxUnitsPerProvider ?? projectQueries.length;
3578
3644
  const todayPeriod = getCurrentUsageDay();
3579
3645
  for (const p of activeProviders) {
3580
3646
  const providerScope = `${projectId}:${p.adapter.name}`;
@@ -3591,7 +3657,8 @@ var JobRunner = class {
3591
3657
  for (const provider of activeProviders) {
3592
3658
  executionGates.set(
3593
3659
  provider.adapter.name,
3594
- new ProviderExecutionGate(
3660
+ getSharedProviderExecutionGate(
3661
+ provider.adapter.name,
3595
3662
  provider.config.quotaPolicy.maxConcurrency,
3596
3663
  provider.config.quotaPolicy.maxRequestsPerMinute
3597
3664
  )
@@ -3756,21 +3823,194 @@ var JobRunner = class {
3756
3823
  }
3757
3824
  }
3758
3825
  };
3826
+ const processNodeForProvider = async (registeredProvider, unit) => {
3827
+ const { adapter, config: providerConfig } = registeredProvider;
3828
+ const providerName = adapter.name;
3829
+ const gate = executionGates.get(providerName);
3830
+ if (!gate) {
3831
+ throw new Error(`Missing execution gate for provider ${providerName}`);
3832
+ }
3833
+ const config = unit.requestedModel ? { ...providerConfig, model: unit.requestedModel } : providerConfig;
3834
+ const requestedContext = unit.context;
3835
+ const supportedContext = requestedContext && providerSupportsLocationContext(adapter) ? { status: "applied", resolved: requestedContext } : null;
3836
+ try {
3837
+ await gate.run(async () => {
3838
+ this.throwIfRunCancelled(runId);
3839
+ providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
3840
+ const raw = await adapter.executeTrackedQuery(
3841
+ {
3842
+ query: unit.queryText,
3843
+ canonicalDomains: allDomains,
3844
+ competitorDomains,
3845
+ location: requestedContext ?? void 0
3846
+ },
3847
+ config
3848
+ );
3849
+ this.throwIfRunCancelled(runId);
3850
+ const providerResult = adapter.normalizeResult(raw);
3851
+ const rawGroundingSources = providerResult.groundingSources;
3852
+ const normalized = {
3853
+ ...providerResult,
3854
+ groundingSources: Array.isArray(rawGroundingSources) ? rawGroundingSources : []
3855
+ };
3856
+ let citedUrlCapture;
3857
+ try {
3858
+ citedUrlCapture = await captureCitedUrls(providerName, rawGroundingSources);
3859
+ } catch (err) {
3860
+ citedUrlCapture = {
3861
+ citedUrls: [],
3862
+ captureStatus: "failed",
3863
+ sourceCount: normalized.groundingSources.length,
3864
+ resolvedCount: 0,
3865
+ captureVersion: CITED_URL_CAPTURE_VERSION
3866
+ };
3867
+ log.warn("query.cited-url-capture-failed", {
3868
+ runId,
3869
+ provider: providerName,
3870
+ query: unit.queryText,
3871
+ error: err instanceof Error ? err.message : String(err)
3872
+ });
3873
+ }
3874
+ this.throwIfRunCancelled(runId);
3875
+ const citationState = determineCitationState(normalized, allDomains);
3876
+ const answerMentioned = determineAnswerMentioned(
3877
+ normalized.answerText,
3878
+ allBrandNames,
3879
+ allDomains
3880
+ );
3881
+ const overlap = computeCompetitorOverlap(normalized, competitorDomains);
3882
+ const extractedCompetitors = extractRecommendedCompetitors(
3883
+ normalized.answerText,
3884
+ allDomains,
3885
+ normalized.citedDomains,
3886
+ competitorDomains,
3887
+ allBrandNames
3888
+ );
3889
+ const snapshotId = crypto4.randomUUID();
3890
+ let screenshotRelPath = null;
3891
+ if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
3892
+ const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
3893
+ if (!fs2.existsSync(screenshotDir)) fs2.mkdirSync(screenshotDir, { recursive: true });
3894
+ const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
3895
+ fs2.renameSync(raw.screenshotPath, destPath);
3896
+ screenshotRelPath = `${runId}/${snapshotId}.png`;
3897
+ }
3898
+ this.db.insert(querySnapshots).values({
3899
+ id: snapshotId,
3900
+ runId,
3901
+ queryId: unit.queryId,
3902
+ queryText: unit.queryText,
3903
+ provider: providerName,
3904
+ // `model` is what was REQUESTED and `served_model` is what
3905
+ // answered. The manifest froze the request, so it is the
3906
+ // authority here: an adapter that reports its own default rather
3907
+ // than what it was handed would otherwise overwrite the identity
3908
+ // the revision recorded, and nothing would show that it moved.
3909
+ model: unit.requestedModel ?? raw.model,
3910
+ servedModel: raw.servedModel ?? null,
3911
+ citationState,
3912
+ answerMentioned,
3913
+ answerText: normalized.answerText,
3914
+ citedDomains: normalized.citedDomains,
3915
+ citedUrls: citedUrlCapture.citedUrls,
3916
+ captureStatus: citedUrlCapture.captureStatus,
3917
+ sourceCount: citedUrlCapture.sourceCount,
3918
+ resolvedCount: citedUrlCapture.resolvedCount,
3919
+ captureVersion: citedUrlCapture.captureVersion,
3920
+ retrievalStatus: normalized.retrievalStatus,
3921
+ retrievalContract: raw.retrievalContract,
3922
+ competitorOverlap: overlap,
3923
+ recommendedCompetitors: extractedCompetitors,
3924
+ // Only claim the geography the provider actually honoured. A
3925
+ // requested-but-unsupported context stores `location: null` —
3926
+ // "no claim" — rather than the label we asked for, mirroring
3927
+ // `supportedContext` itself: this field is never non-null when
3928
+ // that one is null.
3929
+ location: supportedContext ? requestedContext?.label ?? null : null,
3930
+ measurementExecutionId: unit.executionId,
3931
+ requestedContext,
3932
+ supportedContext,
3933
+ screenshotPath: screenshotRelPath,
3934
+ rawResponse: JSON.stringify({
3935
+ model: raw.model,
3936
+ servedModel: raw.servedModel ?? null,
3937
+ groundingSources: normalized.groundingSources,
3938
+ searchQueries: normalized.searchQueries,
3939
+ apiResponse: raw.rawResponse
3940
+ }),
3941
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3942
+ }).run();
3943
+ totalSnapshotsInserted++;
3944
+ log.info("query.citation", {
3945
+ runId,
3946
+ provider: providerName,
3947
+ query: unit.queryText,
3948
+ executionId: unit.executionId,
3949
+ location: requestedContext?.label ?? null,
3950
+ citationState,
3951
+ answerMentioned
3952
+ });
3953
+ });
3954
+ } catch (err) {
3955
+ if (err instanceof RunCancelledError) {
3956
+ throw err;
3957
+ }
3958
+ const msg = err instanceof Error ? err.message : String(err);
3959
+ const stack = err instanceof Error ? err.stack : void 0;
3960
+ log.error("query.failed", { runId, provider: providerName, query: unit.queryText, executionId: unit.executionId, error: msg, stack });
3961
+ if (!providerErrors.has(providerName)) {
3962
+ providerErrors.set(providerName, msg);
3963
+ }
3964
+ }
3965
+ };
3759
3966
  providerCallStart = Date.now();
3760
- await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
3761
- await Promise.all(projectQueries.map(async (q) => {
3762
- await processQueryForProvider(registeredProvider, q);
3763
- }));
3764
- });
3765
- for (const registeredProvider of browserProviders) {
3766
- for (const q of projectQueries) {
3767
- await processQueryForProvider(registeredProvider, q);
3967
+ if (planExecution) {
3968
+ const plan = planExecution;
3969
+ const unitsFor = (provider) => plan.unitsByProvider.get(provider.adapter.name.trim().toLocaleLowerCase("en")) ?? [];
3970
+ log.info("run.plan-dispatch", {
3971
+ runId,
3972
+ expectedSlots: plan.manifest.expectedSlots.length,
3973
+ executionNodes: plan.nodeCount,
3974
+ providers: [...plan.unitsByProvider.keys()]
3975
+ });
3976
+ await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
3977
+ await Promise.all(unitsFor(registeredProvider).map(async (unit) => {
3978
+ await processNodeForProvider(registeredProvider, unit);
3979
+ }));
3980
+ });
3981
+ for (const registeredProvider of browserProviders) {
3982
+ for (const unit of unitsFor(registeredProvider)) {
3983
+ await processNodeForProvider(registeredProvider, unit);
3984
+ }
3985
+ }
3986
+ } else {
3987
+ await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
3988
+ await Promise.all(projectQueries.map(async (q) => {
3989
+ await processQueryForProvider(registeredProvider, q);
3990
+ }));
3991
+ });
3992
+ for (const registeredProvider of browserProviders) {
3993
+ for (const q of projectQueries) {
3994
+ await processQueryForProvider(registeredProvider, q);
3995
+ }
3768
3996
  }
3769
3997
  }
3770
3998
  providerCallEnd = Date.now();
3771
3999
  this.throwIfRunCancelled(runId);
3772
- const allFailed = totalSnapshotsInserted === 0 && providerErrors.size > 0;
3773
- const someFailed = providerErrors.size > 0;
4000
+ if (planExecution) {
4001
+ for (const [provider, units] of planExecution.unitsByProvider) {
4002
+ const providerName = provider;
4003
+ const dispatched = providerDispatchCounts.get(providerName) ?? 0;
4004
+ if (dispatched >= units.length || providerErrors.has(providerName)) continue;
4005
+ providerErrors.set(
4006
+ providerName,
4007
+ `${units.length - dispatched} expected measurement(s) did not run: no ${provider} provider was available to this worker.`
4008
+ );
4009
+ }
4010
+ }
4011
+ const planShortfall = planExecution ? Math.max(0, planExecution.manifest.expectedSlots.length - totalSnapshotsInserted) : 0;
4012
+ const allFailed = totalSnapshotsInserted === 0 && (providerErrors.size > 0 || planShortfall > 0);
4013
+ const someFailed = providerErrors.size > 0 || planShortfall > 0;
3774
4014
  if (allFailed) {
3775
4015
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
3776
4016
  this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq2(runs.id, runId)).run();
@@ -3822,7 +4062,7 @@ var JobRunner = class {
3822
4062
  const executionContext = {
3823
4063
  providerCount: activeProviders.length,
3824
4064
  providers: activeProviders.map((provider) => provider.adapter.name),
3825
- queryCount: projectQueries.length,
4065
+ queryCount: planExecution?.nodeCount ?? projectQueries.length,
3826
4066
  ...runLocation ? { location: runLocation.label } : {},
3827
4067
  ...runTrigger ? { trigger: runTrigger } : {},
3828
4068
  ...canonicalDomain ? { canonicalDomain } : {}
@@ -3916,11 +4156,13 @@ var JobRunner = class {
3916
4156
  finishedAt: runs.finishedAt,
3917
4157
  error: runs.error,
3918
4158
  trigger: runs.trigger,
3919
- queries: runs.queries
4159
+ queries: runs.queries,
4160
+ measurementPlanVersionId: runs.measurementPlanVersionId,
4161
+ measurementManifest: runs.measurementManifest
3920
4162
  }).from(runs).where(eq2(runs.id, runId)).get();
3921
4163
  }
3922
4164
  isRunCancelled(runId) {
3923
- return this.getRunState(runId)?.status === "cancelled";
4165
+ return this.db.select({ status: runs.status }).from(runs).where(eq2(runs.id, runId)).get()?.status === "cancelled";
3924
4166
  }
3925
4167
  throwIfRunCancelled(runId) {
3926
4168
  if (this.isRunCancelled(runId)) {
@@ -7178,7 +7420,7 @@ function readStoredGroundingSources(rawResponse) {
7178
7420
  return result;
7179
7421
  }
7180
7422
  async function backfillInsightsCommand(project, opts) {
7181
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-V6JYDYGF.js");
7423
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-DVMEVHAG.js");
7182
7424
  const config = loadConfig();
7183
7425
  const db = createClient(config.database);
7184
7426
  migrate(db);
@@ -8435,12 +8677,17 @@ var Scheduler = class {
8435
8677
  resolvedLocation = loc;
8436
8678
  }
8437
8679
  const locationLabel = resolvedLocation?.label ?? null;
8680
+ const scheduleProviders = currentSchedule.providers;
8681
+ const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
8438
8682
  const queueResult = queueRunIfProjectIdle(this.db, {
8439
8683
  createdAt: now,
8440
8684
  kind: "answer-visibility",
8441
8685
  projectId,
8442
8686
  trigger: "scheduled",
8443
- location: locationLabel
8687
+ location: locationLabel,
8688
+ providers,
8689
+ runnableProviders: this.callbacks.getRunnableProviderNames?.(),
8690
+ providerModels: this.callbacks.getEffectiveProviderModels?.()
8444
8691
  });
8445
8692
  if (queueResult.conflict) {
8446
8693
  log13.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
@@ -8456,8 +8703,6 @@ var Scheduler = class {
8456
8703
  nextRunAt,
8457
8704
  updatedAt: now
8458
8705
  }).where(eq14(schedules.id, currentSchedule.id)).run();
8459
- const scheduleProviders = currentSchedule.providers;
8460
- const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
8461
8706
  log13.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
8462
8707
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
8463
8708
  } catch (err) {
@@ -8692,6 +8937,15 @@ var Notifier = class {
8692
8937
  computeTransitions(runId, projectId) {
8693
8938
  const thisRun = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
8694
8939
  if (!thisRun) return [];
8940
+ const completeness = measurementRunCompleteness(this.db, runId);
8941
+ if (completeness.planned && !completeness.complete) {
8942
+ log15.info("transitions.skipped-incomplete", {
8943
+ runId,
8944
+ executed: completeness.executed,
8945
+ expected: completeness.expected
8946
+ });
8947
+ return [];
8948
+ }
8695
8949
  const groupSiblings = this.db.select().from(runs).where(and12(
8696
8950
  eq15(runs.projectId, projectId),
8697
8951
  eq15(runs.kind, thisRun.kind),
@@ -8843,6 +9097,21 @@ var RunCoordinator = class {
8843
9097
  log16.info("probe.skip-side-effects", { runId, projectId, kind });
8844
9098
  return;
8845
9099
  }
9100
+ const completeness = measurementRunCompleteness(this.db, runId);
9101
+ if (completeness.planned && !completeness.complete) {
9102
+ log16.info("measurement.incomplete-skip-derived", {
9103
+ runId,
9104
+ projectId,
9105
+ executed: completeness.executed,
9106
+ expected: completeness.expected
9107
+ });
9108
+ try {
9109
+ await this.notifier.onRunCompleted(runId, projectId);
9110
+ } catch (err) {
9111
+ log16.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
9112
+ }
9113
+ return;
9114
+ }
8846
9115
  let insightCount = 0;
8847
9116
  let criticalOrHigh = 0;
8848
9117
  if (kind === RunKinds["answer-visibility"]) {
@@ -11298,70 +11567,6 @@ function formatAuditFactorScore(factor) {
11298
11567
  var log19 = createLogger("Snapshot");
11299
11568
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
11300
11569
  var SNAPSHOT_QUERY_COUNT = 6;
11301
- var ProviderExecutionGate2 = class {
11302
- constructor(maxConcurrency, maxPerMinute) {
11303
- this.maxConcurrency = maxConcurrency;
11304
- this.maxPerMinute = maxPerMinute;
11305
- }
11306
- maxConcurrency;
11307
- maxPerMinute;
11308
- window = [];
11309
- waiters = [];
11310
- rateLimitChain = Promise.resolve();
11311
- inFlight = 0;
11312
- async run(task) {
11313
- await this.acquire();
11314
- try {
11315
- await this.waitForRateLimit();
11316
- return await task();
11317
- } finally {
11318
- this.release();
11319
- }
11320
- }
11321
- async acquire() {
11322
- if (this.inFlight < Math.max(1, this.maxConcurrency)) {
11323
- this.inFlight++;
11324
- return;
11325
- }
11326
- await new Promise((resolve) => {
11327
- this.waiters.push(resolve);
11328
- });
11329
- this.inFlight++;
11330
- }
11331
- release() {
11332
- this.inFlight = Math.max(0, this.inFlight - 1);
11333
- const next = this.waiters.shift();
11334
- next?.();
11335
- }
11336
- async waitForRateLimit() {
11337
- let releaseChain;
11338
- const previousChain = this.rateLimitChain;
11339
- this.rateLimitChain = new Promise((resolve) => {
11340
- releaseChain = resolve;
11341
- });
11342
- await previousChain;
11343
- try {
11344
- const now = Date.now();
11345
- const windowStart = now - 6e4;
11346
- while (this.window.length > 0 && this.window[0] < windowStart) {
11347
- this.window.shift();
11348
- }
11349
- if (this.window.length >= this.maxPerMinute) {
11350
- const oldestInWindow = this.window[0];
11351
- const waitMs = oldestInWindow + 6e4 - now + 50;
11352
- await new Promise((resolve) => setTimeout(resolve, waitMs));
11353
- const nowAfterWait = Date.now();
11354
- const newWindowStart = nowAfterWait - 6e4;
11355
- while (this.window.length > 0 && this.window[0] < newWindowStart) {
11356
- this.window.shift();
11357
- }
11358
- }
11359
- this.window.push(Date.now());
11360
- } finally {
11361
- releaseChain?.();
11362
- }
11363
- }
11364
- };
11365
11570
  var SnapshotService = class {
11366
11571
  constructor(registry) {
11367
11572
  this.registry = registry;
@@ -11495,7 +11700,8 @@ var SnapshotService = class {
11495
11700
  for (const provider of ctx.providers) {
11496
11701
  gates.set(
11497
11702
  provider.adapter.name,
11498
- new ProviderExecutionGate2(
11703
+ getSharedProviderExecutionGate(
11704
+ provider.adapter.name,
11499
11705
  provider.config.quotaPolicy.maxConcurrency,
11500
11706
  provider.config.quotaPolicy.maxRequestsPerMinute
11501
11707
  )
@@ -11909,7 +12115,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
11909
12115
  const domains = effectiveDomains(project);
11910
12116
  const brands = effectiveBrandNames(project);
11911
12117
  const config = { ...provider.config, model: run.resolvedModel };
11912
- const gate = new ProviderExecutionGate(provider.config.quotaPolicy.maxConcurrency, provider.config.quotaPolicy.maxRequestsPerMinute);
12118
+ const gate = getSharedProviderExecutionGate(run.provider, provider.config.quotaPolicy.maxConcurrency, provider.config.quotaPolicy.maxRequestsPerMinute);
11913
12119
  await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
11914
12120
  try {
11915
12121
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -11998,6 +12204,14 @@ var DEFAULT_QUOTA = {
11998
12204
  };
11999
12205
  var SESSION_COOKIE_NAME = "canonry_session";
12000
12206
  var SESSION_TTL_MS = 12 * 60 * 60 * 1e3;
12207
+ function effectiveProviderModels(registry) {
12208
+ const models = {};
12209
+ for (const provider of registry.getAll()) {
12210
+ const model = provider.config.model ?? provider.adapter.modelRegistry.defaultModel;
12211
+ if (model) models[provider.adapter.name] = model;
12212
+ }
12213
+ return models;
12214
+ }
12001
12215
  function parseBooleanEnv(value) {
12002
12216
  const normalized = value?.trim().toLowerCase();
12003
12217
  if (!normalized) return void 0;
@@ -12204,8 +12418,10 @@ async function createServer(opts) {
12204
12418
  }
12205
12419
  }
12206
12420
  } : true;
12421
+ const trustProxy = resolveTrustProxy(process.env.CANONRY_TRUST_PROXY);
12207
12422
  const app = Fastify({
12208
- logger
12423
+ logger,
12424
+ trustProxy
12209
12425
  });
12210
12426
  const registry = new ProviderRegistry();
12211
12427
  const providers = opts.config.providers ?? {};
@@ -12606,6 +12822,10 @@ async function createServer(opts) {
12606
12822
  app.log.error({ runId, err }, "Scheduled job runner failed");
12607
12823
  });
12608
12824
  },
12825
+ // Same source of truth the HTTP routes use, so a scheduled sweep resolves
12826
+ // "all configured providers" exactly as a hand-triggered one does.
12827
+ getRunnableProviderNames: () => registry.getAll().map((provider) => provider.adapter.name),
12828
+ getEffectiveProviderModels: () => effectiveProviderModels(registry),
12609
12829
  onTrafficSyncRequested: (projectName, sourceId) => {
12610
12830
  aeroClient.trafficSync(projectName, sourceId).catch((err) => {
12611
12831
  app.log.error(
@@ -12967,7 +13187,12 @@ async function createServer(opts) {
12967
13187
  const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(parts[1]))).get();
12968
13188
  return Boolean(key && !key.revokedAt);
12969
13189
  };
13190
+ const namedAccountsInUse = () => anyUsersExist(opts.db);
13191
+ const NAMED_ACCOUNTS_MESSAGE = "This install uses named accounts. Sign in with your name and password.";
12970
13192
  app.get(apiPrefix + "/session", async (request, reply) => {
13193
+ if (namedAccountsInUse()) {
13194
+ return reply.send({ authenticated: false, setupRequired: false });
13195
+ }
12971
13196
  if (!dashboardRequirePassword) {
12972
13197
  return reply.send({ authenticated: true, setupRequired: false });
12973
13198
  }
@@ -12978,6 +13203,10 @@ async function createServer(opts) {
12978
13203
  });
12979
13204
  });
12980
13205
  app.post(apiPrefix + "/session/setup", async (request, reply) => {
13206
+ if (namedAccountsInUse()) {
13207
+ const err = forbidden(NAMED_ACCOUNTS_MESSAGE);
13208
+ return reply.status(err.statusCode).send(err.toJSON());
13209
+ }
12981
13210
  if (!dashboardRequirePassword) {
12982
13211
  return reply.send({ authenticated: true, setupRequired: false });
12983
13212
  }
@@ -13005,6 +13234,10 @@ async function createServer(opts) {
13005
13234
  return reply.send({ authenticated: true });
13006
13235
  });
13007
13236
  app.post(apiPrefix + "/session", async (request, reply) => {
13237
+ if (namedAccountsInUse()) {
13238
+ const err2 = forbidden(NAMED_ACCOUNTS_MESSAGE);
13239
+ return reply.status(err2.statusCode).send(err2.toJSON());
13240
+ }
13008
13241
  if (!dashboardRequirePassword) {
13009
13242
  return reply.send({ authenticated: true, setupRequired: false });
13010
13243
  }
@@ -13117,6 +13350,21 @@ async function createServer(opts) {
13117
13350
  skipAuth: false,
13118
13351
  sessionCookieName: SESSION_COOKIE_NAME,
13119
13352
  resolveSessionApiKeyId,
13353
+ // Named-account sessions ride the same cookie path as the older dashboard
13354
+ // cookie, so a sub-path mount behaves the same for both. `secure` is only
13355
+ // forced ON: the config flag knows an https public URL when there is one,
13356
+ // but it cannot see a reverse proxy terminating TLS in front of a
13357
+ // plain-http bind. Passing `false` there would mark the cookie insecure on
13358
+ // exactly the deployment that needs it most, so an unset flag defers to
13359
+ // how the request actually arrived.
13360
+ userSessionCookie: {
13361
+ path: sessionCookiePath,
13362
+ ...sessionCookieSecure ? { secure: true } : {}
13363
+ },
13364
+ // Whether an operator has declared which hops to believe. The per-caller
13365
+ // sign-in budget depends on this being the truth rather than something
13366
+ // inferred from a header the caller controls.
13367
+ trustProxyConfigured: trustProxy !== false,
13120
13368
  ...embed.enabled && embed.projectTabs ? { embedProjectTabs: embed.projectTabs } : {},
13121
13369
  explainContentRecommendation,
13122
13370
  briefContentRecommendation,
@@ -13405,6 +13653,7 @@ async function createServer(opts) {
13405
13653
  });
13406
13654
  },
13407
13655
  getRunnableProviderNames: () => registry.getAll().map((provider) => provider.adapter.name),
13656
+ getEffectiveProviderModels: () => effectiveProviderModels(registry),
13408
13657
  onProviderUpdate: (providerName, apiKey, model, baseUrl, incomingQuota) => {
13409
13658
  const name = providerName;
13410
13659
  if (!adapterMap[name]) return null;