@canonry/canonry 5.12.0 → 5.13.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/agent-workspace/skills/canonry/references/canonry-cli.md +5 -0
  2. package/assets/assets/{AuditHistoryPanel-DL_NSpfm.js → AuditHistoryPanel-DWDOPf8c.js} +1 -1
  3. package/assets/assets/{BacklinksPage-BtcBybtX.js → BacklinksPage-DGxq-iU6.js} +1 -1
  4. package/assets/assets/{HistoryPage-jXj-qi9c.js → HistoryPage-DR91m-gT.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-DKw401ok.js → MeasurementPropertyPage-a8poltfM.js} +1 -1
  6. package/assets/assets/{ProjectPage-og4qfFFD.js → ProjectPage-CCCJE7rA.js} +1 -1
  7. package/assets/assets/{RunRow-nK5iaZe1.js → RunRow-Xe63DyPK.js} +1 -1
  8. package/assets/assets/{RunsPage-CMgr3YSF.js → RunsPage-DK119ZZw.js} +1 -1
  9. package/assets/assets/{SettingsPage-QtElCcew.js → SettingsPage-G461foD-.js} +1 -1
  10. package/assets/assets/{SiteHealthSection-DNdVVl30.js → SiteHealthSection-CJnHkGM2.js} +3 -3
  11. package/assets/assets/{TrafficPage-C0WgvfRj.js → TrafficPage-AsWgc7Ot.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-jOTedQtt.js → TrafficSourceDetailPage-D10yBQoD.js} +1 -1
  13. package/assets/assets/{extract-error-message-DO_053o0.js → extract-error-message-PP4HD_Dp.js} +1 -1
  14. package/assets/assets/{index-CnVwWE3V.js → index-CCIeAE4J.js} +22 -22
  15. package/assets/assets/{react-sigma_core.esm.min-DJttq2UK.js → react-sigma_core.esm.min-CkyyYkKU.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-BC_oa__K.js → v2-overview-adapter-DW-In1hp.js} +1 -1
  17. package/assets/index.html +1 -1
  18. package/dist/{chunk-S4BIBUCK.js → chunk-6YLNO2T5.js} +2 -2
  19. package/dist/{chunk-LWGYRN3T.js → chunk-IDZQ5H3J.js} +2 -2
  20. package/dist/{chunk-WJGUBZSW.js → chunk-IPGBK7XR.js} +65 -1
  21. package/dist/{chunk-DOC4QQLS.js → chunk-Q35D2R2P.js} +2490 -2094
  22. package/dist/{chunk-KBSAEALE.js → chunk-TDHRUXN3.js} +398 -151
  23. package/dist/{chunk-NYRRWWEE.js → chunk-UWJAATGC.js} +3015 -2923
  24. package/dist/cli.js +133 -7
  25. package/dist/{demo-server-OTXS6ZHS.js → demo-server-IZFTJI7E.js} +3 -3
  26. package/dist/index.js +5 -5
  27. package/dist/{intelligence-service-K4U55A2J.js → intelligence-service-PM7MC5VC.js} +2 -2
  28. package/dist/mcp.js +3 -3
  29. package/package.json +9 -9
@@ -17,12 +17,12 @@ import {
17
17
  resolveOperatorApiKeyIds,
18
18
  runWithUsageTags,
19
19
  saveConfigPatch
20
- } from "./chunk-WJGUBZSW.js";
20
+ } from "./chunk-IPGBK7XR.js";
21
21
  import {
22
22
  deleteSiteCrawlGraphLayout,
23
23
  persistSiteCrawlGraphLayout,
24
24
  prepareSiteCrawlGraphLayout
25
- } from "./chunk-LWGYRN3T.js";
25
+ } from "./chunk-IDZQ5H3J.js";
26
26
  import {
27
27
  PACKAGE_VERSION
28
28
  } from "./chunk-AOJU5LKX.js";
@@ -134,7 +134,10 @@ import {
134
134
  loadDuckdb,
135
135
  markSessionFailed,
136
136
  measurementRunCompleteness,
137
+ measurementRunSlotState,
138
+ measurementSlotKey,
137
139
  migrate,
140
+ newerFullSweep,
138
141
  nextRunFromCron,
139
142
  nextRunFromSchedule,
140
143
  notifications,
@@ -164,6 +167,7 @@ import {
164
167
  resolveVercelSyncDeadlineMs,
165
168
  resolveWebhookTarget,
166
169
  runChecks,
170
+ runFills,
167
171
  runs,
168
172
  schedules,
169
173
  serializeUserSessionCookie,
@@ -179,7 +183,7 @@ import {
179
183
  siteLivenessState,
180
184
  toAlertView,
181
185
  usageCounters
182
- } from "./chunk-DOC4QQLS.js";
186
+ } from "./chunk-Q35D2R2P.js";
183
187
  import {
184
188
  AGENT_MEMORY_VALUE_MAX_BYTES,
185
189
  AGENT_NONE,
@@ -211,6 +215,7 @@ import {
211
215
  ONBOARDING_FLOW_VERSION,
212
216
  ProviderNames,
213
217
  ROUTE_CAPABLE_CITED_URL_PROVIDERS,
218
+ RUN_FILL_PROVIDER_BREAKER,
214
219
  ResearchQueryStatuses,
215
220
  ResearchRunStatuses,
216
221
  RunKinds,
@@ -272,6 +277,7 @@ import {
272
277
  filterCapturedCitedUrls,
273
278
  forbidden,
274
279
  formatIsoDateInTimeZone,
280
+ formatRunErrorOneLine,
275
281
  frameAncestorsHeaderValue,
276
282
  googleAdsAccessibleCustomerDtoSchema,
277
283
  googleAdsAccessibleCustomersResponseSchema,
@@ -313,6 +319,7 @@ import {
313
319
  observeTemplateLinkEdges,
314
320
  parseMeasurementRunManifestV1,
315
321
  parseOriginList,
322
+ parseRunError,
316
323
  providerError,
317
324
  providerSupportsLocationContext,
318
325
  registrableDomain,
@@ -337,7 +344,7 @@ import {
337
344
  visibilityReportRequestSchema,
338
345
  winnabilityClassLabel,
339
346
  withRetry
340
- } from "./chunk-NYRRWWEE.js";
347
+ } from "./chunk-UWJAATGC.js";
341
348
 
342
349
  // src/runtime-context.ts
343
350
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -6483,7 +6490,7 @@ function nonBlank(value) {
6483
6490
  return trimmed ? trimmed : void 0;
6484
6491
  }
6485
6492
  function resolveBuildCommit(env = process.env) {
6486
- const embedded = true ? "3cbbec137ff9c498f0b59f30a5e2b84c99af6f40" : void 0;
6493
+ const embedded = true ? "894086f6a60be75c0ade892884adf57ef7a38291" : void 0;
6487
6494
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
6488
6495
  }
6489
6496
  function resolveInstanceIdentity(env = process.env) {
@@ -6831,6 +6838,8 @@ var JobRunner = class {
6831
6838
  this.registry = registry;
6832
6839
  }
6833
6840
  recoverStaleRuns() {
6841
+ const staleFills = this.db.update(runFills).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: "Server restarted while the fill was in progress" }).where(inArray(runFills.status, ["queued", "running"])).run();
6842
+ if (staleFills.changes > 0) log2.warn("fill.recovered-stale", { count: staleFills.changes });
6834
6843
  const stale = this.db.select({
6835
6844
  id: runs.id,
6836
6845
  projectId: runs.projectId,
@@ -7156,146 +7165,19 @@ var JobRunner = class {
7156
7165
  }
7157
7166
  }
7158
7167
  };
7159
- const processNodeForProvider = async (registeredProvider, unit) => {
7160
- const { adapter, config: providerConfig } = registeredProvider;
7161
- const providerName = adapter.name;
7162
- const gate = executionGates.get(providerName);
7163
- if (!gate) {
7164
- throw new Error(`Missing execution gate for provider ${providerName}`);
7165
- }
7166
- const config = unit.requestedModel ? { ...providerConfig, model: unit.requestedModel } : providerConfig;
7167
- const requestedContext = unit.context;
7168
- const supportedContext = requestedContext && providerSupportsLocationContext(adapter) ? { status: "applied", resolved: requestedContext } : null;
7169
- try {
7170
- await gate.run(async () => {
7171
- this.throwIfRunCancelled(runId);
7172
- providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
7173
- const raw = await adapter.executeTrackedQuery(
7174
- {
7175
- query: unit.queryText,
7176
- canonicalDomains: allDomains,
7177
- competitorDomains,
7178
- location: requestedContext ?? void 0
7179
- },
7180
- config
7181
- );
7182
- this.throwIfRunCancelled(runId);
7183
- const providerResult = adapter.normalizeResult(raw);
7184
- const rawGroundingSources = providerResult.groundingSources;
7185
- const normalized = {
7186
- ...providerResult,
7187
- groundingSources: Array.isArray(rawGroundingSources) ? rawGroundingSources : []
7188
- };
7189
- let citedUrlCapture;
7190
- try {
7191
- citedUrlCapture = await captureCitedUrls(providerName, rawGroundingSources);
7192
- } catch (err) {
7193
- citedUrlCapture = {
7194
- citedUrls: [],
7195
- captureStatus: "failed",
7196
- sourceCount: normalized.groundingSources.length,
7197
- resolvedCount: 0,
7198
- captureVersion: CITED_URL_CAPTURE_VERSION
7199
- };
7200
- log2.warn("query.cited-url-capture-failed", {
7201
- runId,
7202
- provider: providerName,
7203
- query: unit.queryText,
7204
- error: describeError(err)
7205
- });
7206
- }
7207
- this.throwIfRunCancelled(runId);
7208
- const citationState = determineCitationState(normalized, allDomains);
7209
- const answerMentioned = determineAnswerMentioned(
7210
- normalized.answerText,
7211
- allBrandNames,
7212
- allDomains
7213
- );
7214
- const overlap = computeCompetitorOverlap(normalized, competitorDomains);
7215
- const extractedCompetitors = extractRecommendedCompetitors(
7216
- normalized.answerText,
7217
- allDomains,
7218
- normalized.citedDomains,
7219
- competitorDomains,
7220
- allBrandNames
7221
- );
7222
- const snapshotId = crypto6.randomUUID();
7223
- let screenshotRelPath = null;
7224
- if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
7225
- const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
7226
- if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
7227
- const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
7228
- fs3.renameSync(raw.screenshotPath, destPath);
7229
- screenshotRelPath = `${runId}/${snapshotId}.png`;
7230
- }
7231
- this.db.insert(querySnapshots).values({
7232
- id: snapshotId,
7233
- runId,
7234
- queryId: unit.queryId,
7235
- queryText: unit.queryText,
7236
- provider: providerName,
7237
- // `model` is what was REQUESTED and `served_model` is what
7238
- // answered. The manifest froze the request, so it is the
7239
- // authority here: an adapter that reports its own default rather
7240
- // than what it was handed would otherwise overwrite the identity
7241
- // the revision recorded, and nothing would show that it moved.
7242
- model: unit.requestedModel ?? raw.model,
7243
- servedModel: raw.servedModel ?? null,
7244
- citationState,
7245
- answerMentioned,
7246
- answerText: normalized.answerText,
7247
- citedDomains: normalized.citedDomains,
7248
- citedUrls: citedUrlCapture.citedUrls,
7249
- captureStatus: citedUrlCapture.captureStatus,
7250
- sourceCount: citedUrlCapture.sourceCount,
7251
- resolvedCount: citedUrlCapture.resolvedCount,
7252
- captureVersion: citedUrlCapture.captureVersion,
7253
- retrievalStatus: normalized.retrievalStatus,
7254
- retrievalContract: raw.retrievalContract,
7255
- competitorOverlap: overlap,
7256
- recommendedCompetitors: extractedCompetitors,
7257
- // Only claim the geography the provider actually honoured. A
7258
- // requested-but-unsupported context stores `location: null` —
7259
- // "no claim" — rather than the label we asked for, mirroring
7260
- // `supportedContext` itself: this field is never non-null when
7261
- // that one is null.
7262
- location: supportedContext ? requestedContext?.label ?? null : null,
7263
- measurementExecutionId: unit.executionId,
7264
- requestedContext,
7265
- supportedContext,
7266
- screenshotPath: screenshotRelPath,
7267
- rawResponse: JSON.stringify({
7268
- model: raw.model,
7269
- servedModel: raw.servedModel ?? null,
7270
- groundingSources: normalized.groundingSources,
7271
- searchQueries: normalized.searchQueries,
7272
- apiResponse: raw.rawResponse
7273
- }),
7274
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
7275
- }).run();
7276
- totalSnapshotsInserted++;
7277
- log2.info("query.citation", {
7278
- runId,
7279
- provider: providerName,
7280
- query: unit.queryText,
7281
- executionId: unit.executionId,
7282
- location: requestedContext?.label ?? null,
7283
- citationState,
7284
- answerMentioned
7285
- });
7286
- });
7287
- } catch (err) {
7288
- if (err instanceof RunCancelledError) {
7289
- throw err;
7290
- }
7291
- const msg = describeError(err);
7292
- const stack = err instanceof Error ? err.stack : void 0;
7293
- log2.error("query.failed", { runId, provider: providerName, query: unit.queryText, executionId: unit.executionId, error: msg, stack });
7294
- if (!providerErrors.has(providerName)) {
7295
- providerErrors.set(providerName, msg);
7296
- }
7168
+ const slotContext = {
7169
+ runId,
7170
+ allDomains,
7171
+ competitorDomains,
7172
+ allBrandNames,
7173
+ executionGates,
7174
+ providerDispatchCounts,
7175
+ providerErrors,
7176
+ onInserted: () => {
7177
+ totalSnapshotsInserted++;
7297
7178
  }
7298
7179
  };
7180
+ const processNodeForProvider = (registeredProvider, unit) => this.executePlanSlot(slotContext, registeredProvider, unit);
7299
7181
  if (existingRun.kind === RunKinds["answer-visibility"] && existingRun.trigger !== RunTriggers.probe && existingRun.measurementPlanVersionId === null) {
7300
7182
  const definition = buildSimpleMeasurementDefinition({
7301
7183
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7442,7 +7324,7 @@ var JobRunner = class {
7442
7324
  status: "failed",
7443
7325
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
7444
7326
  error: errorMessage
7445
- }).where(eq2(runs.id, runId)).run();
7327
+ }).where(and2(eq2(runs.id, runId), inArray(runs.status, ["queued", "running"]))).run();
7446
7328
  this.flushProviderUsage(providerDispatchCounts, providerReservations);
7447
7329
  const abortReason2 = classifyRunAbortReason(errorMessage);
7448
7330
  const phases = buildPhases({ startTime, providerCallStart, providerCallEnd });
@@ -7483,6 +7365,363 @@ var JobRunner = class {
7483
7365
  }
7484
7366
  }
7485
7367
  }
7368
+ /**
7369
+ * Complete a partial plan run in place: record only the expected slots it
7370
+ * never answered, under the same run id, then finalize the run once.
7371
+ *
7372
+ * The parent run is never written from an error path and only ever leaves
7373
+ * `partial` through the completion compare-and-set, so a failed or
7374
+ * interrupted fill leaves the run as the sweep left it, plus any answers the
7375
+ * fill did record. Its timestamps, manifest and identity are never touched:
7376
+ * it stays the same sweep, with the same place in every history.
7377
+ */
7378
+ async executeRunFill(fillId) {
7379
+ const claim = this.db.update(runFills).set({ status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and2(eq2(runFills.id, fillId), eq2(runFills.status, "queued"))).run();
7380
+ if (claim.changes !== 1) {
7381
+ log2.warn("fill.not-claimed", { fillId });
7382
+ return;
7383
+ }
7384
+ try {
7385
+ await this.runClaimedFill(fillId);
7386
+ } catch (err) {
7387
+ const error = describeError(err);
7388
+ log2.error("fill.aborted", { fillId, error });
7389
+ this.db.update(runFills).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error }).where(and2(eq2(runFills.id, fillId), eq2(runFills.status, "running"))).run();
7390
+ }
7391
+ }
7392
+ async runClaimedFill(fillId) {
7393
+ const fill = this.db.select().from(runFills).where(eq2(runFills.id, fillId)).get();
7394
+ if (!fill) return;
7395
+ const { runId, projectId } = fill;
7396
+ const requested = new Set(parseJsonColumn(fill.providers, []).map((provider) => provider.trim().toLocaleLowerCase("en")));
7397
+ const providerDispatchCounts = /* @__PURE__ */ new Map();
7398
+ const providerReservations = /* @__PURE__ */ new Map();
7399
+ const providerErrors = /* @__PURE__ */ new Map();
7400
+ let filled = 0;
7401
+ let fatal = null;
7402
+ let superseded = false;
7403
+ try {
7404
+ const run = this.getRunState(runId);
7405
+ if (!run) throw new Error(`Run ${runId} not found`);
7406
+ if (run.status !== "partial") throw new Error(`Run ${runId} is ${run.status}; only a partial run can be filled`);
7407
+ const runCreatedAt = this.db.select({ createdAt: runs.createdAt }).from(runs).where(eq2(runs.id, runId)).get().createdAt;
7408
+ const project = this.db.select().from(projects).where(eq2(projects.id, projectId)).get();
7409
+ if (!project) throw new Error(`Project ${projectId} not found`);
7410
+ const projectQueries = this.db.select().from(queries).where(eq2(queries.projectId, projectId)).all();
7411
+ const plan = resolvePlanExecution(run, projectQueries);
7412
+ if (!plan) throw new Error(`Run ${runId} did not measure a published plan`);
7413
+ const recorded = new Set(
7414
+ this.db.select({ executionId: querySnapshots.measurementExecutionId, provider: querySnapshots.provider }).from(querySnapshots).where(eq2(querySnapshots.runId, runId)).all().flatMap((row) => row.executionId ? [measurementSlotKey(row.executionId, row.provider)] : [])
7415
+ );
7416
+ const unitsByProvider = /* @__PURE__ */ new Map();
7417
+ for (const [provider, units] of plan.unitsByProvider) {
7418
+ if (requested.size > 0 && !requested.has(provider)) continue;
7419
+ const missing = units.filter((unit) => !recorded.has(measurementSlotKey(unit.executionId, provider)));
7420
+ if (missing.length > 0) unitsByProvider.set(provider, missing);
7421
+ }
7422
+ const activeProviders = [];
7423
+ for (const provider of unitsByProvider.keys()) {
7424
+ const registered = this.registry.get(provider);
7425
+ if (registered) activeProviders.push(registered);
7426
+ else providerErrors.set(provider, `No ${provider} provider is configured on this instance, so its missing answers did not run.`);
7427
+ }
7428
+ const projectCompetitors = this.db.select().from(competitors).where(eq2(competitors.projectId, projectId)).all();
7429
+ const competitorDomains = projectCompetitors.map((c) => c.domain);
7430
+ const allDomains = effectiveDomains({ canonicalDomain: project.canonicalDomain, ownedDomains: project.ownedDomains });
7431
+ const allBrandNames = effectiveBrandNames({ displayName: project.displayName, aliases: project.aliases });
7432
+ const todayPeriod = getCurrentUsageDay();
7433
+ const dispatchable = [];
7434
+ for (const registered of activeProviders) {
7435
+ const name = registered.adapter.name;
7436
+ const count = unitsByProvider.get(name)?.length ?? 0;
7437
+ const scope = `${projectId}:${name}`;
7438
+ const limit = registered.config.quotaPolicy.maxRequestsPerDay;
7439
+ const quota = reserveDailyQueryQuota(this.db, { scope, period: todayPeriod, count, limit });
7440
+ if (!quota.reserved) {
7441
+ providerErrors.set(name, `Daily quota exceeded for ${name}: ${quota.used} queries used today, limit is ${limit}. This fill needs ${count} more.`);
7442
+ continue;
7443
+ }
7444
+ providerReservations.set(name, { scope, period: todayPeriod, reserved: count });
7445
+ dispatchable.push(registered);
7446
+ }
7447
+ const executionGates = /* @__PURE__ */ new Map();
7448
+ for (const registered of dispatchable) {
7449
+ executionGates.set(
7450
+ registered.adapter.name,
7451
+ getSharedProviderExecutionGate(
7452
+ registered.adapter.name,
7453
+ registered.config.quotaPolicy.maxConcurrency,
7454
+ registered.config.quotaPolicy.maxRequestsPerMinute
7455
+ )
7456
+ );
7457
+ }
7458
+ const consecutiveFailures = /* @__PURE__ */ new Map();
7459
+ const stopped = /* @__PURE__ */ new Set();
7460
+ const newerSweepExists = () => {
7461
+ if (!superseded && newerFullSweep(this.db, { id: runId, projectId, createdAt: runCreatedAt })) {
7462
+ superseded = true;
7463
+ log2.warn("fill.superseded", { fillId, runId });
7464
+ }
7465
+ return superseded;
7466
+ };
7467
+ let removed = false;
7468
+ const attemptRemoved = () => {
7469
+ if (!removed && this.db.select({ status: runFills.status }).from(runFills).where(eq2(runFills.id, fillId)).get()?.status !== "running") {
7470
+ removed = true;
7471
+ log2.warn("fill.attempt-removed", { fillId, runId });
7472
+ }
7473
+ return removed;
7474
+ };
7475
+ const slotRecorded = (provider, executionId) => this.db.select({ id: querySnapshots.id }).from(querySnapshots).where(and2(
7476
+ eq2(querySnapshots.runId, runId),
7477
+ eq2(querySnapshots.measurementExecutionId, executionId),
7478
+ eq2(querySnapshots.provider, provider)
7479
+ )).get() !== void 0;
7480
+ const ctx = {
7481
+ runId,
7482
+ allDomains,
7483
+ competitorDomains,
7484
+ allBrandNames,
7485
+ executionGates,
7486
+ providerDispatchCounts,
7487
+ providerErrors,
7488
+ onInserted: () => {
7489
+ filled++;
7490
+ },
7491
+ fill: {
7492
+ shouldSkip: (provider, executionId) => stopped.has(provider) || attemptRemoved() || newerSweepExists() || slotRecorded(provider, executionId),
7493
+ onOutcome: (provider, ok) => {
7494
+ if (ok) {
7495
+ consecutiveFailures.set(provider, 0);
7496
+ return;
7497
+ }
7498
+ const failures = (consecutiveFailures.get(provider) ?? 0) + 1;
7499
+ consecutiveFailures.set(provider, failures);
7500
+ if (failures >= RUN_FILL_PROVIDER_BREAKER && !stopped.has(provider)) {
7501
+ stopped.add(provider);
7502
+ log2.warn("fill.provider-stopped", { fillId, runId, provider, failures });
7503
+ }
7504
+ }
7505
+ }
7506
+ };
7507
+ log2.info("fill.dispatch", {
7508
+ fillId,
7509
+ runId,
7510
+ missing: Object.fromEntries([...unitsByProvider].map(([provider, units]) => [provider, units.length]))
7511
+ });
7512
+ const unitsFor = (registered) => unitsByProvider.get(registered.adapter.name) ?? [];
7513
+ const apiProviders = dispatchable.filter((p) => !isBrowserProvider(p.adapter.name));
7514
+ const browserProviders = dispatchable.filter((p) => isBrowserProvider(p.adapter.name));
7515
+ await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registered) => {
7516
+ await runWithConcurrency(
7517
+ unitsFor(registered),
7518
+ Math.max(1, registered.config.quotaPolicy.maxConcurrency),
7519
+ (unit) => this.executePlanSlot(ctx, registered, unit)
7520
+ );
7521
+ });
7522
+ for (const registered of browserProviders) {
7523
+ for (const unit of unitsFor(registered)) await this.executePlanSlot(ctx, registered, unit);
7524
+ }
7525
+ } catch (err) {
7526
+ fatal = describeError(err);
7527
+ log2.error("fill.failed", { fillId, runId, error: fatal });
7528
+ } finally {
7529
+ this.flushProviderUsage(providerDispatchCounts, providerReservations);
7530
+ }
7531
+ const outcome = this.finalizeRunFill({ fillId, runId, filled, fatal, providerErrors, superseded });
7532
+ log2.info("fill.finished", { fillId, runId, filled, ...outcome });
7533
+ if (outcome.justCompleted && !outcome.superseded && this.onRunCompleted) {
7534
+ this.onRunCompleted(runId, projectId, { origin: "fill" }).catch((err) => {
7535
+ log2.error("notification.callback-failed", { runId, error: describeError(err) });
7536
+ });
7537
+ }
7538
+ }
7539
+ /** Record the fill's outcome and, if the run is now whole, complete it exactly once. */
7540
+ finalizeRunFill(input) {
7541
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
7542
+ return this.db.transaction((tx) => {
7543
+ const txDb = tx;
7544
+ const parent = txDb.select({ projectId: runs.projectId, createdAt: runs.createdAt, error: runs.error }).from(runs).where(eq2(runs.id, input.runId)).get();
7545
+ const superseded = input.superseded || parent !== void 0 && newerFullSweep(txDb, { id: input.runId, projectId: parent.projectId, createdAt: parent.createdAt }) !== void 0;
7546
+ const state = measurementRunSlotState(txDb, input.runId);
7547
+ const complete3 = state.planned && state.readable && state.missing.length === 0 && !state.hasUnboundSnapshot;
7548
+ let justCompleted = false;
7549
+ if (complete3) {
7550
+ justCompleted = txDb.update(runs).set({ status: "completed", error: null }).where(and2(eq2(runs.id, input.runId), eq2(runs.status, "partial"))).run().changes === 1;
7551
+ } else if (state.readable && !input.fatal) {
7552
+ const previous = parseRunError(parent?.error)?.providers ?? {};
7553
+ const fresh = buildRunErrorFromMessages(input.providerErrors).providers ?? {};
7554
+ const remaining = /* @__PURE__ */ new Map();
7555
+ for (const slot of state.missing) remaining.set(slot.provider, (remaining.get(slot.provider) ?? 0) + 1);
7556
+ const providers = {};
7557
+ for (const [provider, count] of remaining) {
7558
+ providers[provider] = fresh[provider] ?? previous[provider] ?? { message: `${count} expected measurement(s) have not run.` };
7559
+ }
7560
+ txDb.update(runs).set({ error: serializeRunError({ providers }) }).where(and2(eq2(runs.id, input.runId), eq2(runs.status, "partial"))).run();
7561
+ }
7562
+ const status = complete3 ? "completed" : input.filled > 0 ? "partial" : "failed";
7563
+ const reason = input.fatal ?? (superseded ? "Stopped because a newer sweep started; no answers were added behind it." : null) ?? (input.providerErrors.size > 0 ? formatRunErrorOneLine(buildRunErrorFromMessages(input.providerErrors)) : null);
7564
+ txDb.update(runFills).set({ status, filled: input.filled, error: complete3 ? null : reason, finishedAt }).where(eq2(runFills.id, input.fillId)).run();
7565
+ return { justCompleted, superseded };
7566
+ });
7567
+ }
7568
+ /**
7569
+ * The plan-aware unit of work: one execution node, one provider.
7570
+ *
7571
+ * Deliberately a separate worker from the legacy query-by-query path rather
7572
+ * than a generalization of it. The legacy path is the behaviour every
7573
+ * planless project already depends on; leaving its body alone is what
7574
+ * makes "planless is byte-identical" a fact rather than a hope.
7575
+ *
7576
+ * A method rather than a closure inside `executeRun` so a sweep and a fill
7577
+ * record a slot through the same code: a filled answer is indistinguishable
7578
+ * from one the sweep recorded itself, which is what lets it count.
7579
+ */
7580
+ async executePlanSlot(ctx, registeredProvider, unit) {
7581
+ const { adapter, config: providerConfig } = registeredProvider;
7582
+ const providerName = adapter.name;
7583
+ const gate = ctx.executionGates.get(providerName);
7584
+ if (!gate) {
7585
+ throw new Error(`Missing execution gate for provider ${providerName}`);
7586
+ }
7587
+ const config = unit.requestedModel ? { ...providerConfig, model: unit.requestedModel } : providerConfig;
7588
+ const requestedContext = unit.context;
7589
+ const supportedContext = requestedContext && providerSupportsLocationContext(adapter) ? { status: "applied", resolved: requestedContext } : null;
7590
+ if (ctx.fill?.shouldSkip(providerName, unit.executionId)) return;
7591
+ try {
7592
+ await gate.run(async () => {
7593
+ this.throwIfRunCancelled(ctx.runId);
7594
+ if (ctx.fill?.shouldSkip(providerName, unit.executionId)) return;
7595
+ ctx.providerDispatchCounts.set(providerName, (ctx.providerDispatchCounts.get(providerName) ?? 0) + 1);
7596
+ const raw = await adapter.executeTrackedQuery(
7597
+ {
7598
+ query: unit.queryText,
7599
+ canonicalDomains: ctx.allDomains,
7600
+ competitorDomains: ctx.competitorDomains,
7601
+ location: requestedContext ?? void 0
7602
+ },
7603
+ config
7604
+ );
7605
+ this.throwIfRunCancelled(ctx.runId);
7606
+ const providerResult = adapter.normalizeResult(raw);
7607
+ const rawGroundingSources = providerResult.groundingSources;
7608
+ const normalized = {
7609
+ ...providerResult,
7610
+ groundingSources: Array.isArray(rawGroundingSources) ? rawGroundingSources : []
7611
+ };
7612
+ let citedUrlCapture;
7613
+ try {
7614
+ citedUrlCapture = await captureCitedUrls(providerName, rawGroundingSources);
7615
+ } catch (err) {
7616
+ citedUrlCapture = {
7617
+ citedUrls: [],
7618
+ captureStatus: "failed",
7619
+ sourceCount: normalized.groundingSources.length,
7620
+ resolvedCount: 0,
7621
+ captureVersion: CITED_URL_CAPTURE_VERSION
7622
+ };
7623
+ log2.warn("query.cited-url-capture-failed", {
7624
+ runId: ctx.runId,
7625
+ provider: providerName,
7626
+ query: unit.queryText,
7627
+ error: describeError(err)
7628
+ });
7629
+ }
7630
+ this.throwIfRunCancelled(ctx.runId);
7631
+ const citationState = determineCitationState(normalized, ctx.allDomains);
7632
+ const answerMentioned = determineAnswerMentioned(
7633
+ normalized.answerText,
7634
+ ctx.allBrandNames,
7635
+ ctx.allDomains
7636
+ );
7637
+ const overlap = computeCompetitorOverlap(normalized, ctx.competitorDomains);
7638
+ const extractedCompetitors = extractRecommendedCompetitors(
7639
+ normalized.answerText,
7640
+ ctx.allDomains,
7641
+ normalized.citedDomains,
7642
+ ctx.competitorDomains,
7643
+ ctx.allBrandNames
7644
+ );
7645
+ const snapshotId = crypto6.randomUUID();
7646
+ let screenshotRelPath = null;
7647
+ if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
7648
+ const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", ctx.runId);
7649
+ if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
7650
+ const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
7651
+ fs3.renameSync(raw.screenshotPath, destPath);
7652
+ screenshotRelPath = `${ctx.runId}/${snapshotId}.png`;
7653
+ }
7654
+ const insert = this.db.insert(querySnapshots).values({
7655
+ id: snapshotId,
7656
+ runId: ctx.runId,
7657
+ queryId: unit.queryId,
7658
+ queryText: unit.queryText,
7659
+ provider: providerName,
7660
+ // `model` is what was REQUESTED and `served_model` is what
7661
+ // answered. The manifest froze the request, so it is the
7662
+ // authority here: an adapter that reports its own default rather
7663
+ // than what it was handed would otherwise overwrite the identity
7664
+ // the revision recorded, and nothing would show that it moved.
7665
+ model: unit.requestedModel ?? raw.model,
7666
+ servedModel: raw.servedModel ?? null,
7667
+ citationState,
7668
+ answerMentioned,
7669
+ answerText: normalized.answerText,
7670
+ citedDomains: normalized.citedDomains,
7671
+ citedUrls: citedUrlCapture.citedUrls,
7672
+ captureStatus: citedUrlCapture.captureStatus,
7673
+ sourceCount: citedUrlCapture.sourceCount,
7674
+ resolvedCount: citedUrlCapture.resolvedCount,
7675
+ captureVersion: citedUrlCapture.captureVersion,
7676
+ retrievalStatus: normalized.retrievalStatus,
7677
+ retrievalContract: raw.retrievalContract,
7678
+ competitorOverlap: overlap,
7679
+ recommendedCompetitors: extractedCompetitors,
7680
+ // Only claim the geography the provider actually honoured. A
7681
+ // requested-but-unsupported context stores `location: null` —
7682
+ // "no claim" — rather than the label we asked for, mirroring
7683
+ // `supportedContext` itself: this field is never non-null when
7684
+ // that one is null.
7685
+ location: supportedContext ? requestedContext?.label ?? null : null,
7686
+ measurementExecutionId: unit.executionId,
7687
+ requestedContext,
7688
+ supportedContext,
7689
+ screenshotPath: screenshotRelPath,
7690
+ rawResponse: JSON.stringify({
7691
+ model: raw.model,
7692
+ servedModel: raw.servedModel ?? null,
7693
+ groundingSources: normalized.groundingSources,
7694
+ searchQueries: normalized.searchQueries,
7695
+ apiResponse: raw.rawResponse
7696
+ }),
7697
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
7698
+ });
7699
+ const written = ctx.fill ? insert.onConflictDoNothing().run() : insert.run();
7700
+ if (written.changes > 0) ctx.onInserted();
7701
+ ctx.fill?.onOutcome(providerName, true);
7702
+ log2.info("query.citation", {
7703
+ runId: ctx.runId,
7704
+ provider: providerName,
7705
+ query: unit.queryText,
7706
+ executionId: unit.executionId,
7707
+ location: requestedContext?.label ?? null,
7708
+ citationState,
7709
+ answerMentioned
7710
+ });
7711
+ });
7712
+ } catch (err) {
7713
+ if (err instanceof RunCancelledError) {
7714
+ throw err;
7715
+ }
7716
+ const msg = describeError(err);
7717
+ const stack = err instanceof Error ? err.stack : void 0;
7718
+ log2.error("query.failed", { runId: ctx.runId, provider: providerName, query: unit.queryText, executionId: unit.executionId, error: msg, stack });
7719
+ if (!ctx.providerErrors.has(providerName)) {
7720
+ ctx.providerErrors.set(providerName, msg);
7721
+ }
7722
+ ctx.fill?.onOutcome(providerName, false);
7723
+ }
7724
+ }
7486
7725
  incrementUsage(scope, metric, count) {
7487
7726
  const now = (/* @__PURE__ */ new Date()).toISOString();
7488
7727
  const period = now.slice(0, 10);
@@ -11777,7 +12016,7 @@ function readStoredGroundingSources(rawResponse) {
11777
12016
  return result2;
11778
12017
  }
11779
12018
  async function backfillInsightsCommand(project, opts) {
11780
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-K4U55A2J.js");
12019
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-PM7MC5VC.js");
11781
12020
  const config = loadConfig();
11782
12021
  const db = createClient(config.database);
11783
12022
  migrate(db);
@@ -13024,8 +13263,8 @@ var Notifier = class {
13024
13263
  this.serverUrl = serverUrl;
13025
13264
  }
13026
13265
  /** Called after a run completes (success, partial, or failed). */
13027
- async onRunCompleted(runId, projectId) {
13028
- log17.info("run.completed", { runId, projectId });
13266
+ async onRunCompleted(runId, projectId, opts) {
13267
+ log17.info("run.completed", { runId, projectId, origin: opts?.origin ?? "sweep" });
13029
13268
  const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
13030
13269
  if (notifs.length === 0) {
13031
13270
  log17.info("notifications.none-enabled", { projectId });
@@ -13045,7 +13284,7 @@ var Notifier = class {
13045
13284
  const transitions = this.computeTransitions(runId, projectId);
13046
13285
  const events = [];
13047
13286
  log17.info("run.status", { runId: run.id, status: run.status, projectId });
13048
- if (run.status === "completed" || run.status === "partial") {
13287
+ if ((run.status === "completed" || run.status === "partial") && opts?.origin !== "fill") {
13049
13288
  events.push("run.completed");
13050
13289
  }
13051
13290
  if (run.status === "failed") {
@@ -13496,7 +13735,7 @@ var RunCoordinator = class {
13496
13735
  intelligenceService;
13497
13736
  onInsightsGenerated;
13498
13737
  onAeroEvent;
13499
- async onRunCompleted(runId, projectId) {
13738
+ async onRunCompleted(runId, projectId, opts) {
13500
13739
  const runRow = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
13501
13740
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
13502
13741
  if (runRow?.trigger === RunTriggers.probe) {
@@ -13558,7 +13797,7 @@ var RunCoordinator = class {
13558
13797
  }
13559
13798
  }
13560
13799
  try {
13561
- await this.notifier.onRunCompleted(runId, projectId);
13800
+ await (opts ? this.notifier.onRunCompleted(runId, projectId, opts) : this.notifier.onRunCompleted(runId, projectId));
13562
13801
  } catch (err) {
13563
13802
  log18.error("notifier.failed", { runId, error: describeError(err) });
13564
13803
  }
@@ -17990,7 +18229,7 @@ async function createServer(opts) {
17990
18229
  void sessionRegistry.drainNow(project.name);
17991
18230
  }
17992
18231
  );
17993
- jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
18232
+ jobRunner.onRunCompleted = (runId, projectId, opts2) => opts2 ? runCoordinator.onRunCompleted(runId, projectId, opts2) : runCoordinator.onRunCompleted(runId, projectId);
17994
18233
  const snapshotService = new SnapshotService(registry);
17995
18234
  const liveProjectIds = new Set(
17996
18235
  opts.db.select({ id: projects.id }).from(projects).all().map((project) => project.id)
@@ -19347,6 +19586,14 @@ async function createServer(opts) {
19347
19586
  app.log.error({ runId, err }, "Job runner failed");
19348
19587
  });
19349
19588
  },
19589
+ getProviderDailyLimits: () => Object.fromEntries(
19590
+ registry.getAll().map((provider) => [provider.adapter.name, provider.config.quotaPolicy.maxRequestsPerDay])
19591
+ ),
19592
+ onRunFillCreated: (fillId, runId) => {
19593
+ jobRunner.executeRunFill(fillId).catch((err) => {
19594
+ app.log.error({ fillId, runId, err: describeError(err) }, "Run fill failed");
19595
+ });
19596
+ },
19350
19597
  onRunCancelled: (runId) => {
19351
19598
  const controller = siteAuditAbortControllers.get(runId);
19352
19599
  if (controller && !controller.signal.aborted) {