@canonry/canonry 5.11.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 (30) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +5 -0
  2. package/assets/assets/{AuditHistoryPanel-Cq8CoZV5.js → AuditHistoryPanel-DWDOPf8c.js} +1 -1
  3. package/assets/assets/{BacklinksPage-B5_QeuXd.js → BacklinksPage-DGxq-iU6.js} +1 -1
  4. package/assets/assets/{HistoryPage-Foj5--rM.js → HistoryPage-DR91m-gT.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-2t8jVNFL.js → MeasurementPropertyPage-a8poltfM.js} +1 -1
  6. package/assets/assets/{ProjectPage-By0UYm8r.js → ProjectPage-CCCJE7rA.js} +1 -1
  7. package/assets/assets/{RunRow-CbTtzDK5.js → RunRow-Xe63DyPK.js} +1 -1
  8. package/assets/assets/{RunsPage-Cjv7YiXR.js → RunsPage-DK119ZZw.js} +1 -1
  9. package/assets/assets/{SettingsPage-DIQgJQ8Y.js → SettingsPage-G461foD-.js} +1 -1
  10. package/assets/assets/{SiteHealthSection-CUNhD0To.js → SiteHealthSection-CJnHkGM2.js} +3 -3
  11. package/assets/assets/{TrafficPage-abuZqCEU.js → TrafficPage-AsWgc7Ot.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-B-zVt0Sa.js → TrafficSourceDetailPage-D10yBQoD.js} +1 -1
  13. package/assets/assets/{extract-error-message-C5-X8IcG.js → extract-error-message-PP4HD_Dp.js} +1 -1
  14. package/assets/assets/{index-Dzg_siX8.js → index-CCIeAE4J.js} +27 -27
  15. package/assets/assets/{react-sigma_core.esm.min-Ybxr8Ybd.js → react-sigma_core.esm.min-CkyyYkKU.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-BMOnF8IE.js → v2-overview-adapter-DW-In1hp.js} +1 -1
  17. package/assets/index.html +1 -1
  18. package/dist/{chunk-NV3X6O5P.js → chunk-6YLNO2T5.js} +2 -2
  19. package/dist/{chunk-JNU6GLAV.js → chunk-IDZQ5H3J.js} +2 -2
  20. package/dist/{chunk-JW5DCORY.js → chunk-IPGBK7XR.js} +87 -1
  21. package/dist/{chunk-SMYBGUVV.js → chunk-Q35D2R2P.js} +2515 -2097
  22. package/dist/{chunk-UYSTNPTX.js → chunk-TDHRUXN3.js} +487 -172
  23. package/dist/{chunk-MQMJJZ6W.js → chunk-UWJAATGC.js} +3021 -2925
  24. package/dist/cli.js +133 -7
  25. package/dist/{demo-server-76IZBMJ3.js → demo-server-IZFTJI7E.js} +3 -3
  26. package/dist/index.d.ts +28 -1
  27. package/dist/index.js +5 -5
  28. package/dist/{intelligence-service-KF5WWPGR.js → intelligence-service-PM7MC5VC.js} +2 -2
  29. package/dist/mcp.js +3 -3
  30. package/package.json +8 -8
@@ -17,12 +17,12 @@ import {
17
17
  resolveOperatorApiKeyIds,
18
18
  runWithUsageTags,
19
19
  saveConfigPatch
20
- } from "./chunk-JW5DCORY.js";
20
+ } from "./chunk-IPGBK7XR.js";
21
21
  import {
22
22
  deleteSiteCrawlGraphLayout,
23
23
  persistSiteCrawlGraphLayout,
24
24
  prepareSiteCrawlGraphLayout
25
- } from "./chunk-JNU6GLAV.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-SMYBGUVV.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,
@@ -300,6 +306,7 @@ import {
300
306
  isVertexGroundingRedirect,
301
307
  mapWithConcurrency,
302
308
  maskTelemetryAnonymousId,
309
+ missingDependency,
303
310
  nextScheduleUpdatedAt,
304
311
  normalizeAgentSlug,
305
312
  normalizeIdTokens,
@@ -312,6 +319,7 @@ import {
312
319
  observeTemplateLinkEdges,
313
320
  parseMeasurementRunManifestV1,
314
321
  parseOriginList,
322
+ parseRunError,
315
323
  providerError,
316
324
  providerSupportsLocationContext,
317
325
  registrableDomain,
@@ -336,7 +344,7 @@ import {
336
344
  visibilityReportRequestSchema,
337
345
  winnabilityClassLabel,
338
346
  withRetry
339
- } from "./chunk-MQMJJZ6W.js";
347
+ } from "./chunk-UWJAATGC.js";
340
348
 
341
349
  // src/runtime-context.ts
342
350
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -6482,7 +6490,7 @@ function nonBlank(value) {
6482
6490
  return trimmed ? trimmed : void 0;
6483
6491
  }
6484
6492
  function resolveBuildCommit(env = process.env) {
6485
- const embedded = true ? "d2370877dec1d1e40b88e6c56803b3eea6af4eb8" : void 0;
6493
+ const embedded = true ? "894086f6a60be75c0ade892884adf57ef7a38291" : void 0;
6486
6494
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
6487
6495
  }
6488
6496
  function resolveInstanceIdentity(env = process.env) {
@@ -6830,6 +6838,8 @@ var JobRunner = class {
6830
6838
  this.registry = registry;
6831
6839
  }
6832
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 });
6833
6843
  const stale = this.db.select({
6834
6844
  id: runs.id,
6835
6845
  projectId: runs.projectId,
@@ -7155,146 +7165,19 @@ var JobRunner = class {
7155
7165
  }
7156
7166
  }
7157
7167
  };
7158
- const processNodeForProvider = async (registeredProvider, unit) => {
7159
- const { adapter, config: providerConfig } = registeredProvider;
7160
- const providerName = adapter.name;
7161
- const gate = executionGates.get(providerName);
7162
- if (!gate) {
7163
- throw new Error(`Missing execution gate for provider ${providerName}`);
7164
- }
7165
- const config = unit.requestedModel ? { ...providerConfig, model: unit.requestedModel } : providerConfig;
7166
- const requestedContext = unit.context;
7167
- const supportedContext = requestedContext && providerSupportsLocationContext(adapter) ? { status: "applied", resolved: requestedContext } : null;
7168
- try {
7169
- await gate.run(async () => {
7170
- this.throwIfRunCancelled(runId);
7171
- providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
7172
- const raw = await adapter.executeTrackedQuery(
7173
- {
7174
- query: unit.queryText,
7175
- canonicalDomains: allDomains,
7176
- competitorDomains,
7177
- location: requestedContext ?? void 0
7178
- },
7179
- config
7180
- );
7181
- this.throwIfRunCancelled(runId);
7182
- const providerResult = adapter.normalizeResult(raw);
7183
- const rawGroundingSources = providerResult.groundingSources;
7184
- const normalized = {
7185
- ...providerResult,
7186
- groundingSources: Array.isArray(rawGroundingSources) ? rawGroundingSources : []
7187
- };
7188
- let citedUrlCapture;
7189
- try {
7190
- citedUrlCapture = await captureCitedUrls(providerName, rawGroundingSources);
7191
- } catch (err) {
7192
- citedUrlCapture = {
7193
- citedUrls: [],
7194
- captureStatus: "failed",
7195
- sourceCount: normalized.groundingSources.length,
7196
- resolvedCount: 0,
7197
- captureVersion: CITED_URL_CAPTURE_VERSION
7198
- };
7199
- log2.warn("query.cited-url-capture-failed", {
7200
- runId,
7201
- provider: providerName,
7202
- query: unit.queryText,
7203
- error: describeError(err)
7204
- });
7205
- }
7206
- this.throwIfRunCancelled(runId);
7207
- const citationState = determineCitationState(normalized, allDomains);
7208
- const answerMentioned = determineAnswerMentioned(
7209
- normalized.answerText,
7210
- allBrandNames,
7211
- allDomains
7212
- );
7213
- const overlap = computeCompetitorOverlap(normalized, competitorDomains);
7214
- const extractedCompetitors = extractRecommendedCompetitors(
7215
- normalized.answerText,
7216
- allDomains,
7217
- normalized.citedDomains,
7218
- competitorDomains,
7219
- allBrandNames
7220
- );
7221
- const snapshotId = crypto6.randomUUID();
7222
- let screenshotRelPath = null;
7223
- if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
7224
- const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
7225
- if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
7226
- const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
7227
- fs3.renameSync(raw.screenshotPath, destPath);
7228
- screenshotRelPath = `${runId}/${snapshotId}.png`;
7229
- }
7230
- this.db.insert(querySnapshots).values({
7231
- id: snapshotId,
7232
- runId,
7233
- queryId: unit.queryId,
7234
- queryText: unit.queryText,
7235
- provider: providerName,
7236
- // `model` is what was REQUESTED and `served_model` is what
7237
- // answered. The manifest froze the request, so it is the
7238
- // authority here: an adapter that reports its own default rather
7239
- // than what it was handed would otherwise overwrite the identity
7240
- // the revision recorded, and nothing would show that it moved.
7241
- model: unit.requestedModel ?? raw.model,
7242
- servedModel: raw.servedModel ?? null,
7243
- citationState,
7244
- answerMentioned,
7245
- answerText: normalized.answerText,
7246
- citedDomains: normalized.citedDomains,
7247
- citedUrls: citedUrlCapture.citedUrls,
7248
- captureStatus: citedUrlCapture.captureStatus,
7249
- sourceCount: citedUrlCapture.sourceCount,
7250
- resolvedCount: citedUrlCapture.resolvedCount,
7251
- captureVersion: citedUrlCapture.captureVersion,
7252
- retrievalStatus: normalized.retrievalStatus,
7253
- retrievalContract: raw.retrievalContract,
7254
- competitorOverlap: overlap,
7255
- recommendedCompetitors: extractedCompetitors,
7256
- // Only claim the geography the provider actually honoured. A
7257
- // requested-but-unsupported context stores `location: null` —
7258
- // "no claim" — rather than the label we asked for, mirroring
7259
- // `supportedContext` itself: this field is never non-null when
7260
- // that one is null.
7261
- location: supportedContext ? requestedContext?.label ?? null : null,
7262
- measurementExecutionId: unit.executionId,
7263
- requestedContext,
7264
- supportedContext,
7265
- screenshotPath: screenshotRelPath,
7266
- rawResponse: JSON.stringify({
7267
- model: raw.model,
7268
- servedModel: raw.servedModel ?? null,
7269
- groundingSources: normalized.groundingSources,
7270
- searchQueries: normalized.searchQueries,
7271
- apiResponse: raw.rawResponse
7272
- }),
7273
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
7274
- }).run();
7275
- totalSnapshotsInserted++;
7276
- log2.info("query.citation", {
7277
- runId,
7278
- provider: providerName,
7279
- query: unit.queryText,
7280
- executionId: unit.executionId,
7281
- location: requestedContext?.label ?? null,
7282
- citationState,
7283
- answerMentioned
7284
- });
7285
- });
7286
- } catch (err) {
7287
- if (err instanceof RunCancelledError) {
7288
- throw err;
7289
- }
7290
- const msg = describeError(err);
7291
- const stack = err instanceof Error ? err.stack : void 0;
7292
- log2.error("query.failed", { runId, provider: providerName, query: unit.queryText, executionId: unit.executionId, error: msg, stack });
7293
- if (!providerErrors.has(providerName)) {
7294
- providerErrors.set(providerName, msg);
7295
- }
7168
+ const slotContext = {
7169
+ runId,
7170
+ allDomains,
7171
+ competitorDomains,
7172
+ allBrandNames,
7173
+ executionGates,
7174
+ providerDispatchCounts,
7175
+ providerErrors,
7176
+ onInserted: () => {
7177
+ totalSnapshotsInserted++;
7296
7178
  }
7297
7179
  };
7180
+ const processNodeForProvider = (registeredProvider, unit) => this.executePlanSlot(slotContext, registeredProvider, unit);
7298
7181
  if (existingRun.kind === RunKinds["answer-visibility"] && existingRun.trigger !== RunTriggers.probe && existingRun.measurementPlanVersionId === null) {
7299
7182
  const definition = buildSimpleMeasurementDefinition({
7300
7183
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7441,7 +7324,7 @@ var JobRunner = class {
7441
7324
  status: "failed",
7442
7325
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
7443
7326
  error: errorMessage
7444
- }).where(eq2(runs.id, runId)).run();
7327
+ }).where(and2(eq2(runs.id, runId), inArray(runs.status, ["queued", "running"]))).run();
7445
7328
  this.flushProviderUsage(providerDispatchCounts, providerReservations);
7446
7329
  const abortReason2 = classifyRunAbortReason(errorMessage);
7447
7330
  const phases = buildPhases({ startTime, providerCallStart, providerCallEnd });
@@ -7482,6 +7365,363 @@ var JobRunner = class {
7482
7365
  }
7483
7366
  }
7484
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
+ }
7485
7725
  incrementUsage(scope, metric, count) {
7486
7726
  const now = (/* @__PURE__ */ new Date()).toISOString();
7487
7727
  const period = now.slice(0, 10);
@@ -11776,7 +12016,7 @@ function readStoredGroundingSources(rawResponse) {
11776
12016
  return result2;
11777
12017
  }
11778
12018
  async function backfillInsightsCommand(project, opts) {
11779
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-KF5WWPGR.js");
12019
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-PM7MC5VC.js");
11780
12020
  const config = loadConfig();
11781
12021
  const db = createClient(config.database);
11782
12022
  migrate(db);
@@ -13023,8 +13263,8 @@ var Notifier = class {
13023
13263
  this.serverUrl = serverUrl;
13024
13264
  }
13025
13265
  /** Called after a run completes (success, partial, or failed). */
13026
- async onRunCompleted(runId, projectId) {
13027
- log17.info("run.completed", { runId, projectId });
13266
+ async onRunCompleted(runId, projectId, opts) {
13267
+ log17.info("run.completed", { runId, projectId, origin: opts?.origin ?? "sweep" });
13028
13268
  const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
13029
13269
  if (notifs.length === 0) {
13030
13270
  log17.info("notifications.none-enabled", { projectId });
@@ -13044,7 +13284,7 @@ var Notifier = class {
13044
13284
  const transitions = this.computeTransitions(runId, projectId);
13045
13285
  const events = [];
13046
13286
  log17.info("run.status", { runId: run.id, status: run.status, projectId });
13047
- if (run.status === "completed" || run.status === "partial") {
13287
+ if ((run.status === "completed" || run.status === "partial") && opts?.origin !== "fill") {
13048
13288
  events.push("run.completed");
13049
13289
  }
13050
13290
  if (run.status === "failed") {
@@ -13495,7 +13735,7 @@ var RunCoordinator = class {
13495
13735
  intelligenceService;
13496
13736
  onInsightsGenerated;
13497
13737
  onAeroEvent;
13498
- async onRunCompleted(runId, projectId) {
13738
+ async onRunCompleted(runId, projectId, opts) {
13499
13739
  const runRow = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
13500
13740
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
13501
13741
  if (runRow?.trigger === RunTriggers.probe) {
@@ -13557,7 +13797,7 @@ var RunCoordinator = class {
13557
13797
  }
13558
13798
  }
13559
13799
  try {
13560
- await this.notifier.onRunCompleted(runId, projectId);
13800
+ await (opts ? this.notifier.onRunCompleted(runId, projectId, opts) : this.notifier.onRunCompleted(runId, projectId));
13561
13801
  } catch (err) {
13562
13802
  log18.error("notifier.failed", { runId, error: describeError(err) });
13563
13803
  }
@@ -13865,13 +14105,15 @@ function resolveAgentId(providerOrPiAi) {
13865
14105
  return void 0;
13866
14106
  }
13867
14107
  function buildAgentProvidersResponse(config) {
14108
+ const pinned = coerceAgentProvider(config.agent?.provider ?? void 0);
14109
+ const pinnedModel = pinned ? config.agent?.model ?? void 0 : void 0;
13868
14110
  const providers = listAgentProviders().map((id) => {
13869
14111
  const entry = AGENT_PROVIDERS[id];
13870
14112
  const source = resolveApiKeySource(id, config);
13871
14113
  return {
13872
14114
  id,
13873
14115
  label: entry.label,
13874
- defaultModel: entry.defaultModel,
14116
+ defaultModel: id === pinned && pinnedModel ? pinnedModel : entry.defaultModel,
13875
14117
  configured: source !== void 0,
13876
14118
  keySource: source?.source ?? null
13877
14119
  };
@@ -13879,7 +14121,7 @@ function buildAgentProvidersResponse(config) {
13879
14121
  const firstConfigured = agentProvidersByPriority().find((p) => resolveApiKeySource(p, config));
13880
14122
  return {
13881
14123
  providers,
13882
- defaultProvider: firstConfigured ?? null
14124
+ defaultProvider: pinned ?? firstConfigured ?? null
13883
14125
  };
13884
14126
  }
13885
14127
 
@@ -14845,6 +15087,27 @@ function missingProviderMessage() {
14845
15087
  const envHints = agentProvidersByPriority().map(agentProviderApiKeyEnvVar).join(" / ");
14846
15088
  return `No agent LLM provider configured. Add an API key for one of: ${configHints} in ~/.canonry/config.yaml, or export ${envHints}.`;
14847
15089
  }
15090
+ function resolveConfiguredAgentProvider(config) {
15091
+ return coerceAgentProvider(config.agent?.provider);
15092
+ }
15093
+ function describeAgentPin(config) {
15094
+ const provider = resolveConfiguredAgentProvider(config);
15095
+ if (!provider) return null;
15096
+ const model = config.agent?.model ?? getAgentProvider(provider).defaultModel;
15097
+ let modelError = null;
15098
+ try {
15099
+ resolveAeroModel(provider, model);
15100
+ } catch (err) {
15101
+ modelError = err instanceof Error ? err.message : String(err);
15102
+ }
15103
+ return {
15104
+ provider,
15105
+ model,
15106
+ configured: Boolean(resolveApiKeyFor(provider, config)),
15107
+ envVar: agentProviderApiKeyEnvVar(provider),
15108
+ modelError
15109
+ };
15110
+ }
14848
15111
  function detectAgentProvider(config) {
14849
15112
  for (const provider of agentProvidersByPriority()) {
14850
15113
  if (resolveApiKeyFor(provider, config)) return provider;
@@ -14915,9 +15178,11 @@ function createAeroSession(opts) {
14915
15178
  return agent;
14916
15179
  }
14917
15180
  function resolveSessionProviderAndModel(config, opts) {
14918
- const provider = opts?.provider ?? detectAgentProvider(config);
15181
+ const pinned = resolveConfiguredAgentProvider(config);
15182
+ const provider = opts?.provider ?? pinned ?? detectAgentProvider(config);
14919
15183
  if (!provider) throw new Error(missingProviderMessage());
14920
- const modelId = opts?.modelId ?? getAgentProvider(provider).defaultModel;
15184
+ const pinnedModelId = provider === pinned ? config.agent?.model : void 0;
15185
+ const modelId = opts?.modelId ?? pinnedModelId ?? getAgentProvider(provider).defaultModel;
14921
15186
  return { provider, modelId };
14922
15187
  }
14923
15188
 
@@ -15346,9 +15611,8 @@ var SessionRegistry = class {
15346
15611
  if (row.systemPrompt !== systemPrompt) {
15347
15612
  this.persistPromptSnapshot(projectId, systemPrompt);
15348
15613
  }
15349
- const effectiveProvider = preferences?.provider ?? row.modelProvider;
15350
- const effectiveModelId = preferences?.modelId ?? row.modelId;
15351
- if (preferences?.provider || preferences?.modelId) {
15614
+ const { provider: effectiveProvider, modelId: effectiveModelId } = this.resolveTurnModel(preferences, { provider: row.modelProvider, modelId: row.modelId });
15615
+ if (effectiveProvider !== row.modelProvider || effectiveModelId !== row.modelId) {
15352
15616
  this.opts.db.update(agentSessions).set({
15353
15617
  modelProvider: effectiveProvider,
15354
15618
  modelId: effectiveModelId,
@@ -15481,17 +15745,19 @@ ${lines.join("\n")}
15481
15745
  * - align `state.tools` to the requested scope/profile (CLI full vs
15482
15746
  * dashboard read-only share the same cached Agent; each request
15483
15747
  * re-scopes it).
15484
- * - align `state.model` when the caller passes `provider` or `modelId`,
15485
- * honoring `--provider` / `--model` on hot sessions (not just on
15486
- * fresh/hydrated construction).
15748
+ * - align `state.model` on every turn (see `resolveTurnModel`): an
15749
+ * explicit `provider` / `modelId` wins for this turn, then the
15750
+ * `agent.provider` pin, then the session's stored model.
15751
+ * - refuse, naming `agent.provider`, a pinned provider that has no key.
15487
15752
  *
15488
- * Persists the new model choice to the DB row so subsequent invocations
15489
- * stay on it unless overridden again.
15753
+ * Persists the resolved model to the DB row as provenance. Without a pin an
15754
+ * override therefore sticks, as it always has; with a pin, the next turn
15755
+ * that names no provider returns to it.
15490
15756
  */
15491
15757
  async acquireForTurn(projectName, preferences) {
15492
15758
  preferences?.signal?.throwIfAborted();
15493
15759
  if (this.acquisitions.has(projectName)) throw agentBusy(projectName);
15494
- const agent = this.getOrCreate(projectName);
15760
+ const agent = this.getOrCreate(projectName, preferences);
15495
15761
  if (agent.state.isStreaming) {
15496
15762
  throw agentBusy(projectName);
15497
15763
  }
@@ -15508,9 +15774,8 @@ ${lines.join("\n")}
15508
15774
  scope: preferences?.toolScope ?? AeroToolScopes.all,
15509
15775
  profile: preferences?.toolProfile ?? AeroToolProfiles.default
15510
15776
  });
15511
- if (preferences?.provider || preferences?.modelId) {
15512
- this.alignModel(projectName, agent, preferences);
15513
- }
15777
+ this.alignModel(projectName, agent, preferences ?? {});
15778
+ this.assertTurnProviderUsable(projectId);
15514
15779
  await this.mergeExternalTools(agent);
15515
15780
  const view = { client: this.opts.client, projectName, basePath: this.opts.config.basePath, context: preferences?.context };
15516
15781
  const evidence = preferences?.context ? await readAeroViewEvidence(view) : void 0;
@@ -15589,15 +15854,56 @@ ${lines.join("\n")}
15589
15854
  this.scopes.set(projectName, want.scope);
15590
15855
  this.profiles.set(projectName, want.profile);
15591
15856
  }
15857
+ /**
15858
+ * Throw a named, actionable error when the `agent.provider` pin chose this
15859
+ * turn's provider and that provider has no key. Auto-detection only ever
15860
+ * picks a keyed provider, so the pin is the one path that can land a turn on
15861
+ * a keyless one without anything saying so. An explicitly requested provider
15862
+ * keeps its existing behaviour: the caller named it, and the provider's own
15863
+ * error reports the missing key when the prompt is sent.
15864
+ */
15865
+ assertTurnProviderUsable(projectId) {
15866
+ const provider = coerceAgentProvider(this.loadRow(projectId)?.modelProvider ?? void 0);
15867
+ if (!provider || provider !== resolveConfiguredAgentProvider(this.opts.config)) return;
15868
+ if (resolveApiKeyFor(provider, this.opts.config)) return;
15869
+ throw missingDependency(
15870
+ `Aero is pinned to ${provider} by agent.provider, but no key is configured for it. Set providers.${provider}.apiKey in config.yaml or export ${agentProviderApiKeyEnvVar(provider)}, or change agent.provider.`,
15871
+ { provider, pinned: true }
15872
+ );
15873
+ }
15874
+ /**
15875
+ * Resolve the provider/model a turn should use: an explicit request outranks
15876
+ * the `agent.provider` pin, which outranks whatever the row last recorded.
15877
+ *
15878
+ * The row is provenance (what answered), not policy (what answers next), so
15879
+ * a configured pin reasserts itself every turn instead of being frozen at
15880
+ * whatever the first session happened to pick -- and survives the
15881
+ * conversation-delete path, which drops the row entirely. With a pin, an
15882
+ * explicit request therefore lasts one turn.
15883
+ *
15884
+ * The model is a function of the request and the config, never of which
15885
+ * provider ran last: naming the pinned provider gets `agent.model` whether or
15886
+ * not the session just came back from another one. The row's model is used
15887
+ * only when nothing is pinned and nothing was requested, which keeps an
15888
+ * unpinned install's override sticky as it always was. A model id is never
15889
+ * forwarded across providers: any other case takes the provider's default.
15890
+ */
15891
+ resolveTurnModel(preferences, current) {
15892
+ const pinned = resolveConfiguredAgentProvider(this.opts.config);
15893
+ const provider = preferences?.provider ?? pinned ?? current.provider ?? AgentProviderIds.claude;
15894
+ const carried = !preferences?.provider && !pinned && provider === current.provider ? current.modelId ?? void 0 : void 0;
15895
+ const pinnedModelId = provider === pinned ? this.opts.config.agent?.model ?? void 0 : void 0;
15896
+ const modelId = preferences?.modelId ?? pinnedModelId ?? carried ?? getAgentProvider(provider).defaultModel;
15897
+ return { provider, modelId };
15898
+ }
15592
15899
  alignModel(projectName, agent, preferences) {
15593
15900
  const projectId = this.tryResolveProjectId(projectName);
15594
15901
  if (!projectId) return;
15595
15902
  const row = this.loadRow(projectId);
15596
- const currentProvider = row?.modelProvider ?? AgentProviderIds.claude;
15597
- const currentModelId = row?.modelId;
15598
- const nextProvider = preferences.provider ?? currentProvider;
15599
- const nextModelId = preferences.modelId ?? (preferences.provider ? getAgentProvider(nextProvider).defaultModel : currentModelId);
15600
- if (!nextModelId) return;
15903
+ if (!row) return;
15904
+ const currentProvider = row.modelProvider;
15905
+ const currentModelId = row.modelId;
15906
+ const { provider: nextProvider, modelId: nextModelId } = this.resolveTurnModel(preferences, { provider: row.modelProvider, modelId: row.modelId });
15601
15907
  if (nextProvider === currentProvider && nextModelId === currentModelId) return;
15602
15908
  agent.state.model = resolveAeroModel(nextProvider, nextModelId);
15603
15909
  this.opts.db.update(agentSessions).set({
@@ -17923,7 +18229,7 @@ async function createServer(opts) {
17923
18229
  void sessionRegistry.drainNow(project.name);
17924
18230
  }
17925
18231
  );
17926
- jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
18232
+ jobRunner.onRunCompleted = (runId, projectId, opts2) => opts2 ? runCoordinator.onRunCompleted(runId, projectId, opts2) : runCoordinator.onRunCompleted(runId, projectId);
17927
18233
  const snapshotService = new SnapshotService(registry);
17928
18234
  const liveProjectIds = new Set(
17929
18235
  opts.db.select({ id: projects.id }).from(projects).all().map((project) => project.id)
@@ -18981,6 +19287,7 @@ async function createServer(opts) {
18981
19287
  // Resolved fresh each call so a key added at runtime (settings API) shows
18982
19288
  // up immediately in the `config.agent-providers` doctor check.
18983
19289
  getAgentProviderSummary: () => buildAgentProvidersResponse(opts.config).providers,
19290
+ getAgentPin: () => describeAgentPin(opts.config),
18984
19291
  getGoogleMarketingDoctorInput: (ctx) => {
18985
19292
  if (!ctx.project) return null;
18986
19293
  const googleAds = opts.db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, ctx.project.id)).get();
@@ -19279,6 +19586,14 @@ async function createServer(opts) {
19279
19586
  app.log.error({ runId, err }, "Job runner failed");
19280
19587
  });
19281
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
+ },
19282
19597
  onRunCancelled: (runId) => {
19283
19598
  const controller = siteAuditAbortControllers.get(runId);
19284
19599
  if (controller && !controller.signal.aborted) {